diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/JwsAlgorithms.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/JwsAlgorithms.java index ed8976c22ff..7ef439ad64b 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/JwsAlgorithms.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/JwsAlgorithms.java @@ -22,6 +22,7 @@ * the JWS Protected Header and JWS Payload. * * @author Joe Grandja + * @author Andrey Litvitski * @since 5.0 * @see JSON Web Algorithms * (JWA) @@ -93,6 +94,21 @@ public final class JwsAlgorithms { */ public static final String PS512 = "PS512"; + /** + * EdDSA signature algorithms (optional). + */ + public static final String EdDSA = "EdDSA"; + + /** + * EdDSA signature algorithms using Ed448 curve (optional). + */ + public static final String ED448 = "Ed448"; + + /** + * EdDSA signature algorithms using Ed25519 curve (optional). + */ + public static final String ED25519 = "Ed25519"; + private JwsAlgorithms() { } diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/SignatureAlgorithm.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/SignatureAlgorithm.java index 764601892c2..ba124085a0e 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/SignatureAlgorithm.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/SignatureAlgorithm.java @@ -79,7 +79,22 @@ public enum SignatureAlgorithm implements JwsAlgorithm { /** * RSASSA-PSS using SHA-512 and MGF1 with SHA-512 (Optional) */ - PS512(JwsAlgorithms.PS512); + PS512(JwsAlgorithms.PS512), + + /** + * EdDSA signature algorithms (optional). + */ + EdDSA(JwsAlgorithms.EdDSA), + + /** + * EdDSA signature algorithms using Ed448 curve (optional). + */ + ED448(JwsAlgorithms.ED448), + + /** + * EdDSA signature algorithms using Ed25519 curve (optional). + */ + ED25519(JwsAlgorithms.ED25519); private final String name; diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWKS.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWKS.java index a8da54e17ca..a1d41129ac9 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWKS.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWKS.java @@ -32,6 +32,7 @@ import com.nimbusds.jose.jwk.ECKey; import com.nimbusds.jose.jwk.KeyOperation; import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.jwk.OctetKeyPair; import com.nimbusds.jose.jwk.OctetSequenceKey; import com.nimbusds.jose.jwk.RSAKey; @@ -84,4 +85,15 @@ static RSAKey.Builder signingWithRsa(RSAPublicKey pub, RSAPrivateKey key) throws .notBeforeTime(issued); } + static OctetKeyPair.Builder signingWithOkp(OctetKeyPair octetKeyPair) throws JOSEException { + Date issued = new Date(); + return new OctetKeyPair.Builder(octetKeyPair.getCurve(), octetKeyPair.getX()).d(octetKeyPair.getD()) + .keyOperations(Set.of(KeyOperation.SIGN)) + .keyUse(KeyUse.SIGNATURE) + .algorithm(JWSAlgorithm.EdDSA) + .keyIDFromThumbprint() + .issueTime(issued) + .notBeforeTime(issued); + } + } diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoder.java index 9f6ee0eba5e..32acbdaf0b4 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoder.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoder.java @@ -50,6 +50,7 @@ import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.KeyType; import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.jwk.OctetKeyPair; import com.nimbusds.jose.jwk.OctetSequenceKey; import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jose.jwk.source.ImmutableJWKSet; @@ -84,6 +85,7 @@ * @author Joe Grandja * @author Josh Cummings * @author Suraj Bhadrike + * @author Andrey Litvitski * @since 5.6 * @see JwtEncoder * @see com.nimbusds.jose.jwk.source.JWKSource @@ -250,6 +252,16 @@ else if (JWSAlgorithm.Family.HMAC_SHA.contains(jwsAlgorithm)) { .build(); // @formatter:on } + else if (JWSAlgorithm.Family.ED.contains(jwsAlgorithm)) { + // @formatter:off + return new JWKMatcher.Builder() + .keyType(KeyType.forAlgorithm(jwsAlgorithm)) + .keyID(headers.getKeyId()) + .keyUses(KeyUse.SIGNATURE, null) + .algorithms(jwsAlgorithm, null) + .build(); + // @formatter:on + } return null; } @@ -445,6 +457,16 @@ public static EcKeyPairJwtEncoderBuilder withKeyPair(ECPublicKey publicKey, ECPr return new EcKeyPairJwtEncoderBuilder(publicKey, privateKey); } + /** + * Creates a builder for constructing a {@link NimbusJwtEncoder} using the provided + * @param keyPair the {@link OctetKeyPair} to use for signing JWTs + * @return a {@link OctetKeyPairJwtEncoderBuilder} + * @since 7.0 + */ + public static OctetKeyPairJwtEncoderBuilder withKeyPair(OctetKeyPair keyPair) { + return new OctetKeyPairJwtEncoderBuilder(keyPair); + } + /** * Creates a builder for constructing a {@link NimbusJwtEncoder} using the provided * @param secretKey @@ -625,4 +647,46 @@ public NimbusJwtEncoder build() { } + /** + * A builder for creating {@link NimbusJwtEncoder} instances configured with a + * {@link OctetKeyPair}. + *

+ * This builder is used to create a {@link NimbusJwtEncoder} + * + * @since 7.0 + */ + public static final class OctetKeyPairJwtEncoderBuilder { + + private static final ThrowingFunction defaultJwk = JWKS::signingWithOkp; + + private final OctetKeyPair.Builder builder; + + private OctetKeyPairJwtEncoderBuilder(OctetKeyPair keyPair) { + Assert.notNull(keyPair, "keyPair cannot be null"); + Assert.isTrue(keyPair.isPrivate(), "keyPair must contain a private key"); + this.builder = defaultJwk.apply(keyPair); + } + + /** + * Post-process the {@link JWK} using the given {@link Consumer}. For example, you + * may use this to override the default {@code jwk} + * @param jwkPostProcessor the post-processor to use + * @return this builder instance for method chaining + */ + public OctetKeyPairJwtEncoderBuilder jwkPostProcessor(Consumer jwkPostProcessor) { + Assert.notNull(jwkPostProcessor, "jwkPostProcessor cannot be null"); + jwkPostProcessor.accept(this.builder); + return this; + } + + /** + * Builds the {@link NimbusJwtEncoder} instance. + * @return the configured {@link NimbusJwtEncoder} + */ + public NimbusJwtEncoder build() { + return new NimbusJwtEncoder(this.builder.build()); + } + + } + } diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/TestJwks.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/TestJwks.java index 2f75e4e8937..dc85a817c7a 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/TestJwks.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/TestJwks.java @@ -28,11 +28,13 @@ import com.nimbusds.jose.jwk.Curve; import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.OctetKeyPair; import com.nimbusds.jose.jwk.OctetSequenceKey; import com.nimbusds.jose.jwk.RSAKey; /** * @author Joe Grandja + * @author Andrey Litvitski */ public final class TestJwks { @@ -70,6 +72,13 @@ public final class TestJwks { ).build(); // @formatter:on + // @formatter:off + public static final OctetKeyPair DEFAULT_OKP_JWK = + jwk( + TestKeys.DEFAULT_OKP_KEY_PAIR + ).build(); + // @formatter:on + private TestJwks() { } @@ -111,4 +120,12 @@ public static OctetSequenceKey.Builder jwk(SecretKey secretKey) { // @formatter:on } + public static OctetKeyPair.Builder jwk(OctetKeyPair octetKeyPair) { + // @formatter:off + return new OctetKeyPair.Builder(octetKeyPair.getCurve(), octetKeyPair.getX()) + .d(octetKeyPair.getD()) + .keyID("okp-jwk-kid"); + // @formatter:on + } + } diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/TestKeys.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/TestKeys.java index ee8be9a696d..8402dbfdcda 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/TestKeys.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/TestKeys.java @@ -35,8 +35,14 @@ import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.jwk.Curve; +import com.nimbusds.jose.jwk.OctetKeyPair; +import com.nimbusds.jose.jwk.gen.OctetKeyPairGenerator; + /** * @author Joe Grandja + * @author Andrey Litvitski * @since 5.2 */ public final class TestKeys { @@ -120,6 +126,8 @@ public final class TestKeys { public static final KeyPair DEFAULT_EC_KEY_PAIR = generateEcKeyPair(); + public static final OctetKeyPair DEFAULT_OKP_KEY_PAIR = generateOkpKeyPair(); + static KeyPair generateEcKeyPair() { EllipticCurve ellipticCurve = new EllipticCurve( new ECFieldFp(new BigInteger( @@ -144,6 +152,15 @@ static KeyPair generateEcKeyPair() { return keyPair; } + static OctetKeyPair generateOkpKeyPair() { + try { + return new OctetKeyPairGenerator(Curve.Ed25519).generate(); + } + catch (JOSEException ex) { + throw new IllegalStateException(ex); + } + } + private TestKeys() { } diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/jws/SignatureAlgorithmTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/jws/SignatureAlgorithmTests.java index 14f8341c044..69065939e84 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/jws/SignatureAlgorithmTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/jws/SignatureAlgorithmTests.java @@ -24,6 +24,7 @@ * Tests for {@link SignatureAlgorithm} * * @author Joe Grandja + * @author Andrey Litvitski * @since 5.2 */ public class SignatureAlgorithmTests { @@ -39,6 +40,9 @@ public void fromWhenAlgorithmValidThenResolves() { assertThat(SignatureAlgorithm.from(JwsAlgorithms.PS256)).isEqualTo(SignatureAlgorithm.PS256); assertThat(SignatureAlgorithm.from(JwsAlgorithms.PS384)).isEqualTo(SignatureAlgorithm.PS384); assertThat(SignatureAlgorithm.from(JwsAlgorithms.PS512)).isEqualTo(SignatureAlgorithm.PS512); + assertThat(SignatureAlgorithm.from(JwsAlgorithms.EdDSA)).isEqualTo(SignatureAlgorithm.EdDSA); + assertThat(SignatureAlgorithm.from(JwsAlgorithms.ED448)).isEqualTo(SignatureAlgorithm.ED448); + assertThat(SignatureAlgorithm.from(JwsAlgorithms.ED25519)).isEqualTo(SignatureAlgorithm.ED25519); } @Test diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoderTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoderTests.java index 95e8a1e1c51..f44f979a5c1 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoderTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoderTests.java @@ -39,9 +39,11 @@ import com.nimbusds.jose.jwk.JWKSelector; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.jwk.OctetKeyPair; import com.nimbusds.jose.jwk.OctetSequenceKey; import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jose.jwk.gen.ECKeyGenerator; +import com.nimbusds.jose.jwk.gen.OctetKeyPairGenerator; import com.nimbusds.jose.jwk.gen.RSAKeyGenerator; import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.proc.SecurityContext; @@ -72,6 +74,7 @@ * Tests for {@link NimbusJwtEncoder}. * * @author Joe Grandja + * @author Andrey Litvitski */ public class NimbusJwtEncoderTests { @@ -109,6 +112,25 @@ public void encodeWhenClaimsNullThenThrowIllegalArgumentException() { .withMessage("claims cannot be null"); } + @Test + void keyPairBuilderWithOkpWhenPublicKeyOnlyThenThrowIllegalArgumentException() throws JOSEException { + OctetKeyPair key = new OctetKeyPairGenerator(Curve.Ed25519).generate(); + + assertThatIllegalArgumentException().isThrownBy(() -> NimbusJwtEncoder.withKeyPair(key.toPublicJWK())) + .withMessage("keyPair must contain a private key"); + } + + @Test + void encodeWhenEdDsaAlgorithmThenSuccess() throws JOSEException { + OctetKeyPair key = new OctetKeyPairGenerator(Curve.Ed25519).generate(); + this.jwkList.add(key); + JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.EdDSA).build(); + JwtClaimsSet claims = buildClaims(); + Jwt jwt = this.jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims)); + assertJwt(jwt); + assertThat(jwt.getHeaders()).containsEntry(JoseHeaderNames.ALG, SignatureAlgorithm.EdDSA); + } + @Test public void encodeWhenJwkSelectFailedThenThrowJwtEncodingException() throws Exception { this.jwkSource = mock(JWKSource.class); @@ -389,6 +411,17 @@ void keyPairBuilderWithSecretKeyDefaultAlgorithm() { assertThat(jwt.getHeaders()).containsKey(JoseHeaderNames.KID); } + @Test + void keyPairBuilderWithOkpDefaultAlgorithm() throws JOSEException { + OctetKeyPair key = new OctetKeyPairGenerator(Curve.Ed25519).generate(); + NimbusJwtEncoder jwtEncoder = NimbusJwtEncoder.withKeyPair(key).build(); + JwtClaimsSet claims = buildClaims(); + Jwt jwt = jwtEncoder.encode(JwtEncoderParameters.from(claims)); + assertJwt(jwt); + assertThat(jwt.getHeaders()).containsEntry(JoseHeaderNames.ALG, SignatureAlgorithm.EdDSA); + assertThat(jwt.getHeaders()).containsKey(JoseHeaderNames.KID); + } + // With custom algorithm @Test void keyPairBuilderWithRsaWithAlgorithm() throws JOSEException { @@ -490,6 +523,20 @@ void keyPairBuilderWithSecretKeyWithAlgorithmAndJwkSource() { assertThat(jwt.getHeaders()).containsEntry(JoseHeaderNames.KID, keyId); } + @Test + void keyPairBuilderWithOkpAlgorithmAndJwkSource() throws JOSEException { + OctetKeyPair key = new OctetKeyPairGenerator(Curve.Ed25519).generate(); + String keyId = UUID.randomUUID().toString(); + NimbusJwtEncoder jwtEncoder = NimbusJwtEncoder.withKeyPair(key) + .jwkPostProcessor((builder) -> builder.keyID(keyId)) + .build(); + JwtClaimsSet claims = buildClaims(); + Jwt jwt = jwtEncoder.encode(JwtEncoderParameters.from(claims)); + assertJwt(jwt); + assertThat(jwt.getHeaders()).containsEntry(JoseHeaderNames.ALG, SignatureAlgorithm.EdDSA); + assertThat(jwt.getHeaders()).containsEntry(JoseHeaderNames.KID, keyId); + } + private JwtClaimsSet buildClaims() { Instant now = Instant.now(); return JwtClaimsSet.builder() @@ -558,8 +605,11 @@ private void init() { OctetSequenceKey secretJwk = TestJwks.jwk(TestKeys.DEFAULT_SECRET_KEY) .keyID("secret-jwk-" + this.keyId++) .build(); + OctetKeyPair okpJwk = TestJwks.jwk(TestKeys.DEFAULT_OKP_KEY_PAIR) + .keyID("okp-jwk-" + this.keyId++) + .build(); // @formatter:on - this.jwkSet = new JWKSet(Arrays.asList(rsaJwk, ecJwk, secretJwk)); + this.jwkSet = new JWKSet(Arrays.asList(rsaJwk, ecJwk, secretJwk, okpJwk)); } private void rotate() {