1 概述
本文主要讲述了如何利用Openssl生成ECDSA密钥对,并利用Auth0库进行Token生成及验证的过程。
2 ECDSA
2.1 简介
ECC(Elliptic Curve Cryptography,椭圆曲线加密)是一种基于椭圆曲线数学的公钥加密算法,而ECDSA是使用ECC对数字签名算法(DSA)的模拟,总的来说ECC相比起常见的RSA更加安全并且生成密钥对的过程会更快。本文不会涉及过多原理性的东西,只是作简单的介绍,想要详情了解这些算法的可以戳这里。
2.2 密钥对生成
在Openssl中生成ECDSA密钥对的流程如下:
1openssl ecparam -genkey -name secp521r1 -out private.pem #生成私钥 2openssl ec -in private.pem -pubout -out public.pem #生成公钥
参数说明如下:
ecparam:EC参数设置以及生成命令-genkey:使用特定参数生成EC私钥-name:ec参数,可以使用openssl ecparam -list_curves查看,这里用的是secp521r1-out:输出文件名ec:EC密钥处理命令-in:输入文件-pubout:默认情况下会输出私钥,加上该选项会变成输出公钥(如果输入是公钥的情况下该参数会自动设置)
执行完命令后就成功生成密钥对了,可以查看一下:

密钥对生成之后就可以准备一下生成Token了。
3 Auth0中的Token应用
3.1 Auth0
Auth0提供了验证以及授权服务,这里利用官方提供的Java实现去生成Token(这里插一句题外话,Java常用的Token实现还有一个叫JJWT的库),首先引入包:
1<dependency> 2 <groupId>com.auth0</groupId> 3 <artifactId>java-jwt</artifactId> 4 <version>3.12.0</version> 5</dependency>
Gradle:
compile group: 'com.auth0', name: 'java-jwt', version: '3.12.0'
引入后来看一下支持的加密算法,如下图:

最简单的使用HMAC算法生成的Token如下:
System.out.println(JWT.create().withIssuer("issuer").withAudience("content").sign(Algorithm.HMAC512("password")));
当然这不是本文的重点,本文的重点是介绍如何利用ECDSA去生成Token。
首先Auth0提供的签名api如下:
JWT.create().sign(Algorithm)
其中Algorithm可以取值如下:


想要使用ECDSA算法需要提供一个ECDSAKeyProvider或一个ECPublicKey和一个ECPrivateKey,这里选择后一种方式实现。
3.2 密钥对处理
官方并没有提供如何生成ECPublicKey/ECPrivateKey的方法,甚至连从文件读取密钥对的方法都没有提供,笔者从官方提供的测试代码中发现了如下方法:

其中核心就是读取密钥对的两个方法:
readPublicKeyFromFilereadPrivateKeyFromFile
从import结果可以看到这是一个工具类:

但问题是官方该工具类是测试使用的,换句话说不对外暴露的,在IDEA中直接引入会报错:

