diff --git a/opa-builtins/opa-builtins-token/src/main/java/io/github/open_policy_agent/opa/ast/builtin/impls/TokenBuiltins.java b/opa-builtins/opa-builtins-token/src/main/java/io/github/open_policy_agent/opa/ast/builtin/impls/TokenBuiltins.java index f7721bdd..b82113ea 100644 --- a/opa-builtins/opa-builtins-token/src/main/java/io/github/open_policy_agent/opa/ast/builtin/impls/TokenBuiltins.java +++ b/opa-builtins/opa-builtins-token/src/main/java/io/github/open_policy_agent/opa/ast/builtin/impls/TokenBuiltins.java @@ -16,9 +16,11 @@ import com.nimbusds.jose.crypto.MACVerifier; import com.nimbusds.jose.crypto.RSASSASigner; import com.nimbusds.jose.crypto.RSASSAVerifier; +import com.nimbusds.jose.jwk.Curve; import com.nimbusds.jose.jwk.ECKey; import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.OctetKeyPair; import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; @@ -30,11 +32,13 @@ import io.github.open_policy_agent.opa.ast.types.RegoValue; import java.io.IOException; import java.io.StringReader; +import java.security.KeyFactory; import java.security.PrivateKey; import java.security.PublicKey; import java.security.Security; import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAPublicKey; +import java.security.spec.X509EncodedKeySpec; import java.text.ParseException; import java.util.List; import java.util.Map; @@ -90,6 +94,7 @@ public Map> builti Map.entry("io.jwt.verify_es256", instance::verifyES256), Map.entry("io.jwt.verify_es384", instance::verifyES384), Map.entry("io.jwt.verify_es512", instance::verifyES512), + Map.entry("io.jwt.verify_eddsa", instance::verifyEdDSA), Map.entry("io.jwt.encode_sign", instance::encodeSign), Map.entry("io.jwt.encode_sign_raw", instance::encodeSignRaw)); } @@ -296,6 +301,28 @@ private static PublicKey extractPublicKey(JWK jwk) throws JOSEException { return ((RSAKey) jwk).toPublicKey(); } else if (jwk instanceof ECKey) { return ((ECKey) jwk).toPublicKey(); + } else if (jwk instanceof OctetKeyPair) { + // Nimbus OctetKeyPair.toPublicKey() is unsupported in this library version; + // build a JCA Ed25519 PublicKey from the raw OKP "x" coordinate via SPKI DER. + OctetKeyPair okp = (OctetKeyPair) jwk; + if (!Curve.Ed25519.equals(okp.getCurve())) { + throw new BuiltinError("Unsupported JWK key type: " + jwk.getKeyType()); + } + byte[] raw = okp.getDecodedX(); + // Ed25519 SubjectPublicKeyInfo prefix (12 bytes) + 32-byte public key = 44 bytes total + byte[] spkiPrefix = + new byte[] { + 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00 + }; + byte[] encoded = new byte[spkiPrefix.length + raw.length]; + System.arraycopy(spkiPrefix, 0, encoded, 0, spkiPrefix.length); + System.arraycopy(raw, 0, encoded, spkiPrefix.length, raw.length); + try { + return KeyFactory.getInstance("Ed25519", "BC") + .generatePublic(new X509EncodedKeySpec(encoded)); + } catch (java.security.GeneralSecurityException e) { + throw new BuiltinError("failed to convert Ed25519 JWK to PublicKey: " + e.getMessage()); + } } else { throw new BuiltinError("Unsupported JWK key type: " + jwk.getKeyType()); } @@ -870,6 +897,31 @@ public RegoBoolean verifyES512(EvaluationContext ctx, RegoValue[] args) { return _verifyECDSA(jwt.getValue(), cert.getValue(), ctx.isStrictBuiltinErrors(), "ES512"); } + @OpaBuiltin( + name = "io.jwt.verify_eddsa", + description = "Verifies if a EdDSA JWT signature is valid.", + args = { + @OpaType( + type = "string", + name = "jwt", + description = "JWT token whose signature is to be verified"), + @OpaType( + type = "string", + name = "certificate", + description = + "PEM encoded certificate, PEM encoded public key, or the JWK key (set) used to verify the signature") + }, + result = + @OpaType( + type = "boolean", + name = "result", + description = "`true` if the signature is valid, `false` otherwise")) + public RegoBoolean verifyEdDSA(EvaluationContext ctx, RegoValue[] args) { + RegoString jwt = getArg(args, 0, RegoString.class); + RegoString cert = getArg(args, 1, RegoString.class); + return _verifyEdDSA(jwt.getValue(), cert.getValue(), ctx.isStrictBuiltinErrors()); + } + @OpaBuiltin( name = "io.jwt.verify_rs256", description = "Verifies if a RS256 JWT signature is valid.", diff --git a/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/ast/builtin/impls/TokenBuiltinsEd25519JwkTest.java b/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/ast/builtin/impls/TokenBuiltinsEd25519JwkTest.java new file mode 100644 index 00000000..92dc3073 --- /dev/null +++ b/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/ast/builtin/impls/TokenBuiltinsEd25519JwkTest.java @@ -0,0 +1,73 @@ +package io.github.open_policy_agent.opa.ast.builtin.impls; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +import io.github.open_policy_agent.opa.ast.types.RegoBoolean; +import io.github.open_policy_agent.opa.ast.types.RegoString; +import io.github.open_policy_agent.opa.ast.types.RegoValue; +import io.github.open_policy_agent.opa.rego.EvaluationContext; + +import org.junit.jupiter.api.Test; + +/** + * Covers the JWK (OKP / Ed25519) path for {@code io.jwt.verify_eddsa}, which is not exercised by + * compliance fixtures (those only use PEM public keys for verify_eddsa). + */ +public class TokenBuiltinsEd25519JwkTest { + + // From jwtencodesign/test-jwtencodesign-eddsa.json + private static final String PRIVATE_JWK = + "{\"kty\":\"OKP\",\"alg\":\"EdDSA\",\"crv\":\"Ed25519\"," + + "\"x\":\"wEZFfoAj1rFKTLOOmjJjVZlCHwksuvMb2I5y_hg70E8\"," + + "\"d\":\"9XI34uQzYUJfWhDZf_0nYsLMBRVu8a6dFsy60P8uugk\"}"; + + private static final String PUBLIC_JWK = + "{\"kty\":\"OKP\",\"crv\":\"Ed25519\"," + + "\"x\":\"wEZFfoAj1rFKTLOOmjJjVZlCHwksuvMb2I5y_hg70E8\"}"; + + // Different Ed25519 public key (same length, wrong material) + private static final String WRONG_PUBLIC_JWK = + "{\"kty\":\"OKP\",\"crv\":\"Ed25519\"," + + "\"x\":\"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo\"}"; + + private static final String HEADERS = "{\"alg\":\"EdDSA\",\"typ\":\"JWT\"}"; + private static final String PAYLOAD = + "{\"iss\":\"joe\",\"exp\":1300819380,\"http://example.com/is_root\":true}"; + + private final TokenBuiltins builtins = new TokenBuiltins(); + private final EvaluationContext ctx = new EvaluationContext.Builder().build(); + + @Test + public void verifyEddsaWithMatchingPublicJwkReturnsTrue() { + RegoString jwt = + builtins.encodeSignRaw( + ctx, + new RegoValue[] { + new RegoString(HEADERS), new RegoString(PAYLOAD), new RegoString(PRIVATE_JWK) + }); + + RegoBoolean result = + builtins.verifyEdDSA( + ctx, new RegoValue[] {jwt, new RegoString(PUBLIC_JWK)}); + + assertEquals(RegoBoolean.TRUE, result); + } + + @Test + public void verifyEddsaWithMismatchedPublicJwkReturnsFalse() { + RegoString jwt = + builtins.encodeSignRaw( + ctx, + new RegoValue[] { + new RegoString(HEADERS), new RegoString(PAYLOAD), new RegoString(PRIVATE_JWK) + }); + + RegoBoolean result = + builtins.verifyEdDSA( + ctx, new RegoValue[] {jwt, new RegoString(WRONG_PUBLIC_JWK)}); + + assertInstanceOf(RegoBoolean.class, result); + assertEquals(RegoBoolean.FALSE, result); + } +} diff --git a/opa-evaluator/src/test/resources/compliance/known-missing-builtins.txt b/opa-evaluator/src/test/resources/compliance/known-missing-builtins.txt index e3629446..7979170a 100644 --- a/opa-evaluator/src/test/resources/compliance/known-missing-builtins.txt +++ b/opa-evaluator/src/test/resources/compliance/known-missing-builtins.txt @@ -54,8 +54,6 @@ http.send internal.template_string -io.jwt.verify_eddsa - rego.parse_module strings.render_template