1 概述
本文主要講述了如何利用Openssl
生成ECDSA
密鑰對,並利用Auth0
庫進行Token
生成及驗證的過程。
2 ECDSA
2.1 簡介
ECC
(Elliptic Curve Cryptography
,橢圓曲線加密)是一種基於橢圓曲線數學的公鑰加密算法,而ECDSA
是使用ECC
對數字簽名算法(DSA
)的模擬,總的來說ECC
相比起常見的RSA
更加安全並且生成密鑰對的過程會更快。本文不會涉及過多原理性的東西,只是作簡單的介紹,想要詳情了解這些算法的可以戳這里。
2.2 密鑰對生成
在Openssl
中生成ECDSA
密鑰對的流程如下:
openssl ecparam -genkey -name secp521r1 -out private.pem #生成私鑰
openssl 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
的庫),首先引入包:
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.12.0</version>
</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
的方法,甚至連從文件讀取密鑰對的方法都沒有提供,筆者從官方提供的測試代碼中發現了如下方法:
其中核心就是讀取密鑰對的兩個方法:
readPublicKeyFromFile
readPrivateKeyFromFile
從import
結果可以看到這是一個工具類:
但問題是官方該工具類是測試使用的,換句話說不對外暴露的,在IDEA
中直接引入會報錯:
因此直接找到該工具類的源碼(鏈接可以戳這里,需要引入bouncycastle
包,Maven
倉庫鏈接可以戳這里)
package com.auth0.jwt;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.EncodedKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
public class PemUtils {
private static byte[] parsePEMFile(File pemFile) throws IOException {
if (!pemFile.isFile() || !pemFile.exists()) {
throw new FileNotFoundException(String.format("The file '%s' doesn't exist.", pemFile.getAbsolutePath()));
}
PemReader reader = new PemReader(new FileReader(pemFile));
PemObject pemObject = reader.readPemObject();
byte[] content = pemObject.getContent();
reader.close();
return content;
}
private static PublicKey getPublicKey(byte[] keyBytes, String algorithm) {
PublicKey publicKey = null;
try {
KeyFactory kf = KeyFactory.getInstance(algorithm);
EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
publicKey = kf.generatePublic(keySpec);
} catch (NoSuchAlgorithmException e) {
System.out.println("Could not reconstruct the public key, the given algorithm could not be found.");
} catch (InvalidKeySpecException e) {
System.out.println("Could not reconstruct the public key");
}
return publicKey;
}
private static PrivateKey getPrivateKey(byte[] keyBytes, String algorithm) {
PrivateKey privateKey = null;
try {
KeyFactory kf = KeyFactory.getInstance(algorithm);
EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
privateKey = kf.generatePrivate(keySpec);
} catch (NoSuchAlgorithmException e) {
System.out.println("Could not reconstruct the private key, the given algorithm could not be found.");
} catch (InvalidKeySpecException e) {
System.out.println("Could not reconstruct the private key");
}
return privateKey;
}
public static PublicKey readPublicKeyFromFile(String filepath, String algorithm) throws IOException {
byte[] bytes = PemUtils.parsePEMFile(new File(filepath));
return PemUtils.getPublicKey(bytes, algorithm);
}
public static PrivateKey readPrivateKeyFromFile(String filepath, String algorithm) throws IOException {
byte[] bytes = PemUtils.parsePEMFile(new File(filepath));
return PemUtils.getPrivateKey(bytes, algorithm);
}
}
直接復制該工具類后,將前一步生成的private.pem
以及public.pem
放置合適位置,通過工具類讀取並生成Token
:
public class Main {
public static void main(String[] args) {
try {
ECPublicKey publicKey = (ECPublicKey) PemUtils.readPublicKeyFromFile("src/main/resources/public.pem","EC");
ECPrivateKey privateKey = (ECPrivateKey) PemUtils.readPrivateKeyFromFile("src/main/resources/private.pem","EC");
Algorithm algorithm = Algorithm.ECDSA512(publicKey,privateKey);
String token = JWT.create()
.withIssuer("issuer")
.sign(algorithm);
JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(JWT.decode(token));
} catch (Exception e) {
e.printStackTrace();
}
}
}
但是會報錯說私鑰是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
如果覺得文章好看,歡迎點贊。
同時歡迎關注微信公眾號:氷泠之路。