dev.fitko.fitconnect.core.cases.EventLogVerifier Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of client Show documentation
Show all versions of client Show documentation
Library that provides client access to the FIT-Connect api-endpoints for sending, subscribing and
routing
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();
}
}