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

craterdog.notary.V1NotarizationProvider Maven / Gradle / Ivy

/************************************************************************
 * Copyright (c) Crater Dog Technologies(TM).  All Rights Reserved.     *
 ************************************************************************
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.        *
 *                                                                      *
 * This code is free software; you can redistribute it and/or modify it *
 * under the terms of The MIT License (MIT), as published by the Open   *
 * Source Initiative. (See http://opensource.org/licenses/MIT)          *
 ************************************************************************/
package craterdog.notary;

import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import craterdog.primitives.Tag;
import craterdog.notary.mappers.NotaryModule;
import craterdog.security.MessageCryptex;
import craterdog.security.RsaAesMessageCryptex;
import craterdog.smart.SmartObject;
import craterdog.utils.Base32Utils;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.KeyPair;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.LinkedHashMap;
import java.util.Map;
import org.joda.time.DateTime;
import org.slf4j.ext.XLogger;
import org.slf4j.ext.XLoggerFactory;

/**
 * This class can be used to sign and verify digital notarized documents using a private/public
 * key pair.  It uses RSA-2048 for asymmetric (public/private) key signature generation and
 * verification.
 *
 * @author Derk Norton
 */
public final class V1NotarizationProvider implements Notarization {

    static private final XLogger logger = XLoggerFactory.getXLogger(V1NotarizationProvider.class);

    static private final MessageCryptex cryptex = new RsaAesMessageCryptex();

    /**
     * The hashing algorithm used to generate hash values for the documents.
     */
    public final String hashingAlgorithm = cryptex.getHashAlgorithm();

    /**
     * The signing algorithm used to sign and verify the documents.
     */
    public final String signingAlgorithm = cryptex.getAsymmetricSignatureAlgorithm();

    /**
     * The major version number of the implementation of this digital notary.
     */
    public final int majorVersion = 1;

    /**
     * The minor version number of the implementation of this digital notary.
     */
    public final int minorVersion = 0;


    @Override
    public Watermark generateWatermark(int secondsToLive) {
        logger.entry(secondsToLive);
        Watermark watermark = new Watermark();
        watermark.hashingAlgorithm = hashingAlgorithm;
        watermark.signingAlgorithm = signingAlgorithm;
        watermark.majorVersion = majorVersion;
        watermark.minorVersion = minorVersion;
        watermark.creationTimestamp = DateTime.now();
        watermark.expirationTimestamp = watermark.creationTimestamp.plusSeconds(secondsToLive);
        logger.exit(watermark);
        return watermark;
    }


    @Override
    public void validateWatermark(Watermark watermark, Map errors) {
        logger.entry(watermark, errors);
        if (watermark == null) {
            logger.error("The watermark is missing...");
            errors.put("watermark.is.missing", watermark);
        } else {
            if (watermark.hashingAlgorithm == null) {
                logger.error("The watermark hashing algorithm is missing...");
                errors.put("watermark.hashing.algorithm.is.missing", watermark);
            }
            if (watermark.signingAlgorithm == null) {
                logger.error("The watermark signing algorithm is missing...");
                errors.put("watermark.signing.algorithm.is.missing", watermark);
            }
            if (watermark.creationTimestamp == null) {
                logger.error("The watermark creation timestamp is missing...");
                errors.put("watermark.creation.timestamp.is.missing", watermark);
            }
            if (watermark.expirationTimestamp == null) {
                logger.error("The watermark expiration timestamp is missing...");
                errors.put("watermark.expiration.timestamp.is.missing", watermark);
            } else if (watermark.expirationTimestamp.isBeforeNow()) {
                logger.error("The watermark has expired...");
                errors.put("watermark.has.expired", watermark);
            }
        }
        logger.exit(errors);
    }


    @Override
    public DocumentCitation generateDocumentCitation(URI location, String document) {
        logger.entry(location, document);
        DocumentCitation citation = new DocumentCitation();
        citation.documentLocation = location;
        citation.documentHash = hashDocument(document);
        logger.exit(citation);
        return citation;
    }


