前言

一般生成 JWT 時,都是透過設定好的固定字串當作 JWT 的密鑰(Secret key),然後再將 JWT 傳送到後端,並用相同的 Secret key 去做驗證,但這樣的做法安全性相對較低,如果固定的 Secret Key 被洩漏,攻擊者可以偽造 JWT,並且無法防止重放攻擊。

當使用 JWT(JSON Web Token)搭配 ECDH(橢圓曲線 Diffie-Hellman)密鑰交換來提升安全性時,主要目的是利用 ECDH 生成的共享秘密密鑰來簽名和驗證 JWT。這樣可以確保 JWT 的完整性和來源的可信性,並且在通信過程中不會暴露實際的簽名密鑰。

此篇將探討如何使用 ECDH 橢圓曲線進行密鑰交換生成 Secret Key 提升 JWT 的安全性。

使用情境

假設有一個 IoT 設備(Device)需要對雲端服務(Server)發送 API 請求,請求中需要包含生成的 JTW,透過 JWT 驗證請求是否有效。以下是一個具體的使用情境:

  1. Device 初始化:第一次啟用 Device 時要做的事

    • Device 中生成一對 ECC 密鑰對(dPubdPri),並將 dPub 傳送到後端 DB 儲存。(每個 Device 都有一個儲存自己 dPub 的欄位)
    • Server 中生成一對 ECC 密鑰對(sPubsPri),將 sPub 傳送給 Device 中儲存(亦可直接編譯在 Device 內)

    這樣 Server 端就會有 dPub, sPub, sPri 三個,而 Device 內就會有 dPub, dPri, sPub 三個

  2. 當 Device 要對 Server 發送 API 請求時

    • Device 透過dPrisPub生成共享密鑰 (Secret Key)
    • Device 用這個 Secret key 跟 Payload 簽名 JWT (payload 中包含 Device UUID)
    • Device 將簽名好的 JWT 發送到 Server
    • Server 收到 JWT 後,將 sPridPub生成共享密鑰 (Secret Key)
    • Server 用這組密鑰驗證 JWT 是否有效,有效則處理 request

具體步驟與程式碼範例

下面就用 ASP.NET 來做個範例,用於生成和驗證使用 ECDH 密鑰交換的 JWT:

1. 生成密鑰對

1
2
3
4
5
6
7
8
9
private static ECDsa _ecdsa;
private static ECDsa _ecdsaByDevice;
static JwtValidatorController()
{
// 當Controller第一次被使用時生成ECDsa密鑰
_ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
_ecdsaByDevice = ECDsa.Create(ECCurve.NamedCurves.nistP256);
}

2. 生成 Server 端和 Device 端的公私鑰對

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
[HttpGet("generateKeyByServer")]
public IActionResult GenerateKeys()
{
byte[] publicKey = _ecdsa.ExportSubjectPublicKeyInfo();
string publicKeyBase64 = Convert.ToBase64String(publicKey);

byte[] privateKey = _ecdsa.ExportPkcs8PrivateKey();
string privateKeyBase64 = Convert.ToBase64String(privateKey);

var keys = new
{
PublicKey = publicKeyBase64,
PrivateKey = privateKeyBase64
};

return Ok(keys);
}

[HttpGet("generateKeyByDevice")]
public IActionResult GenerateKeysByDevice()
{
byte[] publicKey = _ecdsaByDevice.ExportSubjectPublicKeyInfo();
string publicKeyBase64 = Convert.ToBase64String(publicKey);

byte[] privateKey = _ecdsaByDevice.ExportPkcs8PrivateKey();
string privateKeyBase64 = Convert.ToBase64String(privateKey);

var keys = new
{
PublicKey = publicKeyBase64,
PrivateKey = privateKeyBase64
};

return Ok(keys);
}

3. 創建 JWT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class CreateJwtRequest
{
public string ServerPublicKey { get; set; }
public string DevciePrivateKey { get; set; }
}

[HttpPost("create-jwt")]
public IActionResult CreateJwt([FromBody] CreateJwtRequest request)
{
var diffieHellmanA = ECDiffieHellman.Create();
diffieHellmanA.ImportSubjectPublicKeyInfo(Convert.FromBase64String(request.ServerPublicKey), out _);

var diffieHellmanB = ECDiffieHellman.Create();
diffieHellmanB.ImportPkcs8PrivateKey(Convert.FromBase64String(request.DevciePrivateKey), out _);

var derivedKey = diffieHellmanB.DeriveKeyMaterial(diffieHellmanA.PublicKey);
var signingCredentials = new SigningCredentials(new SymmetricSecurityKey(derivedKey), SecurityAlgorithms.HmacSha256);

var header = new JwtHeader(signingCredentials);
var payload = new JwtPayload
{
{ JwtRegisteredClaimNames.Sub, "testuser" },
{ JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString() },
{ "exp", DateTimeOffset.UtcNow.AddMinutes(5).ToUnixTimeSeconds() }
};

var token = new JwtSecurityToken(header, payload);
var tokenHandler = new JwtSecurityTokenHandler();
string jwt = tokenHandler.WriteToken(token);

return Ok(new { Token = jwt });
}

4. 驗證 JWT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class VaildateJwtKey
{
public string Jwt { get; set; }
public string DevicePublicKey { get; set; }
public string ServerPrivateKey { get; set; }
}

[HttpPost("validate-jwt")]
public IActionResult ValidateJwt([FromBody] VaildateJwtKey request)
{
var tokenHandler = new JwtSecurityTokenHandler();
var jwtToken = tokenHandler.ReadJwtToken(request.Jwt);

var diffieHellmanA = ECDiffieHellman.Create();
diffieHellmanA.ImportPkcs8PrivateKey(Convert.FromBase64String(request.ServerPrivateKey), out _);

var diffieHellmanB = ECDiffieHellman.Create();
diffieHellmanB.ImportSubjectPublicKeyInfo(Convert.FromBase64String(request.DevicePublicKey), out _);

var derivedKey = diffieHellmanA.DeriveKeyMaterial(diffieHellmanB.PublicKey);
var validationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
IssuerSigningKey = new SymmetricSecurityKey(derivedKey)
};

try
{
var principal = tokenHandler.ValidateToken(request.Jwt, validationParameters, out var validatedToken);
var claims = principal.Claims.Select(c => new { c.Type, c.Value });

return Ok(new { Valid = true, Claims = claims });
}
catch (Exception ex)
{
return BadRequest(new { Valid = false, Error = ex.Message });
}
}

這樣,你就可以使用 ECDH 密鑰交換來生成共享的秘密密鑰,並使用該密鑰來簽名和驗證 JWT,從而提升 JWT 的安全性。

ECDH 密鑰交換優缺點

優點

高安全性:
◦ ECDH 密鑰交換協議確保只有通信雙方能生成相同的共享秘密密鑰,第三方無法獲取這個密鑰。
◦ 每次通信都可以生成新的共享秘密密鑰,防止重放攻擊。

動態密鑰生成:
◦ 每次通信都可以生成新的共享秘密密鑰,進一步提高安全性。

防篡改和偽造:
◦ 由於密鑰交換過程中不傳輸私鑰,攻擊者無法偽造 JWT。

缺點

實現複雜:
◦ 需要實現 ECDH 密鑰交換協議,增加了實現的複雜性。
◦ 需要管理公私鑰對,增加了開發和運維的負擔。

性能較低:
◦ 生成和驗證 JWT 的過程中需要進行複雜的計算,性能較低。

總結

使用 ECDH 密鑰交換來生成共享的秘密密鑰,並使用該秘密密鑰來簽名和驗證 JWT,具有以下優點:

  1. 提高安全性
    • ECDH 密鑰交換確保只有通信雙方能生成相同的共享秘密密鑰,第三方無法獲取這個密鑰。
    • 使用共享的秘密密鑰來簽名和驗證 JWT,確保 JWT 的完整性和來源的可信性。
  2. 簡化密鑰管理
    • 雙方只需交換公鑰,無需共享私鑰,減少了密鑰管理的複雜性和風險。
  3. 動態密鑰生成
    • 每次通信都可以生成新的共享秘密密鑰,進一步提高安全性。