Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package net.blueshell.api.platform.oidc

import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.jose.jwk.JWKSelector
import com.nimbusds.jose.jwk.JWKSet
import com.nimbusds.jose.jwk.KeyUse
import com.nimbusds.jose.jwk.RSAKey
import com.nimbusds.jose.jwk.source.JWKSource
import com.nimbusds.jose.proc.SecurityContext
Expand Down Expand Up @@ -47,7 +49,17 @@ class VaultTransitJwkSource(

private fun refresh() {
val publicKeys = client.readPublicKeys(keyName)
val jwks = publicKeys.map { RSAKey.parseFromPEMEncodedObjects(it.publicKeyPem) as RSAKey }
// kid/alg/use MUST be set on the published JWK: go-oidc-v3 (Vault's verifier)
// skips any JWKS key whose `kid` doesn't match the JWT header's `kid`, and
// VaultTransitJwtEncoder writes kid="$keyName:v$version" into the header.
val jwks = publicKeys.map { vk ->
val parsed = RSAKey.parseFromPEMEncodedObjects(vk.publicKeyPem) as RSAKey
RSAKey.Builder(parsed)
.keyID("$keyName:v${vk.keyVersion}")
.algorithm(JWSAlgorithm.RS256)
.keyUse(KeyUse.SIGNATURE)
.build()
}
if (jwks.isEmpty()) error("Vault returned no public keys for transit key '$keyName'")
current.set(JWKSet(jwks))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class VaultTransitJwkSourceTest {
private val selector = JWKSelector(JWKMatcher.Builder().build())

@Test
fun `initial refresh succeeds and get returns the cached key`() {
fun `initial refresh succeeds and get returns the cached key with kid alg use`() {
val (pem, modulus) = generateRsaPem()
val client: VaultTransitClient = mock()
whenever(client.readPublicKeys(eq(keyName)))
Expand All @@ -30,7 +30,13 @@ class VaultTransitJwkSourceTest {

val keys = source.get(selector, null)
assertThat(keys).hasSize(1)
assertThat((keys.single() as RSAKey).modulus.decodeToBigInteger()).isEqualTo(modulus)
val rsa = keys.single() as RSAKey
assertThat(rsa.modulus.decodeToBigInteger()).isEqualTo(modulus)
// Must match the kid VaultTransitJwtEncoder writes into the JWT header;
// go-oidc-v3 (Vault) filters JWKS keys by kid and would skip otherwise.
assertThat(rsa.keyID).isEqualTo("$keyName:v1")
assertThat(rsa.algorithm?.name).isEqualTo("RS256")
assertThat(rsa.keyUse?.identifier()).isEqualTo("sig")
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,5 +105,13 @@ class VaultTransitJwksPlaywrightTest {
assertThat(first["kty"]?.asString()).isEqualTo("RSA")
assertThat(first["n"]?.asString()).isNotBlank()
assertThat(first["e"]?.asString()).isNotBlank()
// kid/alg/use must be present and match what VaultTransitJwtEncoder
// stamps into the JWT header — go-oidc-v3 (Vault) filters JWKS keys
// by kid and refuses to verify when there's no match.
assertThat(first["kid"]?.asString())
.withFailMessage("JWKS key must carry kid='api-jwt:v<N>' so Vault's go-oidc verifier picks it up")
.startsWith("api-jwt:v")
assertThat(first["alg"]?.asString()).isEqualTo("RS256")
assertThat(first["use"]?.asString()).isEqualTo("sig")
}
}
Loading