    @Override
    public void validateDocumentCitation(DocumentCitation citation, String document, Map errors) {
        logger.entry(citation, document, errors);
        if (citation == null) {
            logger.error("The document citation is missing...");
            errors.put("citation.is.missing", citation);
        } else {
            if (citation.documentLocation == null) {
                logger.error("The document citation location is missing...");
                errors.put("citation.location.is.missing", citation);
            }
            if (citation.documentHash == null || citation.documentHash.isEmpty()) {
                logger.error("The document citation hash is missing...");
                errors.put("citation.hash.is.missing", citation);
            }
            if (!citation.documentHash.equals(hashDocument(document))) {
                logger.error("The document citation hash does not match the document hash...");
                errors.put("citation.hash.is.invalid", citation);
                errors.put("cited.document.does.not.match", document);
            }
        }
        logger.exit(errors);
    }


    @Override
    public NotaryKey generateNotaryKey(URI baseUri) {
        logger.entry(baseUri);
        NotaryKey notaryKey = generateNotaryKey(baseUri, null, null);
        logger.exit(notaryKey);
        return notaryKey;
    }


    @Override
    public NotaryKey generateNotaryKey(URI baseUri, Map additionalAttributes) {
        logger.entry(baseUri, additionalAttributes);
        NotaryKey notaryKey = generateNotaryKey(baseUri, additionalAttributes, null);
        logger.exit(notaryKey);
        return notaryKey;
    }


    @Override
    public NotaryKey generateNotaryKey(URI baseUri, NotaryKey previousKey) {
        logger.entry(baseUri, previousKey);
        NotaryKey notaryKey = generateNotaryKey(baseUri, null, previousKey);
        logger.exit(notaryKey);
        return notaryKey;
    }


    @Override
    public NotaryKey generateNotaryKey(URI baseUri, Map additionalAttributes, NotaryKey previousKey) {
        logger.entry(baseUri, additionalAttributes, previousKey);

        logger.debug("Generating a new RSA key pair...");
        KeyPair keyPair = cryptex.generateKeyPair();
        PrivateKey privateKey = keyPair.getPrivate();
        PublicKey publicKey = keyPair.getPublic();

        logger.debug("Creating the watermark...");
        Watermark watermark = generateWatermark(VALID_FOR_ONE_YEAR);

        logger.debug("Wrapping the verification key in a certificate...");
        NotaryCertificate certificate = generateNotaryCertificate(baseUri, publicKey, privateKey, additionalAttributes, watermark, previousKey);

        logger.debug("Creating a document citation to the verification certificate...");
        URI documentLocation = certificate.attributes.myLocation;
        String document = certificate.toString();
        DocumentCitation citation = generateDocumentCitation(documentLocation, document);

        logger.debug("Assembling the notary key...");
        NotaryKey notaryKey = new NotaryKey();
        notaryKey.watermark = watermark;
        notaryKey.signingKey = privateKey;
        notaryKey.verificationCertificate = certificate;
        notaryKey.verificationCitation = citation;

        logger.exit(notaryKey);
        return notaryKey;
    }


    @Override
    public String serializeNotaryKey(NotaryKey notaryKey, char[] password) {
        logger.entry(notaryKey);

        logger.debug("Marshalling the notary key into a JSON string...");
        String json;
        try {
            ObjectMapper mapper = SmartObject.createMapper(new NotaryModule(password));
            json = mapper.writeValueAsString(notaryKey);
        } catch (Exception e) {
            RuntimeException exception = new RuntimeException("An unexpected exception occurred while attempting to serialize a notary key.", e);
            throw logger.throwing(exception);
        }

        logger.exit(json);
        return json;
    }


