All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.mosip.certify.proof.JwtProofValidator Maven / Gradle / Ivy

/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */
package io.mosip.certify.proof;

import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.proc.BadJOSEException;
import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier;
import com.nimbusds.jose.proc.JWSKeySelector;
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.JWTParser;
import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import io.mosip.certify.core.constants.ErrorConstants;
import io.mosip.certify.core.dto.CredentialProof;
import io.mosip.certify.core.exception.InvalidRequestException;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.util.*;

@Slf4j
@Component
public class JwtProofValidator implements ProofValidator {

    private static final String HEADER_TYP = "openid4vci-proof+jwt";
    private static final String DID_JWK_PREFIX = "did:jwk:";

    @Value("#{${mosip.certify.supported.jwt-proof-alg}}")
    private List supportedAlgorithms;

    @Value("${mosip.certify.identifier}")
    private String credentialIdentifier;

    @Override
    public String getProofType() {
        return "jwt";
    }

    private static final Set allowedSignatureAlgorithms;

    private static Set REQUIRED_CLAIMS;

    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
    public boolean validate(String clientId, String cNonce, CredentialProof credentialProof) {
        if(credentialProof.getJwt() == null || credentialProof.getJwt().isBlank()) {
            log.error("Found invalid jwt in the credential proof");
            return false;
        }

        try {
            SignedJWT jwt = (SignedJWT) JWTParser.parse(credentialProof.getJwt());
            validateHeaderClaims(jwt.getHeader());

            JWK jwk = getKeyFromHeader(jwt.getHeader());
            if(jwk.isPrivate()) {
                log.error("Provided key material contains private key! Rejecting proof.");
                throw new InvalidRequestException(ErrorConstants.PROOF_HEADER_INVALID_KEY);
            }

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

            JWSKeySelector keySelector = new JWSVerificationKeySelector(allowedSignatureAlgorithms,
                    new ImmutableJWKSet(new JWKSet(jwk)));
            ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor();
            jwtProcessor.setJWSKeySelector(keySelector);
            jwtProcessor.setJWSTypeVerifier(new DefaultJOSEObjectTypeVerifier(new JOSEObjectType(HEADER_TYP)));
            jwtProcessor.setJWTClaimsSetVerifier(claimsSetVerifier);
            jwtProcessor.process(credentialProof.getJwt(), null);
            return true;
        } catch (InvalidRequestException e) {
            log.error("Invalid proof : {}", e.getErrorCode());
        }  catch (ParseException e) {
            log.error("Failed to parse jwt in the credential proof", e);
        } catch (BadJOSEException | JOSEException e) {
            log.error("JWT proof verification failed", e);
        }
        return false;
    }

    @Override
    public String getKeyMaterial(CredentialProof credentialProof) {
        try {
            SignedJWT jwt = (SignedJWT) JWTParser.parse(credentialProof.getJwt());
            JWK jwk = getKeyFromHeader(jwt.getHeader());
            byte[] keyBytes = jwk.toJSONString().getBytes(StandardCharsets.UTF_8);
            return DID_JWK_PREFIX.concat(Base64.getUrlEncoder().encodeToString(keyBytes));
        } catch (ParseException e) {
            log.error("Failed to parse jwt in the credential proof", e);
        }
        throw new InvalidRequestException(ErrorConstants.PROOF_HEADER_INVALID_KEY);
    }

    private void validateHeaderClaims(JWSHeader jwsHeader) {
        if(Objects.isNull(jwsHeader.getType()) || !HEADER_TYP.equals(jwsHeader.getType().getType()))
            throw new InvalidRequestException(ErrorConstants.PROOF_HEADER_INVALID_TYP);

        if(Objects.isNull(jwsHeader.getAlgorithm()) || !supportedAlgorithms.contains(jwsHeader.getAlgorithm().getName()))
            throw new InvalidRequestException(ErrorConstants.PROOF_HEADER_INVALID_ALG);

        if(Objects.isNull(jwsHeader.getKeyID()) && Objects.isNull(jwsHeader.getJWK()))
            throw new InvalidRequestException(ErrorConstants.PROOF_HEADER_INVALID_KEY);

        //both cannot be present, either one of them is only allowed
        if(Objects.nonNull(jwsHeader.getKeyID()) && Objects.nonNull(jwsHeader.getJWK()))
            throw new InvalidRequestException(ErrorConstants.PROOF_HEADER_AMBIGUOUS_KEY);

        //TODO x5c and trust_chain validation
    }

    private JWK getKeyFromHeader(JWSHeader jwsHeader) {
        if(Objects.nonNull(jwsHeader.getJWK()))
            return jwsHeader.getJWK();

        return resolveDID(jwsHeader.getKeyID());
    }

    /**
     * Currently only handles did:jwk, Need to handle other methods
     * @param keyId
     * @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));
                jsonKey.put("kid", did);
                return JWK.parse(jsonKey.toString());
            } catch (IllegalArgumentException e) {
                log.error("Invalid base64 encoded ID : {}", did, e);
            } catch (ParseException | JSONException e) {
                log.error("Invalid jwk : {}", did, e);
            }
        }
        throw new InvalidRequestException(ErrorConstants.PROOF_HEADER_INVALID_KEY);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy