ENT-4628: Harmonize CryptoService and BCCryptoService between OS and ENT (#5822)

This commit is contained in:
Denis Rekalov 2019-12-18 16:54:39 +00:00 committed by Matthew Nesbit
parent 8d5781db43
commit bc96bea24a
23 changed files with 944 additions and 43 deletions

View File

@ -80,6 +80,7 @@ buildscript {
ext.deterministic_rt_version = constants.getProperty('deterministicRtVersion')
ext.okhttp_version = '3.14.2'
ext.netty_version = '4.1.29.Final'
ext.tcnative_version = '2.0.14.Final'
ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion")
ext.fileupload_version = '1.4'
// Legacy JUnit 4 version
@ -376,11 +377,15 @@ allprojects {
// Demand that everything uses our given version of Netty.
eachDependency { details ->
if (details.requested.group == 'io.netty' && details.requested.name.startsWith('netty-')) {
if (details.requested.name.startsWith('netty-tcnative')){
details.useVersion tcnative_version
} else {
details.useVersion netty_version
}
}
}
}
}
compile {
// We want to use SLF4J's version of these bindings: jcl-over-slf4j
// Remove any transitive dependency on Apache's version.

View File

@ -195,6 +195,7 @@
<ID>ComplexMethod:StartedFlowTransition.kt$StartedFlowTransition$override fun transition(): TransitionResult</ID>
<ID>ComplexMethod:StatusTransitions.kt$StatusTransitions$ fun verify(tx: LedgerTransaction)</ID>
<ID>ComplexMethod:StringToMethodCallParser.kt$StringToMethodCallParser$ @Throws(UnparseableCallException::class) fun parse(target: T?, command: String): ParsedMethodCall</ID>
<ID>ComplexMethod:TlsDiffAlgorithmsTest.kt$TlsDiffAlgorithmsTest$@Test fun testClientServerTlsExchange()</ID>
<ID>ComplexMethod:TlsDiffProtocolsTest.kt$TlsDiffProtocolsTest$@Test fun testClientServerTlsExchange()</ID>
<ID>ComplexMethod:TransactionBuilder.kt$TransactionBuilder$ fun withItems(vararg items: Any)</ID>
<ID>ComplexMethod:TransactionBuilder.kt$TransactionBuilder$ private fun addMissingDependency(services: ServicesForResolution, wireTx: WireTransaction): Boolean</ID>
@ -986,6 +987,10 @@
<ID>MagicNumber:MockNetworkParametersService.kt$MockNetworkParametersStorage$20</ID>
<ID>MagicNumber:MockServices.kt$MockServices$10000</ID>
<ID>MagicNumber:MockServices.kt$MockServices.Companion.&lt;no name provided&gt;$512</ID>
<ID>MagicNumber:NettyTestClient.kt$NettyTestClient$5</ID>
<ID>MagicNumber:NettyTestHandler.kt$NettyTestHandler$5</ID>
<ID>MagicNumber:NettyTestServer.kt$NettyTestServer$100</ID>
<ID>MagicNumber:NettyTestServer.kt$NettyTestServer$5</ID>
<ID>MagicNumber:Network.kt$Network$0.8</ID>
<ID>MagicNumber:Network.kt$Network$1.2</ID>
<ID>MagicNumber:Network.kt$Network$10</ID>
@ -1343,9 +1348,6 @@
<ID>MaxLineLength:AddressBindingFailureTests.kt$AddressBindingFailureTests$@Test fun `rpc admin address`()</ID>
<ID>MaxLineLength:AddressBindingFailureTests.kt$AddressBindingFailureTests$assertThat(exception.addresses).contains(address).withFailMessage("Expected addresses to contain $address but was ${exception.addresses}.")</ID>
<ID>MaxLineLength:AddressBindingFailureTests.kt$AddressBindingFailureTests$assertThatThrownBy { startNode(customOverrides = overrides(address)).getOrThrow() }</ID>
<ID>MaxLineLength:AliasPrivateKeyTest.kt$AliasPrivateKeyTest$signingCertStore.query { setPrivateKey(alias, aliasPrivateKey, listOf(NOT_YET_REGISTERED_MARKER_KEYS_AND_CERTS.ECDSAR1_CERT), "entrypassword") }</ID>
<ID>MaxLineLength:AliasPrivateKeyTest.kt$AliasPrivateKeyTest$val signingCertStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory, "keystorepass").get(createNew = true)</ID>
<ID>MaxLineLength:AliasPrivateKeyTest.kt$AliasPrivateKeyTest${ val alias = "01234567890" val aliasPrivateKey = AliasPrivateKey(alias) val certificatesDirectory = tempFolder.root.toPath() val signingCertStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory, "keystorepass").get(createNew = true) signingCertStore.query { setPrivateKey(alias, aliasPrivateKey, listOf(NOT_YET_REGISTERED_MARKER_KEYS_AND_CERTS.ECDSAR1_CERT), "entrypassword") } // We can retrieve the certificate. assertTrue { signingCertStore.contains(alias) } // We can retrieve the certificate. assertEquals(NOT_YET_REGISTERED_MARKER_KEYS_AND_CERTS.ECDSAR1_CERT, signingCertStore[alias]) // Although we can store an AliasPrivateKey, we cannot retrieve it. But, it's fine as we use certStore for storing/handling certs only. assertThatIllegalArgumentException().isThrownBy { signingCertStore.query { getPrivateKey(alias, "entrypassword") } }.withMessage("Unrecognised algorithm: 1.3.6.1.4.1.50530.1.2") }</ID>
<ID>MaxLineLength:AllButBlacklisted.kt$AllButBlacklisted$throw IllegalStateException("The $matchType $aMatch of ${type.name} is blacklisted, so it cannot be used in serialization.")</ID>
<ID>MaxLineLength:Amount.kt$Amount&lt;T : Any&gt; : Comparable</ID>
<ID>MaxLineLength:Amount.kt$AmountTransfer$remaining = SourceAndAmount(payer, balance.amount.copy(quantity = Math.subtractExact(balance.amount.quantity, residual)), newRef)</ID>
@ -1464,8 +1466,12 @@
<ID>MaxLineLength:AutoOfferFlow.kt$AutoOfferFlow.Requester$val notary = serviceHub.networkMapCache.notaryIdentities.first() // TODO We should pass the notary as a parameter to the flow, not leave it to random choice.</ID>
<ID>MaxLineLength:AutoOfferFlow.kt$AutoOfferFlow.Requester$val otherParty = excludeHostNode(serviceHub, groupAbstractPartyByWellKnownParty(serviceHub, dealToBeOffered.participants)).keys.single()</ID>
<ID>MaxLineLength:BCCryptoService.kt$BCCryptoService : CryptoService</ID>
<ID>MaxLineLength:BCCryptoService.kt$BCCryptoService$/** * JKS keystore does not support storage for secret keys, so the existing keystore cannot be re-used. * JCEKS keystore supports storage of symmetric keys according to the spec, but there are several issues around classloaders and deserialization filtering (see links below). * - https://stackoverflow.com/questions/49990904/what-is-the-cause-of-java-security-unrecoverablekeyexception-rejected-by-the-j * - https://stackoverflow.com/questions/50393533/java-io-ioexception-invalid-secret-key-format-when-opening-jceks-key-store-wi * Thus, PKCS12 is used for storing the wrapping key. */ private val wrappingKeyStore: KeyStore by lazy { loadOrCreateKeyStore(wrappingKeyStorePath!!, certificateStore.password, "PKCS12") }</ID>
<ID>MaxLineLength:BCCryptoService.kt$BCCryptoService$return ContentSignerBuilder.build(signatureScheme, privateKey, Crypto.findProvider(signatureScheme.providerName), newSecureRandom())</ID>
<ID>MaxLineLength:BCCryptoService.kt$BCCryptoService$throw CryptoServiceException("Cannot generate key for alias $alias and signature scheme ${scheme.schemeCodeName} (id ${scheme.schemeNumberID})", e)</ID>
<ID>MaxLineLength:BCCryptoService.kt$BCCryptoService$val privateKey = cipher.unwrap(wrappedPrivateKey.keyMaterial, keyAlgorithmFromScheme(wrappedPrivateKey.signatureScheme), Cipher.PRIVATE_KEY) as PrivateKey</ID>
<ID>MaxLineLength:BCCryptoService.kt$BCCryptoService$wrappingKeyStore.setEntry(alias, KeyStore.SecretKeyEntry(wrappingKey), KeyStore.PasswordProtection(certificateStore.entryPassword.toCharArray()))</ID>
<ID>MaxLineLength:BCCryptoServiceTests.kt$BCCryptoServiceTests$private</ID>
<ID>MaxLineLength:BFTNotaryServiceTests.kt$BFTNotaryServiceTests$addOutputState(DummyContract.SingleOwnerState(owner = info.singleIdentity()), DummyContract.PROGRAM_ID, AlwaysAcceptAttachmentConstraint)</ID>
<ID>MaxLineLength:BFTNotaryServiceTests.kt$BFTNotaryServiceTests.Companion$fun startBftClusterAndNode(clusterSize: Int, mockNet: InternalMockNetwork, exposeRaces: Boolean = false): Pair&lt;Party, TestStartedNode&gt;</ID>
<ID>MaxLineLength:BFTSmart.kt$BFTSmart$Client : SingletonSerializeAsToken</ID>
@ -1966,6 +1972,9 @@
<ID>MaxLineLength:Crypto.kt$Crypto$is BCRSAPrivateKey, is BCSphincs256PrivateKey -&gt; true</ID>
<ID>MaxLineLength:Crypto.kt$Crypto$is BCRSAPublicKey -&gt; key.modulus.bitLength() &gt;= 2048</ID>
<ID>MaxLineLength:Crypto.kt$Crypto$val signableData = SignableData(originalSignedHash(txId, transactionSignature.partialMerkleTree), transactionSignature.signatureMetadata)</ID>
<ID>MaxLineLength:CryptoService.kt$CryptoService$ fun createWrappingKey(alias: String, failIfExists: Boolean = true)</ID>
<ID>MaxLineLength:CryptoService.kt$CryptoService$ fun generateWrappedKeyPair(masterKeyAlias: String, childKeyScheme: SignatureScheme = defaultWrappingSignatureScheme()): Pair&lt;PublicKey, WrappedPrivateKey&gt;</ID>
<ID>MaxLineLength:CryptoService.kt$CryptoService$ fun getWrappingMode(): WrappingMode?</ID>
<ID>MaxLineLength:CryptoServiceFactory.kt$CryptoServiceFactory.Companion$throw IllegalArgumentException("Currently only BouncyCastle is used as a crypto service. A valid signing certificate store is required.")</ID>
<ID>MaxLineLength:CryptoUtils.kt$ @DeleteForDJVM @Throws(NoSuchAlgorithmException::class) fun newSecureRandom(): SecureRandom</ID>
<ID>MaxLineLength:CryptoUtils.kt$ @DeleteForDJVM @Throws(NoSuchAlgorithmException::class) fun secureRandomBytes(numOfBytes: Int): ByteArray</ID>
@ -2479,6 +2488,8 @@
<ID>MaxLineLength:MockAttachmentStorage.kt$MockAttachmentStorage$val version = try { Integer.parseInt(baseAttachment.openAsJAR().manifest?.mainAttributes?.getValue(Attributes.Name.IMPLEMENTATION_VERSION)) } catch (e: Exception) { DEFAULT_CORDAPP_VERSION }</ID>
<ID>MaxLineLength:MockContractAttachment.kt$// A valid zip file with 1 entry. val simpleZip = byteArrayOf(80, 75, 3, 4, 20, 0, 8, 8, 8, 0, 15, 113, 79, 78, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 4, 0, 47, 97, -2, -54, 0, 0, 75, 4, 0, 80, 75, 7, 8, 67, -66, -73, -24, 3, 0, 0, 0, 1, 0, 0, 0, 80, 75, 1, 2, 20, 0, 20, 0, 8, 8, 8, 0, 15, 113, 79, 78, 67, -66, -73, -24, 3, 0, 0, 0, 1, 0, 0, 0, 2, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 97, -2, -54, 0, 0, 80, 75, 5, 6, 0, 0, 0, 0, 1, 0, 1, 0, 52, 0, 0, 0, 55, 0, 0, 0, 0, 0)</ID>
<ID>MaxLineLength:MockCryptoService.kt$MockCryptoService$return ContentSignerBuilder.build(signatureScheme, privateKey, Crypto.findProvider(signatureScheme.providerName), newSecureRandom())</ID>
<ID>MaxLineLength:MockCryptoService.kt$MockCryptoService$val privateKey = cipher.unwrap(wrappedPrivateKey.keyMaterial, keyAlgorithmFromScheme(wrappedPrivateKey.signatureScheme), Cipher.PRIVATE_KEY) as PrivateKey</ID>
<ID>MaxLineLength:MockCryptoService.kt$MockCryptoService$val wrappingKey = wrappingKeys[masterKeyAlias] ?: throw IllegalStateException("There is no master key under the alias: $masterKeyAlias")</ID>
<ID>MaxLineLength:MockNetwork.kt$MockNetwork$ fun createPartyNode(legalName: CordaX500Name? = null): StartedMockNode</ID>
<ID>MaxLineLength:MockNetwork.kt$MockNetwork$val servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = defaultParameters.servicePeerAllocationStrategy</ID>
<ID>MaxLineLength:MockNetwork.kt$MockNetworkParameters$fun withCordappsForAllNodes(cordappsForAllNodes: Collection&lt;TestCordapp&gt;): MockNetworkParameters</ID>
@ -3368,6 +3379,11 @@
<ID>MaxLineLength:TimedFlowTests.kt$TimedFlowTests.TestNotaryService$override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic&lt;Void?&gt;</ID>
<ID>MaxLineLength:TimedFlowTests.kt$TimedFlowTests.TestNotaryService$private</ID>
<ID>MaxLineLength:TimedFlowTests.kt$TimedFlowTests.TestNotaryService.&lt;no name provided&gt;$override</ID>
<ID>MaxLineLength:TlsDiffAlgorithmsTest.kt$TlsDiffAlgorithmsTest$val clientKeyStore = CertificateStore.fromResource("net/corda/nodeapi/internal/crypto/keystores/bridge_$clientAlgo.jks", "bridgepass", "bridgepass")</ID>
<ID>MaxLineLength:TlsDiffAlgorithmsTest.kt$TlsDiffAlgorithmsTest$val serverKeyStore = CertificateStore.fromResource("net/corda/nodeapi/internal/crypto/keystores/float_$serverAlgo.jks", "floatpass", "floatpass")</ID>
<ID>MaxLineLength:TlsDiffAlgorithmsTest.kt$TlsDiffAlgorithmsTest.Companion$arrayOf("ec", "ec", CIPHER_SUITES_ALL, false)</ID>
<ID>MaxLineLength:TlsDiffAlgorithmsTest.kt$TlsDiffAlgorithmsTest.Companion$arrayOf("ec", "ec", CIPHER_SUITES_JUST_EC, false)</ID>
<ID>MaxLineLength:TlsDiffAlgorithmsTest.kt$TlsDiffAlgorithmsTest.Companion$arrayOf("ec", "ec", CIPHER_SUITES_JUST_RSA, true)</ID>
<ID>MaxLineLength:TlsDiffProtocolsTest.kt$TlsDiffProtocolsTest$logger.info("Testing: ServerAlgo: $serverAlgo, ClientAlgo: $clientAlgo, Suites: $cipherSuites, Server protocols: $serverProtocols, Client protocols: $clientProtocols, Should fail: $shouldFail")</ID>
<ID>MaxLineLength:TlsDiffProtocolsTest.kt$TlsDiffProtocolsTest$val clientKeyStore = CertificateStore.fromResource("net/corda/nodeapi/internal/crypto/keystores/bridge_$clientAlgo.jks", "bridgepass", "bridgepass")</ID>
<ID>MaxLineLength:TlsDiffProtocolsTest.kt$TlsDiffProtocolsTest$val serverKeyStore = CertificateStore.fromResource("net/corda/nodeapi/internal/crypto/keystores/float_$serverAlgo.jks", "floatpass", "floatpass")</ID>
@ -4043,6 +4059,7 @@
<ID>TooGenericExceptionCaught:MockNodeMessagingService.kt$MockNodeMessagingService$e: Exception</ID>
<ID>TooGenericExceptionCaught:MyCustomNotaryService.kt$MyValidatingNotaryFlow$e: Exception</ID>
<ID>TooGenericExceptionCaught:NamedCacheTest.kt$NamedCacheTest$e: Exception</ID>
<ID>TooGenericExceptionCaught:NettyTestHandler.kt$NettyTestHandler$e: Throwable</ID>
<ID>TooGenericExceptionCaught:NetworkBootstrapper.kt$NetworkBootstrapper$e: Exception</ID>
<ID>TooGenericExceptionCaught:NetworkMapServer.kt$NetworkMapServer.InMemoryNetworkMapService$e: Exception</ID>
<ID>TooGenericExceptionCaught:NetworkMapUpdater.kt$NetworkMapUpdater$e: Exception</ID>
@ -4101,6 +4118,7 @@
<ID>TooGenericExceptionCaught:StringToMethodCallParser.kt$StringToMethodCallParser$e: Exception</ID>
<ID>TooGenericExceptionCaught:TLSAuthenticationTests.kt$TLSAuthenticationTests$ex: Exception</ID>
<ID>TooGenericExceptionCaught:ThrowableSerializer.kt$ThrowableSerializer$e: Exception</ID>
<ID>TooGenericExceptionCaught:TlsDiffAlgorithmsTest.kt$TlsDiffAlgorithmsTest$ex: Exception</ID>
<ID>TooGenericExceptionCaught:TlsDiffProtocolsTest.kt$TlsDiffProtocolsTest$ex: Exception</ID>
<ID>TooGenericExceptionCaught:TraderDemo.kt$TraderDemo$e: Exception</ID>
<ID>TooGenericExceptionCaught:TransactionBuilder.kt$TransactionBuilder$e: Throwable</ID>
@ -4151,6 +4169,7 @@
<ID>TooManyFunctions:ActionExecutorImpl.kt$ActionExecutorImpl : ActionExecutor</ID>
<ID>TooManyFunctions:AppendOnlyPersistentMap.kt$AppendOnlyPersistentMapBase&lt;K, V, E, out EK&gt;</ID>
<ID>TooManyFunctions:ArtemisTcpTransport.kt$ArtemisTcpTransport$Companion</ID>
<ID>TooManyFunctions:BCCryptoService.kt$BCCryptoService : CryptoService</ID>
<ID>TooManyFunctions:BFTSmart.kt$BFTSmart$Replica : DefaultRecoverable</ID>
<ID>TooManyFunctions:BaseTransaction.kt$BaseTransaction : NamedByHash</ID>
<ID>TooManyFunctions:ClassCarpenter.kt$ClassCarpenterImpl : ClassCarpenter</ID>
@ -4387,6 +4406,8 @@
<ID>WildcardImport:AttachmentsClassLoader.kt$import net.corda.core.serialization.*</ID>
<ID>WildcardImport:AttachmentsClassLoaderStaticContractTests.kt$import net.corda.core.contracts.*</ID>
<ID>WildcardImport:AutoOfferFlow.kt$import net.corda.core.flows.*</ID>
<ID>WildcardImport:BCCryptoService.kt$import java.security.*</ID>
<ID>WildcardImport:BCCryptoService.kt$import net.corda.nodeapi.internal.cryptoservice.*</ID>
<ID>WildcardImport:BCCryptoServiceTests.kt$import java.security.*</ID>
<ID>WildcardImport:BFTNotaryServiceTests.kt$import net.corda.core.crypto.*</ID>
<ID>WildcardImport:BFTNotaryServiceTests.kt$import net.corda.testing.node.internal.*</ID>
@ -4709,6 +4730,7 @@
<ID>WildcardImport:MessageState.kt$import net.corda.core.contracts.*</ID>
<ID>WildcardImport:MigrationServicesForResolution.kt$import net.corda.core.contracts.*</ID>
<ID>WildcardImport:MockAttachmentStorage.kt$import net.corda.core.node.services.vault.*</ID>
<ID>WildcardImport:MockCryptoService.kt$import net.corda.nodeapi.internal.cryptoservice.*</ID>
<ID>WildcardImport:MockKeyManagementService.kt$import net.corda.core.crypto.*</ID>
<ID>WildcardImport:MockNetworkTest.kt$import org.junit.Assert.*</ID>
<ID>WildcardImport:MockNodeMessagingService.kt$import net.corda.node.services.messaging.*</ID>
@ -4959,6 +4981,7 @@
<ID>WildcardImport:ThrowableSerializer.kt$import net.corda.serialization.internal.amqp.*</ID>
<ID>WildcardImport:TimedFlowTests.kt$import net.corda.core.flows.*</ID>
<ID>WildcardImport:TimedFlowTests.kt$import net.corda.testing.node.internal.*</ID>
<ID>WildcardImport:TlsDiffAlgorithmsTest.kt$import javax.net.ssl.*</ID>
<ID>WildcardImport:TlsDiffProtocolsTest.kt$import javax.net.ssl.*</ID>
<ID>WildcardImport:TopLevelTransition.kt$import net.corda.node.services.statemachine.*</ID>
<ID>WildcardImport:TraderDemoTest.kt$import net.corda.testing.driver.*</ID>

View File

@ -158,6 +158,7 @@ This file is based on or incorporates material from the projects listed below (T
150. snappy
151. sshd-core
152. sshd-pam
153. netty-tcnative-boringssl-static
================================================
================================================
@ -395,6 +396,10 @@ netty-resolver 4.1.29.Final
https://netty.io/
https://github.com/netty/netty/blob/4.1/LICENSE.txt
netty-tcnative-boringssl-static 2.0.14.Final
https://netty.io/
https://github.com/netty/netty/blob/4.1/LICENSE.txt
netty-transport 4.1.29.Final
https://netty.io/
https://github.com/netty/netty/blob/4.1/LICENSE.txt

View File

@ -51,6 +51,7 @@ dependencies {
testCompile "org.assertj:assertj-core:$assertj_version"
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
testCompile project(':node-driver')
testCompile project(':test-utils')
compile ("org.apache.activemq:artemis-amqp-protocol:${artemis_version}") {
// Gains our proton-j version from core module.

View File

@ -50,7 +50,7 @@ object X509Utilities {
const val CORDA_CLIENT_CA = "cordaclientca"
// 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].
// future and stick to [a-z0-9].
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"

View File

@ -1,6 +1,7 @@
package net.corda.nodeapi.internal.cryptoservice
import net.corda.core.DoNotImplement
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SignatureScheme
import net.corda.nodeapi.internal.crypto.X509Utilities
import org.bouncycastle.operator.ContentSigner
@ -64,6 +65,87 @@ interface CryptoService : SignOnlyCryptoService {
* Returns the type of the service.
*/
fun getType(): SupportedCryptoServices
// ******************************************************
// ENTERPRISE ONLY CODE FOR WRAPPING KEYS API STARTS HERE
// ******************************************************
/**
* Generates a new key to be used as a wrapping key.
*
* @param alias the alias under which the created wrapping key is created.
* @param failIfExists a flag indicating whether the method should fail if a key already exists under the provided alias or return normally without overriding the key.
* The default value is true.
*
* @throws IllegalArgumentException if a key already exists under this alias (and [failIfExists] is set to true].
*/
fun createWrappingKey(alias: String, failIfExists: Boolean = true)
/**
* The size of the wrapping key, as specified in the standard. See: https://csrc.nist.gov/publications/detail/sp/800-38f/final
* All underlying implementations should use this key size, unless there is a specific reason for not doing so.
*/
fun wrappingKeySize(): Int = 256
/**
* Generates an asymmetric key pair, returning the public key and the private key material wrapped using the specified wrapping key.
*
* @param masterKeyAlias the alias of the key to be used as a wrapping key.
* @param childKeyScheme the parameters of the key pair to be generated.
*
* @throws IllegalStateException if there is no master key existing under the alias specified ([masterKeyAlias]).
*/
fun generateWrappedKeyPair(masterKeyAlias: String, childKeyScheme: SignatureScheme = defaultWrappingSignatureScheme()): Pair<PublicKey, WrappedPrivateKey>
/**
* Unwraps the provided wrapped key, using the specified wrapping key and signs the provided payload.
*
* @param masterKeyAlias the alias of the key to be used as a wrapping key.
* @param wrappedPrivateKey the private key to be used for signing in a wrapped form.
* @param payloadToSign the payload to be signed.
*
* @throws IllegalStateException if there is no master key existing under the alias specified ([masterKeyAlias]).
*/
fun sign(masterKeyAlias: String, wrappedPrivateKey: WrappedPrivateKey, payloadToSign: ByteArray): ByteArray
/**
* Returns the [WrappingMode] supported by the associated implementation.
*
* If no mode is supported, then null will be returned by this method and all the associated operations will throw an [UnsupportedOperationException].
* Note: the long-term plan is to completely eradicate this case and have all implementations support one of the modes, even the degraded one.
* As a result, this optionality is introduced for practical reasons and might be removed in the next major release,
* when support will be added for all the existing implementations.
*/
fun getWrappingMode(): WrappingMode?
/**
* Returns the [SignatureScheme] that should be used with this [CryptoService] when generating wrapped keys
*/
fun defaultWrappingSignatureScheme(): SignatureScheme = Crypto.ECDSA_SECP256R1_SHA256
// *****************************************************
// ENTERPRISE ONLY CODE FOR WRAPPING KEYS API ENDS HERE
// *****************************************************
}
open class CryptoServiceException(message: String?, cause: Throwable? = null) : Exception(message, cause)
/**
* If an exception is deemed unrecoverable then it must be set with the flag ``isRecoverable=false``
*
* [ManagedCryptoService] will assume any [Throwable] which isn't a [CryptoServiceException] is recoverable and
* will wrap it in a [CryptoServiceException] with ``isRecoverable=true``.
*/
open class CryptoServiceException(message: String?, cause: Throwable? = null, val isRecoverable: Boolean = true) : Exception(message, cause)
enum class WrappingMode {
/**
* In degraded mode, wrapped keys' material is encrypted at rest, but it's temporarily exposed during key generation and signing.
*/
DEGRADED_WRAPPED,
/**
* In normal mode, wrapped keys' material is encrypted, never exposed to the application and only accessible from inside the HSM.
*/
WRAPPED
}
class WrappedPrivateKey(val keyMaterial: ByteArray, val signatureScheme: SignatureScheme)

View File

@ -2,29 +2,44 @@ package net.corda.nodeapi.internal.cryptoservice.bouncycastle
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SignatureScheme
import net.corda.core.crypto.internal.Instances.getSignatureInstance
import net.corda.core.crypto.internal.cordaBouncyCastleProvider
import net.corda.core.crypto.newSecureRandom
import net.corda.core.crypto.sha256
import net.corda.core.utilities.detailedLogger
import net.corda.core.utilities.trace
import net.corda.nodeapi.internal.config.CertificateStore
import net.corda.nodeapi.internal.config.CertificateStoreSupplier
import net.corda.nodeapi.internal.crypto.ContentSignerBuilder
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.crypto.loadOrCreateKeyStore
import net.corda.nodeapi.internal.crypto.save
import net.corda.nodeapi.internal.cryptoservice.*
import net.corda.nodeapi.internal.cryptoservice.CryptoService
import net.corda.nodeapi.internal.cryptoservice.CryptoServiceException
import net.corda.nodeapi.internal.cryptoservice.SupportedCryptoServices
import org.bouncycastle.operator.ContentSigner
import java.security.KeyPair
import java.security.KeyStore
import java.security.PublicKey
import java.security.Signature
import java.nio.file.Path
import java.security.*
import java.security.spec.ECGenParameterSpec
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.security.auth.x500.X500Principal
/**
* Basic implementation of a [CryptoService] that uses BouncyCastle for cryptographic operations
* and a Java KeyStore in the form of [CertificateStore] to store private keys.
* This service reuses the [NodeConfiguration.signingCertificateStore] to store keys.
*
* The [wrappingKeyStorePath] must be provided in order to execute any wrapping operations (e.g. [createWrappingKey], [generateWrappedKeyPair])
*/
class BCCryptoService(private val legalName: X500Principal, private val certificateStoreSupplier: CertificateStoreSupplier) : CryptoService {
class BCCryptoService(private val legalName: X500Principal,
private val certificateStoreSupplier: CertificateStoreSupplier,
private val wrappingKeyStorePath: Path? = null) : CryptoService {
private companion object {
val detailedLogger = detailedLogger()
}
override fun getType(): SupportedCryptoServices = SupportedCryptoServices.BC_SIMPLE
@ -32,9 +47,22 @@ class BCCryptoService(private val legalName: X500Principal, private val certific
// TODO make it private when E2ETestKeyManagementService does not require direct access to the private key.
var certificateStore: CertificateStore = certificateStoreSupplier.get(true)
/**
* JKS keystore does not support storage for secret keys, so the existing keystore cannot be re-used.
* JCEKS keystore supports storage of symmetric keys according to the spec, but there are several issues around classloaders and deserialization filtering (see links below).
* - https://stackoverflow.com/questions/49990904/what-is-the-cause-of-java-security-unrecoverablekeyexception-rejected-by-the-j
* - https://stackoverflow.com/questions/50393533/java-io-ioexception-invalid-secret-key-format-when-opening-jceks-key-store-wi
* Thus, PKCS12 is used for storing the wrapping key.
*/
private val wrappingKeyStore: KeyStore by lazy {
loadOrCreateKeyStore(wrappingKeyStorePath!!, certificateStore.password, "PKCS12")
}
override fun generateKeyPair(alias: String, scheme: SignatureScheme): PublicKey {
try {
detailedLogger.trace { "CryptoService(action=generate_key_pair_start;alias=$alias;scheme=$scheme)" }
val keyPair = Crypto.generateKeyPair(scheme)
detailedLogger.trace { "CryptoService(action=generate_key_pair_end;alias=$alias;scheme=$scheme)" }
importKey(alias, keyPair)
return keyPair.public
} catch (e: Exception) {
@ -43,14 +71,19 @@ class BCCryptoService(private val legalName: X500Principal, private val certific
}
override fun containsKey(alias: String): Boolean {
return certificateStore.contains(alias)
return if (wrappingKeyStorePath == null) {
certificateStore.contains(alias)
} else {
certificateStore.contains(alias) || wrappingKeyStore.containsAlias(alias)
}
}
override fun getPublicKey(alias: String): PublicKey {
try {
return certificateStore.query { getPublicKey(alias) }
} catch (e: Exception) {
throw CryptoServiceException("Cannot get public key for alias $alias", e)
throw CryptoServiceException("Cannot get public key for alias $alias", e, isRecoverable = false)
}
}
@ -68,13 +101,16 @@ class BCCryptoService(private val legalName: X500Principal, private val certific
private fun signWithAlgorithm(alias: String, data: ByteArray, signAlgorithm: String): ByteArray {
val privateKey = certificateStore.query { getPrivateKey(alias, certificateStore.entryPassword) }
val signature = Signature.getInstance(signAlgorithm, cordaBouncyCastleProvider)
detailedLogger.trace { "CryptoService(action=signing_start;alias=$alias;algorithm=$signAlgorithm)" }
signature.initSign(privateKey, newSecureRandom())
signature.update(data)
detailedLogger.trace { "CryptoService(action=signing_end;alias=$alias;algorithm=$signAlgorithm)" }
return signature.sign()
}
override fun getSigner(alias: String): ContentSigner {
try {
detailedLogger.trace { "CryptoService(action=get_signer;alias=$alias)" }
val privateKey = certificateStore.query { getPrivateKey(alias, certificateStore.entryPassword) }
val signatureScheme = Crypto.findSignatureScheme(privateKey)
return ContentSignerBuilder.build(signatureScheme, privateKey, Crypto.findProvider(signatureScheme.providerName), newSecureRandom())
@ -105,10 +141,81 @@ class BCCryptoService(private val legalName: X500Principal, private val certific
try {
// Store a self-signed certificate, as Keystore requires to store certificates instead of public keys.
// We could probably add a null cert, but we store a self-signed cert that will be used to retrieve the public key.
detailedLogger.trace { "CryptoService(action=key_import;alias=$alias)" }
val cert = X509Utilities.createSelfSignedCACertificate(legalName, keyPair)
certificateStore.query { setPrivateKey(alias, keyPair.private, listOf(cert), certificateStore.entryPassword) }
} catch (e: Exception) {
throw CryptoServiceException("Cannot import key with alias $alias", e)
}
}
@Synchronized
override fun createWrappingKey(alias: String, failIfExists: Boolean) {
if (wrappingKeyStore.containsAlias(alias)) {
when (failIfExists) {
true -> throw IllegalArgumentException("There is an existing key with the alias: $alias")
false -> return
}
}
val keyGenerator = KeyGenerator.getInstance("AES")
keyGenerator.init(wrappingKeySize())
val wrappingKey = keyGenerator.generateKey()
wrappingKeyStore.setEntry(alias, KeyStore.SecretKeyEntry(wrappingKey), KeyStore.PasswordProtection(certificateStore.entryPassword.toCharArray()))
wrappingKeyStore.save(wrappingKeyStorePath!!, certificateStore.password)
}
override fun generateWrappedKeyPair(masterKeyAlias: String, childKeyScheme: SignatureScheme): Pair<PublicKey, WrappedPrivateKey> {
if (!wrappingKeyStore.containsAlias(masterKeyAlias)) {
throw IllegalStateException("There is no master key under the alias: $masterKeyAlias")
}
val wrappingKey = wrappingKeyStore.getKey(masterKeyAlias, certificateStore.entryPassword.toCharArray())
val cipher = Cipher.getInstance("AES", cordaBouncyCastleProvider)
cipher.init(Cipher.WRAP_MODE, wrappingKey)
val keyPairGenerator = keyPairGeneratorFromScheme(childKeyScheme)
val keyPair = keyPairGenerator.generateKeyPair()
val privateKeyMaterialWrapped = cipher.wrap(keyPair.private)
return Pair(keyPair.public, WrappedPrivateKey(privateKeyMaterialWrapped, childKeyScheme))
}
override fun sign(masterKeyAlias: String, wrappedPrivateKey: WrappedPrivateKey, payloadToSign: ByteArray): ByteArray {
if (!wrappingKeyStore.containsAlias(masterKeyAlias)) {
throw IllegalStateException("There is no master key under the alias: $masterKeyAlias")
}
val wrappingKey = wrappingKeyStore.getKey(masterKeyAlias, certificateStore.entryPassword.toCharArray())
val cipher = Cipher.getInstance("AES", cordaBouncyCastleProvider)
cipher.init(Cipher.UNWRAP_MODE, wrappingKey)
val privateKey = cipher.unwrap(wrappedPrivateKey.keyMaterial, keyAlgorithmFromScheme(wrappedPrivateKey.signatureScheme), Cipher.PRIVATE_KEY) as PrivateKey
val signature = getSignatureInstance(wrappedPrivateKey.signatureScheme.signatureName, cordaBouncyCastleProvider)
signature.initSign(privateKey, newSecureRandom())
signature.update(payloadToSign)
return signature.sign()
}
override fun getWrappingMode(): WrappingMode? = WrappingMode.DEGRADED_WRAPPED
private fun keyPairGeneratorFromScheme(scheme: SignatureScheme): KeyPairGenerator {
val algorithm = keyAlgorithmFromScheme(scheme)
val keyPairGenerator = KeyPairGenerator.getInstance(algorithm, cordaBouncyCastleProvider)
when (scheme) {
Crypto.ECDSA_SECP256R1_SHA256 -> keyPairGenerator.initialize(ECGenParameterSpec("secp256r1"))
Crypto.ECDSA_SECP256K1_SHA256 -> keyPairGenerator.initialize(ECGenParameterSpec("secp256k1"))
Crypto.RSA_SHA256 -> keyPairGenerator.initialize(scheme.keySize!!)
else -> throw IllegalArgumentException("No mapping for scheme ID ${scheme.schemeNumberID}")
}
return keyPairGenerator
}
private fun keyAlgorithmFromScheme(scheme: SignatureScheme): String = when (scheme) {
Crypto.ECDSA_SECP256R1_SHA256, Crypto.ECDSA_SECP256K1_SHA256 -> "EC"
Crypto.RSA_SHA256 -> "RSA"
else -> throw IllegalArgumentException("No algorithm for scheme ID ${scheme.schemeNumberID}")
}
}

View File

@ -29,8 +29,6 @@ class AliasPrivateKeyTest {
assertTrue { signingCertStore.contains(alias) }
// We can retrieve the certificate.
assertEquals(NOT_YET_REGISTERED_MARKER_KEYS_AND_CERTS.ECDSAR1_CERT, signingCertStore[alias])
// Although we can store an AliasPrivateKey, we cannot retrieve it. But, it's fine as we use certStore for storing/handling certs
// only.
assertEquals(aliasPrivateKey, signingCertStore.query { getPrivateKey(alias, "entrypassword") })
}
}

View File

@ -0,0 +1,173 @@
package net.corda.nodeapi.internal.crypto
import net.corda.core.crypto.newSecureRandom
import net.corda.core.utilities.Try
import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.config.CertificateStore
import net.corda.nodeapi.internal.protonwrapper.netty.init
import org.assertj.core.api.Assertions
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.IOException
import java.net.InetAddress
import java.net.InetSocketAddress
import javax.net.ssl.*
import kotlin.concurrent.thread
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
@RunWith(Parameterized::class)
class TlsDiffAlgorithmsTest(private val serverAlgo: String, private val clientAlgo: String,
private val cipherSuites: Array<String>, private val shouldFail: Boolean) {
companion object {
private val CIPHER_SUITES_ALL = arrayOf(
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
)
private val CIPHER_SUITES_JUST_RSA = arrayOf(
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
)
private val CIPHER_SUITES_JUST_EC = arrayOf(
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"
)
@Parameterized.Parameters(name = "ServerAlgo: {0}, ClientAlgo: {1}, Should fail: {3}")
@JvmStatic
fun data() = listOf(
arrayOf("ec", "ec", CIPHER_SUITES_ALL, false), arrayOf("rsa", "rsa", CIPHER_SUITES_ALL, false), arrayOf("ec", "rsa", CIPHER_SUITES_ALL, false), arrayOf("rsa", "ec", CIPHER_SUITES_ALL, false),
arrayOf("ec", "ec", CIPHER_SUITES_JUST_RSA, true), arrayOf("rsa", "rsa", CIPHER_SUITES_JUST_RSA, false), arrayOf("ec", "rsa", CIPHER_SUITES_JUST_RSA, true), arrayOf("rsa", "ec", CIPHER_SUITES_JUST_RSA, false),
arrayOf("ec", "ec", CIPHER_SUITES_JUST_EC, false), arrayOf("rsa", "rsa", CIPHER_SUITES_JUST_EC, true), arrayOf("ec", "rsa", CIPHER_SUITES_JUST_EC, false), arrayOf("rsa", "ec", CIPHER_SUITES_JUST_EC, true)
)
private val logger = contextLogger()
}
@Rule
@JvmField
val tempFolder = TemporaryFolder()
@Test
fun testClientServerTlsExchange() {
//System.setProperty("javax.net.debug", "all")
logger.info("Testing: ServerAlgo: $serverAlgo, ClientAlgo: $clientAlgo, Suites: ${cipherSuites.toList()}, Should fail: $shouldFail")
val trustStore = CertificateStore.fromResource("net/corda/nodeapi/internal/crypto/keystores/trust.jks", "trustpass", "trustpass")
val rootCa = trustStore.value.getCertificate("root")
val serverKeyStore = CertificateStore.fromResource("net/corda/nodeapi/internal/crypto/keystores/float_$serverAlgo.jks", "floatpass", "floatpass")
val serverCa = serverKeyStore.value.getCertificateAndKeyPair("floatcert", "floatpass")
val clientKeyStore = CertificateStore.fromResource("net/corda/nodeapi/internal/crypto/keystores/bridge_$clientAlgo.jks", "bridgepass", "bridgepass")
//val clientCa = clientKeyStore.value.getCertificateAndKeyPair("bridgecert", "bridgepass")
val serverSocketFactory = createSslContext(serverKeyStore, trustStore).serverSocketFactory
val clientSocketFactory = createSslContext(clientKeyStore, trustStore).socketFactory
val serverSocket = (serverSocketFactory.createServerSocket(0) as SSLServerSocket).apply {
// use 0 to get first free socket
val serverParams = SSLParameters(cipherSuites, arrayOf("TLSv1.2"))
serverParams.wantClientAuth = true
serverParams.needClientAuth = true
serverParams.endpointIdentificationAlgorithm = null // Reconfirm default no server name indication, use our own validator.
sslParameters = serverParams
useClientMode = false
}
val clientSocket = (clientSocketFactory.createSocket() as SSLSocket).apply {
val clientParams = SSLParameters(cipherSuites, arrayOf("TLSv1.2"))
clientParams.endpointIdentificationAlgorithm = null // Reconfirm default no server name indication, use our own validator.
sslParameters = clientParams
useClientMode = true
// We need to specify this explicitly because by default the client binds to 'localhost' and we want it to bind
// to whatever <hostname> resolves to(as that's what the server binds to). In particular on Debian <hostname>
// resolves to 127.0.1.1 instead of the external address of the interface, so the ssl handshake fails.
bind(InetSocketAddress(InetAddress.getLocalHost(), 0))
}
val lock = Object()
var done = false
var serverError = false
val testPhrase = "Hello World"
val serverThread = thread {
try {
val sslServerSocket = serverSocket.accept()
assertTrue(sslServerSocket.isConnected)
val serverInput = DataInputStream(sslServerSocket.inputStream)
val receivedString = serverInput.readUTF()
assertEquals(testPhrase, receivedString)
synchronized(lock) {
done = true
lock.notifyAll()
}
sslServerSocket.close()
} catch (ex: Exception) {
serverError = true
}
}
clientSocket.connect(InetSocketAddress(InetAddress.getLocalHost(), serverSocket.localPort))
assertTrue(clientSocket.isConnected)
// Double check hostname manually
val peerChainTry = Try.on { clientSocket.session.peerCertificates.x509 }
assertEquals(!shouldFail, peerChainTry.isSuccess)
when(peerChainTry) {
is Try.Success -> {
val peerChain = peerChainTry.getOrThrow()
val peerX500Principal = peerChain[0].subjectX500Principal
assertEquals(serverCa.certificate.subjectX500Principal, peerX500Principal)
X509Utilities.validateCertificateChain(rootCa, peerChain)
with(DataOutputStream(clientSocket.outputStream)) {
writeUTF(testPhrase)
}
var timeout = 0
synchronized(lock) {
while (!done) {
timeout++
if (timeout > 10) throw IOException("Timed out waiting for server to complete")
lock.wait(1000)
}
}
clientSocket.close()
serverThread.join(1000)
assertFalse { serverError }
serverSocket.close()
assertTrue(done)
}
is Try.Failure -> {
Assertions.assertThatThrownBy {
peerChainTry.getOrThrow()
}.isInstanceOf(SSLPeerUnverifiedException::class.java)
// Tidy-up in case of failure
clientSocket.close()
serverSocket.close()
serverThread.interrupt()
}
}
}
private fun createSslContext(keyStore: CertificateStore, trustStore: CertificateStore): SSLContext {
return SSLContext.getInstance("TLS").apply {
val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
keyManagerFactory.init(keyStore)
val keyManagers = keyManagerFactory.keyManagers
val trustMgrFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustMgrFactory.init(trustStore)
val trustManagers = trustMgrFactory.trustManagers
init(keyManagers, trustManagers, newSecureRandom())
}
}
}

View File

@ -18,13 +18,12 @@ import java.io.IOException
import java.net.InetAddress
import java.net.InetSocketAddress
import javax.net.ssl.*
import javax.net.ssl.SNIHostName
import kotlin.concurrent.thread
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlin.test.assertNotNull
import javax.net.ssl.SNIHostName
import javax.net.ssl.StandardConstants
import kotlin.test.assertTrue
/**
* This test checks compatibility of TLS 1.2 and 1.3 communication using different cipher suites with SNI header

View File

@ -1,5 +1,9 @@
package net.corda.nodeapi.internal.crypto
import io.netty.handler.ssl.ClientAuth
import io.netty.handler.ssl.SslContextBuilder
import io.netty.handler.ssl.SslProvider
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.Crypto.COMPOSITE_KEY
import net.corda.core.crypto.Crypto.ECDSA_SECP256K1_SHA256
@ -15,6 +19,8 @@ import net.corda.core.internal.div
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.utilities.days
import net.corda.core.utilities.hours
import net.corda.node.serialization.amqp.AMQPServerSerializationScheme
import net.corda.nodeapi.internal.config.MutualSslConfiguration
import net.corda.nodeapi.internal.createDevNodeCa
@ -30,12 +36,15 @@ import net.corda.serialization.internal.amqp.amqpMagic
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.TestIdentity
import net.corda.testing.driver.internal.incrementalPortAllocation
import net.corda.testing.internal.NettyTestClient
import net.corda.testing.internal.NettyTestHandler
import net.corda.testing.internal.NettyTestServer
import net.corda.testing.internal.createDevIntermediateCaCertPath
import net.corda.testing.internal.stubs.CertificateStoreStubs
import net.i2p.crypto.eddsa.EdDSAPrivateKey
import org.assertj.core.api.Assertions.assertThat
import org.bouncycastle.asn1.x509.*
import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPrivateCrtKey
import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PrivateKey
import org.junit.Rule
import org.junit.Test
@ -66,6 +75,9 @@ class X509UtilitiesTest {
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
)
val portAllocation = incrementalPortAllocation()
// We ensure that all of the algorithms are both used (at least once) as first and second in the following [Pair]s.
// We also add [DEFAULT_TLS_SIGNATURE_SCHEME] and [DEFAULT_IDENTITY_SIGNATURE_SCHEME] combinations for consistency.
val certChainSchemeCombinations = listOf(
@ -349,6 +361,65 @@ class X509UtilitiesTest {
assertTrue(done)
}
@Test
fun `create server cert and use in OpenSSL channel`() {
val sslConfig = CertificateStoreStubs.P2P.withCertificatesDirectory(tempFolder.root.toPath(), keyStorePassword = "serverstorepass")
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
// Generate server cert and private key and populate another keystore suitable for SSL
sslConfig.keyStore.get(true).registerDevP2pCertificates(MEGA_CORP.name, rootCa.certificate, intermediateCa)
sslConfig.createTrustStore(rootCa.certificate)
val keyStore = sslConfig.keyStore.get()
val trustStore = sslConfig.trustStore.get()
val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
keyManagerFactory.init(keyStore)
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.init(trustStore)
val sslServerContext = SslContextBuilder
.forServer(keyManagerFactory)
.trustManager(trustManagerFactory)
.clientAuth(ClientAuth.REQUIRE)
.ciphers(CIPHER_SUITES.toMutableList())
.sslProvider(SslProvider.OPENSSL)
.protocols("TLSv1.2")
.build()
val sslClientContext = SslContextBuilder
.forClient()
.keyManager(keyManagerFactory)
.trustManager(trustManagerFactory)
.ciphers(CIPHER_SUITES.toMutableList())
.sslProvider(SslProvider.OPENSSL)
.protocols("TLSv1.2")
.build()
val serverHandler = NettyTestHandler { ctx, msg -> ctx?.writeAndFlush(msg) }
val clientHandler = NettyTestHandler { _, msg -> assertEquals("Hello", NettyTestHandler.readString(msg)) }
NettyTestServer(sslServerContext, serverHandler, portAllocation.nextPort()).use { server ->
server.start()
NettyTestClient(sslClientContext, InetAddress.getLocalHost().canonicalHostName, server.port, clientHandler).use { client ->
client.start()
clientHandler.writeString("Hello")
val readCalled = clientHandler.waitForReadCalled()
clientHandler.rethrowIfFailed()
serverHandler.rethrowIfFailed()
assertTrue(readCalled)
assertEquals(1, serverHandler.readCalledCounter)
assertEquals(1, clientHandler.readCalledCounter)
val peerChain = client.engine!!.session.peerCertificates.x509
val peerX500Principal = peerChain[0].subjectX500Principal
assertEquals(MEGA_CORP.name.x500Principal, peerX500Principal)
X509Utilities.validateCertificateChain(rootCa.certificate, peerChain)
}
}
}
private fun tempFile(name: String): Path = tempFolder.root.toPath() / name
private fun MutualSslConfiguration.createTrustStore(rootCert: X509Certificate) {
@ -452,4 +523,34 @@ class X509UtilitiesTest {
assertEquals(childKeyPair.public, reloadedPublicKey)
assertEquals(childKeyPair.private, reloadedPrivateKey)
}
@Test
fun `check certificate validity or print warning if expiry is within 30 days`() {
val keyPair = generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME)
val testName = X500Principal("CN=Test,O=R3 Ltd,L=London,C=GB")
val cert = X509Utilities.createSelfSignedCACertificate(testName, keyPair, 0.days to 50.days)
val today = cert.notBefore
var warnings = 0
cert.checkValidity({ "No error expected" }, { fail("No warning expected") }, today)
cert.checkValidity({ "No error expected" }, { fail("No warning expected") }, Date.from(today.toInstant() + 20.days))
cert.checkValidity({ "No error expected" }, { daysToExpiry ->
assertEquals(30, daysToExpiry)
warnings++
}, Date.from(today.toInstant() + 20.days + 3.hours))
cert.checkValidity({ "No error expected" }, { daysToExpiry ->
assertEquals(11, daysToExpiry)
warnings++
}, Date.from(today.toInstant() + 40.days))
cert.checkValidity({ "No error expected" }, { daysToExpiry ->
assertEquals(1, daysToExpiry)
warnings++
}, Date.from(today.toInstant() + 49.days + 20.hours))
assertEquals(3, warnings)
assertFailsWith(IllegalArgumentException::class, "Error text") {
cert.checkValidity({ "Error text" }, { }, Date.from(today.toInstant() + 50.days + 1.hours))
}
assertFailsWith(IllegalArgumentException::class, "Error text") {
cert.checkValidity({ "Error text" }, { }, Date.from(today.toInstant() + 51.days))
}
}
}

View File

@ -2,37 +2,35 @@ package net.corda.nodeapi.internal.cryptoservice.bouncycastle
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SignatureScheme
import net.corda.core.crypto.internal.cordaBouncyCastleProvider
import net.corda.core.internal.div
import net.corda.core.utilities.days
import net.corda.nodeapi.internal.config.CertificateStoreSupplier
import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.cryptoservice.CryptoService
import net.corda.nodeapi.internal.cryptoservice.CryptoServiceException
import net.corda.nodeapi.internal.cryptoservice.WrappedPrivateKey
import net.corda.nodeapi.internal.cryptoservice.WrappingMode
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.internal.stubs.CertificateStoreStubs
import net.i2p.crypto.eddsa.EdDSAEngine
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers
import org.bouncycastle.asn1.sec.SECObjectIdentifiers
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
import org.bouncycastle.asn1.x9.X9ObjectIdentifiers
import org.bouncycastle.jce.ECNamedCurveTable
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.FileOutputStream
import java.nio.file.Path
import java.security.*
import java.time.Duration
import java.util.*
import javax.security.auth.x500.X500Principal
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class BCCryptoServiceTests {
companion object {
val clearData = "data".toByteArray()
}
@ -46,16 +44,20 @@ class BCCryptoServiceTests {
@JvmField
val temporaryKeystoreFolder = TemporaryFolder()
lateinit var certificatesDirectory: Path
lateinit var wrappingKeyStorePath: Path
@Before
fun setUp() {
val baseDirectory = temporaryFolder.root.toPath()
val certificatesDirectory = baseDirectory / "certificates"
certificatesDirectory = baseDirectory / "certificates"
signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory)
wrappingKeyStorePath = certificatesDirectory / "wrappingkeystore.pkcs12"
}
@Test
fun `BCCryptoService generate key pair and sign both data and cert`() {
val cryptoService = BCCryptoService(ALICE_NAME.x500Principal, signingCertificateStore)
val cryptoService = BCCryptoService(ALICE_NAME.x500Principal, signingCertificateStore, wrappingKeyStorePath)
// Testing every supported scheme.
Crypto.supportedSignatureSchemes().filter { it != Crypto.COMPOSITE_KEY }.forEach { generateKeyAndSignForScheme(cryptoService, it) }
}
@ -87,7 +89,7 @@ class BCCryptoServiceTests {
@Test
fun `BCCryptoService generate key pair and sign with existing schemes`() {
val cryptoService = BCCryptoService(ALICE_NAME.x500Principal, signingCertificateStore)
val cryptoService = BCCryptoService(ALICE_NAME.x500Principal, signingCertificateStore, wrappingKeyStorePath)
// Testing every supported scheme.
Crypto.supportedSignatureSchemes().filter { it != Crypto.COMPOSITE_KEY
&& it.signatureName != "SHA512WITHSPHINCS256"}.forEach {
@ -121,7 +123,7 @@ class BCCryptoServiceTests {
private fun signAndVerify(signAlgo: String, alias: String, keyTypeAlgo: String): Boolean {
val keyPairGenerator = KeyPairGenerator.getInstance(keyTypeAlgo)
val keyPair = keyPairGenerator.genKeyPair()
val cryptoService = BCCryptoService(ALICE_NAME.x500Principal, createKeystore(alias, keyPair))
val cryptoService = BCCryptoService(ALICE_NAME.x500Principal, createKeystore(alias, keyPair), wrappingKeyStorePath)
assertTrue { cryptoService.containsKey(alias) }
val signatureData = cryptoService.sign(alias, clearData, signAlgo)
return verify(signAlgo, cryptoService.getPublicKey(alias), signatureData, clearData)
@ -164,10 +166,90 @@ class BCCryptoServiceTests {
@Test
fun `When key does not exist getPublicKey, sign and getSigner should throw`() {
val nonExistingAlias = "nonExistingAlias"
val cryptoService = BCCryptoService(ALICE_NAME.x500Principal, signingCertificateStore)
val cryptoService = BCCryptoService(ALICE_NAME.x500Principal, signingCertificateStore, wrappingKeyStorePath)
assertFalse { cryptoService.containsKey(nonExistingAlias) }
assertFailsWith<CryptoServiceException> { cryptoService.getPublicKey(nonExistingAlias) }
assertFailsWith<CryptoServiceException> { cryptoService.sign(nonExistingAlias, clearData) }
assertFailsWith<CryptoServiceException> { cryptoService.getSigner(nonExistingAlias) }
}
@Test
fun `cryptoService supports degraded mode of wrapping`() {
val cryptoService = BCCryptoService(ALICE_NAME.x500Principal, signingCertificateStore, wrappingKeyStorePath)
val supportedMode = cryptoService.getWrappingMode()
assertThat(supportedMode).isEqualTo(WrappingMode.DEGRADED_WRAPPED)
}
@Test
fun `cryptoService does not fail when requested to create same wrapping key twice with failIfExists is false`() {
val cryptoService = BCCryptoService(ALICE_NAME.x500Principal, signingCertificateStore, wrappingKeyStorePath)
val keyAlias = UUID.randomUUID().toString()
cryptoService.createWrappingKey(keyAlias)
cryptoService.createWrappingKey(keyAlias, failIfExists = false)
}
@Test
fun `cryptoService does fail when requested to create same wrapping key twice with failIfExists is true`() {
val cryptoService = BCCryptoService(ALICE_NAME.x500Principal, signingCertificateStore, wrappingKeyStorePath)
val keyAlias = UUID.randomUUID().toString()
cryptoService.createWrappingKey(keyAlias)
assertThat(cryptoService.containsKey(keyAlias)).isTrue()
assertThatThrownBy { cryptoService.createWrappingKey(keyAlias) }
.isInstanceOf(IllegalArgumentException::class.java)
.hasMessage("There is an existing key with the alias: $keyAlias")
}
@Test
fun `cryptoService fails when asked to generate wrapped key pair or sign, but the master key specified does not exist`() {
val cryptoService = BCCryptoService(ALICE_NAME.x500Principal, signingCertificateStore, wrappingKeyStorePath)
val wrappingKeyAlias = UUID.randomUUID().toString()
assertThatThrownBy { cryptoService.generateWrappedKeyPair(wrappingKeyAlias) }
.isInstanceOf(IllegalStateException::class.java)
.hasMessage("There is no master key under the alias: $wrappingKeyAlias")
val dummyWrappedPrivateKey = WrappedPrivateKey("key".toByteArray(), Crypto.ECDSA_SECP256R1_SHA256)
val data = "data".toByteArray()
assertThatThrownBy { cryptoService.sign(wrappingKeyAlias, dummyWrappedPrivateKey, data) }
.isInstanceOf(IllegalStateException::class.java)
.hasMessage("There is no master key under the alias: $wrappingKeyAlias")
}
@Test
fun `cryptoService can generate wrapped key pair and sign with the private key successfully, using default algorithm`() {
val cryptoService = BCCryptoService(ALICE_NAME.x500Principal, signingCertificateStore, wrappingKeyStorePath)
val wrappingKeyAlias = UUID.randomUUID().toString()
cryptoService.createWrappingKey(wrappingKeyAlias)
generateWrappedKeyPairSignAndVerify(cryptoService, wrappingKeyAlias)
}
@Test
fun `cryptoService can generate wrapped key pair and sign with the private key successfully`() {
val cryptoService = BCCryptoService(ALICE_NAME.x500Principal, signingCertificateStore, wrappingKeyStorePath)
val wrappingKeyAlias = UUID.randomUUID().toString()
cryptoService.createWrappingKey(wrappingKeyAlias)
generateWrappedKeyPairSignAndVerify(cryptoService, wrappingKeyAlias, Crypto.ECDSA_SECP256R1_SHA256)
generateWrappedKeyPairSignAndVerify(cryptoService, wrappingKeyAlias, Crypto.ECDSA_SECP256K1_SHA256)
generateWrappedKeyPairSignAndVerify(cryptoService, wrappingKeyAlias, Crypto.RSA_SHA256)
}
private fun generateWrappedKeyPairSignAndVerify(cryptoService: CryptoService, wrappingKeyAlias: String, algorithm: SignatureScheme? = null) {
val data = "data".toByteArray()
val (publicKey, wrappedPrivateKey) = if (algorithm != null) {
cryptoService.generateWrappedKeyPair(wrappingKeyAlias, algorithm)
} else {
cryptoService.generateWrappedKeyPair(wrappingKeyAlias)
}
val signature = cryptoService.sign(wrappingKeyAlias, wrappedPrivateKey, data)
Crypto.doVerify(publicKey, signature, data)
}
}

View File

@ -18,21 +18,21 @@ keytool.exe -exportcert -rfc -alias floatroot -keystore floatca.jks -storepass c
keytool.exe -importcert -noprompt -file root.pem -alias root -keystore trust.jks -storepass trustpass
// Create a chain for EC Bridge
keytool.exe -certreq -alias bridgecert -keystore bridge_ec.jks -storepass bridgepass -keypass bridgepass |keytool.exe -gencert -ext ku:c=dig,keyEncipherment -ext: eku:true=serverAuth,clientAuth -rfc -keystore floatca.jks -alias floatroot -storepass capass -keypass cakeypass > bridge_ec.pem
keytool.exe -certreq -alias bridgecert -keystore bridge_ec.jks -storepass bridgepass -keypass bridgepass |keytool.exe -gencert -validity 1000 -ext ku:c=dig,keyEncipherment -ext: eku:true=serverAuth,clientAuth -rfc -keystore floatca.jks -alias floatroot -storepass capass -keypass cakeypass > bridge_ec.pem
type root.pem bridge_ec.pem >> bridgechain_ec.pem
keytool.exe -importcert -noprompt -file bridgechain_ec.pem -alias bridgecert -keystore bridge_ec.jks -storepass bridgepass -keypass bridgepass
// Create a chain for RSA Bridge
keytool.exe -certreq -alias bridgecert -keystore bridge_rsa.jks -storepass bridgepass -keypass bridgepass |keytool.exe -gencert -ext ku:c=dig,keyEncipherment -ext: eku:true=serverAuth,clientAuth -rfc -keystore floatca.jks -alias floatroot -storepass capass -keypass cakeypass > bridge_rsa.pem
keytool.exe -certreq -alias bridgecert -keystore bridge_rsa.jks -storepass bridgepass -keypass bridgepass |keytool.exe -gencert -validity 1000 -ext ku:c=dig,keyEncipherment -ext: eku:true=serverAuth,clientAuth -rfc -keystore floatca.jks -alias floatroot -storepass capass -keypass cakeypass > bridge_rsa.pem
type root.pem bridge_rsa.pem >> bridgechain_rsa.pem
keytool.exe -importcert -noprompt -file bridgechain_rsa.pem -alias bridgecert -keystore bridge_rsa.jks -storepass bridgepass -keypass bridgepass
// Create a chain for EC Float
keytool.exe -certreq -alias floatcert -keystore float_ec.jks -storepass floatpass -keypass floatpass |keytool.exe -gencert -ext ku:c=dig,keyEncipherment -ext: eku::true=serverAuth,clientAuth -rfc -keystore floatca.jks -alias floatroot -storepass capass -keypass cakeypass > float_ec.pem
keytool.exe -certreq -alias floatcert -keystore float_ec.jks -storepass floatpass -keypass floatpass |keytool.exe -gencert -validity 1000 -ext ku:c=dig,keyEncipherment -ext: eku::true=serverAuth,clientAuth -rfc -keystore floatca.jks -alias floatroot -storepass capass -keypass cakeypass > float_ec.pem
type root.pem float_ec.pem >> floatchain_ec.pem
keytool.exe -importcert -noprompt -file floatchain_ec.pem -alias floatcert -keystore float_ec.jks -storepass floatpass -keypass floatpass
// Create a chain for RSA Float
keytool.exe -certreq -alias floatcert -keystore float_rsa.jks -storepass floatpass -keypass floatpass |keytool.exe -gencert -ext ku:c=dig,keyEncipherment -ext: eku::true=serverAuth,clientAuth -rfc -keystore floatca.jks -alias floatroot -storepass capass -keypass cakeypass > float_rsa.pem
keytool.exe -certreq -alias floatcert -keystore float_rsa.jks -storepass floatpass -keypass floatpass |keytool.exe -gencert -validity 1000 -ext ku:c=dig,keyEncipherment -ext: eku::true=serverAuth,clientAuth -rfc -keystore floatca.jks -alias floatroot -storepass capass -keypass cakeypass > float_rsa.pem
type root.pem float_rsa.pem >> floatchain_rsa.pem
keytool.exe -importcert -noprompt -file floatchain_rsa.pem -alias floatcert -keystore float_rsa.jks -storepass floatpass -keypass floatpass

View File

@ -230,6 +230,9 @@ dependencies {
// Optional New Relic JVM reporter, used to push metrics to the configured account associated with a newrelic.yml configuration. See https://mvnrepository.com/artifact/com.palominolabs.metrics/metrics-new-relic
compile "com.palominolabs.metrics:metrics-new-relic:${metrics_new_relic_version}"
// Adding native SSL library to allow using native SSL with Artemis and AMQP
compile "io.netty:netty-tcnative-boringssl-static:$tcnative_version"
// Required by JVMAgentUtil (x-compatible java 8 & 11 agent lookup mechanism)
compile files("${System.properties['java.home']}/../lib/tools.jar")

View File

@ -2,17 +2,23 @@ package net.corda.testing.node.internal
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SignatureScheme
import net.corda.core.crypto.internal.Instances
import net.corda.core.crypto.internal.cordaBouncyCastleProvider
import net.corda.core.crypto.newSecureRandom
import net.corda.core.crypto.sha256
import net.corda.nodeapi.internal.crypto.ContentSignerBuilder
import net.corda.nodeapi.internal.cryptoservice.*
import net.corda.nodeapi.internal.cryptoservice.CryptoService
import net.corda.nodeapi.internal.cryptoservice.CryptoServiceException
import net.corda.nodeapi.internal.cryptoservice.SupportedCryptoServices
import org.bouncycastle.operator.ContentSigner
import java.security.KeyPair
import java.security.PrivateKey
import java.security.PublicKey
import java.security.Signature
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
class MockCryptoService(initialKeyPairs: Map<String, KeyPair>) : CryptoService {
@ -20,6 +26,8 @@ class MockCryptoService(initialKeyPairs: Map<String, KeyPair>) : CryptoService {
private val aliasToKey: MutableMap<String, KeyPair> = mutableMapOf()
private val wrappingKeys: MutableMap<String, SecretKey> = mutableMapOf()
init {
initialKeyPairs.forEach {
aliasToKey[it.key] = it.value
@ -68,4 +76,48 @@ class MockCryptoService(initialKeyPairs: Map<String, KeyPair>) : CryptoService {
aliasToKey[alias] = keyPair
return keyPair.public
}
@Synchronized
override fun createWrappingKey(alias: String, failIfExists: Boolean) {
if (wrappingKeys[alias] != null) {
when (failIfExists) {
true -> throw IllegalArgumentException("There is an existing key with the alias: $alias")
false -> return
}
}
val keyGenerator = KeyGenerator.getInstance("AES")
keyGenerator.init(wrappingKeySize())
val wrappingKey = keyGenerator.generateKey()
wrappingKeys[alias] = wrappingKey
}
override fun generateWrappedKeyPair(masterKeyAlias: String, childKeyScheme: SignatureScheme): Pair<PublicKey, WrappedPrivateKey> {
val wrappingKey = wrappingKeys[masterKeyAlias] ?: throw IllegalStateException("There is no master key under the alias: $masterKeyAlias")
val keyPair = Crypto.generateKeyPair(childKeyScheme)
val cipher = Cipher.getInstance("AES", cordaBouncyCastleProvider)
cipher.init(Cipher.WRAP_MODE, wrappingKey)
val privateKeyMaterialWrapped = cipher.wrap(keyPair.private)
return Pair(keyPair.public, WrappedPrivateKey(privateKeyMaterialWrapped, childKeyScheme))
}
override fun sign(masterKeyAlias: String, wrappedPrivateKey: WrappedPrivateKey, payloadToSign: ByteArray): ByteArray {
val wrappingKey = wrappingKeys[masterKeyAlias] ?: throw IllegalStateException("There is no master key under the alias: $masterKeyAlias")
val cipher = Cipher.getInstance("AES", cordaBouncyCastleProvider)
cipher.init(Cipher.UNWRAP_MODE, wrappingKey)
val privateKey = cipher.unwrap(wrappedPrivateKey.keyMaterial, keyAlgorithmFromScheme(wrappedPrivateKey.signatureScheme), Cipher.PRIVATE_KEY) as PrivateKey
val signature = Instances.getSignatureInstance(wrappedPrivateKey.signatureScheme.signatureName, cordaBouncyCastleProvider)
signature.initSign(privateKey, newSecureRandom())
signature.update(payloadToSign)
return signature.sign()
}
private fun keyAlgorithmFromScheme(scheme: SignatureScheme): String = when (scheme) {
Crypto.ECDSA_SECP256R1_SHA256, Crypto.ECDSA_SECP256K1_SHA256 -> "EC"
Crypto.RSA_SHA256 -> "RSA"
else -> throw IllegalArgumentException("No algorithm for scheme ID ${scheme.schemeNumberID}")
}
override fun getWrappingMode(): WrappingMode? {
return WrappingMode.DEGRADED_WRAPPED
}
}

View File

@ -0,0 +1,99 @@
package net.corda.testing.internal
import io.netty.bootstrap.Bootstrap
import io.netty.channel.ChannelFuture
import io.netty.channel.ChannelInboundHandlerAdapter
import io.netty.handler.ssl.SslContext
import io.netty.channel.ChannelInitializer
import io.netty.channel.ChannelOption
import io.netty.channel.nio.NioEventLoopGroup
import io.netty.channel.socket.SocketChannel
import io.netty.channel.socket.nio.NioSocketChannel
import io.netty.handler.ssl.SslHandler
import java.io.Closeable
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import java.util.concurrent.locks.ReentrantLock
import javax.net.ssl.SSLEngine
import kotlin.concurrent.thread
class NettyTestClient(
val sslContext: SslContext?,
val targetHost: String,
val targetPort: Int,
val handler: ChannelInboundHandlerAdapter
) : Closeable {
internal var mainThread: Thread? = null
internal var channelFuture: ChannelFuture? = null
// lock/condition to make sure that start only returns when the server is actually running
private val lock = ReentrantLock()
private val condition = lock.newCondition()
var engine: SSLEngine? = null
private set
fun start() {
try {
lock.lock()
mainThread = thread(start = true) { run() }
if (!condition.await(5, TimeUnit.SECONDS)) {
throw TimeoutException("Netty test server failed to start")
}
} finally {
lock.unlock()
}
}
private fun run() {
// Configure the client.
val group = NioEventLoopGroup()
try {
val b = Bootstrap()
b.group(group)
.channel(NioSocketChannel::class.java)
.option(ChannelOption.TCP_NODELAY, true)
.handler(object : ChannelInitializer<SocketChannel>() {
@Throws(Exception::class)
public override fun initChannel(ch: SocketChannel) {
val p = ch.pipeline()
if (sslContext != null) {
engine = sslContext.newEngine(ch.alloc(), targetHost, targetPort)
p.addLast(SslHandler(engine))
}
//p.addLast(new LoggingHandler(LogLevel.INFO));
p.addLast(handler)
}
})
// Start the client.
val f = b.connect(targetHost, targetPort)
try {
lock.lock()
condition.signal()
channelFuture = f.sync()
} finally {
lock.unlock()
}
// Wait until the connection is closed.
f.channel().closeFuture().sync()
} finally {
// Shut down the event loop to terminate all threads.
group.shutdownGracefully()
}
}
fun stop() {
channelFuture?.channel()?.close()
mainThread?.join()
mainThread = null
channelFuture = null
}
override fun close() {
stop()
}
}

View File

@ -0,0 +1,74 @@
package net.corda.testing.internal
import io.netty.buffer.ByteBuf
import io.netty.buffer.Unpooled
import io.netty.channel.Channel
import io.netty.channel.ChannelDuplexHandler
import io.netty.channel.ChannelHandlerContext
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock
class NettyTestHandler(val onMessageFunc: (ctx: ChannelHandlerContext?, msg: Any?) -> Unit = { _, _ -> }) : ChannelDuplexHandler() {
private var channel: Channel? = null
private var failure: Throwable? = null
private val lock = ReentrantLock()
private val condition = lock.newCondition()
var readCalledCounter: Int = 0
private set
override fun channelRegistered(ctx: ChannelHandlerContext?) {
channel = ctx?.channel()
super.channelRegistered(ctx)
}
override fun channelRead(ctx: ChannelHandlerContext?, msg: Any?) {
try {
lock.lock()
readCalledCounter++
onMessageFunc(ctx, msg)
} catch( e: Throwable ){
failure = e
} finally {
condition.signal()
lock.unlock()
}
}
fun writeString(msg: String) {
val buffer = Unpooled.wrappedBuffer(msg.toByteArray())
require(channel != null) { "Channel must be registered before sending messages" }
channel!!.writeAndFlush(buffer)
}
fun rethrowIfFailed() {
failure?.also { throw it }
}
fun waitForReadCalled(numberOfExpectedCalls: Int = 1): Boolean {
try {
lock.lock()
if (readCalledCounter >= numberOfExpectedCalls) {
return true
}
while (readCalledCounter < numberOfExpectedCalls) {
if (!condition.await(5, TimeUnit.SECONDS)) {
return false
}
}
return true
} finally {
lock.unlock()
}
}
companion object {
fun readString(buffer: Any?): String {
checkNotNull(buffer)
val ar = ByteArray((buffer as ByteBuf).readableBytes())
buffer.readBytes(ar)
return String(ar)
}
}
}

View File

@ -0,0 +1,97 @@
package net.corda.testing.internal
import io.netty.bootstrap.ServerBootstrap
import io.netty.channel.ChannelFuture
import io.netty.channel.ChannelInboundHandlerAdapter
import io.netty.channel.ChannelInitializer
import io.netty.channel.ChannelOption
import io.netty.channel.nio.NioEventLoopGroup
import io.netty.channel.socket.SocketChannel
import io.netty.channel.socket.nio.NioServerSocketChannel
import io.netty.handler.logging.LogLevel
import io.netty.handler.logging.LoggingHandler
import io.netty.handler.ssl.SslContext
import java.io.Closeable
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import java.util.concurrent.locks.Condition
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.thread
class NettyTestServer(
private val sslContext: SslContext?,
val messageHandler: ChannelInboundHandlerAdapter,
val port: Int
) : Closeable {
internal var mainThread: Thread? = null
internal var channel: ChannelFuture? = null
// lock/condition to make sure that start only returns when the server is actually running
val lock = ReentrantLock()
val condition: Condition = lock.newCondition()
fun start() {
try {
lock.lock()
mainThread = thread(start = true) { run() }
if (!condition.await(5, TimeUnit.SECONDS)) {
throw TimeoutException("Netty test server failed to start")
}
} finally {
lock.unlock()
}
}
fun run() {
// Configure the server.
val bossGroup = NioEventLoopGroup(1)
val workerGroup = NioEventLoopGroup()
try {
val b = ServerBootstrap()
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel::class.java)
.option(ChannelOption.SO_BACKLOG, 100)
.handler(LoggingHandler(LogLevel.INFO))
.childHandler(object : ChannelInitializer<SocketChannel>() {
@Throws(Exception::class)
public override fun initChannel(ch: SocketChannel) {
val p = ch.pipeline()
if (sslContext != null) {
p.addLast(sslContext.newHandler(ch.alloc()))
}
//p.addLast(new LoggingHandler(LogLevel.INFO));
p.addLast(messageHandler)
}
})
// Start the server.
val f = b.bind(port)
try {
lock.lock()
channel = f.sync()
condition.signal()
} finally {
lock.unlock()
}
// Wait until the server socket is closed.
channel!!.channel().closeFuture().sync()
} finally {
// Shut down all event loops to terminate all threads.
bossGroup.shutdownGracefully()
workerGroup.shutdownGracefully()
}
}
fun stop() {
channel?.channel()?.close()
mainThread?.join()
channel = null
mainThread = null
}
override fun close() {
stop()
}
}