From 5391236571a7a27b133ba4135f06b7e115bea7b6 Mon Sep 17 00:00:00 2001 From: Muhammad Umer Hammad Date: Mon, 13 Jul 2026 00:39:42 -0400 Subject: [PATCH 1/3] Implement io.jwt.verify_eddsa builtin (closes #145) Signed-off-by: Muhammad Umer Hammad --- .../opa/ast/builtin/impls/TokenBuiltins.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) 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 3a5a0f28..95b142ac 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 @@ -69,6 +69,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)); } @@ -849,6 +850,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.", From 4fb00ae4f4f202aaaafb3276ed1177ab1a470010 Mon Sep 17 00:00:00 2001 From: Muhammad Umer Hammad Date: Tue, 21 Jul 2026 20:51:10 -0400 Subject: [PATCH 2/3] Address review: support Ed25519 (OKP) JWKs in extractPublicKey Signed-off-by: Muhammad Umer Hammad --- .../opa/ast/builtin/impls/TokenBuiltins.java | 24 ++++++ .../impls/TokenBuiltinsEd25519JwkTest.java | 73 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 opa-evaluator/src/test/java/io/github/open_policy_agent/opa/ast/builtin/impls/TokenBuiltinsEd25519JwkTest.java 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 95b142ac..978b0d03 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 @@ -8,11 +8,13 @@ import com.nimbusds.jwt.*; 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; @@ -276,6 +278,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()); } 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); + } +} From 6d55983ec4129e5018082f49aaeba901d52980ea Mon Sep 17 00:00:00 2001 From: Sebastian Spaink Date: Tue, 11 Aug 2026 10:35:06 -0500 Subject: [PATCH 3/3] Fixup into 4fb00ae: import Curve/OctetKeyPair, drop ratchet entry The OKP branch added in 4fb00ae compiled on this branch's base, where TokenBuiltins used a wildcard `com.nimbusds.jose.jwk.*` import. Main has since expanded that wildcard (c75bba7, #171) into explicit imports that do not cover Curve or OctetKeyPair, so the merge builds cleanly in git but fails javac. Add the two imports. The compliance ratchet added on main also needs io.jwt.verify_eddsa removed from known-missing-builtins.txt, since the builtin now resolves and the suite fails on stale entries. Squash before merge. Signed-off-by: Sebastian Spaink --- .../open_policy_agent/opa/ast/builtin/impls/TokenBuiltins.java | 2 ++ .../src/test/resources/compliance/known-missing-builtins.txt | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) 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 55cfc748..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; 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