ENT-4628: Harmonize net.corda.nodeapi.internal.crypto between OS and ENT (#5820)

* ENT-4628: Harmonize net.corda.nodeapi.internal.crypto between OS and ENT

* ENT-4628: Fix detekt
This commit is contained in:
Denis Rekalov 2019-12-18 13:59:30 +00:00 committed by Matthew Nesbit
parent af30e40397
commit 8d5781db43
8 changed files with 99 additions and 37 deletions

View File

@ -223,6 +223,7 @@
<ID>EmptyDefaultConstructor:FlowRetryTest.kt$RetryFlow$()</ID>
<ID>EmptyDefaultConstructor:FlowRetryTest.kt$ThrowingFlow$()</ID>
<ID>EmptyElseBlock:CordaCliWrapper.kt${ }</ID>
<ID>EmptyIfBlock:ContentSignerBuilder.kt$ContentSignerBuilder.SignatureOutputStream$if (alreadySigned) throw IllegalStateException("Cannot write to already signed object")</ID>
<ID>EmptyIfBlock:InMemoryIdentityService.kt$InMemoryIdentityService${ }</ID>
<ID>EmptyKtFile:KryoHook.kt$.KryoHook.kt</ID>
<ID>EmptyKtFile:ValidatingNotaryService.kt$.ValidatingNotaryService.kt</ID>
@ -1316,8 +1317,6 @@
<ID>MaxLineLength:AbstractNode.kt$AbstractNode$parseSecureHashConfiguration(configuration.blacklistedAttachmentSigningKeys) { "Error while adding signing key $it to blacklistedAttachmentSigningKeys" }</ID>
<ID>MaxLineLength:AbstractNode.kt$AbstractNode$parseSecureHashConfiguration(configuration.cordappSignerKeyFingerprintBlacklist) { "Error while adding key fingerprint $it to blacklistedAttachmentSigningKeys" }</ID>
<ID>MaxLineLength:AbstractNode.kt$AbstractNode$private</ID>
<ID>MaxLineLength:AbstractNode.kt$AbstractNode$throw ConfigurationException("The name '$legalName' for $NODE_IDENTITY_ALIAS_PREFIX doesn't match what's in the key store: $subject")</ID>
<ID>MaxLineLength:AbstractNode.kt$AbstractNode$throw ConfigurationException("The name of the notary service '$serviceLegalName' for $DISTRIBUTED_NOTARY_ALIAS_PREFIX doesn't " + "match what's in the key store: $subject. You might need to adjust the configuration of `notary.serviceLegalName`.")</ID>
<ID>MaxLineLength:AbstractNode.kt$AbstractNode$throw IllegalStateException("CryptoService and signingCertificateStore are not aligned, the entry for key-alias: $alias is only found in $keyExistsIn")</ID>
<ID>MaxLineLength:AbstractNode.kt$AbstractNode$val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(configuration.cordappDirectories), attachments).tokenize()</ID>
<ID>MaxLineLength:AbstractNode.kt$AbstractNode$val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersStorage, transactionStorage).also { attachments.servicesForResolution = it }</ID>
@ -1845,6 +1844,7 @@
<ID>MaxLineLength:ConstraintsUtils.kt$input is AutomaticPlaceholderConstraint || output is AutomaticPlaceholderConstraint -&gt; throw IllegalArgumentException("Illegal constraint: AutomaticPlaceholderConstraint.")</ID>
<ID>MaxLineLength:ConstraintsUtils.kt$input.isAutomaticHashConstraint() || output.isAutomaticHashConstraint() -&gt; throw IllegalArgumentException("Illegal constraint: AutomaticHashConstraint.")</ID>
<ID>MaxLineLength:ConstraintsUtils.kt$when { // These branches should not happen, as this has been already checked. input is AutomaticPlaceholderConstraint || output is AutomaticPlaceholderConstraint -&gt; throw IllegalArgumentException("Illegal constraint: AutomaticPlaceholderConstraint.") input.isAutomaticHashConstraint() || output.isAutomaticHashConstraint() -&gt; throw IllegalArgumentException("Illegal constraint: AutomaticHashConstraint.") // Transition to the same constraint. input == output -&gt; true // You can't transition from the AlwaysAcceptAttachmentConstraint to anything else, as it could hide something illegal. input is AlwaysAcceptAttachmentConstraint &amp;&amp; output !is AlwaysAcceptAttachmentConstraint -&gt; false // Nothing can be migrated from the HashConstraint except a HashConstraint with the same Hash. (This check is redundant, but added for clarity) input is HashAttachmentConstraint &amp;&amp; output is HashAttachmentConstraint -&gt; input == output // Anything (except the AlwaysAcceptAttachmentConstraint) can be transformed to a HashAttachmentConstraint. input !is HashAttachmentConstraint &amp;&amp; output is HashAttachmentConstraint -&gt; true // The SignatureAttachmentConstraint allows migration from a Signature constraint with the same key. // TODO - we don't support currently third party signers. When we do, the output key will have to be stronger then the input key. input is SignatureAttachmentConstraint &amp;&amp; output is SignatureAttachmentConstraint -&gt; input.key == output.key // HashAttachmentConstraint can be transformed to a SignatureAttachmentConstraint when hash constraint verification checking disabled. HashAttachmentConstraint.disableHashConstraints &amp;&amp; input is HashAttachmentConstraint &amp;&amp; output is SignatureAttachmentConstraint -&gt; true // You can transition from the WhitelistConstraint to the SignatureConstraint only if all signers of the JAR are required to sign in the future. input is WhitelistedByZoneAttachmentConstraint &amp;&amp; output is SignatureAttachmentConstraint -&gt; attachment.signerKeys.isNotEmpty() &amp;&amp; output.key.keys.containsAll(attachment.signerKeys) else -&gt; false }</ID>
<ID>MaxLineLength:ContentSignerBuilder.kt$ContentSignerBuilder.SignatureOutputStream$private fun checkNotSigned(func: () -&gt; Unit)</ID>
<ID>MaxLineLength:ContractAttachment.kt$ContractAttachment$return "ContractAttachment(attachment=${attachment.id}, contracts='$allContracts', uploader='$uploader', signed='$isSigned', version='$version')"</ID>
<ID>MaxLineLength:ContractHierarchyTest.kt$ContractHierarchyTest$PrepareTransaction : FlowLogic</ID>
<ID>MaxLineLength:ContractHierarchyTest.kt$ContractHierarchyTest$mockNet = InternalMockNetwork(networkSendManuallyPumped = false, threadPerNode = true, cordappsForAllNodes = listOf(enclosedCordapp()))</ID>

View File

@ -12,8 +12,9 @@ import net.corda.nodeapi.internal.config.SslConfiguration
import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509KeyStore
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.crypto.X509Utilities.DISTRIBUTED_NOTARY_ALIAS_PREFIX
import net.corda.nodeapi.internal.crypto.X509Utilities.NODE_IDENTITY_ALIAS_PREFIX
import net.corda.nodeapi.internal.crypto.X509Utilities.DISTRIBUTED_NOTARY_COMPOSITE_KEY_ALIAS
import net.corda.nodeapi.internal.crypto.X509Utilities.DISTRIBUTED_NOTARY_KEY_ALIAS
import net.corda.nodeapi.internal.crypto.X509Utilities.NODE_IDENTITY_KEY_ALIAS
import org.slf4j.LoggerFactory
import java.nio.file.Path
import java.security.KeyPair
@ -41,7 +42,7 @@ object DevIdentityGenerator {
val nodeKeyStore = signingCertStore.get(true).also { it.installDevNodeCaCertPath(legalName) }
p2pSslConfig.keyStore.get(true).also { it.registerDevP2pCertificates(legalName) }
val identity = nodeKeyStore.storeLegalIdentity("$NODE_IDENTITY_ALIAS_PREFIX-private-key")
val identity = nodeKeyStore.storeLegalIdentity(NODE_IDENTITY_KEY_ALIAS)
return identity.party
}
@ -86,7 +87,7 @@ object DevIdentityGenerator {
private fun setPrivateKey(keyStore: X509KeyStore, keyPair: KeyPair, notaryPrincipal: X500Principal) {
val serviceKeyCert = createCertificate(keyPair.public, notaryPrincipal)
keyStore.setPrivateKey(
"$DISTRIBUTED_NOTARY_ALIAS_PREFIX-private-key",
DISTRIBUTED_NOTARY_KEY_ALIAS,
keyPair.private,
listOf(serviceKeyCert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate),
DEV_CA_KEY_STORE_PASS // Unfortunately we have to use the same password for private key due to Artemis limitation, for more details please see:
@ -97,7 +98,7 @@ object DevIdentityGenerator {
private fun setCompositeKey(keyStore: X509KeyStore, compositeKey: PublicKey, notaryPrincipal: X500Principal) {
val compositeKeyCert = createCertificate(compositeKey, notaryPrincipal)
keyStore.setCertificate("$DISTRIBUTED_NOTARY_ALIAS_PREFIX-composite-key", compositeKeyCert)
keyStore.setCertificate(DISTRIBUTED_NOTARY_COMPOSITE_KEY_ALIAS, compositeKeyCert)
}
private fun createCertificate(publicKey: PublicKey, principal: X500Principal): X509Certificate {

View File

@ -16,9 +16,15 @@ import java.security.Signature
* This builder will use BouncyCastle's JcaContentSignerBuilder as fallback for unknown algorithm.
*/
object ContentSignerBuilder {
fun build(signatureScheme: SignatureScheme, privateKey: PrivateKey, provider: Provider, random: SecureRandom? = null): ContentSigner {
fun build(signatureScheme: SignatureScheme, privateKey: PrivateKey, provider: Provider,
random: SecureRandom? = null, optimised: Boolean = true): ContentSigner {
val sigAlgId = signatureScheme.signatureOID
val sig = Instances.getSignatureInstance(signatureScheme.signatureName, provider).apply {
val signatureInstance = if (optimised)
Instances.getSignatureInstance(signatureScheme.signatureName, provider)
else
Signature.getInstance(signatureScheme.signatureName, provider)
val sig = signatureInstance.apply {
// TODO special handling for Sphincs due to a known BouncyCastle's Sphincs bug we reported.
// It is fixed in BC 161b12, so consider updating the below if-statement after updating BouncyCastle.
if (random != null && signatureScheme != SPHINCS256_SHA256) {
@ -28,17 +34,28 @@ object ContentSignerBuilder {
}
}
return object : ContentSigner {
private val stream = SignatureOutputStream(sig)
private val stream = SignatureOutputStream(sig, optimised)
override fun getAlgorithmIdentifier(): AlgorithmIdentifier = sigAlgId
override fun getOutputStream(): OutputStream = stream
override fun getSignature(): ByteArray = stream.signature
}
}
private class SignatureOutputStream(private val sig: Signature) : OutputStream() {
internal val signature: ByteArray get() = sig.sign()
override fun write(bytes: ByteArray, off: Int, len: Int) = sig.update(bytes, off, len)
override fun write(bytes: ByteArray) = sig.update(bytes)
override fun write(b: Int) = sig.update(b.toByte())
private class SignatureOutputStream(private val sig: Signature, private val optimised: Boolean) : OutputStream() {
private var alreadySigned = false
internal val signature: ByteArray by lazy {
try {
alreadySigned = true
sig.sign()
} finally {
if (optimised) {
Instances.releaseSignatureInstance(sig)
}
}
}
private fun checkNotSigned(func: () -> Unit) { if (alreadySigned) throw IllegalStateException("Cannot write to already signed object"); func()}
override fun write(bytes: ByteArray, off: Int, len: Int) = checkNotSigned { sig.update(bytes, off, len) }
override fun write(bytes: ByteArray) = checkNotSigned { sig.update(bytes) }
override fun write(b: Int) = checkNotSigned { sig.update(b.toByte()) }
}
}

View File

@ -18,11 +18,18 @@ const val KEYSTORE_TYPE = "JKS"
* @param keyStoreFilePath location of KeyStore file.
* @param storePassword password to open the store. This does not have to be the same password as any keys stored,
* but for SSL purposes this is recommended.
* @param keystoreType the type of the keystore to be loaded. Defaults to "JKS".
* @param provider KeyStore provider, optional.
* @return returns the KeyStore opened/created.
*/
fun loadOrCreateKeyStore(keyStoreFilePath: Path, storePassword: String): KeyStore {
fun loadOrCreateKeyStore(keyStoreFilePath: Path, storePassword: String, keystoreType: String = KEYSTORE_TYPE,
provider: Provider? = null): KeyStore {
val pass = storePassword.toCharArray()
val keyStore = KeyStore.getInstance(KEYSTORE_TYPE)
val keyStore = if (provider != null) {
KeyStore.getInstance(keystoreType, provider)
} else {
KeyStore.getInstance(keystoreType)
}
if (keyStoreFilePath.exists()) {
keyStoreFilePath.read { keyStore.load(it, pass) }
} else {
@ -144,6 +151,6 @@ fun KeyStore.getX509Certificate(alias: String): X509Certificate {
*/
fun KeyStore.getSupportedKey(alias: String, keyPassword: String): PrivateKey {
val keyPass = keyPassword.toCharArray()
val key = getKey(alias, keyPass) as PrivateKey
val key = requireNotNull(getKey(alias, keyPass)) { "Key for alias: '$alias' cannot be found" } as PrivateKey
return Crypto.toSupportedPrivateKey(key)
}

View File

@ -37,6 +37,8 @@ import java.util.*
import javax.security.auth.x500.X500Principal
object X509Utilities {
// Note that this default value only applies to BCCryptoService. Other implementations of CryptoService may have to use different
// schemes (for instance `UtimacoCryptoService.DEFAULT_IDENTITY_SIGNATURE_SCHEME`).
val DEFAULT_IDENTITY_SIGNATURE_SCHEME = Crypto.EDDSA_ED25519_SHA512
val DEFAULT_TLS_SIGNATURE_SCHEME = Crypto.ECDSA_SECP256R1_SHA256
@ -47,15 +49,31 @@ object X509Utilities {
const val CORDA_CLIENT_TLS = "cordaclienttls"
const val CORDA_CLIENT_CA = "cordaclientca"
// TODO These don't need to be prefixes, but can be the full aliases. However, because they are used as key aliases
// we should ensure that:
// a) they always contain valid characters, preferably [A-Za-z0-9] in order to be supported by the majority of
// crypto service implementations (i.e., HSMs).
// b) they are at most 127 chars in length (i.e., as of 2018, Azure Key Vault does not support bigger aliases).
const val NODE_IDENTITY_ALIAS_PREFIX = "identity"
// TODO Hyphen (-) seems to be supported by the major HSM vendors, but we should consider remove it in the
// future and stick to [A-Za-z0-9].
const val DISTRIBUTED_NOTARY_ALIAS_PREFIX = "distributed-notary"
const val NODE_IDENTITY_KEY_ALIAS = "identity-private-key"
const val DISTRIBUTED_NOTARY_KEY_ALIAS = "distributed-notary-private-key"
const val DISTRIBUTED_NOTARY_COMPOSITE_KEY_ALIAS = "distributed-notary-composite-key"
const val TLS_CERTIFICATE_DAYS_TO_EXPIRY_WARNING_THRESHOLD = 30
private const val KEY_ALIAS_REGEX = "[a-z0-9-]+"
private const val KEY_ALIAS_MAX_LENGTH = 100
/**
* Checks if the provided key alias does not exceed maximum length and
* only contains alphanumeric characters.
*/
fun isKeyAliasValid(alias: String): Boolean {
if (alias.length > KEY_ALIAS_MAX_LENGTH) return false
return KEY_ALIAS_REGEX.toRegex().matches(alias)
}
/**
* The error message to be displayed to the user when the alias validation fails.
*/
fun invalidKeyAliasErrorMessage(alias: String): String {
return "Alias '$alias' must contain only lowercase alphanumeric characters and not exceed 100 characters length."
}
val DEFAULT_VALIDITY_WINDOW = Pair(0.millis, 3650.days)
@ -378,6 +396,24 @@ fun PKCS10CertificationRequest.isSignatureValid(): Boolean {
return this.isSignatureValid(JcaContentVerifierProviderBuilder().build(this.subjectPublicKeyInfo))
}
/**
* Check certificate validity or print warning if expiry is within 30 days
*/
fun X509Certificate.checkValidity(errorMessage: () -> Any, warningBlock: (daysToExpiry: Int) -> Unit, date: Date = Date()) {
try {
checkValidity(date)
}
catch (e: CertificateException) {
throw IllegalArgumentException(errorMessage().toString(), e)
}
// Number of full days until midnight of expiry date: today is not included
val daysToExpiry = ChronoUnit.DAYS.between(date.toInstant(), notAfter.toInstant()).toInt()
if (daysToExpiry < X509Utilities.TLS_CERTIFICATE_DAYS_TO_EXPIRY_WARNING_THRESHOLD) {
// Also include today, e.g. return 1 for tomorrow expiry
warningBlock(daysToExpiry + 1)
}
}
/**
* Wraps a [CertificateFactory] to remove boilerplate. It's unclear whether [CertificateFactory] is threadsafe so best
* so assume this class is not.

View File

@ -10,7 +10,7 @@ import net.corda.core.messaging.startFlow
import net.corda.core.utilities.getOrThrow
import net.corda.node.internal.NodeStartup
import net.corda.node.services.Permissions.Companion.startFlow
import net.corda.nodeapi.internal.crypto.X509Utilities.NODE_IDENTITY_ALIAS_PREFIX
import net.corda.nodeapi.internal.crypto.X509Utilities.NODE_IDENTITY_KEY_ALIAS
import net.corda.nodeapi.internal.installDevNodeCaCertPath
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
@ -92,7 +92,7 @@ class BootTests {
}
val logFolder = alice.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME
val logFile = logFolder.list { it.filter { a -> a.isRegularFile() && a.fileName.toString().startsWith("node") }.findFirst().get() }
val lines = logFile.readLines { lines -> lines.filter { "$NODE_IDENTITY_ALIAS_PREFIX-private-key" in it }.toArray() }
val lines = logFile.readLines { lines -> lines.filter { NODE_IDENTITY_KEY_ALIAS in it }.toArray() }
assertTrue(lines.count() > 0)
}
}

View File

@ -150,8 +150,9 @@ import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_CA
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_TLS
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA
import net.corda.nodeapi.internal.crypto.X509Utilities.DEFAULT_VALIDITY_WINDOW
import net.corda.nodeapi.internal.crypto.X509Utilities.DISTRIBUTED_NOTARY_ALIAS_PREFIX
import net.corda.nodeapi.internal.crypto.X509Utilities.NODE_IDENTITY_ALIAS_PREFIX
import net.corda.nodeapi.internal.crypto.X509Utilities.DISTRIBUTED_NOTARY_COMPOSITE_KEY_ALIAS
import net.corda.nodeapi.internal.crypto.X509Utilities.DISTRIBUTED_NOTARY_KEY_ALIAS
import net.corda.nodeapi.internal.crypto.X509Utilities.NODE_IDENTITY_KEY_ALIAS
import net.corda.nodeapi.internal.cryptoservice.CryptoServiceFactory
import net.corda.nodeapi.internal.cryptoservice.SupportedCryptoServices
import net.corda.nodeapi.internal.cryptoservice.bouncycastle.BCCryptoService
@ -969,7 +970,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
* Note that obtainIdentity returns a KeyPair with an [AliasPrivateKey].
*/
private fun obtainIdentity(): Pair<PartyAndCertificate, KeyPair> {
val legalIdentityPrivateKeyAlias = "$NODE_IDENTITY_ALIAS_PREFIX-private-key"
val legalIdentityPrivateKeyAlias = "$NODE_IDENTITY_KEY_ALIAS"
var signingCertificateStore = configuration.signingCertificateStore.get()
if (!cryptoService.containsKey(legalIdentityPrivateKeyAlias) && !signingCertificateStore.contains(legalIdentityPrivateKeyAlias)) {
@ -1000,7 +1001,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
val subject = CordaX500Name.build(certificates.first().subjectX500Principal)
val legalName = configuration.myLegalName
if (subject != legalName) {
throw ConfigurationException("The name '$legalName' for $NODE_IDENTITY_ALIAS_PREFIX doesn't match what's in the key store: $subject")
throw ConfigurationException("The configured legalName '$legalName' doesn't match what's in the key store: $subject")
}
return getPartyAndCertificatePlusAliasKeyPair(certificates, legalIdentityPrivateKeyAlias)
@ -1016,8 +1017,8 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
/** Loads pre-generated notary service cluster identity. */
private fun loadNotaryClusterIdentity(serviceLegalName: CordaX500Name): Pair<PartyAndCertificate, KeyPair> {
val privateKeyAlias = "$DISTRIBUTED_NOTARY_ALIAS_PREFIX-private-key"
val compositeKeyAlias = "$DISTRIBUTED_NOTARY_ALIAS_PREFIX-composite-key"
val privateKeyAlias = "$DISTRIBUTED_NOTARY_KEY_ALIAS"
val compositeKeyAlias = "$DISTRIBUTED_NOTARY_COMPOSITE_KEY_ALIAS"
val signingCertificateStore = configuration.signingCertificateStore.get()
val privateKeyAliasCertChain = try {
@ -1040,7 +1041,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
val subject = CordaX500Name.build(certificates.first().subjectX500Principal)
if (subject != serviceLegalName) {
throw ConfigurationException("The name of the notary service '$serviceLegalName' for $DISTRIBUTED_NOTARY_ALIAS_PREFIX doesn't " +
throw ConfigurationException("The name of the notary service '$serviceLegalName' doesn't " +
"match what's in the key store: $subject. You might need to adjust the configuration of `notary.serviceLegalName`.")
}
return getPartyAndCertificatePlusAliasKeyPair(certificates, privateKeyAlias)

View File

@ -21,7 +21,7 @@ import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509KeyStore
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA
import net.corda.nodeapi.internal.crypto.X509Utilities.DISTRIBUTED_NOTARY_ALIAS_PREFIX
import net.corda.nodeapi.internal.crypto.X509Utilities.DISTRIBUTED_NOTARY_KEY_ALIAS
import net.corda.nodeapi.internal.crypto.X509Utilities.createSelfSignedCACertificate
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.internal.createDevIntermediateCaCertPath
@ -193,7 +193,7 @@ class NetworkRegistrationHelperTest {
assertThat(config.p2pSslOptions.keyStore.getOptional()).isNull()
assertThat(config.p2pSslOptions.trustStore.getOptional()).isNull()
val serviceIdentityAlias = "$DISTRIBUTED_NOTARY_ALIAS_PREFIX-private-key"
val serviceIdentityAlias = DISTRIBUTED_NOTARY_KEY_ALIAS
nodeKeystore.run {
assertFalse(contains(X509Utilities.CORDA_INTERMEDIATE_CA))
@ -255,7 +255,7 @@ class NetworkRegistrationHelperTest {
certService,
config.certificatesDirectory / networkRootTrustStoreFileName,
networkRootTrustStorePassword,
"$DISTRIBUTED_NOTARY_ALIAS_PREFIX-private-key",
DISTRIBUTED_NOTARY_KEY_ALIAS,
CertRole.SERVICE_IDENTITY)
else -> throw IllegalArgumentException("Unsupported cert role.")
}