Skip to content

Commit

Permalink
Fix JWK Thumbprint calculation
Browse files Browse the repository at this point in the history
  • Loading branch information
nodh authored and JesusMcCloud committed Jul 22, 2024
1 parent 9dc035b commit b1c30ee
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 69 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@

### NEXT

**Fixes**
* Fix calculation of JWK thumbprints according to [RFC7638](https://www.rfc-editor.org/rfc/rfc7638.html)

**Changes**
* Add `provider` module that actually implements cryptography!
* Add `COSE_Key` header to `CoseHeader`, defined in OpenID for Verifiable Credential Issuance draft 13
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,29 +21,56 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.buildJsonObject
import okio.ByteString.Companion.toByteString

/**
* JSON Web Key as per [RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517#section-4)
* JSON Web Key as per [RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517#section-4).
*
* Note that the members are ordered lexicographically, as required for JWK Thumbprint calculation,
* see [RFC7638 s3](https://www.rfc-editor.org/rfc/rfc7638.html#section-3)
*/
@Serializable
data class JsonWebKey(
/**
* The "alg" (algorithm) parameter identifies the algorithm intended for
* use with the key. The values used should either be registered in the
* IANA "JSON Web Signature and Encryption Algorithms" registry
* established by [JWA] or be a value that contains a Collision-
* Resistant Name. The "alg" value is a case-sensitive ASCII string.
* Use of this member is OPTIONAL.
*/
@SerialName("alg")
val algorithm: JsonWebAlgorithm? = null,

/**
* Set for EC keys only
*/
@SerialName("crv")
val curve: ECCurve? = null,

/**
* The "kty" (key type) parameter identifies the cryptographic algorithm
* family used with the key, such as "RSA" or "EC". "kty" values should
* either be registered in the IANA "JSON Web Key Types" registry
* established by (JWA) or be a value that contains a Collision-Resistant
* Name. The "kty" value is a case-sensitive string. This
* member MUST be present in a JWK.
* Set for RSA keys only
*/
@SerialName("kty")
val type: JwkType? = null,
@SerialName("e")
@Serializable(with = ByteArrayBase64UrlSerializer::class)
val e: ByteArray? = null,

/**
* Set for symmetric keys only
*/
@SerialName("k")
@Serializable(with = ByteArrayBase64UrlSerializer::class)
val k: ByteArray? = null,

/**
* The "key_ops" (key operations) parameter identifies the operation(s)
* for which the key is intended to be used. The "key_ops" parameter is
* intended for use cases in which public, private, or symmetric keys
* may be present.
*/
@SerialName("key_ops")
val keyOperations: Set<String>? = null,

/**
* The "kid" (key ID) parameter is used to match a specific key. This
Expand All @@ -62,18 +89,15 @@ data class JsonWebKey(
val keyId: String? = null,

/**
* Set for EC keys only
*/
@SerialName("x")
@Serializable(with = ByteArrayBase64UrlSerializer::class)
val x: ByteArray? = null,

/**
* Set for EC keys only
* The "kty" (key type) parameter identifies the cryptographic algorithm
* family used with the key, such as "RSA" or "EC". "kty" values should
* either be registered in the IANA "JSON Web Key Types" registry
* established by (JWA) or be a value that contains a Collision-Resistant
* Name. The "kty" value is a case-sensitive string. This
* member MUST be present in a JWK.
*/
@SerialName("y")
@Serializable(with = ByteArrayBase64UrlSerializer::class)
val y: ByteArray? = null,
@SerialName("kty")
val type: JwkType? = null,

/**
* Set for RSA keys only
Expand All @@ -82,20 +106,6 @@ data class JsonWebKey(
@Serializable(with = ByteArrayBase64UrlSerializer::class)
val n: ByteArray? = null,

/**
* Set for RSA keys only
*/
@SerialName("e")
@Serializable(with = ByteArrayBase64UrlSerializer::class)
val e: ByteArray? = null,

/**
* Set for symmetric keys only
*/
@SerialName("k")
@Serializable(with = ByteArrayBase64UrlSerializer::class)
val k: ByteArray? = null,

/**
* The "use" (public key use) parameter identifies the intended use of
* the public key. The "use" parameter is employed to indicate whether
Expand All @@ -106,41 +116,11 @@ data class JsonWebKey(
val publicKeyUse: String? = null,

/**
* The "key_ops" (key operations) parameter identifies the operation(s)
* for which the key is intended to be used. The "key_ops" parameter is
* intended for use cases in which public, private, or symmetric keys
* may be present.
*/
@SerialName("key_ops")
val keyOperations: Set<String>? = null,

/**
* The "alg" (algorithm) parameter identifies the algorithm intended for
* use with the key. The values used should either be registered in the
* IANA "JSON Web Signature and Encryption Algorithms" registry
* established by [JWA] or be a value that contains a Collision-
* Resistant Name. The "alg" value is a case-sensitive ASCII string.
* Use of this member is OPTIONAL.
*/
@SerialName("alg")
val algorithm: JsonWebAlgorithm? = null,

/**
* The "x5u" (X.509 URL) parameter is a URI (RFC3986) that refers to a
* resource for an X.509 public key certificate or certificate chain
* (RFC5280). The identified resource MUST provide a representation of
* the certificate or certificate chain that conforms to RFC 5280
* (RFC5280) in PEM-encoded form, with each certificate delimited as
* specified in Section 6.1 of RFC 4945 (RFC4945). The key in the first
* certificate MUST match the public key represented by other members of
* the JWK. The protocol used to acquire the resource MUST provide
* integrity protection; an HTTP GET request to retrieve the certificate
* MUST use TLS (RFC2818) (RFC5246); the identity of the server MUST be
* validated, as per Section 6 of RFC 6125 (RFC6125). Use of this
* member is OPTIONAL.
* Set for EC keys only
*/
@SerialName("x5u")
val certificateUrl: String? = null,
@SerialName("x")
@Serializable(with = ByteArrayBase64UrlSerializer::class)
val x: ByteArray? = null,

/**
* The "x5c" (X.509 certificate chain) parameter contains a chain of one
Expand Down Expand Up @@ -170,6 +150,23 @@ data class JsonWebKey(
@Serializable(with = ByteArrayBase64UrlSerializer::class)
val certificateSha1Thumbprint: ByteArray? = null,

/**
* The "x5u" (X.509 URL) parameter is a URI (RFC3986) that refers to a
* resource for an X.509 public key certificate or certificate chain
* (RFC5280). The identified resource MUST provide a representation of
* the certificate or certificate chain that conforms to RFC 5280
* (RFC5280) in PEM-encoded form, with each certificate delimited as
* specified in Section 6.1 of RFC 4945 (RFC4945). The key in the first
* certificate MUST match the public key represented by other members of
* the JWK. The protocol used to acquire the resource MUST provide
* integrity protection; an HTTP GET request to retrieve the certificate
* MUST use TLS (RFC2818) (RFC5246); the identity of the server MUST be
* validated, as per Section 6 of RFC 6125 (RFC6125). Use of this
* member is OPTIONAL.
*/
@SerialName("x5u")
val certificateUrl: String? = null,

/**
* The "x5t#S256" (X.509 certificate SHA-256 thumbprint) parameter is a
* base64url-encoded SHA-256 thumbprint (a.k.a. digest) of the DER
Expand All @@ -181,6 +178,13 @@ data class JsonWebKey(
@SerialName("x5t#S256")
@Serializable(with = ByteArrayBase64UrlSerializer::class)
val certificateSha256Thumbprint: ByteArray? = null,

/**
* Set for EC keys only
*/
@SerialName("y")
@Serializable(with = ByteArrayBase64UrlSerializer::class)
val y: ByteArray? = null,
) : SpecializedCryptoPublicKey {

/**
Expand All @@ -189,7 +193,10 @@ data class JsonWebKey(
* See [RFC9278](https://www.rfc-editor.org/rfc/rfc9278.html)
*/
val jwkThumbprint: String by lazy {
val thumbprint = Json.encodeToString(this).encodeToByteArray().toByteString().sha256().base64Url()
val jsonEncoded = Json.encodeToString(this.toMinimalJsonWebKey().getOrNull() ?: this)
.also { println(it) }
val thumbprint = jsonEncoded
.encodeToByteArray().toByteString().sha256().toByteArray().encodeToString(Base64UrlStrict)
"urn:ietf:params:oauth:jwk-thumbprint:sha256:${thumbprint}"
}

Expand Down Expand Up @@ -306,6 +313,19 @@ data class JsonWebKey(
}
}

/**
* @return a copy of this key with the minimal required members as listed in
* [RFC7638 3.2](https://www.rfc-editor.org/rfc/rfc7638.html#section-3.2)
*/
fun toMinimalJsonWebKey(): KmmResult<JsonWebKey> = catching {
when (type) {
JwkType.EC -> JsonWebKey(type = JwkType.EC, curve = curve, x = x, y = y)
JwkType.RSA -> JsonWebKey(type = JwkType.RSA, n = n, e = e)
JwkType.SYM -> JsonWebKey(type = JwkType.SYM, k = k)
else -> throw IllegalArgumentException("Illegal key type")
}
}

/**
* Contains convenience functions
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package at.asitplus.crypto.datatypes.jws

import at.asitplus.crypto.datatypes.ECCurve
import at.asitplus.crypto.datatypes.io.Base64UrlStrict
import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.shouldBe
import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray
import kotlin.random.Random

class JsonWebKeyTest : FreeSpec({

lateinit var curve: ECCurve
lateinit var x: ByteArray
lateinit var y: ByteArray
lateinit var ecKey: JsonWebKey
lateinit var n: ByteArray
lateinit var e: ByteArray
lateinit var rsaKey: JsonWebKey

beforeTest {
curve = ECCurve.SECP_256_R_1
x = Random.Default.nextBytes(32)
y = Random.Default.nextBytes(32)
ecKey = JsonWebKey(type = JwkType.EC, curve = curve, x = x, y = y)
n = Random.Default.nextBytes(1024)
e = Random.Default.nextBytes(16)
rsaKey = JsonWebKey(type = JwkType.RSA, n = n, e = e)
}

"Thumbprint for minimal EC Key" - {
val newKey = JsonWebKey(type = JwkType.EC, curve = curve, x = x, y = y)

newKey.jwkThumbprint shouldBe ecKey.jwkThumbprint
}

"Thumbprint for EC Key with additional properties" - {
val newKey = JsonWebKey(type = JwkType.EC, curve = curve, x = x, y = y, publicKeyUse = "foo")

newKey.jwkThumbprint shouldBe ecKey.jwkThumbprint
}

"Thumbprint for EC Key with keyId" - {
val newKey = JsonWebKey(type = JwkType.EC, curve = curve, x = x, y = y, keyId = "foo")

newKey.jwkThumbprint shouldBe ecKey.jwkThumbprint
}

"Thumbprint for minimal RSA Key" - {
val newKey = JsonWebKey(type = JwkType.RSA, n = n, e = e)

newKey.jwkThumbprint shouldBe rsaKey.jwkThumbprint
}

"Thumbprint for RSA Key with additional properties" - {
val newKey = JsonWebKey(type = JwkType.RSA, n = n, e = e, algorithm = JwsAlgorithm.RS256)

newKey.jwkThumbprint shouldBe rsaKey.jwkThumbprint
}

"Thumbprint for RSA Key with keyId" - {
val newKey = JsonWebKey(type = JwkType.RSA, n = n, e = e, keyId = "foo")

newKey.jwkThumbprint shouldBe rsaKey.jwkThumbprint
}

"Thumbprint for fixed Key from RFC 7638" - {
val parsedN = ("0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2" +
"aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCi" +
"FV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65Y" +
"GjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n" +
"91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_x" +
"BniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw").decodeToByteArray(Base64UrlStrict)
val parsedE = "AQAB".decodeToByteArray(Base64UrlStrict)
val key = JsonWebKey(type = JwkType.RSA, n = parsedN, e = parsedE)

key.jwkThumbprint shouldBe "urn:ietf:params:oauth:jwk-thumbprint:sha256:NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"
}

})

0 comments on commit b1c30ee

Please sign in to comment.