Skip to content

Commit

Permalink
Merge pull request mosip#236 from Infosys/INJICERT-895
Browse files Browse the repository at this point in the history
[INJICERT-895]

Signed-off-by: Hitesh Jain <jainhitesh9998@gmail.com>
  • Loading branch information
vishwa-vyom authored and jainhitesh9998 committed Feb 21, 2025
1 parent 869c85b commit 6560da8
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,11 @@ public String getProofType() {

private static final Set<JWSAlgorithm> allowedSignatureAlgorithms;

private static Set<String> REQUIRED_CLAIMS;
private static final Set<String> DEFAULT_REQUIRED_CLAIMS = Set.of("aud", "iat");

static {
allowedSignatureAlgorithms = new HashSet<>();
allowedSignatureAlgorithms.addAll(List.of(JWSAlgorithm.Family.SIGNATURE.toArray(new JWSAlgorithm[0])));

REQUIRED_CLAIMS = new HashSet<>();
REQUIRED_CLAIMS.add("aud");
REQUIRED_CLAIMS.add("exp");
REQUIRED_CLAIMS.add("iss");
REQUIRED_CLAIMS.add("iat");
}

@Override
Expand All @@ -87,11 +81,22 @@ public boolean validate(String clientId, String cNonce, CredentialProof credenti
throw new InvalidRequestException(ErrorConstants.PROOF_HEADER_INVALID_KEY);
}

DefaultJWTClaimsVerifier claimsSetVerifier = new DefaultJWTClaimsVerifier(new JWTClaimsSet.Builder()
JWTClaimsSet.Builder proofJwtClaimsBuilder = new JWTClaimsSet.Builder()
.audience(credentialIdentifier)
.issuer(clientId)
.claim("nonce", cNonce)
.build(), REQUIRED_CLAIMS);
.claim("nonce", cNonce);

// if the proof contains issuer claim, then it should match with the client id ref: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-ID1.html#section-7.2.1.1-2.2.2.1
// https://github.com/openid/OpenID4VCI/issues/349
Set<String> requiredClaims = new HashSet<>(DEFAULT_REQUIRED_CLAIMS);
if(jwt.getJWTClaimsSet().getClaim("iss") != null) {
proofJwtClaimsBuilder.issuer(clientId);
}
if(jwt.getJWTClaimsSet().getClaim("exp") != null) {
requiredClaims.add("exp");
}

DefaultJWTClaimsVerifier claimsSetVerifier = new DefaultJWTClaimsVerifier(proofJwtClaimsBuilder.build(), requiredClaims);

claimsSetVerifier.setMaxClockSkew(0);
JWSKeySelector keySelector;
if(JWSAlgorithm.ES256K.equals(jwt.getHeader().getAlgorithm())) {
Expand Down Expand Up @@ -164,17 +169,21 @@ private JWK getKeyFromHeader(JWSHeader jwsHeader) {

/**
* Currently only handles did:jwk, Need to handle other methods
* @param keyId
* @param did kid of jwk in didLjwk format. ref: https://github.com/quartzjer/did-jwk/blob/main/spec.md#to-create-the-did-url
* @return
*/
private JWK resolveDID(String did) {
if(did.startsWith(DID_JWK_PREFIX)) {
try {
//Ignoring fragment part as did:jwk only contains single key, the DID URL fragment identifier is always
//a fixed #0 value. If the JWK contains a kid value it is not used as the reference, #0 is the only valid value.
did = did.split("#")[0];
byte[] jwkBytes = Base64.getUrlDecoder().decode(did.substring(DID_JWK_PREFIX.length()));
org.json.JSONObject jsonKey = new org.json.JSONObject(new String(jwkBytes));
String base64JWK = did.split("#")[0].substring(DID_JWK_PREFIX.length());
// Decode JWK from Base64
byte[] jwkBytes = Base64.getUrlDecoder().decode(base64JWK);
String jwkJson = new String(jwkBytes, StandardCharsets.UTF_8);

// Parse JWK
org.json.JSONObject jsonKey = new org.json.JSONObject(jwkJson);
jsonKey.put("kid", did);
return JWK.parse(jsonKey.toString());
} catch (IllegalArgumentException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import io.mosip.certify.core.exception.InvalidRequestException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.util.*;
Expand All @@ -25,6 +26,7 @@
import static org.junit.Assert.*;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
import static org.mockito.Mockito.*;

class JwtProofValidatorTest {
Expand Down Expand Up @@ -143,6 +145,109 @@ private String createValidJWT() throws Exception {
return jwt.serialize();
}

private String createValidJWTWithDid(String issuer, Long expiryMillis, Boolean validDid ) throws Exception {
// Generate a 2048-bit RSA key pair
RSAKey rsaJWK = new RSAKeyGenerator(2048)
.keyID(UUID.randomUUID().toString())
.generate();

// Extract public key
RSAKey rsaPublicJWK = rsaJWK.toPublicJWK();

String didJWK;

if(validDid)
// Construct the did:jwk identifier
didJWK = "did:jwk:" + Base64.getUrlEncoder().withoutPadding().encodeToString(rsaPublicJWK.toJSONString().getBytes()) + "#0";
else
didJWK = "did:jwk:invalid";

JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256)
.type(new JOSEObjectType("openid4vci-proof+jwt"))
.keyID(didJWK) // Set kid as did:jwk
.build();

// Build JWT claims
JWTClaimsSet.Builder claimsBuilder = new JWTClaimsSet.Builder()
.audience("test-credential-id")
.claim("nonce", "test-nonce")
.issueTime(new Date());

if (issuer != null) {
claimsBuilder.issuer(issuer);
}

if (expiryMillis != null) {
claimsBuilder.expirationTime(new Date(System.currentTimeMillis() + expiryMillis));
}

SignedJWT jwt = new SignedJWT(header, claimsBuilder.build());

// Sign JWT using private key
JWSSigner signer = new RSASSASigner(rsaJWK);
jwt.sign(signer);

return jwt.serialize();
}

@Test
public void testValidate_DIDJWK_ValidJWT_WithClientID_and_Expiry() throws Exception {
String jwt = createValidJWTWithDid("test-client", 60000L, true);
CredentialProof credentialProof = new CredentialProof();
credentialProof.setJwt(jwt);

boolean result = jwtProofValidator.validate("test-client", "test-nonce", credentialProof);

assertTrue(result, "JWT should be valid");
}

@Test
public void testValidate_DIDJWK_ValidJWT_NoClientID_and_Expiry() throws Exception {
String jwt = createValidJWTWithDid(null, 60000L, true);
CredentialProof credentialProof = new CredentialProof();
credentialProof.setJwt(jwt);

boolean result = jwtProofValidator.validate("test-client", "test-nonce", credentialProof);

assertTrue(result, "JWT should be valid");
}

@Test
public void testValidate_DIDJWK_ValidJWT_NoClientID_and_No_Expiry() throws Exception {
String jwt = createValidJWTWithDid(null, null, true);
CredentialProof credentialProof = new CredentialProof();
credentialProof.setJwt(jwt);

boolean result = jwtProofValidator.validate("test-client", "test-nonce", credentialProof);

assertTrue(result, "JWT should be valid");
}
@Test
public void testValidate_DIDJWK_ValidJWT_WrongClientID() throws Exception {
String jwt = createValidJWTWithDid("client-id-1", 600000L, true);
CredentialProof credentialProof = new CredentialProof();
credentialProof.setJwt(jwt);

boolean result = jwtProofValidator.validate("test-client", "test-nonce", credentialProof);

assertFalse(result, "Client id should match");
}

@Test
public void testValidate_InvalidDID_JWK() {
String jwt = null;
try {
jwt = createValidJWTWithDid("test-client", 60000L, false);
} catch (Exception e) {
//do nothing here
}
CredentialProof credentialProof = new CredentialProof();
credentialProof.setJwt(jwt);
//exception are handled by the validator logic presently
assertFalse( jwtProofValidator.validate("test-client", "test-nonce", credentialProof));

}

@Test
public void testValidate_InvalidJwt_MissingClaims() throws ParseException, JOSEException {
RSAKey rsaJWK = new RSAKeyGenerator(2048)
Expand Down

0 comments on commit 6560da8

Please sign in to comment.