    @Override
    public NotaryKey deserializeNotaryKey(String json, char[] password) throws IOException {
        logger.entry(json);

        logger.debug("Unmarshalling the notary key from a JSON string...");
        NotaryKey notaryKey;
        try {
            ObjectMapper mapper = SmartObject.createMapper(new NotaryModule(password));
            notaryKey = mapper.readValue(json, NotaryKey.class);
        } catch (JsonMappingException e) {
            String messageTag = "invalid.notary.key.password";
            Map errors = new LinkedHashMap<>();
            errors.put("json.string", json);
            logger.error("The notary key password is invalid for the following notary key: {}", json);
            throw new ValidationException(messageTag, errors);
        } catch (Exception e) {
            RuntimeException exception = new RuntimeException("An unexpected exception occurred while attempting to deserialize a notary key: " + json, e);
            throw logger.throwing(exception);
        }

        logger.debug("Validating the notary key...");
        Map errors = new LinkedHashMap<>();
        validateNotaryKey(notaryKey, errors);
        throwExceptionOnErrors("invalid.serialized.notary.key", errors);

        logger.exit(notaryKey);
        return notaryKey;
    }


    @Override
    public void validateNotaryCertificate(NotaryCertificate certificate, NotaryCertificate previousCertificate, Map errors) {
        logger.entry(certificate, previousCertificate);
        int errorCount = errors.size();  // record it to see if it changes

        logger.debug("Validating the certificate attributes...");
        validateNotaryCertificate(certificate, errors);

        logger.debug("Validating the certificate attributes...");
        validateNotaryCertificate(previousCertificate, errors);

        if (errors.size() == errorCount) {
            // no new errors, so parameters should be valid
            logger.debug("Validating the certificate seal...");
            validateNotarySeal(certificate.certificationSeal, previousCertificate, errors);
        }

        logger.exit();
    }


    @Override
    public NotarySeal notarizeDocument(String documentType, String document, NotaryKey notaryKey) {
        logger.entry(documentType, document, notaryKey);

        logger.debug("Verifying that the notary key has not expired...");
        Map errors = new LinkedHashMap<>();
        Watermark watermark = notaryKey.watermark;
        validateWatermark(watermark, errors);
        throwExceptionOnErrors("notary.key.has.expired", errors);

        logger.debug("Creating the notary seal attributes...");
        SealAttributes attributes = new SealAttributes();
        attributes.documentType = documentType;
        attributes.documentHash = hashDocument(document);
        attributes.verificationCitation = notaryKey.verificationCitation;
        attributes.watermark = generateWatermark(Notarization.VALID_FOR_FOREVER);

        logger.debug("Signing the notary seal...");
        NotarySeal seal = new NotarySeal();
        PrivateKey signingKey = notaryKey.signingKey;
        seal.attributes = attributes;
        seal.selfSignature = generateDocumentSignature(attributes.toString(), signingKey);

        logger.exit(seal);
        return seal;
    }


    @Override
    public void validateDocument(String document, NotarySeal seal, NotaryCertificate certificate, Map errors) {
        logger.entry(document, seal, certificate, errors);
        int errorCount = errors.size();  // record it to see if it changes

        logger.debug("Validating the notary certificate...");
        validateNotaryCertificate(certificate, errors);

        logger.debug("Validating the digital seal...");
        validateNotarySeal(seal, certificate, errors);

        if (document == null || document.isEmpty()) {
            logger.error("The document to be validated is missing...");
            errors.put("document.is.missing", document);
        }
        if (errorCount == errors.size()) {
            // no new errors, so parameters should be valid
            logger.debug("Validating the hash of the document...");
            String documentHash = seal.attributes.documentHash;
            if (!documentHash.equals(hashDocument(document))) {
                logger.error("The document hash does not match the hash in the notary seal...");
                errors.put("document.hash.is.invalid", document);
            }
        }

        logger.exit(errors);
    }


    @Override
    public void throwExceptionOnErrors(String messageTag, Map errors) throws ValidationException {
        logger.entry(messageTag, errors);
        if (!errors.isEmpty()) {
            logger.error("A validation exception \"" + messageTag + "\" was thrown with the following errors: {}", errors);
            throw new ValidationException(messageTag, errors);
        }
        logger.exit();
    }


    private void validateNotaryKey(NotaryKey notaryKey, Map errors) {
        int errorCount = errors.size();  // record it to see if it changes

        logger.debug("Validating the watermark for the notary key...");
        validateWatermark(notaryKey.watermark, errors);

        logger.debug("Validating the signing key for the notary key...");
        validateSigningKey(notaryKey.signingKey, errors);

        logger.debug("Validating the verification certificate for the notary key...");
        validateNotaryCertificate(notaryKey.verificationCertificate, errors);

        if (errorCount == errors.size()) {
            // no new errors, so parameters should be valid
            logger.debug("Validating the certificate citation for the notary key...");
            DocumentCitation citation = notaryKey.verificationCitation;
            String document = notaryKey.verificationCertificate.toString();
            validateDocumentCitation(citation, document, errors);
        }
    }


    private void validateSigningKey(PrivateKey signingKey, Map errors) {
        if (signingKey == null) {
            logger.error("The signing key is missing...");
            errors.put("signing.key.is.missing", signingKey);
        }
    }


    private void validateNotarySeal(NotarySeal seal, NotaryCertificate certificate, Map errors) {
        if (seal == null) {
            logger.error("The notary seal is missing...");
            errors.put("seal.is.missing", seal);
        } else {
            int errorCount = errors.size();  // record it to see if it changes
            String selfSignature = seal.selfSignature;
            if (selfSignature == null) {
                logger.error("The notary seal self signature is missing...");
                errors.put("seal.self.signature.is.missing", seal);
            }
            SealAttributes attributes = seal.attributes;
            if (attributes == null) {
                logger.error("The notary seal attributes are missing...");
                errors.put("seal.attributes.are.missing", seal);
            } else {
                String documentType = attributes.documentType;
                if (documentType == null || documentType.isEmpty()) {
                    logger.error("The notary seal document type is missing...");
                    errors.put("seal.document.type.is.missing", seal);
                }
                String documentHash = attributes.documentHash;
                if (documentHash == null || documentHash.isEmpty()) {
                    logger.error("The notary seal document hash is missing...");
                    errors.put("seal.document.hash.is.missing", seal);
                }
                Watermark watermark = attributes.watermark;
                if (watermark == null) {
                    logger.error("The notary seal watermark is missing...");
                    errors.put("seal.watermark.is.missing", seal);
                }
                DocumentCitation verificationCitation = attributes.verificationCitation;
                if (verificationCitation == null) {
                    logger.error("The notary seal verification citation is missing...");
                    errors.put("seal.verification.citation.is.missing", seal);
                }
            }
            if (errors.size() == errorCount) {
                // no new errors, so parameters should be valid
                PublicKey verificationKey = certificate.attributes.verificationKey;
                String document = seal.attributes.toString();
                validateDocumentSignature(document, selfSignature, verificationKey, errors);
                DocumentCitation verificationCitation = seal.attributes.verificationCitation;
                validateDocumentCitation(verificationCitation, certificate.toString(), errors);
            }
        }
    }


    private NotaryCertificate generateNotaryCertificate(URI baseUri, PublicKey publicKey, PrivateKey privateKey, Map additionalAttributes, Watermark watermark, NotaryKey previousKey) {
        CertificateAttributes attributes = new CertificateAttributes();
        try {
            attributes.myLocation = new URI(baseUri + "/certificate/" + new Tag());
            if (previousKey != null) {
                // the identity already exists so use it
                attributes.identityLocation = previousKey.verificationCertificate.attributes.identityLocation;
                attributes.sequenceNumber = previousKey.verificationCertificate.attributes.sequenceNumber + 1;
            } else {
                // this is a new identity
                attributes.identityLocation = new URI(baseUri + "/identity/" + new Tag());
                attributes.sequenceNumber = 1;  // first one in the list
            }
        } catch (URISyntaxException e) {
            RuntimeException exception = new RuntimeException("An unexpected exception occurred while attempting to create location URIs from base URI: " + baseUri, e);
            throw logger.throwing(exception);
        }
        attributes.verificationKey = publicKey;
        if (additionalAttributes != null) {
            for (Map.Entry attribute : additionalAttributes.entrySet()) {
                attributes.put(attribute.getKey(), attribute.getValue());
            }
        }
        attributes.watermark = watermark;
        NotaryCertificate certificate = new NotaryCertificate();
        certificate.attributes = attributes;
        certificate.selfSignature = generateDocumentSignature(attributes.toString(), privateKey);
        if (previousKey != null) {
            String documentType = "Self Signature";
            String document = certificate.selfSignature;
            certificate.certificationSeal = notarizeDocument(documentType, document, previousKey);
        }
        return certificate;
    }


