Jwt Token 安全策略使用 ECDSA 椭圆曲线加密算法签名/验证

椭圆曲线密码学(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使用公开密钥,为接受者验证数据的完整性和数据发送者的身份。它也可用于由第三方去确定签名和所签数据的真实性。

Jwt Token 安全策略使用 ECDSA 椭圆曲线加密算法签名/验证_第1张图片

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("\nMAC (Base64):\t" + 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("\nValid Signature");
            } catch (GeneralSecurityException e) {
                System.out.println("In Valid Signature");
            }
            System.out.println("\nPrinting 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

你可能感兴趣的:(Jwt Token 安全策略使用 ECDSA 椭圆曲线加密算法签名/验证)