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

dev.fitko.fitconnect.core.cases.EventLogVerifier Maven / Gradle / Ivy

Go to download

Library that provides client access to the FIT-Connect api-endpoints for sending, subscribing and routing

The newest version!
package dev.fitko.fitconnect.core.cases;

import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.jwk.KeyOperation;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import dev.fitko.fitconnect.api.domain.model.event.Event;
import dev.fitko.fitconnect.api.domain.model.event.EventIssuer;
import dev.fitko.fitconnect.api.domain.model.event.authtags.AuthenticationTags;
import dev.fitko.fitconnect.api.domain.validation.ValidationContext;
import dev.fitko.fitconnect.api.domain.validation.ValidationResult;
import dev.fitko.fitconnect.api.exceptions.internal.EventLogException;
import dev.fitko.fitconnect.api.services.events.EventLogVerificationService;
import dev.fitko.fitconnect.api.services.keys.KeyService;
import dev.fitko.fitconnect.api.services.validation.ValidationService;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;

import static com.nimbusds.jwt.JWTClaimNames.ISSUED_AT;
import static com.nimbusds.jwt.JWTClaimNames.ISSUER;
import static com.nimbusds.jwt.JWTClaimNames.JWT_ID;
import static com.nimbusds.jwt.JWTClaimNames.SUBJECT;
import static dev.fitko.fitconnect.api.domain.model.event.EventClaimFields.CLAIM_EVENTS;
import static dev.fitko.fitconnect.api.domain.model.event.EventClaimFields.CLAIM_SCHEMA;
import static dev.fitko.fitconnect.api.domain.model.event.EventClaimFields.CLAIM_SUB;
import static dev.fitko.fitconnect.api.domain.model.event.EventClaimFields.CLAIM_TXN;
import static dev.fitko.fitconnect.api.domain.model.event.EventClaimFields.HEADER_TYPE;
import static dev.fitko.fitconnect.core.utils.EventLogUtil.getAuthTags;
import static dev.fitko.fitconnect.core.utils.EventLogUtil.getDestinationId;
import static dev.fitko.fitconnect.core.utils.EventLogUtil.getEventFromClaims;
import static dev.fitko.fitconnect.core.utils.EventLogUtil.resolveIssuerType;

public class EventLogVerifier implements EventLogVerificationService {

