Skip to content

Add ECDSA support and fixed DID resolution for hosted public keys #23

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 16, 2025
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 @@ -4,13 +4,16 @@

import java.math.BigInteger;
import java.net.URI;
import java.security.AlgorithmParameters;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.RSAPublicKeySpec;
import java.security.spec.*;
import java.util.Base64;
import java.util.Base64.Decoder;
import java.util.List;
import java.util.Set;

import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
Expand Down Expand Up @@ -75,9 +78,12 @@ private void verifySignature(VerifiableCredential crd, RunContext ctx) throws Ex
ObjectMapper mapper = ((ObjectMapper)ctx.get(RunContext.Key.JACKSON_OBJECTMAPPER));
JsonNode headerObj = mapper.readTree(joseHeader);

//MUST be "RS256"
//MUST be "RS256 or "ES256"
JsonNode alg = headerObj.get("alg");
if(alg == null || !alg.textValue().equals("RS256")) { throw new Exception("alg must be present and must be 'RS256'"); }
Set<String> allowedAlgs = Set.of("RS256", "ES256");
if (alg == null || !allowedAlgs.contains(alg.textValue())) {
throw new Exception("alg must be present and must be either 'RS256' or 'ES256'");
}

// decoded jwt will check timestamps, but shall we explicitly break these out?
// JWT verifier throws and exception with the cause when claims are invalid. Adding that cause
Expand All @@ -100,18 +106,48 @@ private void verifySignature(VerifiableCredential crd, RunContext ctx) throws Ex
jwk = mapper.readTree(jwkResponse);
}

//Clean up may be required. Currently need to cleanse extra double quoting.
String modulusString = jwk.get("n").textValue();
String exponentString = jwk.get("e").textValue();
String kty = jwk.get("kty").asText();

Algorithm algorithm; // Either RSA or ECDSA

if ("RSA".equalsIgnoreCase(kty)) {
// RSA Public Key
String modulusString = jwk.get("n").asText();
String exponentString = jwk.get("e").asText();

BigInteger modulus = new BigInteger(1, decoder.decode(modulusString));
BigInteger exponent = new BigInteger(1, decoder.decode(exponentString));

RSAPublicKeySpec pubSpec = new RSAPublicKeySpec(modulus, exponent);
KeyFactory factory = KeyFactory.getInstance("RSA");
RSAPublicKey pub = (RSAPublicKey) factory.generatePublic(pubSpec);

algorithm = Algorithm.RSA256(pub, null);

} else if ("EC".equalsIgnoreCase(kty)) {
// ECDSA Public Key
String xString = jwk.get("x").asText();
String yString = jwk.get("y").asText();
String crv = jwk.get("crv").asText(); // Should be P-256

BigInteger modulus = new BigInteger(1, decoder.decode(modulusString));
BigInteger exponent = new BigInteger(1, decoder.decode(exponentString));
ECParameterSpec ecSpec = getCurveFromCrv(crv); // helper function below

PublicKey pub = KeyFactory.getInstance("RSA").generatePublic(new RSAPublicKeySpec(modulus, exponent));
ECPoint ecPoint = new ECPoint(
new BigInteger(1, decoder.decode(xString)),
new BigInteger(1, decoder.decode(yString))
);

ECPublicKeySpec pubSpec = new ECPublicKeySpec(ecPoint, ecSpec);
KeyFactory factory = KeyFactory.getInstance("EC");
ECPublicKey pub = (ECPublicKey) factory.generatePublic(pubSpec);

algorithm = Algorithm.ECDSA256(pub, null);
} else {
throw new IllegalArgumentException("Unsupported key type: " + kty);
}

JWTVerifier verifier = JWT.require(algorithm).build();

Algorithm algorithm = Algorithm.RSA256((RSAPublicKey)pub, null);
JWTVerifier verifier = JWT.require(algorithm)
.build(); //Reusable verifier instance
try {
decodedJwt = verifier.verify(jwt);
}
Expand All @@ -136,7 +172,7 @@ private String fetchJwk(String fetchUrl, RunContext ctx){
URI kidUri = new URI(fetchUrl);
if (kidUri.getScheme() == null || kidUri.getScheme().equals("did")) {
DidResolver didResolver = ctx.get(RunContextKey.DID_RESOLVER);
DidResolution didResolution = didResolver.resolve(kidUri, CachingDocumentLoader.DOCUMENT_LOADER);
DidResolution didResolution = didResolver.resolve(kidUri, new CachingDocumentLoader()); // Not using the default document loader options
responseString = didResolution.getPublicKeyJwk();
} else {
CloseableHttpClient client = HttpClients.createDefault();
Expand All @@ -159,6 +195,16 @@ private String fetchJwk(String fetchUrl, RunContext ctx){
return responseString;
}

// Maps curve name from JWK to ECParameterSpec
private static ECParameterSpec getCurveFromCrv(String crv) throws Exception {
if ("P-256".equals(crv)) {
AlgorithmParameters parameters = AlgorithmParameters.getInstance("EC");
parameters.init(new ECGenParameterSpec("secp256r1"));
return parameters.getParameterSpec(ECParameterSpec.class);
}
throw new IllegalArgumentException("Unsupported curve: " + crv);
}

public static final String ID = ExternalProofProbe.class.getSimpleName();

}
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ public DidResolution resolve(URI did, DocumentLoader documentLoader)
}

// 4. If no path has been specified in the URL, append /.well-known.
if (uri.getPath() == null) {
uri = uri.resolve("/well-known");
if (uri.getPath() == null || uri.getPath().isEmpty()) {
uri = uri.resolve("/.well-known");
}

// 5. Append /did.json to complete the URL.
Expand Down Expand Up @@ -175,11 +175,13 @@ private void extractFromVerificationMethod(URI did, JsonObject didDocument, Buil
JsonObject verificationMethod = verificationMethodMaybe.get().asJsonObject();
// assuming a Ed25519VerificationKey2020 document
builder
.controller(verificationMethod.getString("controller"))
.publicKeyMultibase(verificationMethod.getString("publicKeyMultibase"));
.controller(verificationMethod.getString("controller"));
// check JWK
if (verificationMethod.containsKey("publicKeyJwk"))
builder.publicKeyJwk(verificationMethod.getJsonObject("publicKeyJwk").toString());
builder.publicKeyJwk(verificationMethod.getJsonObject("publicKeyJwk").toString());
// check Multibase
if (verificationMethod.containsKey("publicKeyMultibase"))
builder.publicKeyMultibase(verificationMethod.getString("publicKeyMultibase"));

}
}