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

id.unum.service.VerifierService Maven / Gradle / Ivy

The newest version!
package id.unum.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.gson.JsonObject;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Timestamp;
import com.google.protobuf.util.JsonFormat;
import id.unum.crossPlatformInterfaces.Encoding;
import id.unum.dto.*;
import id.unum.enums.CredentialStatusOptions;
import id.unum.enums.PresentationReplyTypes;
import id.unum.enums.PresentationTypes;
import id.unum.error.UnumError;
import id.unum.facade.rest.Client;
import id.unum.facade.rest.UnumAPIService;
import id.unum.facade.rest.request.RegisterVerifierRequest;
import id.unum.facade.rest.request.SendMessageRequest;
import id.unum.protos.credential.v1.Credential;
import id.unum.protos.credential.v1.UnsignedCredential;
import id.unum.protos.crypto.v1.EncryptedData;
import id.unum.protos.crypto.v1.KeyPair;
import id.unum.protos.crypto.v1.KeyPairSet;
import id.unum.protos.crypto.v1.PublicKeyInfo;
import id.unum.protos.didDocument.v1.DidDocument;
import id.unum.protos.externalMessage.v1.ExternalMessage;
import id.unum.protos.presentation.v1.Presentation;
import id.unum.protos.presentation.v1.UnsignedPresentation;
import id.unum.protos.presentationRequest.v1.PresentationRequest;
import id.unum.protos.presentationRequest.v1.UnsignedPresentationRequest;
import id.unum.protos.presentationRequestEnriched.v1.PresentationRequestEnriched;
import id.unum.protos.presentationRequestEnriched.v1.PresentationRequestRepoDto;
import id.unum.protos.proof.v1.Proof;
import id.unum.protos.receipt.v1.Receipt;
import id.unum.protos.verifier.v1.Verifier;
import id.unum.types.CredentialSubject;
import id.unum.types.VerifiedStatus;
import id.unum.types.dto.CredentialRequest;
import id.unum.types.dto.VersionInfo;
import lombok.extern.log4j.Log4j2;
import retrofit2.Call;
import retrofit2.Response;

import java.io.IOException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;

import static com.google.protobuf.util.Timestamps.fromMillis;
import static id.unum.converter.DtoToProto.*;
import static id.unum.converter.ProtoToDto.*;
import static id.unum.utils.CryptoUtils.*;
import static id.unum.utils.Utils.*;

@Log4j2
public class VerifierService implements VerifierServiceInterface{
    private UnumAPIService _unumService;
    private DidDocServiceInterface _didDocService;
    private ReceiptServiceInterface _receiptService;

    public VerifierService(String url) {
        Client client = new Client(url);
        _unumService = client.getRetrofit().create(UnumAPIService.class);
        _didDocService = new DidDocService(url);
        _receiptService = new ReceiptService(url);
    }

    @Override
    public Unum registerVerifier(String customerUuid, String url, String apiKey,
                                               List versionInfoList) throws UnumError {
        log.info("calling register verifier");

        KeyPairSet keyPairSet = generateKeyPairSet(Encoding.PEM);

        RegisterVerifierRequest request = new RegisterVerifierRequest();
        request.setCustomerUuid(customerUuid);
        request.setPublicKeyInfo(extractPublicKeyInfo(keyPairSet, Encoding.PEM));
        request.setUrl(url);
        request.setVersionInfo(versionInfoList);

        Call call = _unumService.registerVerifier("Bearer " + apiKey, request);
        try {
            Response response = call.execute();
            if (!response.isSuccessful()) {
                String message = response.errorBody() != null
                        ? response.errorBody().string() : "Unknown error";
                log.error("Saas error calling register verifier " + message);
                throw new UnumError(response.errorBody().hashCode(), message);
            }

            Verifier verifier = response.body();
            String newAuthToken = response.headers().get("X-Auth-Token");

            RegisteredVerifier body = convertVerifier(verifier);
            Unum result = new Unum<>();
            result.setBody(body);
            result.setAuthToken(handleAuthToken(newAuthToken));

            return result;
        } catch (IOException e) {
            log.error("IOException calling register verifier: " + e.toString());
            e.printStackTrace();
        }
        throw new UnumError(500, "Unknown error registering verifier.");
    }

