bali.notary.V1NotarizationProvider Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of java-bali-digital-notary Show documentation
Show all versions of java-bali-digital-notary Show documentation
This project defines the Java interfaces and classes for a digital notary.
/************************************************************************
* 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 extends Document> 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 extends Document> notarizedDocument) {
logger.entry(reference, notarizedDocument);
Citation citation = generateDocumentCitation(reference, notarizedDocument);
logger.exit(citation);
return citation;
}
@Override
public void validateCitation(Notarized extends Document> 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 extends Document> 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 extends Document> 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 extends Document> 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 extends Document> 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;
}
}