我正在尝试生成 JWT 并使用私钥对其进行签名,在 Android 上,但我未能这样做,因为 jwt.io 或 token.dev 等在线解码器表示 他们无法验证签名 .
{
"alg": "ES256",
"typ": "JWT"
}
payload
必须包含 id 和 iat
,如下所示:{
"id": "7986f03e-a4b8-4033-87b9-af8ae0468a0b", // it's a UUID and this one is an example
"iat": 1702390385 // timestamp in seconds from UTC
}
我已阅读以下有关 Android 的文档:
KeyPair
并将其存储在 Android Key Store
中: https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec#example:-nist-p-256- ec-key-pair-for-signingverification-using-ecdsa这是我想出的代码:
// Initialize KeyPairGenerator for ECDSA 256
val keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, ANDROID_KEY_STORE)
keyPairGenerator.initialize(
KeyGenParameterSpec.Builder(
keyStoreAlias,
KeyProperties.PURPOSE_SIGN,
)
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
.setDigests(KeyProperties.DIGEST_SHA256)
.build()
)
// Generate the key pair (it will be stored automatically in the Android key store)
keyPairGenerator.generateKeyPair()
val encodedHeader = makeJsonAndEncodeToString(
"alg" to "ES256",
"typ" to "JWT",
)
val encodedPayload = makeJsonAndEncodeToString(
"id" to "27d09c71-bd67-4523-97bb-12e933e9acbb",
"iat" to 1702462854,
)
private fun makeJsonAndEncodeToString(vararg data: Pair<String, Any>): String {
return Base64.encodeToString(
JSONObject(data.toMap()).toString().toByteArray(),
Base64.NO_PADDING or Base64.URL_SAFE or Base64.NO_WRAP,
)
}
val signatureInput = "$encodedHeader.$encodedPayload"
val keyStore = loadKeyStore() // helper function to load the Android key store
// Retrieve Private Key
val entry: KeyStore.Entry = keyStore.getEntry(keyStoreAlias, null)
if (entry !is KeyStore.PrivateKeyEntry) {
throw IllegalStateException("No such private key under the alias <$keyStoreAlias>")
}
// Setup signature & sign input
val ecdsaSign = Signature.getInstance("SHA256withECDSA")
ecdsaSign.initSign(entry.privateKey)
ecdsaSign.update(signatureInput.toByteArray()) // defaults to UTF-8
val signedContent = ecdsaSign.sign()
val encodedSignature = Base64.encodeToString(
signedContent,
Base64.NO_PADDING or Base64.URL_SAFE or Base64.NO_WRAP,
)
// JWT complete
val token: String = "$encodedHeader.$encodedPayload.$encodedSignature"
val keyStore = loadKeyStore()
val publicKey = keyStore.getCertificate(keyStoreAlias)?.publicKey ?: return null
val encoded = Base64.encodeToString(publicKey.encoded, Base64.DEFAULT)
return "-----BEGIN PUBLIC KEY-----\n$encoded-----END PUBLIC KEY-----"
使用这个逻辑,这是我得到的最终结果:
eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjI3ZDA5YzcxLWJkNjctNDUyMy05N2JiLTEyZTkzM2U5YWNiYiIsImlhdCI6MTcwMjQ2Mjg1NH0.MEQCIGvk2N-3BDf13FAhAywXe7okYH4DygaViBWk6z6wnrvdAiBgJzHegGu8e9YSC9QiKqHvJxjyCRAX53tmmC_LaaFdRQ
Jwt.io报告解码成功,但签名验证失败。我确保提供我的公钥的 PEM 表示形式。
我在这里做错了什么?
我做了什么调查和调试:
Signature
实例并使用公钥来验证签名。结果是true
。getKey(...)
上的 KeyStore
来检索 PrivateKey
,这也可以工作,但在 jwt.io、token.dev 等上签名验证仍然失败。-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEa5M6OOsgL/+aR4jACkQxnjfRoJgw9fdsYR5ugxC6tBJIZA1oFRpjkc1TdQlbnE7BC6DEbaJPjH3jP0Lcnt2KWQ==
-----END PUBLIC KEY-----
我能够使用在线工具对其进行验证,例如:https://report-uri.com/home/pem_decoder。据报道:
Public Key Data
Key Algorithm: ECDSA prime256v1
Key Size: 256 bits
Raw Data
Array
(
[bits] => 256
[key] => -----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEa5M6OOsgL/+aR4jACkQxnjfRoJgw
9fdsYR5ugxC6tBJIZA1oFRpjkc1TdQlbnE7BC6DEbaJPjH3jP0Lcnt2KWQ==
-----END PUBLIC KEY-----
[ec] => Array
(
[curve_name] => prime256v1
[curve_oid] => 1.2.840.10045.3.1.7
[x] => 6b933a38eb202fff9a4788c00a44319e37d1a09830f5f76c611e6e8310bab412
[y] => 48640d68151a6391cd5375095b9c4ec10ba0c46da24f8c7de33f42dc9edd8a59
)
[type] => 3
)
感谢您的帮助。
您使用了错误的签名格式,即 ECDSA 签名的 ASN.1/DER 编码。对于 JWT,需要 IEEE P1363 格式 (r|s),请参阅 RFC 7518,第 3.4 节。
尝试使用
SHA256withPlain-ECDSA
代替 SHA256withECDSA
(如有必要,使用 BouncyCastle)。