    private void validateNotaryCertificate(NotaryCertificate certificate, Map errors) {
        if (certificate == null) {
            logger.error("The notary certificate is missing...");
            errors.put("certificate.is.missing", certificate);
        } else {
            int errorCount = errors.size();  // record it to see if it changes
            CertificateAttributes attributes = certificate.attributes;
            if (attributes == null) {
                logger.error("The notary certificate attributes are missing...");
                errors.put("certificate.attributes.are.missing", certificate);
            } else {
                if (attributes.myLocation == null) {
                    logger.error("The notary certificate location is missing...");
                    errors.put("certificate.location.is.missing", certificate);
                }
                if (attributes.identityLocation == null) {
                    logger.error("The notary certificate identity location is missing...");
                    errors.put("certificate.identity.location.is.missing", certificate);
                }
                if (attributes.watermark == null) {
                    logger.error("The notary certificate watermark is missing...");
                    errors.put("certificate.watermark.is.missing", certificate);
                }
                if (attributes.verificationKey == null) {
                    logger.error("The notary certificate verification key is missing...");
                    errors.put("certificate.verification.key.is.missing", certificate);
                }
            }
            if (errors.size() == errorCount) {
                // the error count did not change so the parameters should be valid
                String document = certificate.attributes.toString();
                String signature = certificate.selfSignature;
                PublicKey verificationKey = certificate.attributes.verificationKey;
                validateDocumentSignature(document, signature, verificationKey, errors);
            }

            // We cannot valid the certification seal without the previous certificate
            // and we cannot pass in the previous certificate since this ends up being
            // recursive. It is up to the higher level services to validate the chain.
        }
    }


    private String generateDocumentSignature(String document, PrivateKey signingKey) {
        try {
            byte[] documentBytes = document.getBytes("UTF-8");
            byte[] signatureBytes = cryptex.signBytes(signingKey, documentBytes);
            String signature = Base32Utils.encode(signatureBytes);
            return signature;
        } catch (Exception e) {
            RuntimeException exception = new RuntimeException("An unexpected exception occurred while attempting to notarize the following document: " + document, e);
            throw logger.throwing(exception);
        }
    }


    private void validateDocumentSignature(String document, String signature, PublicKey verificationKey, Map errors) {
        try {
            byte[] documentBytes = document.getBytes("UTF-8");
            byte[] signatureBytes = Base32Utils.decode(signature);
            if (!cryptex.bytesAreValid(verificationKey, documentBytes, signatureBytes)) {
                logger.error("The document signature is not valid...");
                errors.put("document.is.not.valid", document);
                errors.put("document.signature.is.not.valid", signature);
                errors.put("document.verification.key.does.not.match", verificationKey);
            }
        } catch (UnsupportedEncodingException e) {
            RuntimeException exception = new RuntimeException("An unexpected exception occurred while attempting to validate the following document: " + document, e);
            throw logger.throwing(exception);
        }
    }


    private String hashDocument(String document) {
        try {
            byte[] bytes = document.getBytes();
            MessageDigest hasher = MessageDigest.getInstance("SHA-256");
            byte[] hash = hasher.digest(bytes);
            String hashString = Base32Utils.encode(hash);
            return hashString;
        } catch (NoSuchAlgorithmException e) {
            RuntimeException exception = new RuntimeException("An unexpected exception occurred while attempting to hash the following document: " + document, e);
            throw logger.throwing(exception);
        }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy