-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3 from adorsys/sd-jwt-no-keycloak
Removed all keycloak dependencies, replaced with nimbus jose
- Loading branch information
Showing
26 changed files
with
377 additions
and
515 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -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)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
|
@@ -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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { | ||
|
@@ -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); | ||
} | ||
|
||
/* | ||
|
@@ -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() | ||
|
@@ -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); | ||
} | ||
|
@@ -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; | ||
|
@@ -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; | ||
|
@@ -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); | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.