因此直接找到该工具类的源码(链接可以戳这里,需要引入bouncycastle包,Maven仓库链接可以戳这里)
1package com.auth0.jwt; 2 3import org.bouncycastle.util.io.pem.PemObject; 4import org.bouncycastle.util.io.pem.PemReader; 5 6import java.io.File; 7import java.io.FileNotFoundException; 8import java.io.FileReader; 9import java.io.IOException; 10import java.security.KeyFactory; 11import java.security.NoSuchAlgorithmException; 12import java.security.PrivateKey; 13import java.security.PublicKey; 14import java.security.spec.EncodedKeySpec; 15import java.security.spec.InvalidKeySpecException; 16import java.security.spec.PKCS8EncodedKeySpec; 17import java.security.spec.X509EncodedKeySpec; 18 19public class PemUtils { 20 21 private static byte[] parsePEMFile(File pemFile) throws IOException { 22 if (!pemFile.isFile() || !pemFile.exists()) { 23 throw new FileNotFoundException(String.format("The file '%s' doesn't exist.", pemFile.getAbsolutePath())); 24 } 25 PemReader reader = new PemReader(new FileReader(pemFile)); 26 PemObject pemObject = reader.readPemObject(); 27 byte[] content = pemObject.getContent(); 28 reader.close(); 29 return content; 30 } 31 32 private static PublicKey getPublicKey(byte[] keyBytes, String algorithm) { 33 PublicKey publicKey = null; 34 try { 35 KeyFactory kf = KeyFactory.getInstance(algorithm); 36 EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes); 37 publicKey = kf.generatePublic(keySpec); 38 } catch (NoSuchAlgorithmException e) { 39 System.out.println("Could not reconstruct the public key, the given algorithm could not be found."); 40 } catch (InvalidKeySpecException e) { 41 System.out.println("Could not reconstruct the public key"); 42 } 43 44 return publicKey; 45 } 46 47 private static PrivateKey getPrivateKey(byte[] keyBytes, String algorithm) { 48 PrivateKey privateKey = null; 49 try { 50 KeyFactory kf = KeyFactory.getInstance(algorithm); 51 EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); 52 privateKey = kf.generatePrivate(keySpec); 53 } catch (NoSuchAlgorithmException e) { 54 System.out.println("Could not reconstruct the private key, the given algorithm could not be found."); 55 } catch (InvalidKeySpecException e) { 56 System.out.println("Could not reconstruct the private key"); 57 } 58 59 return privateKey; 60 } 61 62 public static PublicKey readPublicKeyFromFile(String filepath, String algorithm) throws IOException { 63 byte[] bytes = PemUtils.parsePEMFile(new File(filepath)); 64 return PemUtils.getPublicKey(bytes, algorithm); 65 } 66 67 public static PrivateKey readPrivateKeyFromFile(String filepath, String algorithm) throws IOException { 68 byte[] bytes = PemUtils.parsePEMFile(new File(filepath)); 69 return PemUtils.getPrivateKey(bytes, algorithm); 70 } 71}
直接复制该工具类后,将前一步生成的private.pem以及public.pem放置合适位置,通过工具类读取并生成Token:
1public class Main { 2 public static void main(String[] args) { 3 try { 4 ECPublicKey publicKey = (ECPublicKey) PemUtils.readPublicKeyFromFile("src/main/resources/public.pem","EC"); 5 ECPrivateKey privateKey = (ECPrivateKey) PemUtils.readPrivateKeyFromFile("src/main/resources/private.pem","EC"); 6 Algorithm algorithm = Algorithm.ECDSA512(publicKey,privateKey); 7 String token = JWT.create() 8 .withIssuer("issuer") 9 .sign(algorithm); 10 JWTVerifier verifier = JWT.require(algorithm).build(); 11 verifier.verify(JWT.decode(token)); 12 } catch (Exception e) { 13 e.printStackTrace(); 14 } 15 } 16}
但是会报错说私钥是null:

从官方issue中查到了类似的问题:

回答说是密钥格式的问题,其中提到的pkcs8是私钥格式转换命令:

将私钥的格式进行转换:
openssl pkcs8 -topk8 -inform pem -in private.pem -outform pem -nocrypt -out newprivate.pem
参数说明:
pkcs8:私钥格式转换命令-topk8:读取私钥并转换为PKCS#8格式-inform:指定输入格式,默认pem,使用该参数并配合-topk8后会生成加密后的PKCS#8格式的私钥-in:输入文件-outform:与-inform类似-nocrypt:在这里主要配合-inform+-topk8使用,生成不加密的PrivateKeyInfo而不是加密的PKCS#8 EncryptedPrivateKeyInfo,因为一些软件(比如某些版本的Java代码)使用的是不加密格式的私钥-out:输出文件
转换后就可以生成Token了。
3.3 生成Token
最后readPrivateKeyFromFile中的参数修改为新的私钥即可:
ECPrivateKey privateKey = (ECPrivateKey) PemUtils.readPrivateKeyFromFile("src/main/resources/newprivate.pem","EC");
4 参考源码
包含了示例密钥对以及如何使用(Gradle版的):
5 参考网站
3、Elliptic Curve Cryptography: breaking security and a comparison with RSA
5、StackExange-https://superuser.com/questions/1103401/generate-an-ecdsa-key-and-csr-with-openssl
6、auth0/java-jwt Issue - ECDSA key version mismatch from openssl pem files #270
8、codota-How to useECDSA512methodincom.auth0.jwt.algorithms.Algorithm