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
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -90,6 +94,7 @@ public Map<String, BiFunction<EvaluationContext, RegoValue[], RegoValue>> 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));
}
Expand Down Expand Up @@ -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());
}
Expand Down Expand Up @@ -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")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_verifyEdDSA reaches getPublicKeyextractPublicKey, which only handles RSAKey and ECKey. An Ed25519 JWK parses to an OctetKeyPair (kty: OKP), so it falls into the else branch and throws "Unsupported JWK key type" — meaning this returns false (or errors in strict mode) for a valid Ed25519 JWK, even though the arg doc here advertises "the JWK key (set)".

This diverges from OPA: its getKeysFromCertOrJWK (v1/topdown/tokens.go) parses JWKs via jwx's jwk.Export, which supports OKP keys, so OPA accepts an Ed25519 JWK for io.jwt.verify_eddsa. For parity, consider adding an OKP branch to extractPublicKey (convert OctetKeyPair → Ed25519 PublicKey) rather than only supporting the PEM path. The added test only covers the PEM cert fixture, so the JWK path is untested for EdDSA.

},
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.",
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,6 @@ http.send

internal.template_string

io.jwt.verify_eddsa

rego.parse_module

strings.render_template
Expand Down
Loading