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

bali.notary.V1NotarizationProvider Maven / Gradle / Ivy

There is a newer version: 3.6
Show newest version
/************************************************************************
 * 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 bali.notary;

import craterdog.primitives.Tag;
import craterdog.primitives.VersionString;
import craterdog.security.MessageCryptex;
import craterdog.security.RsaAesMessageCryptex;
import craterdog.utils.RandomUtils;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.security.KeyPair;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Map;
import javax.security.auth.DestroyFailedException;
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 algorithm used to generate hash values for the documents.
     */
    static public final String HASHING_ALGORITHM = cryptex.getHashAlgorithm();

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

    /**
     * The algorithm used to sign and verify the documents.
     */
    static public final String ENCRYPTION_ALGORITHM = cryptex.getAsymmetricEncryptionAlgorithm();

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

    /**
     * The minor version number of the implementation of this digital notary.
     */
    static public final int MINOR_VERSION = 1;

    private final URI certificateRegistry;


    public V1NotarizationProvider(URI certificateRegistry) {
        this.certificateRegistry = certificateRegistry;
    }


    @Override
    public NotaryKey generateKey(char[] passPhrase) {
        logger.entry();  // don't log the passPhrase!

        logger.debug("Generating a new notary key...");
        NotaryKey previousNotaryKey = null;
        NotaryKey notaryKey = generateNotaryKey(passPhrase, previousNotaryKey);

        logger.exit(notaryKey);
        return notaryKey;
    }


    @Override
    public boolean enableKey(NotaryKey notaryKey, char[] passPhrase) {
        logger.entry(notaryKey);
        boolean result;

        try {
            notaryKey.privateKey = cryptex.decodePrivateKey(notaryKey.encodedKey, passPhrase);
            result = true;
        } catch (Exception e) {
            logger.error("Unable to decode the private key for a notary key.", e);
            result = false;
        }

        logger.exit(result);
        return result;
    }


    @Override
    public void disableKey(NotaryKey notaryKey) {
        logger.entry(notaryKey);

        if (notaryKey.privateKey != null) {
            try { notaryKey.privateKey.destroy(); } catch (DestroyFailedException ex) { }
        }
        notaryKey.privateKey = null;

        logger.exit();
    }


    @Override
    public NotaryKey renewKey(NotaryKey notaryKey, char[] passPhrase) {
        logger.entry(notaryKey);  // don't log the passPhrase!

        logger.debug("Renewing an existing notary key...");
        NotaryKey newNotaryKey = generateNotaryKey(passPhrase, notaryKey);

        logger.exit(newNotaryKey);
        return newNotaryKey;
    }


    @Override
    public  Notarized notarizeNewDocument(D document, NotaryKey notaryKey) {
        logger.entry(document, notaryKey);

        logger.debug("Notarizing a new document...");
        Citation certificateCitation = notaryKey.certificateCitation;
        Notarized notaryCertificate = notaryKey.notaryCertificate;
        document.accountTag = notaryCertificate.document.accountTag;
        PrivateKey privateKey = notaryKey.privateKey;
        Notarized notarizedDocument = notarizeDocument(document, 0, null, certificateCitation, privateKey);

        logger.exit(notarizedDocument);
        return notarizedDocument;
    }


    @Override
    public  Notarized notarizeNextVersion(D document, Citation previousCitation, NotaryKey notaryKey) {
        logger.entry(document, previousCitation, notaryKey);

        logger.debug("Notarizing a new version of a document...");
        Citation certificateCitation = notaryKey.certificateCitation;
        Notarized notaryCertificate = notaryKey.notaryCertificate;
        document.accountTag = notaryCertificate.document.accountTag;
        PrivateKey privateKey = notaryKey.privateKey;
        Notarized notarizedDocument = notarizeDocument(document, 0, previousCitation, certificateCitation, privateKey);

        logger.exit(notarizedDocument);
        return notarizedDocument;
    }


    @Override
    public  Notarized notarizeNewSubVersion(D document, int versionLevel, Citation previousCitation, NotaryKey notaryKey) {
        logger.entry(document, versionLevel, previousCitation, notaryKey);

        logger.debug("Notarizing a new version of a document...");
        Citation certificateCitation = notaryKey.certificateCitation;
        Notarized notaryCertificate = notaryKey.notaryCertificate;
        document.accountTag = notaryCertificate.document.accountTag;
        PrivateKey privateKey = notaryKey.privateKey;
        Notarized notarizedDocument = notarizeDocument(document, versionLevel, previousCitation, certificateCitation, privateKey);

        logger.exit(notarizedDocument);
        return notarizedDocument;
    }


    @Override
    public void validateDocument(Notarized notarizedDocument, Certificate certificate, Map errors) {
        logger.entry(notarizedDocument, certificate, errors);
        int errorCount = errors.size();  // record it to see if it changes

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

        if (errorCount == errors.size()) {
            // no new errors, so certificate should be valid
            logger.debug("Validating the notarized document...");
            PublicKey publicKey = certificate.publicKey;
            validateNotarizedDocument(notarizedDocument, publicKey, errors);
        }

        logger.exit(errors);
    }


    @Override
    public Citation generateCitation(URI reference, Notarized notarizedDocument) {
        logger.entry(reference, notarizedDocument);
        Citation citation = generateDocumentCitation(reference, notarizedDocument);
        logger.exit(citation);
        return citation;
    }


    @Override
    public void validateCitation(Notarized notarizedDocument, Citation documentCitation, Map errors) {
        logger.entry(notarizedDocument, documentCitation, errors);
        int errorCount = errors.size();  // record it to see if it changes
        if (documentCitation == null) {
            logger.error("The document citation is missing...");
            errors.put("document.citation.is.missing", documentCitation);
        }
        if (notarizedDocument == null) {
            logger.error("The notarized document is missing...");
            errors.put("notarized.document.is.missing", notarizedDocument);
        }
        if (errorCount == errors.size()) {
            // no new errors, so parameters should be valid
            validateDocumentCitation(documentCitation, notarizedDocument, errors);
        }
        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, null);
        }
        logger.exit();
    }


    private NotaryKey generateNotaryKey(char[] passPhrase, NotaryKey previousKey) {
        logger.debug("Generating a new RSA key pair...");
        char[] copyOfPassPhrase = Arrays.copyOf(passPhrase, passPhrase.length);
        KeyPair keyPair = cryptex.generateKeyPair();
        PublicKey publicKey = keyPair.getPublic();
        PrivateKey privateKey = keyPair.getPrivate();
        String pemValue = cryptex.encodePrivateKey(privateKey, passPhrase);
        String encodedKey = "\n" + pemValue + "\n";

        logger.debug("Slicing the private key using the passPhrase...");
        byte[] passPhraseHash = hashOfPassphrase(copyOfPassPhrase);
        byte[] privateSlice = generatePrivateSlice(privateKey, passPhraseHash);

        logger.debug("Wrapping the public key in a public certificate...");
        Notarized notaryCertificate = generateNotaryCertificate(publicKey, privateKey, privateSlice, previousKey);

        logger.debug("Generating a document citation to the public certificate...");
        Tag certificateTag = notaryCertificate.document.documentTag;
        VersionString certificateVersion = notaryCertificate.document.documentVersion;
        URI certificateReference = URI.create(certificateRegistry + "/" + certificateTag + "." + certificateVersion);
        Citation certificateCitation = generateDocumentCitation(certificateReference, notaryCertificate);

        logger.debug("Populating the new notary key...");
        NotaryKey notaryKey = new NotaryKey();
        notaryKey.certificateCitation = certificateCitation;
        notaryKey.notaryCertificate = notaryCertificate;
        notaryKey.privateKey = privateKey;
        notaryKey.encodedKey = encodedKey;

        return notaryKey;
    }


    private byte[] generatePrivateSlice(PrivateKey privateKey, byte[] hashBytes) {
        byte[] privateKeyBytes = null;
        byte[] randomBytes = null;
        try {
            SecureRandom generator = new SecureRandom(hashBytes);
            privateKeyBytes = privateKey.getEncoded();
            randomBytes = new byte[privateKeyBytes.length];
            generator.nextBytes(randomBytes);
            byte[] privateSlice = xorOfByteArrays(privateKeyBytes, randomBytes);
            return privateSlice;
        } catch (Exception e) {
            RuntimeException exception = new RuntimeException("An unexpected exception occurred while attempting to generate the private slice.", e);
            throw logger.throwing(exception);
        } finally {
            // null out sensitive byte arrays
            if (hashBytes != null) Arrays.fill(hashBytes, (byte) 0);
            if (privateKeyBytes != null) Arrays.fill(privateKeyBytes, (byte) 0);
            if (randomBytes != null) Arrays.fill(randomBytes, (byte) 0);
        }
    }


    private Notarized generateNotaryCertificate(PublicKey publicKey, PrivateKey privateKey, byte[] privateSlice, NotaryKey previousKey) {
        logger.debug("Creating the new certificate...");
        Citation previousCitation = null;
        Certificate certificate;
        if (previousKey != null) {
            // must be notarized by the previous notary key if one exists
            certificate = previousKey.notaryCertificate.document.copy();
            privateKey = previousKey.privateKey;
            previousCitation = previousKey.certificateCitation;
        } else {
            // self signed certificate is for a new account
            certificate = new Certificate();
            certificate.accountTag = new Tag();
        }
        certificate.signingAlgorithm = SIGNING_ALGORITHM;
        certificate.encryptionAlgorithm = ENCRYPTION_ALGORITHM;
        certificate.publicKey = publicKey;
        certificate.privateSlice = privateSlice;

        logger.debug("Notarizing the new certificate...");
        Notarized notaryCertificate = notarizeDocument(certificate, 0, previousCitation, previousCitation, privateKey);

        return notaryCertificate;
    }


    private byte[] generateNotarySeal(Document document, PrivateKey privateKey) {
        try {
            byte[] documentBytes = document.toBytes();
            byte[] notarySeal = cryptex.signBytes(privateKey, documentBytes);
            return notarySeal;
        } 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 Citation generateDocumentCitation(URI reference, Notarized notarizedDocument) {
        Citation citation = new Citation();
        citation.citedDocument = reference;
        citation.hashingAlgorithm = HASHING_ALGORITHM;
        citation.documentHash = hashOfDocument(notarizedDocument, HASHING_ALGORITHM);
        return citation;
    }


    private  Notarized notarizeDocument(D document, int versionLevel, Citation previousCitation, Citation certificateCitation, PrivateKey privateKey) {
        logger.debug("Filling in the document attributes...");
        document.documentType = document.getDocumentType();
        if (previousCitation == null) {
            // assume its a new document
            document.documentTag = new Tag();
            document.documentVersion = new VersionString(1);
        } else {
            // assume we are versioning an existing document
            if (versionLevel > 0) {
                document.documentVersion = VersionString.getNewVersion(document.documentVersion, versionLevel);
            } else {
                document.documentVersion = VersionString.getNextVersion(document.documentVersion);
            }
        }
        document.timestamp = DateTime.now();
        document.notaryCertificateCitation = certificateCitation;
        document.previousVersionCitation = previousCitation;

        logger.debug("Notarizing the document with the notary key...");
        Notarized notarizedDocument = new Notarized<>();
        notarizedDocument.document = document;
        byte[] notarySeal = generateNotarySeal(document, privateKey);
        notarizedDocument.notarySeal = notarySeal;

        return notarizedDocument;
    }


    private void validateNotarizedDocument(Notarized notarizedDocument, PublicKey publicKey, Map errors) {
        int errorCount = errors.size();  // record it to see if it changes
        Document document = notarizedDocument.document;
        byte[] notarySeal = notarizedDocument.notarySeal;
        if (document == null) {
            logger.error("The document is missing...");
            errors.put("document.is.missing", notarizedDocument);
        }
        if (notarySeal == null) {
            logger.error("The notary seal is missing...");
            errors.put("notary.seal.is.missing", notarizedDocument);
        }
        if (errorCount == errors.size()) {
            // no new errors, so parameters should be valid
            validateDocument(document, errors);
            validateNotarySeal(document, notarySeal, publicKey, errors);
        }
    }


    private void validateDocument(Document document, Map errors) {
        if (document.accountTag == null) {
            logger.error("The document account tag is missing...");
            errors.put("document.account.tag.is.missing", document);
        }
        if (document.documentTag == null) {
            logger.error("The document tag is missing...");
            errors.put("document.tag.is.missing", document);
        }
        if (document.documentVersion == null) {
            logger.error("The document version is missing...");
            errors.put("document.version.is.missing", document);
        }
        if (document.documentType == null) {
            logger.error("The document type is missing...");
            errors.put("document.type.is.missing", document);
        }
        if (document.timestamp == null) {
            logger.error("The document timestamp is missing...");
            errors.put("document.timestamp.is.missing", document);
        }
        boolean firstVersion = document.documentVersion.equals(new VersionString(1));
        if (!firstVersion && document.previousVersionCitation == null) {
            logger.error("The previous document citation is missing...");
            errors.put("previous.document.citation.is.missing", document);
        }
        if (firstVersion && document.previousVersionCitation != null) {
            logger.error("The previous document citation should be null for the first version...");
            errors.put("previous.document.citation.should.be.null", document);
        }
        boolean isCertificate = document instanceof Certificate;
        if (!isCertificate && document.notaryCertificateCitation == null) {
            logger.error("The notary certificate citation is missing...");
            errors.put("notary.certificate.citation.is.missing", document);
        }
        if (isCertificate && !firstVersion && document.notaryCertificateCitation == null) {
            logger.error("The notary certificate citation is missing...");
            errors.put("notary.certificate.citation.is.missing", document);
        }
        if (isCertificate && firstVersion && document.notaryCertificateCitation != null) {
            logger.error("The notary certificate citation should be null for the first certificate...");
            errors.put("notary.certificate.citation.should.be.null", document);
        }
    }


    private void validateNotaryKey(NotaryKey notaryKey, Map errors) {
        int errorCount = errors.size();  // record it to see if it changes
        Notarized notaryCertificate = notaryKey.notaryCertificate;
        PrivateKey privateKey = notaryKey.privateKey;
        Certificate certificate = null;
        if (notaryCertificate == null) {
            logger.error("The notary certificate is missing...");
            errors.put("notary.certificate.is.missing", notaryKey);
        } else {
            certificate = notaryCertificate.document;
            PublicKey publicKey = certificate.publicKey;
            validatePrivateKey(privateKey, publicKey, errors);
        }
        if (privateKey == null) {
            logger.error("The private key is missing...");
            errors.put("private.key.is.missing", notaryKey);
        }
        if (errorCount == errors.size()) {
            // no new errors, so parameters should be valid
            validateCertificate(certificate, errors);
        }
    }


    private void validateCertificate(Certificate certificate, Map errors) {
        validateDocument(certificate, errors);
        if (certificate.signingAlgorithm == null) {
            logger.error("The notary certificate signing algorithm is missing...");
            errors.put("notary.certificate.signing.algorithm.is.missing", certificate);
        }
        if (certificate.encryptionAlgorithm == null) {
            logger.error("The notary certificate encryption algorithm is missing...");
            errors.put("notary.certificate.encryption.algorithm.is.missing", certificate);
        }
        if (certificate.publicKey == null) {
            logger.error("The notary certificate public key is missing...");
            errors.put("notary.certificate.public.key.is.missing", certificate);
        }
        if (certificate.privateSlice == null) {
            logger.error("The notary certificate private slice is missing...");
            errors.put("notary.certificate.private.slice.is.missing", certificate);
        }
    }


    private void validatePrivateKey(PrivateKey privateKey, PublicKey publicKey, Map errors) {
        String algorithm = privateKey.getAlgorithm();
        if (!ENCRYPTION_ALGORITHM.startsWith(algorithm)) {
            logger.error("The private key is of the wrong type...");
            errors.put("private.key.is.wrong.type", algorithm);
        }
        byte[] randomBytes = RandomUtils.generateRandomBytes(16);
        byte[] signatureBytes = cryptex.signBytes(privateKey, randomBytes);
        if (!cryptex.bytesAreValid(publicKey, randomBytes, signatureBytes)) {
            logger.error("The private key does not correspond to the public key...");
            errors.put("private.key.not.valid", privateKey);
        }
    }


    private void validateNotarySeal(Document document, byte[] notarySeal, PublicKey publicKey, Map errors) {
        byte[] documentBytes = document.toBytes();
        if (!cryptex.bytesAreValid(publicKey, documentBytes, notarySeal)) {
            logger.error("The document signature is not valid...");
            errors.put("document.is.not.valid", document);
            errors.put("document.notary.seal.is.not.valid", notarySeal);
            errors.put("document.public.key.does.not.match", publicKey);
        }
    }


    private void validateDocumentCitation(Citation documentCitation, Notarized notarizedDocument, Map errors) {
        String hashingAlgorithm = documentCitation.hashingAlgorithm;
        byte[] documentHash = hashOfDocument(notarizedDocument, hashingAlgorithm);
        if (!Arrays.equals(documentHash, documentCitation.documentHash)) {
            logger.error("The document hash is not valid...");
            errors.put("document.hash.is.not.valid", documentCitation);
            errors.put("document.is.not.valid", notarizedDocument);
        }
    }


    private byte[] hashOfDocument(Notarized document, String algorithm) {
        try {
            byte[] documentBytes = document.toBytes();
            MessageDigest hasher = MessageDigest.getInstance(algorithm);
            byte[] documentHash = hasher.digest(documentBytes);
            return documentHash;
        } catch (Exception e) {
            RuntimeException exception = new RuntimeException("An unexpected exception occurred while attempting to hash the following document: " + document, e);
            throw logger.throwing(exception);
        }
    }


    private byte[] hashOfPassphrase(char[] passPhrase) {
        CharBuffer charBuffer = null;
        ByteBuffer byteBuffer = null;
        byte[] passPhraseBytes = null;
        try {
            charBuffer = CharBuffer.wrap(passPhrase);
            byteBuffer = Charset.forName("UTF-8").encode(charBuffer);
            passPhraseBytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit());
            MessageDigest hasher = MessageDigest.getInstance(HASHING_ALGORITHM);
            byte[] hashBytes = hasher.digest(passPhraseBytes);
            return hashBytes;
        } catch (Exception e) {
            RuntimeException exception = new RuntimeException("An unexpected exception occurred while attempting to convert a char[] to byte[].", e);
            throw logger.throwing(exception);
        } finally {
            // null out all traces of the pass phrase
            if (passPhrase != null) Arrays.fill(passPhrase, '\u0000');
            if (charBuffer != null) Arrays.fill(charBuffer.array(), '\u0000');
            if (byteBuffer != null) Arrays.fill(byteBuffer.array(), (byte) 0);
            if (passPhraseBytes != null) Arrays.fill(passPhraseBytes, (byte) 0);
        }
    }


    private byte[] xorOfByteArrays(byte[] first, byte[] second) {
        int length = first.length;
        if (length != second.length) {
            RuntimeException exception = new RuntimeException("The private key and the hashed passPhrase are different lengths: " + first.length + " and " + second.length);
            throw logger.throwing(exception);
        }
        byte[] xor = new byte[length];
        for (int i = 0; i < length; i++) {
            xor[i] = (byte) (0xff & (first[i] ^ second[i]));
        }
        return xor;
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy