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.BinaryString;
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 java.util.Objects;
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 = 0;


    @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) {
        try {
            notaryKey.privateKey = cryptex.decodePrivateKey(notaryKey.encodedKey, passPhrase);
        } catch (Exception e) {
            logger.error("Unable to decode the private key for a notary key.", e);
            return false;
        }
        return true;
    }


    @Override
    public void disableKey(NotaryKey notaryKey) {
        if (notaryKey.privateKey != null) {
            try { notaryKey.privateKey.destroy(); } catch (DestroyFailedException ex) { }
        }
        notaryKey.privateKey = null;
    }


    @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  NotarizedDocument notarizeDocument(D document, NotaryKey notaryKey) {
        logger.entry(document, notaryKey);

        logger.debug("Creating the notary certificate citation...");
        PrivateKey privateKey = notaryKey.privateKey;
        NotarizedDocument notaryCertificate = notaryKey.notaryCertificate;

        logger.debug("Filling in the document attributes...");
        document.accountTag = notaryCertificate.document.accountTag;
        document.timestamp = DateTime.now();
        document.certificateCitation = notaryKey.certificateCitation;

        logger.debug("Notarizing the document with the notary key...");
        NotarizedDocument notarizedDocument = new NotarizedDocument<>();
        notarizedDocument.document = document;
        BinaryString notarySeal = generateNotarySeal(document, privateKey);
        notarizedDocument.notarySeal = notarySeal;

        logger.exit(notarizedDocument);
        return notarizedDocument;
    }


    @Override
    public void validateDocument(NotarizedDocument 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 DocumentCitation generateCitation(URI reference, NotarizedDocument notarizedDocument) {
        logger.entry(notarizedDocument);
        DocumentCitation citation = generateDocumentCitation(reference, notarizedDocument);
        logger.exit(citation);
        return citation;
    }


    @Override
    public void validateCitation(NotarizedDocument notarizedDocument, DocumentCitation documentCitation, Map errors) {
        logger.entry(documentCitation, notarizedDocument, 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();
    }


    static 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);
        BinaryString privateSlice = generatePrivateSlice(privateKey, passPhraseHash);

        logger.debug("Wrapping the public key in a public certificate...");
        NotarizedDocument 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("bali:/" + certificateTag + "." + certificateVersion);
        DocumentCitation 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;
    }


    static private BinaryString 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[] sliceBytes = xorOfByteArrays(privateKeyBytes, randomBytes);
            BinaryString privateSlice = new BinaryString(sliceBytes);
            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);
        }
    }


    static private Certificate generateCertificate(Tag accountTag, Tag documentTag, VersionString documentVersion, PublicKey publicKey, BinaryString privateSlice, DocumentCitation previousCertificate) {
        URI documentType = URI.create("bali:/?name=bali.account.model.Certificate&version=1");
        Certificate certificate = new Certificate();
        certificate.accountTag = accountTag;
        certificate.documentType = documentType;
        certificate.documentTag = documentTag;
        certificate.documentVersion = documentVersion;
        certificate.timestamp = DateTime.now();
        certificate.certificateCitation = previousCertificate;
        certificate.previousDocument = previousCertificate;
        certificate.signingAlgorithm = SIGNING_ALGORITHM;
        certificate.encryptionAlgorithm = ENCRYPTION_ALGORITHM;
        certificate.publicKey = publicKey;
        certificate.privateSlice = privateSlice;
        return certificate;
    }


    static private NotarizedDocument generateNotaryCertificate(PublicKey publicKey, PrivateKey privateKey, BinaryString privateSlice, NotaryKey previousKey) {
        logger.debug("Gathering the certificate attributes...");
        Tag accountTag;
        Tag documentTag;
        VersionString documentVersion;
        DocumentCitation previousDocument;
        if (previousKey == null) {
            accountTag = new Tag();
            documentTag = new Tag();
            documentVersion = new VersionString(1);
            previousDocument = null;
        } else {
            Certificate previousCertificate = previousKey.notaryCertificate.document;
            accountTag = previousCertificate.accountTag;
            documentTag = previousCertificate.documentTag;
            VersionString previousDocumentVersion = previousCertificate.documentVersion;
            documentVersion = VersionString.getNextVersion(previousDocumentVersion);
            previousDocument = previousKey.certificateCitation;
        }

        logger.debug("Creating the new certificate...");
        Certificate certificate = generateCertificate(accountTag, documentTag, documentVersion, publicKey, privateSlice, previousDocument);

        logger.debug("Notarizing the new certificate...");
        BinaryString notarySeal;
        if (previousKey != null) {
            // must be notarized by the previous notary key if one exists
            notarySeal = generateNotarySeal(certificate, previousKey.privateKey);
        } else {
            // otherwise it is a self signed certificate
            notarySeal = generateNotarySeal(certificate, privateKey);
        }
        NotarizedDocument notaryCertificate = new NotarizedDocument<>();
        notaryCertificate.document = certificate;
        notaryCertificate.notarySeal = notarySeal;

        return notaryCertificate;
    }


    static private BinaryString generateNotarySeal(Document document, PrivateKey privateKey) {
        try {
            byte[] documentBytes = document.toBytes();
            byte[] signatureBytes = cryptex.signBytes(privateKey, documentBytes);
            BinaryString notarySeal = new BinaryString(signatureBytes);
            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);
        }
    }


    static private DocumentCitation generateDocumentCitation(URI reference, NotarizedDocument notarizedDocument) {
        DocumentCitation citation = new DocumentCitation();
        citation.citedDocument = reference;
        citation.hashingAlgorithm = HASHING_ALGORITHM;
        citation.documentHash = hashOfDocument(notarizedDocument, HASHING_ALGORITHM);
        return citation;
    }


    static private void validateNotarizedDocument(NotarizedDocument notarizedDocument, PublicKey publicKey, Map errors) {
        int errorCount = errors.size();  // record it to see if it changes
        Document document = notarizedDocument.document;
        BinaryString 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);
        }
    }


    static 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.previousDocument == null) {
            logger.error("The previous document citation is missing...");
            errors.put("previous.document.citation.is.missing", document);
        }
        if (firstVersion && document.previousDocument != 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.certificateCitation == null) {
            logger.error("The notary certificate citation is missing...");
            errors.put("notary.certificate.citation.is.missing", document);
        }
        if (isCertificate && !firstVersion && document.certificateCitation == null) {
            logger.error("The notary certificate citation is missing...");
            errors.put("notary.certificate.citation.is.missing", document);
        }
        if (isCertificate && firstVersion && document.certificateCitation != null) {
            logger.error("The notary certificate citation should be null for the first certificate...");
            errors.put("notary.certificate.citation.should.be.null", document);
        }
    }


    static private void validateNotaryKey(NotaryKey notaryKey, Map errors) {
        int errorCount = errors.size();  // record it to see if it changes
        NotarizedDocument 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);
        }
    }


    static 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);
        }
    }


    static 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);
        }
    }


    static private void validateNotarySeal(Document document, BinaryString notarySeal, PublicKey publicKey, Map errors) {
        byte[] documentBytes = document.toBytes();
        byte[] signatureBytes = notarySeal.toBytes();
        if (!cryptex.bytesAreValid(publicKey, documentBytes, signatureBytes)) {
            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);
        }
    }


    static private void validateDocumentCitation(DocumentCitation documentCitation, NotarizedDocument notarizedDocument, Map errors) {
        String hashingAlgorithm = documentCitation.hashingAlgorithm;
        BinaryString documentHash = hashOfDocument(notarizedDocument, hashingAlgorithm);
        if (!Objects.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);
        }
    }


    static private BinaryString hashOfDocument(NotarizedDocument document, String algorithm) {
        try {
            byte[] documentBytes = document.toBytes();
            MessageDigest hasher = MessageDigest.getInstance(algorithm);
            byte[] hash = hasher.digest(documentBytes);
            BinaryString hashBytes = new BinaryString(hash);
            BinaryString documentHash = hashBytes;
            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);
        }
    }


    static 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);
        }
    }


    static 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 - 2025 Weber Informatics LLC | Privacy Policy