Skip to content
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

Removed all keycloak dependencies, replaced with nimbus jose #3

Merged
merged 2 commits into from
May 6, 2024
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
9 changes: 4 additions & 5 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,12 @@
<artifactId>jackson-databind</artifactId>
<version>2.16.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.keycloak/keycloak-core -->
<!-- https://mvnrepository.com/artifact/com.nimbusds/nimbus-jose-jwt -->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>24.0.2</version>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.38-rc4</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
Expand Down
7 changes: 2 additions & 5 deletions src/main/java/com/adorsys/ssi/sdjwt/DecoyEntry.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@

import java.util.Objects;

import org.keycloak.jose.jws.crypto.HashUtils;

/**
* Handles hash production for a decoy entry from the given salt.
*
*
* @author <a href="mailto:[email protected]">Francis Pouatcha</a>
*
*/
public abstract class DecoyEntry {
private final SdJwtSalt salt;
Expand All @@ -23,6 +20,6 @@ public SdJwtSalt getSalt() {
}

public String getDisclosureDigest(String hashAlg) {
return SdJwtUtils.encodeNoPad(HashUtils.hash(hashAlg, salt.toString().getBytes()));
return SdJwtUtils.encodeNoPad(SdJwtUtils.hash(salt.toString().getBytes(), hashAlg));
}
}
13 changes: 5 additions & 8 deletions src/main/java/com/adorsys/ssi/sdjwt/Disclosable.java
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@

package com.adorsys.ssi.sdjwt;

import java.util.Objects;

import org.keycloak.jose.jws.crypto.HashUtils;

import com.fasterxml.jackson.core.JsonProcessingException;

import java.util.Objects;