    private static final String UUID_V4_PATTERN = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}";

    private final KeyService keyService;
    private final ValidationService validationService;

    public EventLogVerifier(final KeyService keyService, final ValidationService validationService) {
        this.keyService = keyService;
        this.validationService = validationService;
    }

    @Override
    public List validateEventLogs(final ValidationContext ctx, final List eventLogs) {
        eventLogs.forEach(event -> validateEventLogEntry(ctx, event));
        return new ArrayList<>(ctx.getValidationResults());
    }

    private void validateEventLogEntry(final ValidationContext ctx, final SignedJWT signedJWT) {
        try {
            final JWTClaimsSet payload = signedJWT.getJWTClaimsSet();

            final String issuer = payload.getIssuer();
            final JWSHeader header = signedJWT.getHeader();
            final RSAKey verificationKey = getSignatureVerificationKey(issuer, header.getKeyID());

            validateSchema(ctx, payload);
            validateHeader(ctx, header);
            validateHeaderKid(ctx, header.getKeyID());
            validatePayload(ctx, payload);
            validateIssuerClaim(ctx, payload);

            validateSignatureKey(ctx, verificationKey);
            validateSignature(ctx, signedJWT, verificationKey);

            if (ctx.validateAuthTags() && eventContainsAuthTags(payload)) {
                validateAuthenticationTags(ctx, payload);
            }

        } catch (final ParseException | JOSEException | RuntimeException e) {
            ctx.addError(e);
        }
    }

    private void validateSchema(final ValidationContext ctx, final JWTClaimsSet payload) {
        if (getSchemaClaim(payload) == null) {
            ctx.addError("Referenced schema must not be null");
        } else {
            ctx.addResult(validationService.validateSetEventSchema(payload.toString()));
        }
    }

    private void validateHeader(final ValidationContext ctx, final JWSHeader header) {
        if (header.getAlgorithm() == null) {
            ctx.addError("The provided alg in the SET header must not be null.");
        } else {
            ctx.addErrorIfTestFailed(header.getAlgorithm() == JWSAlgorithm.PS512, "The provided alg in the SET header is not allowed.");
        }
        if (header.getType() == null) {
            ctx.addError("The provided typ in the SET header must not be null.");
        } else {
            ctx.addErrorIfTestFailed(header.getType().toString().equals(HEADER_TYPE), "The provided typ in the SET header is not " + HEADER_TYPE);
        }
    }

    private void validateHeaderKid(final ValidationContext ctx, final String keyId) {
        if (keyId == null) {
            ctx.addError("The kid the SET was signed with is not set and must not be null.");
        } else {
            final int length = keyId.length();
            final int minLength = 8;
            final int maxLength = 64;
            if (length < minLength || length > maxLength) {
                ctx.addError("Invalid keyId path parameter. Must be >=" + minLength + " and <=" + maxLength + ".");
            }
        }
    }

    private void validatePayload(final ValidationContext ctx, final JWTClaimsSet claims) throws ParseException {
        ctx.addErrorIfTestFailed(claims.getClaim(ISSUER) != null, "The claim iss is missing in the payload of the SET.");
        ctx.addErrorIfTestFailed(claims.getClaim(ISSUED_AT) != null, "The claim iat is missing in the payload of the SET.");
        ctx.addErrorIfTestFailed(claims.getClaim(JWT_ID) != null, "The claim jti is missing in the payload of the SET.");
        ctx.addErrorIfTestFailed(claims.getClaim(SUBJECT) != null, "The claim sub is missing in the payload of the SET.");
        ctx.addErrorIfTestFailed(claims.getClaim(CLAIM_TXN) != null, "The claim txn is missing in the payload of the SET.");
        ctx.addErrorIfTestFailed(claims.getClaim(CLAIM_EVENTS) != null, "The claim events is missing in the payload of the SET.");
        ctx.addErrorIfTestFailed(claims.getJSONObjectClaim(CLAIM_EVENTS).keySet().size() == 1, "The events claims has must be exactly one event.");
        ctx.addErrorIfTestFailed(claims.getStringClaim(CLAIM_SUB).matches("(submission|case|reply):" + UUID_V4_PATTERN), "The provided subject does not match the allowed pattern.");
        ctx.addErrorIfTestFailed(claims.getStringClaim(CLAIM_TXN).matches("case:" + UUID_V4_PATTERN), "The provided txn does not match the allowed pattern.");
        getEventClaim(claims).ifPresentOrElse(event -> ctx.addErrorIfTestFailed(Event.fromSchemaUri(event) != null, "The provided event is not a valid event supported by this instance."),
                () -> ctx.addError("No events in JWT"));
    }

    private void validateSignatureKey(final ValidationContext ctx, final RSAKey signatureKey) throws JOSEException {
        if (signatureKey == null) {
            ctx.addError("The signature key cannot be validated, it must not be null.");
        } else {
            ctx.addResult(validationService.validatePublicKey(signatureKey, KeyOperation.VERIFY));
        }
    }

    private void validateSignature(final ValidationContext ctx, final SignedJWT signedJWT, final RSAKey signatureKey) throws JOSEException {
        if (signatureKey == null) {
            ctx.addError("The signature cannot validated, signature key is unavailable.");
        } else {
            final JWSVerifier jwsVerifier = new RSASSAVerifier(signatureKey);
            ctx.addErrorIfTestFailed(signedJWT.verify(jwsVerifier), "The signature of the token could not be verified with the specified key.");
        }
    }

    private static Object getSchemaClaim(final JWTClaimsSet payload) {
        return payload.getClaim(CLAIM_SCHEMA);
    }

    private static Optional getEventClaim(final JWTClaimsSet claims) throws ParseException {
        return claims.getJSONObjectClaim(CLAIM_EVENTS).keySet().stream().findFirst();
    }

    private RSAKey getSignatureVerificationKey(final String issuer, final String keyId) throws ParseException, EventLogException {
        final EventIssuer issuerType = resolveIssuerType(issuer);
        if (issuerType == EventIssuer.SUBMISSION_SERVICE) {
            return keyService.getSubmissionServicePublicKey(keyId);
        } else {
            return keyService.getPublicSignatureKey(getDestinationId(issuer), keyId);
        }
    }

    private void validateIssuerClaim(final ValidationContext ctx, final JWTClaimsSet payload) throws EventLogException {
        final String issuer = payload.getIssuer();
        final UUID destinationId = getDestinationId(issuer);
        final Event event = getEventFromClaims(payload);
        if (destinationId == null && resolveIssuerType(issuer).equals(EventIssuer.DESTINATION)) {
            ctx.addError("The event '" + event.getSchemaUri() + "' has to be created by the destination ('iss' claim must be an UUID)");
        }
        if (destinationId != null) {
            ctx.addErrorIfTestFailed(destinationId.equals(ctx.getDestinationId()), "The destination of the submission is not the issuer ('iss' claim must match submission.destinationId)");
        }
    }

    private void validateAuthenticationTags(final ValidationContext ctx, final JWTClaimsSet payload) throws ParseException {
        final AuthenticationTags eventAuthTags = getAuthTags(payload);
        final AuthenticationTags submissionAuthTags = ctx.getAuthenticationTags();
        ctx.addErrorIfTestFailed(eventAuthTags != null, "AuthenticationTags of event must not be null.");
        ctx.addErrorIfTestFailed(submissionAuthTags != null, "AuthenticationTags of submission must not be null.");
        if (eventAuthTags != null && submissionAuthTags != null) {
            validateDataAuthTags(ctx, eventAuthTags.getData(), submissionAuthTags.getData());
            validateMetadataAuthTags(ctx, eventAuthTags.getMetadata(), submissionAuthTags.getMetadata());
            validateAttachmentsAuthTags(ctx, eventAuthTags.getAttachments(), submissionAuthTags.getAttachments());
        }
    }

    private void validateAttachmentsAuthTags(final ValidationContext ctx, final Map eventAttachmentTags, final Map submissionAttachmentTags) {
        if (eventAttachmentTags != null && submissionAttachmentTags != null) {
            ctx.addErrorIfTestFailed(eventAttachmentTags.size() == submissionAttachmentTags.size(), "The events quantity of attachments does not match the submission.");
            eventAttachmentTags.forEach((key, eventTag) -> {
                final String submissionTag = submissionAttachmentTags.get(key);
                ctx.addErrorIfTestFailed(eventTag.equals(submissionTag), "The authentication-tag for the attachment " + key + " does not match the submission.");
            });
        }
    }

    private void validateDataAuthTags(final ValidationContext ctx, final String eventDataAuthTag, final String submissionDataAuthTag) {
        ctx.addErrorIfTestFailed(eventDataAuthTag.equals(submissionDataAuthTag), "The events data authentication-tag does not match the submission.");
    }

    private void validateMetadataAuthTags(final ValidationContext ctx, final String eventMetadataAuthTag, final String submissionMetadataAuthTag) {
        ctx.addErrorIfTestFailed(eventMetadataAuthTag.equals(submissionMetadataAuthTag), "The events metadata authentication-tag does not match the submission.");
    }

    private static boolean eventContainsAuthTags(final JWTClaimsSet payload) throws ParseException {
        final Optional eventClaim = getEventClaim(payload);
        return eventClaim.isPresent() && Event.fromSchemaUri(eventClaim.get()).hasAuthTags();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy