椭圆曲线密码学(Elliptic curve cryptography),简称 ECC,是一种建立公开密钥加密的算法,也就是非对称加密,ECDH 与 ECDSA 是基于 ECC 的算法。类似的还有 RSA,ElGamal 算法等。ECC 被公认为在给定密钥长度下最安全的加密算法。比特币中的公私钥生成以及签名算法 ECDSA 都是基于 ECC 的。之前介绍 JWT 相关的知识介绍过了 HS256(MAC),RS256 (RSA) 相关的签名与验证,还有一种非对称签名算法 ES256 算法(ECDSA)也是推荐使用的一种。这三种算法也是不同语言主要实现,微软 System.IdentityModel.Tokens.Jwt 支持情况可以在 https://jwt.io/ 中找到。
https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/ 一文中提到了使用 JWT的“none”算法的安全性以及提供了一个密钥字段(kid)验证的重要性。
JSON Web Algorithms (JWA)[RFC7518]
+--------------+-------------------------------+--------------------+ | "alg" Param | Digital Signature or MAC | Implementation | | Value | Algorithm | Requirements | +--------------+-------------------------------+--------------------+ | HS256 | HMAC using SHA-256 | Required | | HS384 | HMAC using SHA-384 | Optional | | HS512 | HMAC using SHA-512 | Optional | | RS256 | RSASSA-PKCS1-v1_5 using | Recommended | | | SHA-256 | | | RS384 | RSASSA-PKCS1-v1_5 using | Optional | | | SHA-384 | | | RS512 | RSASSA-PKCS1-v1_5 using | Optional | | | SHA-512 | | | ES256 | ECDSA using P-256 and SHA-256 | Recommended+ | | ES384 | ECDSA using P-384 and SHA-384 | Optional | | ES512 | ECDSA using P-521 and SHA-512 | Optional | | PS256 | RSASSA-PSS using SHA-256 and | Optional | | | MGF1 with SHA-256 | | | PS384 | RSASSA-PSS using SHA-384 and | Optional | | | MGF1 with SHA-384 | | | PS512 | RSASSA-PSS using SHA-512 and | Optional | | | MGF1 with SHA-512 | | | none | No digital signature or MAC | Optional | | | performed | | +--------------+-------------------------------+--------------------+
ECDSA(Digital Signature Algorithm,椭圆曲线签名与校验,数字签名算法)它是另一种公开密钥算法,它不能用作加密,只用作数字签名。DSA使用公开密钥,为接受者验证数据的完整性和数据发送者的身份。它也可用于由第三方去确定签名和所签数据的真实性。
openssl 创建证书
openssl ecparam -genkey -name secp256r1 -out ecdas.key openssl req -new -key ecdas.key -out myreq.csr openssl req -x509 -days 7 -key ecdas.key -in myreq.csr -out ecdas.crt openssl pkcs12 -export -out ecdas.pfx -inkey ecdas.key -in ecdas.crt
简化
openssl ecparam -genkey -name secp256r1 | openssl ec -out ecdas.key openssl req -new -x509 -days 365 -key ecdas.key -out ecdas.crt -subj "/C=CN/L=SH/O=PICC/CN=idsvr4.com" openssl pkcs12 -export -in ecdas.crt -inkey ecdas.key -out ecdas.pfx
.net core 实现
class Program { static void Main(string[] args) { //获得证书文件 var filePath = Path.Combine(AppContext.BaseDirectory, "Certs\ecdas.pfx"); if (!File.Exists(filePath)) { throw new FileNotFoundException("Signing Certificate is missing!"); } var x509Cert = new X509Certificate2(filePath, "123456"); var data = new byte[] { 21, 5, 8, 12, 207 }; //test signature var signature = ECDsaSignData(x509Cert, data); Console.WriteLine(ECDsaVerifyData(x509Cert, data, signature) ? "Valid!" : "Not Valid..."); //test certs signature jwt(openssl) var jwtToken = CreateSignedJwt(x509Cert.GetECDsaPrivateKey()); Console.WriteLine(VerifySignedJwt(x509Cert.GetECDsaPublicKey(), jwtToken) ? "Valid!" : "Not Valid..."); //test certs signature jwt by BouncyCastle string privateKey = "c711e5080f2b58260fe19741a7913e8301c1128ec8e80b8009406e5047e6e1ef"; string publicKey = "04e33993f0210a4973a94c26667007d1b56fe886e8b3c2afdd66aa9e4937478ad20acfbdc666e3cec3510ce85d40365fc2045e5adb7e675198cf57c6638efa1bdb"; var privateECDsa = LoadPrivateKey(FromHexString(privateKey)); var publicECDsa = LoadPublicKey(FromHexString(publicKey)); var jwt = CreateSignedJwt(privateECDsa); var isValid = VerifySignedJwt(publicECDsa, jwt); Console.WriteLine(isValid ? "Valid!" : "Not Valid..."); //test certs signature jwt by Create Private-Public Key pair(https://github.com/smuthiya/EcdsaJwtSigning/blob/master/Program.cs) var key = CngKey.Create(CngAlgorithm.ECDsaP256, "ECDsaKey", new CngKeyCreationParameters { KeyCreationOptions = CngKeyCreationOptions.OverwriteExistingKey, KeyUsage = CngKeyUsages.AllUsages, ExportPolicy = CngExportPolicies.AllowPlaintextExport }); var cngKey_privateKey = new ECDsaCng(CngKey.Import(key.Export(CngKeyBlobFormat.EccPrivateBlob), CngKeyBlobFormat.EccPrivateBlob)); cngKey_privateKey.HashAlgorithm = CngAlgorithm.ECDsaP256; var cngKey_publicKey = new ECDsaCng(CngKey.Import(key.Export(CngKeyBlobFormat.EccPublicBlob), CngKeyBlobFormat.EccPublicBlob)); cngKey_publicKey.HashAlgorithm = CngAlgorithm.ECDsaP256; var jwt_sign = CreateSignedJwt(privateECDsa); Console.WriteLine(VerifySignedJwt(publicECDsa, jwt_sign) ? "Valid!" : "Not Valid..."); Console.ReadKey(); } private static byte[] ECDsaSignData(X509Certificate2 cert, byte[] data) { using (ECDsa ecdsa = cert.GetECDsaPrivateKey()) { if (ecdsa == null) throw new ArgumentException("Cert must have an ECDSA private key", nameof(cert)); return ecdsa.SignData(data, HashAlgorithmName.SHA256); } } private static bool ECDsaVerifyData(X509Certificate2 cert, byte[] data, byte[] signature) { using (ECDsa ecdsa = cert.GetECDsaPublicKey()) { if (ecdsa == null) throw new ArgumentException("Cert must be an ECDSA cert", nameof(cert)); return ecdsa.VerifyData(data, signature, HashAlgorithmName.SHA256); } } private static byte[] FromHexString(string hex) { var numberChars = hex.Length; var hexAsBytes = new byte[numberChars / 2]; for (var i = 0; i < numberChars; i += 2) hexAsBytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16); return hexAsBytes; } private static bool VerifySignedJwt(ECDsa eCDsa, string token) { var tokenHandler = new JwtSecurityTokenHandler(); var claimsPrincipal = tokenHandler.ValidateToken(token, new TokenValidationParameters { ValidIssuer = "me", ValidAudience = "you", IssuerSigningKey = new ECDsaSecurityKey(eCDsa) }, out var parsedToken); return claimsPrincipal.Identity.IsAuthenticated; } private static string CreateSignedJwt(ECDsa eCDsa) { var now = DateTime.UtcNow; var tokenHandler = new JwtSecurityTokenHandler(); var jwtToken = tokenHandler.CreateJwtSecurityToken( issuer: "me", audience: "you", subject: null, notBefore: now, expires: now.AddMinutes(30), issuedAt: now, signingCredentials: new SigningCredentials(new ECDsaSecurityKey(eCDsa), SecurityAlgorithms.EcdsaSha256)); return tokenHandler.WriteToken(jwtToken); } private static ECDsa LoadPrivateKey(byte[] key) { var privKeyInt = new Org.BouncyCastle.Math.BigInteger(+1, key); var parameters = SecNamedCurves.GetByName("secp256r1"); var ecPoint = parameters.G.Multiply(privKeyInt); var privKeyX = ecPoint.Normalize().XCoord.ToBigInteger().ToByteArrayUnsigned(); var privKeyY = ecPoint.Normalize().YCoord.ToBigInteger().ToByteArrayUnsigned(); return ECDsa.Create(new ECParameters { Curve = ECCurve.NamedCurves.nistP256, D = privKeyInt.ToByteArrayUnsigned(), Q = new ECPoint { X = privKeyX, Y = privKeyY } }); } private static ECDsa LoadPublicKey(byte[] key) { var pubKeyX = key.Skip(1).Take(32).ToArray(); var pubKeyY = key.Skip(33).ToArray(); return ECDsa.Create(new ECParameters { Curve = ECCurve.NamedCurves.nistP256, Q = new ECPoint { X = pubKeyX, Y = pubKeyY } }); } }
备注:IdentityServer4 目前暂时仅支持 RS256 ( RSA with SHA256) ,还不支持 ES256 (https://github.com/IdentityServer/IdentityServer4/issues/2493)。
JAVA 的话可以选用 Google 的 Tink
@Test public void testECDSA_P256() { try { TinkConfig.register(); String plaintext = "napier"; KeysetHandle privateKeysetHandle = KeysetHandle.generateNew(SignatureKeyTemplates.ECDSA_P256); PublicKeySign signer = PublicKeySignFactory.getPrimitive(privateKeysetHandle); byte[] signature = signer.sign(plaintext.getBytes()); byte[] encoded = Base64.getEncoder().encode(signature); System.out.println(" MAC (Base64): " + new String(encoded)); System.out.println("MAC (Hex):" + toHexString(encoded)); KeysetHandle publicKeysetHandle = privateKeysetHandle.getPublicKeysetHandle(); PublicKeyVerify verifier = PublicKeyVerifyFactory.getPrimitive(publicKeysetHandle); try { verifier.verify(signature, plaintext.getBytes()); System.out.println(" Valid Signature"); } catch (GeneralSecurityException e) { System.out.println("In Valid Signature"); } System.out.println(" Printing out key:"); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); CleartextKeysetHandle.write(privateKeysetHandle, JsonKeysetWriter.withOutputStream(outputStream)); System.out.println(new String(outputStream.toByteArray())); } catch (Exception e) { System.out.println(e); System.exit(1); } } public static String toHexString(byte[] bytes) { StringBuffer sb = new StringBuffer(bytes.length * 2); for (int i = 0; i < bytes.length; i++) { sb.append(toHex(bytes[i] >> 4)); sb.append(toHex(bytes[i])); } return sb.toString(); } private static char toHex(int nibble) { final char[] hexDigit = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; return hexDigit[nibble & 0xF]; }
备注:
今天找到了一个好的 OIDC 的 JAVA 客户端,也包含了JWT 不同算法签名 ,更多参考:https://connect2id.com/products/nimbus-jose-jwt/examples
REFER:
https://docs.microsoft.com/en-gb/dotnet/api/system.security.cryptography?view=netcore-2.0
https://www.ibm.com/support/knowledgecenter/zh/SSMNED_5.0.0/com.ibm.apic.toolkit.doc/rapim_ref_ootb_policyjwtgen.html
https://www.scottbrady91.com/C-Sharp/JWT-Signing-using-ECDSA-in-dotnet-Core
https://www.openssl.org/docs/manmaster/man1/ecparam.html
https://www.bouncycastle.org/csharp/
https://yq.aliyun.com/articles/551057
http://www.cnblogs.com/linianhui/p/security-based-toolbox.html
http://bobao.360.cn/news/detail/1377.html
https://www.cnblogs.com/Kalafinaian/p/7392505.html
https://zhuanlan.zhihu.com/p/27615345
https://zhuanlan.zhihu.com/p/36326221
http://8btc.com/thread-1240-1-1.html
https://juejin.im/post/5a67f3836fb9a01c9b661bd3
https://zhuanlan.zhihu.com/p/36326221
https://www.bouncycastle.org/
https://medium.com/coinmonks/cryptography-with-google-tink-33a70d71918d