/**
* Handles undisclosed claims and array elements, providing functionality
* to generate disclosure digests from Base64Url encoded strings.
*
* <p>
* Hiding claims and array elements occurs by including their digests
* instead of plaintext in the signed verifiable credential.
*
*
* @author <a href="mailto:[email protected]">Francis Pouatcha</a>
*
*/
public abstract class Disclosable {
private final SdJwtSalt salt;
Expand Down Expand Up @@ -52,7 +49,7 @@ public String getDisclosureString() {
}

public String getDisclosureDigest(String hashAlg) {
return SdJwtUtils.encodeNoPad(HashUtils.hash(hashAlg, getDisclosureString().getBytes()));
return SdJwtUtils.encodeNoPad(SdJwtUtils.hash(getDisclosureString().getBytes(), hashAlg));
}

@Override
Expand Down
52 changes: 30 additions & 22 deletions src/main/java/com/adorsys/ssi/sdjwt/IssuerSignedJWT.java
Original file line number Diff line number Diff line change
@@ -1,36 +1,39 @@

package com.adorsys.ssi.sdjwt;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSObject;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.util.Base64URL;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.jose.jws.JWSInput;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

/**
* Handle verifiable credentials (SD-JWT VC), enabling the parsing
* of existing VCs as well as the creation and signing of new ones.
* It integrates with Keycloak's SignatureSignerContext to facilitate
* It integrates with JOSE's JWSSigner to facilitate
* the generation of issuer signature.
*
* @author <a href="mailto:[email protected]">Francis Pouatcha</a>
*/
public class IssuerSignedJWT extends SdJws {

public static IssuerSignedJWT fromJws(String jwsString) {
return new IssuerSignedJWT(jwsString);
public IssuerSignedJWT(JsonNode payload, JWSSigner signer, String keyId, JWSAlgorithm jwsAlgorithm, String jwsType) {
super(payload, signer, keyId, jwsAlgorithm, jwsType);
}
public IssuerSignedJWT(Base64URL payloadBase64URL, JWSSigner signer, String keyId, JWSAlgorithm jwsAlgorithm, String jwsType) {
super(payloadBase64URL, signer, keyId, jwsAlgorithm, jwsType);
}

public IssuerSignedJWT toSignedJWT(SignatureSignerContext signer, String jwsType) {
JWSInput jwsInput = sign(getPayload(), signer, jwsType);
return new IssuerSignedJWT(getPayload(), jwsInput);
public static IssuerSignedJWT fromJws(String jwsString) {
return new IssuerSignedJWT(jwsString);
}

private IssuerSignedJWT(String jwsString) {
Expand All @@ -42,13 +45,13 @@ private IssuerSignedJWT(List<SdJwtClaim> claims, List<DecoyClaim> decoyClaims, S
super(generatePayloadString(claims, decoyClaims, hashAlg, nestedDisclosures));
}

private IssuerSignedJWT(JsonNode payload, JWSInput jwsInput) {
private IssuerSignedJWT(JsonNode payload, JWSObject jwsInput) {
super(payload, jwsInput);
}

private IssuerSignedJWT(List<SdJwtClaim> claims, List<DecoyClaim> decoyClaims, String hashAlg,
boolean nestedDisclosures, SignatureSignerContext signer, String jwsType) {
super(generatePayloadString(claims, decoyClaims, hashAlg, nestedDisclosures), signer, jwsType);
boolean nestedDisclosures, JWSSigner signer, String keyId, JWSAlgorithm jwsAlgorithm, String jwsType) {
super(generatePayloadString(claims, decoyClaims, hashAlg, nestedDisclosures), signer, keyId, jwsAlgorithm, jwsType);
}

/*
Expand All @@ -63,7 +66,6 @@ private static JsonNode generatePayloadString(List<SdJwtClaim> claims, List<Deco
: Collections.unmodifiableList(claims);
final List<DecoyClaim> decoyClaimsInternal = decoyClaims == null ? Collections.emptyList()
: Collections.unmodifiableList(decoyClaims);

try {
// Check no dupplicate claim names
claimsInternal.stream()
Expand Down Expand Up @@ -96,11 +98,11 @@ private static JsonNode generatePayloadString(List<SdJwtClaim> claims, List<Deco

ObjectNode payload = SdJwtUtils.mapper.createObjectNode();

if (sdArray.size() > 0) {
if (!sdArray.isEmpty()) {
// drop _sd claim if empty
payload.set(CLAIM_NAME_SELECTIVE_DISCLOSURE, sdArray);
}
if (sdArray.size() > 0 || nestedDisclosures) {
if (!sdArray.isEmpty() || nestedDisclosures) {
// add sd alg only if ay disclosure.
payload.put(CLAIM_NAME_SD_HASH_ALGORITHM, hashAlg);
}
Expand Down Expand Up @@ -131,7 +133,8 @@ public static Builder builder() {
public static class Builder {
private List<SdJwtClaim> claims;
private String hashAlg;
private SignatureSignerContext signer;
private JWSSigner signer;
private String keyId;
private List<DecoyClaim> decoyClaims;
private boolean nestedDisclosures;
private String jwsType;
Expand All @@ -151,11 +154,16 @@ public Builder withHashAlg(String hashAlg) {
return this;
}

public Builder withSigner(SignatureSignerContext signer) {
public Builder withSigner(JWSSigner signer) {
this.signer = signer;
return this;
}

public Builder withKeyId(String keyId){
this.keyId = keyId;
return this;
}

public Builder withNestedDisclosures(boolean nestedDisclosures) {
this.nestedDisclosures = nestedDisclosures;
return this;
Expand All @@ -174,7 +182,7 @@ public IssuerSignedJWT build() {
claims = claims == null ? Collections.emptyList() : claims;
decoyClaims = decoyClaims == null ? Collections.emptyList() : decoyClaims;
if (signer != null) {
return new IssuerSignedJWT(claims, decoyClaims, hashAlg, nestedDisclosures, signer, jwsType);
return new IssuerSignedJWT(claims, decoyClaims, hashAlg, nestedDisclosures, signer, keyId, JWSAlgorithm.ES256, jwsType);
} else {
return new IssuerSignedJWT(claims, decoyClaims, hashAlg, nestedDisclosures);
}
Expand Down
119 changes: 59 additions & 60 deletions src/main/java/com/adorsys/ssi/sdjwt/SdJws.java
Original file line number Diff line number Diff line change
@@ -1,122 +1,121 @@

package com.adorsys.ssi.sdjwt;

import com.fasterxml.jackson.databind.JsonNode;
import com.nimbusds.jose.*;
import com.nimbusds.jose.util.Base64URL;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.time.Instant;
import java.util.Objects;

import org.keycloak.common.VerificationException;
import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.crypto.SignatureVerifierContext;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;

import com.fasterxml.jackson.databind.JsonNode;

/**
* Handle jws, either the issuer jwt or the holder key binding jwt.
*
*
* @author <a href="mailto:[email protected]">Francis Pouatcha</a>
*
*/
public class SdJws {
private final JWSInput jwsInput;
public abstract class SdJws {
private final JWSObject signedJwt;

private final String jwsString;
private final JsonNode payload;

public String toJws() {
if (jwsInput == null) {
if (jwsString == null) {
throw new IllegalStateException("JWS not yet signed");
}
return jwsInput.getWireString();
return jwsString;
}

public JsonNode getPayload() {
return payload;
}

public JWSInput getJwsInput() {
return jwsInput;
}

public String getJwsString() {
return jwsInput.getWireString();
}

// Constructor for unsigned JWS
protected SdJws(JsonNode payload) {
this.payload = payload;
this.jwsInput = null;
this.signedJwt = null;
this.jwsString = null;
}

// Constructor from jws string with all parts
protected SdJws(String jwsString) {
this.jwsInput = parse(jwsString);
this.payload = readPayload(jwsInput);
try {
this.jwsString = jwsString;
this.signedJwt = parse(jwsString);
this.payload = readPayload(signedJwt);
} catch (ParseException | IOException e) {
throw new RuntimeException(e);
}
}

// Constructor for signed JWS
protected SdJws(JsonNode payload, JWSInput jwsInput) {
protected SdJws(JsonNode payload, JWSObject signedJwt) {
this.payload = payload;
this.jwsInput = jwsInput;
this.signedJwt = signedJwt;
this.jwsString = signedJwt.serialize();
}

protected SdJws(JsonNode payload, SignatureSignerContext signer, String jwsType) {
protected SdJws(JsonNode payload, JWSSigner signer, String keyId, JWSAlgorithm jwsAlgorithm, String jwsType) {
this.payload = payload;
this.jwsInput = sign(payload, signer, jwsType);
JWSHeader header = new JWSHeader.Builder(jwsAlgorithm).type(new JOSEObjectType(jwsType)).keyID(keyId).build();
this.signedJwt = new JWSObject(header, new Payload(Base64URL.encode(payload.toString())));
try {
this.signedJwt.sign(signer);
} catch (JOSEException e) {
throw new RuntimeException(e);
}
this.jwsString= signedJwt.serialize();
}

protected static JWSInput sign(JsonNode payload, SignatureSignerContext signer, String jwsType) {
String jwsString = new JWSBuilder().type(jwsType).jsonContent(payload).sign(signer);
return parse(jwsString);
protected SdJws(Base64URL payloadBase64Url, JWSSigner signer, String keyId, JWSAlgorithm jwsAlgorithm, String jwsType) {
try {
this.payload = SdJwtUtils.mapper.readTree(payloadBase64Url.decode());
} catch (IOException e) {
throw new RuntimeException(e);
}
JWSHeader header = new JWSHeader.Builder(jwsAlgorithm).type(new JOSEObjectType(jwsType)).keyID(keyId).build();
this.signedJwt = new JWSObject(header, new Payload(payloadBase64Url));
try {
this.signedJwt.sign(signer);
} catch (JOSEException e) {
throw new RuntimeException(e);
}
this.jwsString= signedJwt.serialize();
}

public void verifySignature(SignatureVerifierContext verifier) throws VerificationException {
Objects.requireNonNull(verifier, "verifier must not be null");
try {
if (!verifier.verify(jwsInput.getEncodedSignatureInput().getBytes(StandardCharsets.UTF_8), jwsInput.getSignature())) {
throw new VerificationException("Invalid jws signature");
}
} catch (Exception e) {
throw new VerificationException(e);
public void verifySignature(JWSVerifier verifier) throws JOSEException {
if (!this.signedJwt.verify(verifier)) {
throw new JOSEException("Invalid JWS signature");
}
}

public void verifyExpClaim() throws VerificationException {
public void verifyExpClaim() throws JOSEException {
verifyTimeClaim("exp", "jwt has expired");
}

public void verifyNotBeforeClaim() throws VerificationException {
public void verifyNotBeforeClaim() throws JOSEException {
verifyTimeClaim("nbf", "jwt not valid yet");
}

private void verifyTimeClaim(String claimName, String errorMessage) throws VerificationException {
private void verifyTimeClaim(String claimName, String errorMessage) throws JOSEException {
JsonNode claim = payload.get(claimName);
if (claim == null || !claim.isNumber()) {
throw new VerificationException("Missing or invalid '" + claimName + "' claim");
throw new JOSEException("Missing or invalid '" + claimName + "' claim");
}

long claimTime = claim.asLong();
long currentTime = Instant.now().getEpochSecond();
if (("exp".equals(claimName) && currentTime >= claimTime) || ("nbf".equals(claimName) && currentTime < claimTime)) {
throw new VerificationException(errorMessage);
throw new JOSEException(errorMessage);
}
}

private static JWSInput parse(String jwsString) {
try {
return new JWSInput(Objects.requireNonNull(jwsString, "jwsString must not be null"));
} catch (JWSInputException e) {
throw new RuntimeException(e);
}
private static JWSObject parse(String jwsString) throws ParseException {
return JWSObject.parse(Objects.requireNonNull(jwsString, "jwsString must not be null"));
}

private static JsonNode readPayload(JWSInput jwsInput) {
try {
return SdJwtUtils.mapper.readTree(jwsInput.getContent());
} catch (IOException e) {
throw new RuntimeException(e);
}
private static JsonNode readPayload(JWSObject jwsInput) throws ParseException, IOException {
return SdJwtUtils.mapper.readValue(jwsInput.getParsedParts()[1].decode(), JsonNode.class);
}
}
Loading