    @Override
    public Unum sendRequest(String authToken, String verifier,
                                                                           List credentialRequests,
                                                                           String signingPrivateKey, String holderAppUuid,
                                                                           Date expiresAt, JsonObject metadata) throws UnumError{
        requireAuth(authToken);

        // todo?
        validateSendRequestInput(verifier, credentialRequests, signingPrivateKey, holderAppUuid);

        String id = UUID.randomUUID().toString(); // an identifier to span request versions, however currently only supporting 3.0.0

        /**
         * Need to loop through all versions except most recent so that can issued credentials could be backwards compatible with older holder versions.
         * However, only care to return the most recent Credential type for customers to use.
         * Note: only supporting the most recent version in this sdk for now, 3.0.0
         */
        List versions = getVersionList();
        for (int i = 0; i < versions.size() - 1; i++) {
            // note this should never be reached until the version list has more than one entry
        }

        // Grabbing the latest version as defined in the version list, 3.0.0
        String latestVersion = versions.get(versions.size() - 1);

        try {
            // Create the unsigned presentation request object from the unsignedPresentation object
            UnsignedPresentationRequest unsignedPresentationRequest = constructUnsignedPresentationRequest(verifier,
                    credentialRequests, holderAppUuid, expiresAt, metadata, id, latestVersion);

            // Create the signed presentation request object from the unsignedPresentation object
            PresentationRequest signedPresentationRequest =
                    constructSignedPresentationRequest(unsignedPresentationRequest, signingPrivateKey);

//            PresentationRequestCreateRequest request =
//                    convertPresentationRequestToCreateRequest(signedPresentationRequest);
//
//        PresentationRequestCreateRequest request = new PresentationRequestCreateRequest();
//        request.setCredentialRequests();
//        request.setVerifier(signedPresentationRequest.getVerifier());

            Call call = _unumService.sendRequest(authToken, signedPresentationRequest);

//        try {
            Response response = call.execute();
            if (!response.isSuccessful()) {
                String message = response.errorBody() != null
                        ? response.errorBody().string() : "Unknown error";
                log.error("Saas error creating presentation request" + message);
                throw new UnumError(response.errorBody().hashCode(), message);
            }

            String newAuthToken = response.headers().get("X-Auth-Token");

            Unum result = new Unum<>();
            result.setBody(convertPresentationRequestEnriched(response.body()));
            result.setAuthToken(handleAuthToken(newAuthToken));
            return result;
//        }
        } catch (InvalidProtocolBufferException e) {
            e.printStackTrace();
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        catch (IOException e) {
            log.error("IOException creating presentation request: " + e.toString());
            e.printStackTrace();
        }

        throw new UnumError(500, "Unknown error creating presentation request.");
    }

    private PresentationRequest constructSignedPresentationRequest(UnsignedPresentationRequest unsignedPresentationRequest, String signingPrivateKey) {
        // convert protobuf to byte array
        byte[] bytes = unsignedPresentationRequest.toByteArray();

        Proof proof = createProof(bytes, signingPrivateKey, unsignedPresentationRequest.getVerifier(), "SHA256withECDSA");

        PresentationRequest result = PresentationRequest.newBuilder()
                .setCreatedAt(unsignedPresentationRequest.getCreatedAt())
                .setUpdatedAt(unsignedPresentationRequest.getUpdatedAt())
                .setExpiresAt(unsignedPresentationRequest.getExpiresAt())
                .addAllCredentialRequests(unsignedPresentationRequest.getCredentialRequestsList())
                .setId(unsignedPresentationRequest.getId())
                .setUuid(unsignedPresentationRequest.getUuid())
                .setVerifier(unsignedPresentationRequest.getVerifier())
                .setHolderAppUuid(unsignedPresentationRequest.getHolderAppUuid())
                .setMetadata(unsignedPresentationRequest.getMetadata())
                .setVersion(unsignedPresentationRequest.getVersion())
                .setProof(proof)
                .build();

        return result;
    }

    private UnsignedPresentationRequest constructUnsignedPresentationRequest(String verifier,
                                                                             List credentialRequests, String holderAppUuid, Date expiresAt, JsonObject metadata, String id, String version) throws InvalidProtocolBufferException, JsonProcessingException {
        String uuid = UUID.randomUUID().toString();
        Timestamp now = Timestamp.newBuilder().build();

//        Instant time = Instant.now();
//        Timestamp tenMinutesFromNow = Timestamp.newBuilder().setSeconds(time.getEpochSecond() + 60 * 10)
//                .setNanos(0).build(); // Note: to avoid JSON precision rounding inconsistencies

        Instant nowPlusTenMinutes = Instant.now().plus(10, ChronoUnit.MINUTES);
        Timestamp tenMinutesFromNow = Timestamp.newBuilder().setSeconds(nowPlusTenMinutes.getEpochSecond())
                .setNanos(0).build(); // Note: to avoid JSON precision rounding inconsistencies

        Timestamp end = expiresAt == null ? tenMinutesFromNow : fromMillis(expiresAt.getTime());

        UnsignedPresentationRequest result = UnsignedPresentationRequest.newBuilder()
                .setCreatedAt(now)
                .setUpdatedAt(now)
                .setExpiresAt(end)
                .setVerifier(verifier)
                .setUuid(uuid)
                .setId(id)
                .setVersion(version)
                .addAllCredentialRequests(convertCredentialsToProtos(credentialRequests))
                .setHolderAppUuid(holderAppUuid)
                .setMetadata(metadata != null ? metadata.toString() : "{}")
                .build();

        return result;
    }

    @Override
    public Unum verifyPresentation(String authToken,
                                                          final id.unum.types.EncryptedData encryptedPresentationDto,
                                                          final String verifierDid, final String encryptionPrivateKey,
                                                          final id.unum.types.dto.PresentationRequestEnriched presentationRequestEnriched) throws UnumError {
        requireAuth(authToken);

        try {
            EncryptedData encryptedPresentation = convertEncryptedDataToProto(encryptedPresentationDto);

            // creating a KeyPair to appease the cryptolib
            KeyPair keyPair = KeyPair.newBuilder().setPrivateKey(encryptionPrivateKey).build();

            validateVerifyPresentationInput(encryptedPresentation, verifierDid, keyPair, presentationRequestEnriched);

            log.info("Decrypting presentation");

            // decrypt the presentation
            byte[] presentationBytes = decrypt(keyPair, encryptedPresentation);

            Presentation presentation = Presentation.parseFrom(presentationBytes);

            // todo check env var, if debug print the decrypted presentation

            validatePresentation(presentation);

            // verify the presentation request uuid match
            log.info("Validating presentation request");
            if (presentationRequestEnriched != null && presentationRequestEnriched.getPresentationRequest().getId() != presentation.getPresentationRequestId()) {
                throw new UnumError(400,
                        "Presentation request id provided, " + presentationRequestEnriched.getPresentationRequest().getId() + ", does not match the presentationRequestId that the presentation was in response to. " + presentation.getPresentationRequestId() + ".");
            }

            // verify the presentation request signature if present
//            PresentationRequest presentationRequest = PresentationRequest.newBuilder().build();
            PresentationRequest presentationRequest = null;
            if (presentationRequestEnriched != null){
                // validate the provided presentation request
                    presentationRequest =
                            validatePresentationRequest(presentationRequestEnriched.getPresentationRequest());
            } else if (!presentation.getPresentationRequestId().isEmpty()) {
                // grab the request from the saas
                log.info("Getting presentation request from saas as one wasn't provided: " + presentation.getPresentationRequestId());
                Unum presentationRequestRepoDto = getPresentationRequest(authToken, presentation.getPresentationRequestId());
                authToken = presentationRequestRepoDto.getAuthToken();
                presentationRequest = extractPresentationRequest(presentationRequestRepoDto.getBody());
            }

            if (presentationRequest != null) {
                log.info("Verifying presentation request");
                Unum requestVerificationResult = verifyPresentationRequest(authToken, presentationRequest);
                authToken = requestVerificationResult.getAuthToken();

                // if invalid then can stop here but still send back the decrypted presentation with the verification results
                if (!requestVerificationResult.getBody().isVerified()) {
                    log.info("Presentation request deemed invalid: " + requestVerificationResult.getBody().getMessage());
                    PresentationTypes type = isDeclinedPresentation(presentation) ? PresentationTypes.DeclinedPresentation :
                            PresentationTypes.VerifiablePresentation;

                    DecryptedPresentation body = new DecryptedPresentation();
                    body.setPresentation(convertPresentation(presentation));
                    body.setType(type);
                    body.setVerified(false);
                    body.setMessage(requestVerificationResult.getBody().getMessage());

                    Unum result = new Unum<>();
                    result.setAuthToken(handleAuthToken(authToken));
                    result.setBody(body);

                    return result;
                }
            }

            if (isDeclinedPresentation(presentation)) {
                log.info("Handling declined presentation");
                Unum verificationResult = verifyDeclinedPresentationHelper(authToken, presentation, verifierDid, presentationRequest.getUuid());

                log.info("Declined presentation deemed valid " + verificationResult.getBody().isVerified() + " " + verificationResult.getBody().getMessage());
                DecryptedPresentation body = new DecryptedPresentation();
                body.setPresentation(convertPresentation(presentation));
                body.setType(PresentationTypes.DeclinedPresentation);
                body.setVerified(verificationResult.getBody().isVerified()); // if here ought to always be true
                body.setMessage(verificationResult.getBody().getMessage());

                Unum result = new Unum<>();
                result.setAuthToken(handleAuthToken(verificationResult.getAuthToken()));
                result.setBody(body);

                return result;
            }

            List credentialRequests = presentationRequest.getCredentialRequestsList();
            Unum verificationResult = verifyPresentationHelper(authToken, presentation, verifierDid, credentialRequests, presentationRequest.getUuid());

            log.info("Approved presentation deemed valid " + verificationResult.getBody().isVerified() + (verificationResult.getBody().isVerified() ?  "" : " " + verificationResult.getBody().getMessage()));
            DecryptedPresentation body = new DecryptedPresentation();
            body.setPresentation(convertPresentation(presentation));
            body.setType(PresentationTypes.VerifiablePresentation);
            body.setVerified(verificationResult.getBody().isVerified());
            body.setMessage(verificationResult.getBody().getMessage());

            Unum result = new Unum<>();
            result.setAuthToken(handleAuthToken(verificationResult.getAuthToken()));
            result.setBody(body);

            return result;


        } catch (InvalidProtocolBufferException e) {
            log.error("Protobuff Error decoding byte array to Presentation object: " + e.getMessage());
            e.printStackTrace();
            throw new UnumError(500, "Error decoding byte array to Presentation object: " + e.getMessage());
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            log.error("Json Error decoding byte array to Presentation object: " + e.getMessage());
            throw new UnumError(500, "Error decoding byte array to Presentation object: " + e.getMessage());
        }
    }

    /**
     * Helper to extract the presentationRequest from the PresentationRequestRepo's response, which is a map keyed on version.
     * @param presentationRequestRepoDto
     * @return
     */
    private PresentationRequest extractPresentationRequest(PresentationRequestRepoDto presentationRequestRepoDto) {
        try {
            PresentationRequestEnriched presentationRequestEnriched = presentationRequestRepoDto.getPresentationRequestsMap().get("3.0.0");
            return presentationRequestEnriched.getPresentationRequest();
        } catch (Exception e) {
            throw new UnumError(500, "Error handling presentation request from Saas: Error " + e);
        }
    }

    private Unum verifyPresentationHelper(String authToken, Presentation presentation, String verifierDid, List credentialRequests, String requestUuid) throws InvalidProtocolBufferException {
        log.info("Verifying approved presentation");
        requireAuth(authToken);

        validateVerifiablePresentation(presentation);

        // validate that the presentation as verifiable credentials
        if (presentation.getVerifiableCredentialCount() == 0) {
            log.error("The presentation has undefined verifiableCredential attribute, this should have already be checked.");
            throw new UnumError(500, "presentation as an undefined verifiableCredentials");
        }

        // if specific credential requests, then need to confirm the presentation provided meets the requirements
        if (!credentialRequests.isEmpty()) {
            validatePresentationMeetsRequestedCredentials(presentation, credentialRequests);
        }

        // remove the 'VerifiableCredential' type string in each array
        List credentialTypesFiltered = new ArrayList<>();
        for (Credential credential : presentation.getVerifiableCredentialList()) {
            List credentialTypes =
                    credential.getTypeList().stream().filter(type -> !type.equals("VerifiedCredential")).collect(Collectors.toList());
            credentialTypesFiltered.addAll(credentialTypes);
        }

        // get all credential issuers
        List issuers =
                presentation.getVerifiableCredentialList().stream().map(Credential::getIssuer).collect(Collectors.toList());

        Unum presentationResult = verifyPresentationProof(authToken, presentation, verifierDid);

        if (!presentationResult.getBody().isVerified()) {
            String message = presentationResult.getBody().getMessage();
            try {
                Unum receipt = _receiptService.sendPresentationVerifiedReceipt(authToken, verifierDid, presentation.getProof().getVerificationMethod(), PresentationReplyTypes.approved.toString(), false, presentation.getPresentationRequestId(), requestUuid, message, issuers, credentialTypesFiltered);
                authToken = receipt.getAuthToken();
            } catch (Exception e) {
                log.error("Error creating presentation verified receipt: " + e.getMessage());
            }

            return presentationResult;
        }

        // Need to verify the credentials in the presentation are valid
        boolean areCredentialsValid = true;
        String credentialInvalidMessage = "";
        for (Credential credential : presentation.getVerifiableCredentialList()) {
            boolean isExpired = isCredentialExpired(credential);

            if (isExpired) {
                areCredentialsValid = false;
                credentialInvalidMessage = "Credential " + credential.getType(1) + " " + credential.getId() + " is expired.";
                break;
            }

            Unum credentialStatusInfo = checkCredentialStatus(authToken, credential.getId());
            authToken = credentialStatusInfo.getAuthToken();
            boolean isStatusValid = credentialStatusInfo.getBody().getStatus() == CredentialStatusOptions.valid;

            if (!isStatusValid) {
                areCredentialsValid = false;
                credentialInvalidMessage = "Credential " + credential.getType(1) + " " + credential.getId() + " status is invalid.";
                break;
            }

            Unum isVerifiedResponse = verifyCredential(authToken, credential);
            authToken = isVerifiedResponse.getAuthToken();

            if (!isVerifiedResponse.getBody()) {
                areCredentialsValid = false;
                credentialInvalidMessage = "Credential " + credential.getType(1) + " " + credential.getId() + " signature can not be verified.";
                break;
            }
        }

        if (!areCredentialsValid) {
            try {
                Unum receipt = _receiptService.sendPresentationVerifiedReceipt(authToken, verifierDid, presentation.getProof().getVerificationMethod(), PresentationReplyTypes.approved.toString(), false, presentation.getPresentationRequestId(), requestUuid, credentialInvalidMessage, issuers, credentialTypesFiltered);
                authToken = receipt.getAuthToken();
            } catch (Exception e) {
                log.error("Error creating presentation verified receipt: " + e.getMessage());
            }

            VerifiedStatus status = VerifiedStatus.builder()
                    .isVerified(false)
                    .message(credentialInvalidMessage)
                    .build();

            Unum result = new Unum<>();
            result.setAuthToken(handleAuthToken(authToken));
            result.setBody(status);

            return result;
        }

        try {
            Unum receipt = _receiptService.sendPresentationVerifiedReceipt(authToken, verifierDid, presentation.getProof().getVerificationMethod(), PresentationReplyTypes.approved.toString(), true, presentation.getPresentationRequestId(), requestUuid, "", issuers, credentialTypesFiltered);
            authToken = receipt.getAuthToken();
        } catch (Exception e) {
            log.error("Error creating presentation verified receipt: " + e.getMessage());
        }

        presentationResult.setAuthToken(handleAuthToken(authToken));
        return presentationResult;
    }

    /**
     * Validates the presentation object has the proper attributes.
     * @param presentation
     */
    private void validateVerifiablePresentation(Presentation presentation) {
        if (presentation.getVerifiableCredentialCount() == 0) {
            throw new UnumError(400, "Invalid Presentation: verifiableCredential must be a non-empty array.");
        }

        if (presentation.getContextCount() == 0) {
            throw new UnumError(400, "Invalid Presentation: context must be a non-empty array.");
        }

        if (presentation.getTypeCount() == 0) {
            throw new UnumError(400, "Invalid Presentation: type must be a non-empty array.");
        }

        if (!presentation.getType(0).equals("VerifiablePresentation")) {
            throw new UnumError(400, "Invalid Presentation: type's first array element must be " +
                    "'VerifiablePresentation'");
        }
        
        validateCredentialInput(presentation.getVerifiableCredentialList());
    }

    /**
     * Validates the attributes for a credential from UnumId's Saas
     * @param verifiableCredentialList
     */
    private void validateCredentialInput(List verifiableCredentialList) {
        for (int i = 0; i < verifiableCredentialList.size(); i++) {
            String credentialPositionString = "[" + i + "]";
            Credential credential = verifiableCredentialList.get(i);

            // Validate the existence of elements in Credential object

            String invalidMessage = "Invalid verifiableCredential" + credentialPositionString + ":";
            if (credential.getContextCount() == 0) {
                throw new UnumError(400, invalidMessage + " context must be a non-empty array");
            }

            CredentialSubject credentialSubject = convertCredentialSubject(credential.getCredentialSubject());
            if (credentialSubject.getId() == null) {
                throw new UnumError(400,  invalidMessage + " credentialSubject must contain id property.");
            }

            if (credential.getTypeCount() == 0) {
                throw new UnumError(400,  invalidMessage + " type must be a non-empty array.");
            }

//            validateProof(credential.getProof());
        }
    }

    /**
     * Used to verify the credential signature given the corresponding Did document's public key.
     * @param authToken
     * @param credential
     * @return
     */
    protected Unum verifyCredential(String authToken, final Credential credential) throws InvalidProtocolBufferException {
        Proof proof = credential.getProof();

        Unum didDocument = _didDocService.getDIDDoc(authToken, proof.getVerificationMethod());
        authToken = didDocument.getAuthToken();

        id.unum.protos.crypto.v1.PublicKeyInfo publicKey = _didDocService.getKeyFromDIDDoc(didDocument.getBody(), "secp256r1");

        // Create an UnsignedCredential object
        Credential credentialNoProof = credential.toBuilder().clearProof().build();
        String json = JsonFormat.printer().omittingInsignificantWhitespace().print(credentialNoProof);
        UnsignedCredential.Builder unsignedCredentialBuilder =
                UnsignedCredential.newBuilder();
        JsonFormat.parser().merge(json, unsignedCredentialBuilder);

        UnsignedCredential unsignedCredential = unsignedCredentialBuilder.build();

        // convert to bytes
        byte[] bytes = unsignedCredential.toByteArray();

        // verify the byte array
        boolean isVerified = doVerify(proof.getSignatureValue(), bytes, publicKey, publicKey.getEncoding());

        Unum result = new Unum<>();
        result.setAuthToken(handleAuthToken(authToken));
        result.setBody(isVerified);

        return result;
    }

    /**
     * Validates that:
     * a. all requested credentials types are present
     * b. credentials are only from list of required issuers, if the list is present
     * @param presentation
     * @param credentialRequests
     */
    private void validatePresentationMeetsRequestedCredentials(Presentation presentation, List credentialRequests) {
        if (presentation.getVerifiableCredentialCount() == 0) {
            return; // just skip because this is a declined presentation
        }

        for (id.unum.protos.credential.v1.CredentialRequest requestedCred : credentialRequests) {
            if (requestedCred.getRequired()) {
                // check that the request credential is present in the presentation
                List presentationCreds = presentation.getVerifiableCredentialList();
                boolean found = false;
                for (Credential presentationCred : presentationCreds) {
                    // checking required credential types are present
                    found = presentationCred.getTypeList().contains(requestedCred.getType());

                    if (found) {
                        // checking the required issuers are present
                        if (requestedCred.getIssuersCount() > 0 && !requestedCred.getIssuersList().contains(presentationCred.getIssuer())) {
                            String errorMessage = "Invalid Presentation: credentials provided did not meet the issuer" +
                                    " requirements. Issuer requested: " + requestedCred.getIssuersList() + ". Issuer " +
                                    "of the credential received " + presentationCred.getIssuer();

                            log.warn(errorMessage);
                            throw new UnumError(400, errorMessage);
                        }

                        // can break from inner loop because validation has been met.
                        break;
                    }
                }

                if (!found) {
                    String errorMessage = "Invalid Presentation: credentials provided did not meet type requirements." +
                            " Presented credentials: " + presentation.getVerifiableCredentialList().stream().map(pc -> pc.getTypeList().stream().filter(t -> !t.equals("VerifiableCredential"))).collect(Collectors.toList()) +
                            ". Requested credentials: " + credentialRequests.stream().map(id.unum.protos.credential.v1.CredentialRequest::getType).collect(Collectors.toList()) + ".";
                    log.warn(errorMessage);
                    throw new UnumError(400, errorMessage);
                }
            }
        }
    }

    /**
     * Handler for when a user does not agree to share the information in the credential request.
     * @param authToken
     * @param presentation
     * @param verifierDid
     * @return
     */
    private Unum verifyDeclinedPresentationHelper(String authToken, final Presentation presentation, final String verifierDid, final String requestUuid) throws InvalidProtocolBufferException {
        log.info("Verifying a declined presentation");
        requireAuth(authToken);

        validateDeclinedPresentationInput(presentation);

        String presentationVerifierDid = presentation.getVerifierDid();
        Proof proof = presentation.getProof();
        String verificationMethod = proof.getVerificationMethod();

        Unum result = verifyPresentationProof(authToken, presentation, verifierDid);

        try {
            // Create receipt for Saas
            Unum receipt = _receiptService.sendPresentationVerifiedReceipt(authToken, presentationVerifierDid, verificationMethod, PresentationReplyTypes.declined.toString(), result.getBody().isVerified(), presentation.getPresentationRequestId(), requestUuid, result.getBody().getMessage(), Collections.emptyList(), Collections.emptyList());
            authToken = receipt.getAuthToken();
            result.setAuthToken(authToken);
        } catch (Exception e) {
            log.error("Error creating presentation verified receipt: " + e.getMessage());
        }

        return result;
    }

    private Unum verifyPresentationProof(String authToken, final Presentation presentation,
                                                         final String verifierDid) throws InvalidProtocolBufferException {
        String presentationVerifierDid = presentation.getVerifierDid();
        Proof proof = presentation.getProof();
        String verificationMethod = proof.getVerificationMethod();
        String signatureValue = proof.getSignatureValue();

        // validate that the verifier did provided matches the verifier did in the presentation
        if (!presentationVerifierDid.equals(verifierDid)) {
            VerifiedStatus status = VerifiedStatus.builder()
                    .isVerified(false)
                    .message("The presentation was meant for verifier, " + presentationVerifierDid + ", not the " +
                            "provided verifier, " + verifierDid + ".")
                    .build();

            Unum result = new Unum<>();
            result.setAuthToken(handleAuthToken(authToken));
            result.setBody(status);

            return result;
        }

        PublicKeyInfo keyInfo = null;
        try {
            log.info("Grabbing subject's DID doc to retrieve the public key from presentation proof verification: " + verificationMethod);

            if (verificationMethod.length() == 82 && verificationMethod.contains("#")) {
                // did doc has key id, i.e. did:unum:00e1691b-437e-4096-93d8-0ca523d7d2c9#5e6c4504-15ff-4ced-85a9-baae32d662f7
                log.info("Did doc has key id");
                Unum publicKeyInfoUnum = _didDocService.getDidDocPublicKeyInfoByKeyId(authToken, verificationMethod);
                authToken = publicKeyInfoUnum.getAuthToken();
                keyInfo = publicKeyInfoUnum.getBody();
            } else {
                Unum didDocument = _didDocService.getDIDDoc(authToken, verificationMethod);
                authToken = didDocument.getAuthToken();
                keyInfo = _didDocService.getKeyFromDIDDoc(didDocument.getBody(), "secp256r1");
            }
        } catch (UnumError error) {
            Unum result = new Unum<>();
            result.setAuthToken(handleAuthToken(authToken));
            result.setBody(VerifiedStatus.builder()
                    .isVerified(false)
                    .message("Public key not found for the DID associated with the proof.verificationMethod")
                    .build());

            return result;
        }

        String encoding = keyInfo.getEncoding();

        // Create an UnsignedPresentation object
        Presentation presentationNoProof = presentation.toBuilder().clearProof().build();
        String json = JsonFormat.printer().omittingInsignificantWhitespace().print(presentationNoProof);
        UnsignedPresentation.Builder unsignedPresentationBuilder =
                UnsignedPresentation.newBuilder();
        JsonFormat.parser().merge(json, unsignedPresentationBuilder);

        UnsignedPresentation unsignedPresentation = unsignedPresentationBuilder.build();

        // convert to bytes
        byte[] bytes = unsignedPresentation.toByteArray();

        log.info("Verifying approved presentation with signature " + signatureValue + " with publicKey " + keyInfo.getPublicKey());

        // verify the byte array
        boolean isVerified = doVerify(signatureValue, bytes, keyInfo, encoding);

        if (!isVerified) {
            VerifiedStatus status = VerifiedStatus.builder()
                    .isVerified(false)
                    .message("Presentation signature can not be verified.")
                    .build();

            Unum result = new Unum<>();
            result.setAuthToken(handleAuthToken(authToken));
            result.setBody(status);

            return result;
        }

        VerifiedStatus status = VerifiedStatus.builder()
                .isVerified(true)
                .build();

        Unum result = new Unum<>();
        result.setAuthToken(handleAuthToken(authToken));
        result.setBody(status);

        return result;
    }

    private void validateDeclinedPresentationInput(Presentation presentation) {
        if (presentation.getTypeCount() == 0) {
            throw new UnumError(400, "Invalid Presentation: type must be a non-empty array.");
        }

        if (presentation.getVerifiableCredentialCount() != 0) {
            throw new UnumError(400, "Invalid Declined Presentation: verifiableCredential must be null or empty.");
        }
        
//        validateProof(presentation.getProof());
    }

    /**
     * Verify the PresentationRequest signature as a way to side step verifier MITM attacks where an entity spoofs requests.
     * @param authToken
     * @param presentationRequest
     * @return
     * @throws InvalidProtocolBufferException
     */
    private Unum verifyPresentationRequest(String authToken, PresentationRequest presentationRequest) throws InvalidProtocolBufferException {
        if (presentationRequest == null) {
            throw new UnumError(500, "PresentationRequest is null in verify call. This should never happen");
        }

        Proof proof = presentationRequest.getProof();
        String verificationMethod = proof.getVerificationMethod();
        String signatureValue = proof.getSignatureValue();

        Unum didDocument = _didDocService.getDIDDoc(authToken, verificationMethod);
        authToken = didDocument.getAuthToken();

        PublicKeyInfo keyInfo = _didDocService.getKeyFromDIDDoc(didDocument.getBody(), "secp256r1");

        String publicKey = keyInfo.getPublicKey();
        String encoding = keyInfo.getEncoding();

        // Create an UnsignedPresentationRequest object
        PresentationRequest presentationRequestNoProof = presentationRequest.toBuilder().clearProof().build();
        String json = JsonFormat.printer().omittingInsignificantWhitespace().print(presentationRequestNoProof);
        UnsignedPresentationRequest.Builder unsignedPresentationRequestBuilder =
                UnsignedPresentationRequest.newBuilder();
        JsonFormat.parser().merge(json, unsignedPresentationRequestBuilder);

        UnsignedPresentationRequest unsignedPresentationRequest = unsignedPresentationRequestBuilder.build();

        // convert to bytes
        byte[] bytes = unsignedPresentationRequest.toByteArray();

        // verify the byte array
        boolean isVerified = doVerify(signatureValue, bytes, keyInfo, encoding);

        if (!isVerified) {
            VerifiedStatus status = VerifiedStatus.builder()
                    .isVerified(false)
                    .message("PresentationRequest signature can not be verified.")
                    .build();

            Unum result = new Unum<>();
            result.setAuthToken(handleAuthToken(authToken));
            result.setBody(status);

            return result;
        }

        VerifiedStatus status = VerifiedStatus.builder()
                .isVerified(true)
                .build();

        Unum result = new Unum<>();
        result.setAuthToken(handleAuthToken(authToken));
        result.setBody(status);


        return result;
    }

    protected Unum getPresentationRequest(String authToken, String presentationRequestId) {
        Call call = _unumService.getPresentationRequest(authToken, presentationRequestId);
        try {
            Response response = call.execute();
            if (!response.isSuccessful()) {
                String message = response.errorBody() != null
                        ? response.errorBody().string() : "Unknown error";
                log.error("Saas error calling get presentationRequestRepo " + message);
                throw new UnumError(response.errorBody().hashCode(), message);
            }

            PresentationRequestRepoDto dto = response.body();
            if (dto == null) {
                throw new UnumError(404, "presentationRequestId " + presentationRequestId + " not found");
            }

            String newAuthToken = response.headers().get("X-Auth-Token");

            Unum result = new Unum<>();
            result.setAuthToken(handleAuthToken(newAuthToken));
            result.setBody(dto);

            return result;
        } catch (IOException e) {
            log.error("IOException calling get presentationRequestRepoDto: " + e.toString());
            e.printStackTrace();
        }

        throw new UnumError(500, "Unknown error getting presentationRequests via SaaS repo.");
    }

    /**
     * Validates the presentation request object has the proper attributes.
     * @param presentationRequest
     * @return
     * @throws InvalidProtocolBufferException
     * @throws JsonProcessingException
     */
    private PresentationRequest validatePresentationRequest(id.unum.types.dto.PresentationRequest presentationRequest) throws InvalidProtocolBufferException, JsonProcessingException {
        if (presentationRequest.getCredentialRequests() == null) {
            throw new UnumError(400, "Invalid PresentationRequest: credentialRequests is required.");
        }

        if (presentationRequest.getHolderAppUuid() == null) {
            throw new UnumError(400, "Invalid PresentationRequest: holderAppUuid is required.");
        }

        if (presentationRequest.getProof() == null) {
            throw new UnumError(400, "Invalid PresentationRequest: proof is required.");
        }

        if (presentationRequest.getVerifier() == null) {
            throw new UnumError(400, "Invalid PresentationRequest: verifier is required.");
        }

        validateCredentialRequests(presentationRequest.getCredentialRequests());

        return convertPresentationRequestToProto(presentationRequest);
    }

    /**
     * Validates the attributes for a credential request to UnumID's SaaS.
     * @param credentialRequests
     */
    private void validateCredentialRequests(List credentialRequests) {
        if (credentialRequests.size() == 0) {
            throw new UnumError(400, "Invalid PresentationRequest: credentialRequests must be a non-empty array..");
        }

        for (int i = 0; i < credentialRequests.size(); i++) {
            String credentialPositionString = "[" + i + "]";
            CredentialRequest request = credentialRequests.get(i);

            if (request.getType() == null) {
                throw new UnumError(400,
                        "Invalid PresentationRequest CredentialRequest " + credentialPositionString + ": type must be" +
                                " defined.");
            }

            if (request.getIssuers() == null) {
                throw new UnumError(400,
                        "Invalid PresentationRequest CredentialRequest " + credentialPositionString + ": issuers must" +
                                " be defined.");
            }
        }
    }

    /**
     * Validates the presentation object has the proper attributes.
     * @param presentation
     */
    private void validatePresentation(Presentation presentation) {
        log.info("Validating presentation");
        if (presentation.getContextCount() == 0) {
            throw new UnumError(400, "Invalid Presentation: context must be a non-empty array.");
        }

        if (presentation.getTypeCount() == 0) {
            throw new UnumError(400, "Invalid Presentation: type must be a non-empty array.");
        }
    }

    private void validateVerifyPresentationInput(EncryptedData encryptedPresentation, String verifierDid,
                                                 KeyPair keyPair,
                                                 id.unum.types.dto.PresentationRequestEnriched presentationRequest) {

        if (encryptedPresentation == null) {
            throw new UnumError(400, "encryptedPresentation is required.");
        }

        if (verifierDid == null) {
            throw new UnumError(400, "verifier is required.");
        }

        if (keyPair == null) {
            throw new UnumError(400, "verifier encryptionPrivateKey is required.");
        }

        if (presentationRequest != null && !presentationRequest.getVerifier().getDid().equals(verifierDid)) {
            throw new UnumError(400,
                    "verifier provided " + verifierDid + " does not match request verifier " + presentationRequest.getVerifier().getDid() + ".");
        }
    }

    @Override
    public Unum sendSms(String authToken, String to, String deeplink) throws UnumError {
        log.info("calling send sms");

        requireAuth(authToken);

//        SendMessageRequest request = new SendMessageRequest(to, deeplink);
//        SendMessageRequest request = new SendMessageRequest();
//        request.setTo(to);
//        request.setDeeplink(deeplink);
        ExternalMessage request = ExternalMessage.newBuilder()
                .setDeeplink(deeplink)
                .setTo(to)
                .build();

        Call call = _unumService.sendSms(authToken, request);

        try {
            Response response = call.execute();
            if (!response.isSuccessful()) {
                String message = response.errorBody() != null
                        ? response.errorBody().string() : "Unknown error";
                log.error("Saas error sending sms" + message);
                throw new UnumError(response.errorBody().hashCode(), message);
            }

            String newAuthToken = response.headers().get("X-Auth-Token");

            Unum result = new Unum();
            result.setBody(response.body());
            result.setAuthToken(handleAuthToken(newAuthToken));

            return result;
        } catch (IOException e) {
            log.error("IOException sending sms: " + e.toString());
            e.printStackTrace();
        }

        throw new UnumError(500, "Unknown error sending sms.");
    }

    @Override
    public Unum sendEmail(String authToken, String to, String deeplink) throws UnumError {
        log.info("calling send email");

        requireAuth(authToken);

//        SendMessageRequest request = new SendMessageRequest(to, deeplink);
        SendMessageRequest request = new SendMessageRequest();
        request.setTo(to);
        request.setDeeplink(deeplink);

        Call call = _unumService.sendEmail(authToken, request);

        try {
            Response response = call.execute();
            if (!response.isSuccessful()) {
                String message = response.errorBody() != null
                        ? response.errorBody().string() : "Unknown error";
                log.error("Saas error sending email" + message);
                throw new UnumError(response.errorBody().hashCode(), message);
            }

            String newAuthToken = response.headers().get("X-Auth-Token");

            Unum result = new Unum();
            result.setBody(response.body());
            result.setAuthToken(handleAuthToken(newAuthToken));

            return result;
        } catch (IOException e) {
            log.error("IOException sending email: " + e.toString());
            e.printStackTrace();
        }

        throw new UnumError(500, "Unknown error sending sms.");
    }

    @Override
    public Unum checkCredentialStatus(String authToken, String credentialId) throws UnumError {
        log.info("calling check credential status");

        requireAuth(authToken);

        Call call = _unumService.checkCredentialStatus(authToken, credentialId);
        try {
            Response response = call.execute();
            if (!response.isSuccessful()) {
                String message = response.errorBody() != null
                        ? response.errorBody().string() : "Unknown error";
                log.error("Saas error calling check credential status " + message);
                throw new UnumError(response.errorBody().hashCode(), message);
            }

            String newAuthToken = response.headers().get("X-Auth-Token");

            Unum result = new Unum();
            result.setBody(convertCredentialStatusInfo(response.body()));
            result.setAuthToken(handleAuthToken(newAuthToken));

            return result;
        } catch (IOException e) {
            log.error("IOException calling check credential status: " + e.toString());
            e.printStackTrace();
        }

        throw new UnumError(500, "Unknown error checking credential status.");
    }

    private void validateSendRequestInput(String verifier, List credentialRequests,
                                          String signingPrivateKey, String holderAppUuid) throws UnumError{
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy