mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
Merge branch 'release/os/4.8' into merge-release/os/4.7-release/os/4.8-2023-11-17-6
This commit is contained in:
commit
548242e3cd
@ -1850,13 +1850,15 @@ public final class net.corda.core.crypto.CryptoUtils extends java.lang.Object
|
||||
public static final boolean verify(java.security.PublicKey, byte[], byte[])
|
||||
##
|
||||
public interface net.corda.core.crypto.DigestAlgorithm
|
||||
@NotNull
|
||||
public abstract byte[] componentDigest(byte[])
|
||||
@NotNull
|
||||
public abstract byte[] digest(byte[])
|
||||
@NotNull
|
||||
public abstract String getAlgorithm()
|
||||
public abstract int getDigestLength()
|
||||
@NotNull
|
||||
public abstract byte[] preImageResistantDigest(byte[])
|
||||
public abstract byte[] nonceDigest(byte[])
|
||||
##
|
||||
@CordaSerializable
|
||||
public class net.corda.core.crypto.DigitalSignature extends net.corda.core.utilities.OpaqueBytes
|
||||
@ -5935,6 +5937,13 @@ public @interface net.corda.core.serialization.CordaSerializationTransformRename
|
||||
public @interface net.corda.core.serialization.CordaSerializationTransformRenames
|
||||
public abstract net.corda.core.serialization.CordaSerializationTransformRename[] value()
|
||||
##
|
||||
public interface net.corda.core.serialization.CustomSerializationScheme
|
||||
@NotNull
|
||||
public abstract T deserialize(net.corda.core.utilities.ByteSequence, Class<T>, net.corda.core.serialization.SerializationSchemeContext)
|
||||
public abstract int getSchemeId()
|
||||
@NotNull
|
||||
public abstract net.corda.core.utilities.ByteSequence serialize(T, net.corda.core.serialization.SerializationSchemeContext)
|
||||
##
|
||||
public @interface net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
||||
public abstract int version()
|
||||
##
|
||||
@ -6076,6 +6085,13 @@ public static final class net.corda.core.serialization.SerializationFactory$Comp
|
||||
@NotNull
|
||||
public final net.corda.core.serialization.SerializationFactory getDefaultFactory()
|
||||
##
|
||||
@DoNotImplement
|
||||
public interface net.corda.core.serialization.SerializationSchemeContext
|
||||
@NotNull
|
||||
public abstract ClassLoader getDeserializationClassLoader()
|
||||
@NotNull
|
||||
public abstract net.corda.core.serialization.ClassWhitelist getWhitelist()
|
||||
##
|
||||
public interface net.corda.core.serialization.SerializationToken
|
||||
@NotNull
|
||||
public abstract Object fromToken(net.corda.core.serialization.SerializeAsTokenContext)
|
||||
|
@ -70,6 +70,7 @@ pipeline {
|
||||
stage('Compile') {
|
||||
steps {
|
||||
dir(sameAgentFolder) {
|
||||
authenticateGradleWrapper()
|
||||
sh script: [
|
||||
'./gradlew',
|
||||
COMMON_GRADLE_PARAMS,
|
||||
|
4
.ci/dev/forward-merge/Jenkinsfile
vendored
4
.ci/dev/forward-merge/Jenkinsfile
vendored
@ -13,13 +13,13 @@
|
||||
* the branch name of origin branch, it should match the current branch
|
||||
* and it acts as a fail-safe inside {@code forwardMerger} pipeline
|
||||
*/
|
||||
String originBranch = 'release/os/4.7'
|
||||
String originBranch = 'release/os/4.8'
|
||||
|
||||
/**
|
||||
* the branch name of target branch, it should be the branch with the next version
|
||||
* after the one in current branch.
|
||||
*/
|
||||
String targetBranch = 'release/os/4.8'
|
||||
String targetBranch = 'release/os/4.9'
|
||||
|
||||
/**
|
||||
* Forward merge any changes between #originBranch and #targetBranch
|
||||
|
1
.ci/dev/mswin/Jenkinsfile
vendored
1
.ci/dev/mswin/Jenkinsfile
vendored
@ -56,6 +56,7 @@ pipeline {
|
||||
stage('Unit Tests') {
|
||||
agent { label 'mswin' }
|
||||
steps {
|
||||
authenticateGradleWrapper()
|
||||
bat "./gradlew --no-daemon " +
|
||||
"--stacktrace " +
|
||||
"-Pcompilation.warningsAsErrors=false " +
|
||||
|
1
.ci/dev/nightly-regression/Jenkinsfile
vendored
1
.ci/dev/nightly-regression/Jenkinsfile
vendored
@ -50,6 +50,7 @@ pipeline {
|
||||
stages {
|
||||
stage('Compile') {
|
||||
steps {
|
||||
authenticateGradleWrapper()
|
||||
sh script: [
|
||||
'./gradlew',
|
||||
COMMON_GRADLE_PARAMS,
|
||||
|
2
.ci/dev/open-j9/Jenkinsfile
vendored
2
.ci/dev/open-j9/Jenkinsfile
vendored
@ -3,6 +3,7 @@
|
||||
* Jenkins pipeline to build Corda OS release branches and tags.
|
||||
* PLEASE NOTE: we DO want to run a build for each commit!!!
|
||||
*/
|
||||
@Library('corda-shared-build-pipeline-steps')
|
||||
|
||||
/**
|
||||
* Sense environment
|
||||
@ -47,6 +48,7 @@ pipeline {
|
||||
stages {
|
||||
stage('Unit Tests') {
|
||||
steps {
|
||||
authenticateGradleWrapper()
|
||||
sh "./gradlew clean --continue test --info -Ptests.failFast=true"
|
||||
}
|
||||
}
|
||||
|
2
.ci/dev/pr-code-checks/Jenkinsfile
vendored
2
.ci/dev/pr-code-checks/Jenkinsfile
vendored
@ -30,6 +30,7 @@ pipeline {
|
||||
stages {
|
||||
stage('Detekt check') {
|
||||
steps {
|
||||
authenticateGradleWrapper()
|
||||
sh "./gradlew --no-daemon --parallel --build-cache clean detekt"
|
||||
}
|
||||
}
|
||||
@ -54,6 +55,7 @@ pipeline {
|
||||
GRADLE_USER_HOME = "/host_tmp/gradle"
|
||||
}
|
||||
steps {
|
||||
authenticateGradleWrapper()
|
||||
sh 'mkdir -p ${GRADLE_USER_HOME}'
|
||||
snykDeltaScan(env.SNYK_API_TOKEN, env.C4_OS_SNYK_ORG_ID)
|
||||
}
|
||||
|
1
.ci/dev/publish-api-docs/Jenkinsfile
vendored
1
.ci/dev/publish-api-docs/Jenkinsfile
vendored
@ -33,6 +33,7 @@ pipeline {
|
||||
stage('Publish Archived API Docs to Artifactory') {
|
||||
when { tag pattern: /^docs-release-os-V(\d+\.\d+)(\.\d+){0,1}(-GA){0,1}(-\d{4}-\d\d-\d\d-\d{4}){0,1}$/, comparator: 'REGEXP' }
|
||||
steps {
|
||||
authenticateGradleWrapper()
|
||||
sh "./gradlew :clean :docs:artifactoryPublish -DpublishApiDocs"
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ pipeline {
|
||||
stages {
|
||||
stage('Publish to Artifactory') {
|
||||
steps {
|
||||
authenticateGradleWrapper()
|
||||
rtServer (
|
||||
id: 'R3-Artifactory',
|
||||
url: 'https://software.r3.com/artifactory',
|
||||
|
14
.ci/dev/regression/Jenkinsfile
vendored
14
.ci/dev/regression/Jenkinsfile
vendored
@ -70,6 +70,7 @@ pipeline {
|
||||
stages {
|
||||
stage('Compile') {
|
||||
steps {
|
||||
authenticateGradleWrapper()
|
||||
sh script: [
|
||||
'./gradlew',
|
||||
COMMON_GRADLE_PARAMS,
|
||||
@ -168,6 +169,7 @@ pipeline {
|
||||
}
|
||||
stage('Recompile') {
|
||||
steps {
|
||||
authenticateGradleWrapper()
|
||||
sh script: [
|
||||
'./gradlew',
|
||||
COMMON_GRADLE_PARAMS,
|
||||
@ -390,23 +392,23 @@ pipeline {
|
||||
}
|
||||
}
|
||||
success {
|
||||
script {
|
||||
sendSlackNotifications("good", "BUILD PASSED", false, "#corda-corda4-open-source-build-notifications")
|
||||
script {
|
||||
sendSlackNotifications("good", "BUILD PASSED", false, "#corda-corda4-open-source-build-notifications")
|
||||
if (isReleaseTag || isReleaseCandidate || isReleaseBranch) {
|
||||
snykSecurityScan.generateHtmlElements()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
unstable {
|
||||
script {
|
||||
sendSlackNotifications("warning", "BUILD UNSTABLE", false, "#corda-corda4-open-source-build-notifications")
|
||||
script {
|
||||
sendSlackNotifications("warning", "BUILD UNSTABLE", false, "#corda-corda4-open-source-build-notifications")
|
||||
if (isReleaseTag || isReleaseCandidate || isReleaseBranch) {
|
||||
snykSecurityScan.generateHtmlElements()
|
||||
}
|
||||
if (isReleaseTag || isReleaseCandidate || isReleaseBranch) {
|
||||
snykSecurityScan.generateHtmlElements()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
failure {
|
||||
script {
|
||||
|
3
.github/workflows/jira_create_issue.yml
vendored
3
.github/workflows/jira_create_issue.yml
vendored
@ -16,6 +16,7 @@ jobs:
|
||||
with:
|
||||
jiraBaseUrl: https://r3-cev.atlassian.net
|
||||
project: CORDA
|
||||
squad: Corda
|
||||
issuetype: Bug
|
||||
summary: ${{ github.event.issue.title }}
|
||||
labels: community
|
||||
@ -33,4 +34,4 @@ jobs:
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
Automatically created Jira issue: ${{ steps.create.outputs.issue }}
|
||||
reaction-type: '+1'
|
||||
reaction-type: '+1'
|
||||
|
2
Jenkinsfile
vendored
2
Jenkinsfile
vendored
@ -58,6 +58,7 @@ pipeline {
|
||||
stages {
|
||||
stage('Compile') {
|
||||
steps {
|
||||
authenticateGradleWrapper()
|
||||
sh script: [
|
||||
'./gradlew',
|
||||
COMMON_GRADLE_PARAMS,
|
||||
@ -100,6 +101,7 @@ pipeline {
|
||||
}
|
||||
stage('Recompile') {
|
||||
steps {
|
||||
authenticateGradleWrapper()
|
||||
sh script: [
|
||||
'./gradlew',
|
||||
COMMON_GRADLE_PARAMS,
|
||||
|
@ -101,7 +101,7 @@ buildscript {
|
||||
ext.hibernate_version = '5.4.32.Final'
|
||||
ext.h2_version = '1.4.199' // Update docs if renamed or removed.
|
||||
ext.rxjava_version = '1.3.8'
|
||||
ext.dokka_version = '0.9.17'
|
||||
ext.dokka_version = '0.10.1'
|
||||
ext.eddsa_version = '0.3.0'
|
||||
ext.dependency_checker_version = '5.2.0'
|
||||
ext.commons_collections_version = '4.3'
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.client.rpc
|
||||
|
||||
import io.netty.util.concurrent.DefaultThreadFactory
|
||||
import net.corda.client.rpc.internal.RPCClient
|
||||
import net.corda.client.rpc.internal.ReconnectingCordaRPCOps
|
||||
import net.corda.client.rpc.internal.SerializationEnvironmentHelper
|
||||
@ -52,7 +53,7 @@ class CordaRPCConnection private constructor(
|
||||
sslConfiguration: ClientRpcSslOptions? = null,
|
||||
classLoader: ClassLoader? = null
|
||||
): CordaRPCConnection {
|
||||
val observersPool: ExecutorService = Executors.newCachedThreadPool()
|
||||
val observersPool: ExecutorService = Executors.newCachedThreadPool(DefaultThreadFactory("RPCObserver"))
|
||||
return CordaRPCConnection(null, observersPool, ReconnectingCordaRPCOps(
|
||||
addresses,
|
||||
username,
|
||||
|
@ -17,7 +17,6 @@ import net.corda.core.serialization.internal.nodeSerializationEnv
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcConnectorTcpTransport
|
||||
import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcConnectorTcpTransportsFromList
|
||||
import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcInternalClientTcpTransport
|
||||
import net.corda.nodeapi.internal.RoundRobinConnectionPolicy
|
||||
import net.corda.nodeapi.internal.config.SslConfiguration
|
||||
@ -61,8 +60,12 @@ class RPCClient<I : RPCOps>(
|
||||
sslConfiguration: ClientRpcSslOptions? = null,
|
||||
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT,
|
||||
serializationContext: SerializationContext = SerializationDefaults.RPC_CLIENT_CONTEXT
|
||||
) : this(rpcConnectorTcpTransport(haAddressPool.first(), sslConfiguration),
|
||||
configuration, serializationContext, rpcConnectorTcpTransportsFromList(haAddressPool, sslConfiguration))
|
||||
) : this(
|
||||
rpcConnectorTcpTransport(haAddressPool.first(), sslConfiguration),
|
||||
configuration,
|
||||
serializationContext,
|
||||
haAddressPool.map { rpcConnectorTcpTransport(it, sslConfiguration) }
|
||||
)
|
||||
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.client.rpc.internal
|
||||
|
||||
import io.netty.util.concurrent.DefaultThreadFactory
|
||||
import net.corda.client.rpc.ConnectionFailureException
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.client.rpc.CordaRPCClientConfiguration
|
||||
@ -99,7 +100,8 @@ class ReconnectingCordaRPCOps private constructor(
|
||||
ErrorInterceptingHandler(reconnectingRPCConnection)) as CordaRPCOps
|
||||
}
|
||||
}
|
||||
private val retryFlowsPool = Executors.newScheduledThreadPool(1)
|
||||
private val retryFlowsPool = Executors.newScheduledThreadPool(1, DefaultThreadFactory("FlowRetry"))
|
||||
|
||||
/**
|
||||
* This function runs a flow and retries until it completes successfully.
|
||||
*
|
||||
|
@ -9,4 +9,4 @@ package net.corda.common.logging
|
||||
* (originally added to source control for ease of use)
|
||||
*/
|
||||
|
||||
internal const val CURRENT_MAJOR_RELEASE = "4.7-SNAPSHOT"
|
||||
internal const val CURRENT_MAJOR_RELEASE = "4.8-SNAPSHOT"
|
@ -2,7 +2,7 @@
|
||||
# because some versions here need to be matched by app authors in
|
||||
# their own projects. So don't get fancy with syntax!
|
||||
|
||||
cordaVersion=4.7
|
||||
cordaVersion=4.8
|
||||
versionSuffix=SNAPSHOT
|
||||
gradlePluginsVersion=5.0.12
|
||||
kotlinVersion=1.2.71
|
||||
@ -11,7 +11,7 @@ java8MinUpdateVersion=171
|
||||
# When incrementing platformVersion make sure to update #
|
||||
# net.corda.core.internal.CordaUtilsKt.PLATFORM_VERSION as well. #
|
||||
# ***************************************************************#
|
||||
platformVersion=9
|
||||
platformVersion=10
|
||||
guavaVersion=28.0-jre
|
||||
# Quasar version to use with Java 8:
|
||||
quasarVersion=0.7.15_r3
|
||||
@ -21,7 +21,7 @@ jdkClassifier11=jdk11
|
||||
dockerJavaVersion=3.2.5
|
||||
proguardVersion=6.1.1
|
||||
bouncycastleVersion=1.68
|
||||
classgraphVersion=4.8.90
|
||||
classgraphVersion=4.8.135
|
||||
disruptorVersion=3.4.2
|
||||
typesafeConfigVersion=1.3.4
|
||||
jsr305Version=3.0.2
|
||||
|
@ -23,7 +23,10 @@ def javaHome = System.getProperty('java.home')
|
||||
def jarBaseName = "corda-${project.name}".toString()
|
||||
|
||||
configurations {
|
||||
deterministicLibraries.extendsFrom api
|
||||
deterministicLibraries {
|
||||
canBeConsumed = false
|
||||
extendsFrom api
|
||||
}
|
||||
deterministicArtifacts.extendsFrom deterministicLibraries
|
||||
}
|
||||
|
||||
@ -59,7 +62,7 @@ def originalJar = coreJarTask.map { it.outputs.files.singleFile }
|
||||
|
||||
def patchCore = tasks.register('patchCore', Zip) {
|
||||
dependsOn coreJarTask
|
||||
destinationDirectory = file("$buildDir/source-libs")
|
||||
destinationDirectory = layout.buildDirectory.dir('source-libs')
|
||||
metadataCharset 'UTF-8'
|
||||
archiveClassifier = 'transient'
|
||||
archiveExtension = 'jar'
|
||||
@ -169,7 +172,7 @@ def determinise = tasks.register('determinise', ProGuardTask) {
|
||||
def checkDeterminism = tasks.register('checkDeterminism', ProGuardTask)
|
||||
|
||||
def metafix = tasks.register('metafix', MetaFixerTask) {
|
||||
outputDir file("$buildDir/libs")
|
||||
outputDir = layout.buildDirectory.dir('libs')
|
||||
jars determinise
|
||||
suffix ""
|
||||
|
||||
|
@ -55,12 +55,16 @@ abstract class SerializationFactory {
|
||||
* Change the current context inside the block to that supplied.
|
||||
*/
|
||||
fun <T> withCurrentContext(context: SerializationContext?, block: () -> T): T {
|
||||
val priorContext = _currentContext
|
||||
if (context != null) _currentContext = context
|
||||
try {
|
||||
return block()
|
||||
} finally {
|
||||
if (context != null) _currentContext = priorContext
|
||||
return if (context == null) {
|
||||
block()
|
||||
} else {
|
||||
val priorContext = _currentContext
|
||||
_currentContext = context
|
||||
try {
|
||||
block()
|
||||
} finally {
|
||||
_currentContext = priorContext
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,9 @@ plugins {
|
||||
}
|
||||
|
||||
configurations {
|
||||
testData
|
||||
testData {
|
||||
canBeResolved = false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
@ -9,7 +9,12 @@ apply from: "${rootProject.projectDir}/deterministic.gradle"
|
||||
description 'Test utilities for deterministic contract verification'
|
||||
|
||||
configurations {
|
||||
deterministicArtifacts
|
||||
deterministicArtifacts {
|
||||
canBeResolved = false
|
||||
}
|
||||
|
||||
// Compile against the deterministic artifacts to ensure that we use only the deterministic API subset.
|
||||
compileOnly.extendsFrom deterministicArtifacts
|
||||
runtimeArtifacts.extendsFrom api
|
||||
}
|
||||
|
||||
@ -20,8 +25,6 @@ dependencies {
|
||||
runtimeArtifacts project(':serialization')
|
||||
runtimeArtifacts project(':core')
|
||||
|
||||
// Compile against the deterministic artifacts to ensure that we use only the deterministic API subset.
|
||||
compileOnly configurations.deterministicArtifacts
|
||||
api "junit:junit:$junit_version"
|
||||
runtimeOnly "org.junit.vintage:junit-vintage-engine:$junit_vintage_version"
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import net.corda.core.transactions.WireTransaction
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
//TODO the use of deprecated toLedgerTransaction need to be revisited as resolveContractAttachment requires attachments of the transactions which created input states...
|
||||
//TODO ...to check contract version non downgrade rule, curretly dummy Attachment if not fund is used which sets contract version to '1'
|
||||
//TODO ...to check contract version non downgrade rule, currently dummy Attachment if not fund is used which sets contract version to '1'
|
||||
@CordaSerializable
|
||||
class TransactionVerificationRequest(val wtxToVerify: SerializedBytes<WireTransaction>,
|
||||
val dependencies: Array<SerializedBytes<WireTransaction>>,
|
||||
|
@ -5,8 +5,11 @@ import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.crypto.internal.DigestAlgorithmFactory
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.BLAKE2s256DigestAlgorithm
|
||||
import net.corda.core.internal.SHA256BLAKE2s256DigestAlgorithm
|
||||
import net.corda.core.node.NotaryInfo
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.serialization.deserialize
|
||||
@ -49,12 +52,19 @@ class PartialMerkleTreeTest(private var digestService: DigestService) {
|
||||
val MINI_CORP get() = miniCorp.party
|
||||
val MINI_CORP_PUBKEY get() = miniCorp.publicKey
|
||||
|
||||
init {
|
||||
DigestAlgorithmFactory.registerClass(BLAKE2s256DigestAlgorithm::class.java.name)
|
||||
DigestAlgorithmFactory.registerClass(SHA256BLAKE2s256DigestAlgorithm::class.java.name)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters
|
||||
fun data(): Collection<DigestService> = listOf(
|
||||
DigestService.sha2_256,
|
||||
DigestService.sha2_384,
|
||||
DigestService.sha2_512
|
||||
DigestService.sha2_512,
|
||||
DigestService("BLAKE_TEST"),
|
||||
DigestService("SHA256-BLAKE2S256-TEST")
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -140,7 +140,7 @@ class PartialMerkleTreeWithNamedHashTest {
|
||||
fun `building Merkle tree one node`() {
|
||||
val node = 'a'.serialize().sha2_384()
|
||||
val mt = MerkleTree.getMerkleTree(listOf(node), DigestService.sha2_384)
|
||||
assertEquals(node, mt.hash)
|
||||
assertNotEquals(node, mt.hash)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
|
@ -0,0 +1,29 @@
|
||||
package net.corda.coretests.crypto.internal
|
||||
|
||||
import net.corda.coretesting.internal.DEV_ROOT_CA
|
||||
import net.corda.testing.core.createCRL
|
||||
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
||||
import org.junit.Test
|
||||
|
||||
class ProviderMapTest {
|
||||
// https://github.com/corda/corda/pull/3997
|
||||
@Test(timeout = 300_000)
|
||||
fun `verify CRL algorithms`() {
|
||||
val crl = createCRL(
|
||||
issuer = DEV_ROOT_CA,
|
||||
revokedCerts = emptyList(),
|
||||
signatureAlgorithm = "SHA256withECDSA"
|
||||
)
|
||||
// This should pass.
|
||||
crl.verify(DEV_ROOT_CA.keyPair.public)
|
||||
|
||||
// Try changing the algorithm to EC will fail.
|
||||
assertThatIllegalArgumentException().isThrownBy {
|
||||
createCRL(
|
||||
issuer = DEV_ROOT_CA,
|
||||
revokedCerts = emptyList(),
|
||||
signatureAlgorithm = "EC"
|
||||
)
|
||||
}.withMessage("Unknown signature type requested: EC")
|
||||
}
|
||||
}
|
@ -55,7 +55,8 @@ class AttachmentsClassLoaderSerializationTests {
|
||||
arrayOf(isolatedId, att1, att2).map { storage.openAttachment(it)!! },
|
||||
testNetworkParameters(),
|
||||
SecureHash.zeroHash,
|
||||
{ attachmentTrustCalculator.calculate(it) }, attachmentsClassLoaderCache = null) { classLoader ->
|
||||
{ attachmentTrustCalculator.calculate(it) }, attachmentsClassLoaderCache = null) { serializationContext ->
|
||||
val classLoader = serializationContext.deserializationClassLoader
|
||||
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classLoader)
|
||||
val contract = contractClass.getDeclaredConstructor().newInstance() as Contract
|
||||
assertEquals("helloworld", contract.declaredField<Any?>("magicString").value)
|
||||
|
@ -24,8 +24,9 @@ import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.serialization.internal.AttachmentsClassLoader
|
||||
import net.corda.core.serialization.internal.AttachmentsClassLoaderCacheImpl
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.node.services.attachments.NodeAttachmentTrustCalculator
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
@ -74,7 +75,7 @@ class AttachmentsClassLoaderTests {
|
||||
val BOB = TestIdentity(BOB_NAME, 80).party
|
||||
val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20)
|
||||
val DUMMY_NOTARY get() = dummyNotary.party
|
||||
val PROGRAM_ID: String = "net.corda.testing.contracts.MyDummyContract"
|
||||
const val PROGRAM_ID = "net.corda.testing.contracts.MyDummyContract"
|
||||
}
|
||||
|
||||
@Rule
|
||||
@ -89,7 +90,7 @@ class AttachmentsClassLoaderTests {
|
||||
private lateinit var internalStorage: InternalMockAttachmentStorage
|
||||
private lateinit var attachmentTrustCalculator: AttachmentTrustCalculator
|
||||
private val networkParameters = testNetworkParameters()
|
||||
private val cacheFactory = TestingNamedCacheFactory()
|
||||
private val cacheFactory = TestingNamedCacheFactory(1)
|
||||
|
||||
private fun createClassloader(
|
||||
attachment: AttachmentId,
|
||||
@ -541,6 +542,50 @@ class AttachmentsClassLoaderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `class loader not closed after cache starts evicting`() {
|
||||
tempFolder.root.toPath().let { path ->
|
||||
val transactions = mutableListOf<LedgerTransaction>()
|
||||
val iterations = 10
|
||||
|
||||
val baseOutState = TransactionState(DummyContract.SingleOwnerState(0, ALICE), PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint)
|
||||
val inputs = emptyList<StateAndRef<*>>()
|
||||
val outputs = listOf(baseOutState, baseOutState.copy(notary = ALICE), baseOutState.copy(notary = BOB))
|
||||
val commands = emptyList<CommandWithParties<CommandData>>()
|
||||
val content = createContractString(PROGRAM_ID)
|
||||
val timeWindow: TimeWindow? = null
|
||||
val attachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(cacheFactory)
|
||||
val contractJarPath = ContractJarTestUtils.makeTestContractJar(path, PROGRAM_ID, content = content)
|
||||
val attachments = createAttachments(contractJarPath)
|
||||
|
||||
for(i in 1 .. iterations) {
|
||||
val id = SecureHash.randomSHA256()
|
||||
val privacySalt = PrivacySalt()
|
||||
val transaction = createLedgerTransaction(
|
||||
inputs,
|
||||
outputs,
|
||||
commands,
|
||||
attachments,
|
||||
id,
|
||||
null,
|
||||
timeWindow,
|
||||
privacySalt,
|
||||
testNetworkParameters(),
|
||||
emptyList(),
|
||||
isAttachmentTrusted = { true },
|
||||
attachmentsClassLoaderCache = attachmentsClassLoaderCache
|
||||
)
|
||||
transactions.add(transaction)
|
||||
System.gc()
|
||||
Thread.sleep(1)
|
||||
}
|
||||
|
||||
transactions.forEach {
|
||||
it.verify()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createContractString(contractName: String, versionSeed: Int = 0): String {
|
||||
val pkgs = contractName.split(".")
|
||||
val className = pkgs.last()
|
||||
@ -563,7 +608,7 @@ class AttachmentsClassLoaderTests {
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
System.out.println(output)
|
||||
println(output)
|
||||
return output
|
||||
}
|
||||
|
||||
@ -571,6 +616,7 @@ class AttachmentsClassLoaderTests {
|
||||
|
||||
val attachment = object : AbstractAttachment({contractJarPath.inputStream().readBytes()}, uploader = "app") {
|
||||
@Suppress("OverridingDeprecatedMember")
|
||||
@Deprecated("Use signerKeys. There is no requirement that attachment signers are Corda parties.")
|
||||
override val signers: List<Party> = emptyList()
|
||||
override val signerKeys: List<PublicKey> = emptyList()
|
||||
override val size: Int = 1234
|
||||
@ -581,6 +627,7 @@ class AttachmentsClassLoaderTests {
|
||||
return listOf(
|
||||
object : AbstractAttachment({ISOLATED_CONTRACTS_JAR_PATH.openStream().readBytes()}, uploader = "app") {
|
||||
@Suppress("OverridingDeprecatedMember")
|
||||
@Deprecated("Use signerKeys. There is no requirement that attachment signers are Corda parties.")
|
||||
override val signers: List<Party> = emptyList()
|
||||
override val signerKeys: List<PublicKey> = emptyList()
|
||||
override val size: Int = 1234
|
||||
@ -589,6 +636,7 @@ class AttachmentsClassLoaderTests {
|
||||
object : AbstractAttachment({fakeAttachment("importantDoc.pdf", "I am a pdf!").inputStream().readBytes()
|
||||
}, uploader = "app") {
|
||||
@Suppress("OverridingDeprecatedMember")
|
||||
@Deprecated("Use signerKeys. There is no requirement that attachment signers are Corda parties.")
|
||||
override val signers: List<Party> = emptyList()
|
||||
override val signerKeys: List<PublicKey> = emptyList()
|
||||
override val size: Int = 1234
|
||||
|
@ -0,0 +1,186 @@
|
||||
package net.corda.coretests.transactions
|
||||
|
||||
import net.corda.core.contracts.ComponentGroupEnum.COMMANDS_GROUP
|
||||
import net.corda.core.contracts.ComponentGroupEnum.INPUTS_GROUP
|
||||
import net.corda.core.contracts.ComponentGroupEnum.NOTARY_GROUP
|
||||
import net.corda.core.contracts.ComponentGroupEnum.OUTPUTS_GROUP
|
||||
import net.corda.core.contracts.ComponentGroupEnum.SIGNERS_GROUP
|
||||
import net.corda.core.contracts.ComponentGroupEnum.TIMEWINDOW_GROUP
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.PrivacySalt
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.contracts.TransactionState
|
||||
import net.corda.core.crypto.DigestService
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.crypto.internal.DigestAlgorithmFactory
|
||||
import net.corda.core.internal.SHA256BLAKE2s256DigestAlgorithm
|
||||
import net.corda.core.internal.accessAvailableComponentHashes
|
||||
import net.corda.core.internal.accessAvailableComponentNonces
|
||||
import net.corda.core.internal.accessGroupMerkleRoots
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.ComponentGroup
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.contracts.DummyState
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.core.TestIdentity
|
||||
import net.corda.testing.core.dummyCommand
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class MerkleTreeAgilityTest {
|
||||
private companion object {
|
||||
val DUMMY_KEY_1 = generateKeyPair()
|
||||
val DUMMY_KEY_2 = generateKeyPair()
|
||||
val BOB = TestIdentity(BOB_NAME, 80).party
|
||||
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
|
||||
}
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
|
||||
private val dummyOutState = TransactionState(DummyState(0), DummyContract.PROGRAM_ID, DUMMY_NOTARY)
|
||||
private val stateRef1 = StateRef(SecureHash.randomSHA256(), 0)
|
||||
private val stateRef2 = StateRef(SecureHash.randomSHA256(), 1)
|
||||
private val stateRef3 = StateRef(SecureHash.randomSHA256(), 2)
|
||||
private val stateRef4 = StateRef(SecureHash.randomSHA256(), 3)
|
||||
|
||||
private val singleInput = listOf(stateRef1) // 1 elements.
|
||||
private val threeInputs = listOf(stateRef1, stateRef2, stateRef3) // 3 elements.
|
||||
private val fourInputs = listOf(stateRef1, stateRef2, stateRef3, stateRef4) // 4 elements.
|
||||
|
||||
private val outputs = listOf(dummyOutState, dummyOutState.copy(notary = BOB)) // 2 elements.
|
||||
private val commands = listOf(dummyCommand(DUMMY_KEY_1.public, DUMMY_KEY_2.public)) // 1 element.
|
||||
private val notary = DUMMY_NOTARY
|
||||
private val timeWindow = TimeWindow.fromOnly(Instant.now())
|
||||
private val privacySalt: PrivacySalt = PrivacySalt()
|
||||
|
||||
private val singleInputsGroup by lazy { ComponentGroup(INPUTS_GROUP.ordinal, singleInput.map { it.serialize() }) }
|
||||
private val threeInputsGroup by lazy { ComponentGroup(INPUTS_GROUP.ordinal, threeInputs.map { it.serialize() }) }
|
||||
private val fourInputsGroup by lazy { ComponentGroup(INPUTS_GROUP.ordinal, fourInputs.map { it.serialize() }) }
|
||||
private val outputGroup by lazy { ComponentGroup(OUTPUTS_GROUP.ordinal, outputs.map { it.serialize() }) }
|
||||
private val commandGroup by lazy { ComponentGroup(COMMANDS_GROUP.ordinal, commands.map { it.value.serialize() }) }
|
||||
private val notaryGroup by lazy { ComponentGroup(NOTARY_GROUP.ordinal, listOf(notary.serialize())) }
|
||||
private val timeWindowGroup by lazy { ComponentGroup(TIMEWINDOW_GROUP.ordinal, listOf(timeWindow.serialize())) }
|
||||
private val signersGroup by lazy { ComponentGroup(SIGNERS_GROUP.ordinal, commands.map { it.signers.serialize() }) }
|
||||
|
||||
private val componentGroupsSingle by lazy {
|
||||
listOf(singleInputsGroup, outputGroup, commandGroup, notaryGroup, timeWindowGroup, signersGroup)
|
||||
}
|
||||
|
||||
private val componentGroupsFourInputs by lazy {
|
||||
listOf(fourInputsGroup, outputGroup, commandGroup, notaryGroup, timeWindowGroup, signersGroup)
|
||||
}
|
||||
|
||||
private val componentGroupsThreeInputs by lazy {
|
||||
listOf(threeInputsGroup, outputGroup, commandGroup, notaryGroup, timeWindowGroup, signersGroup)
|
||||
}
|
||||
|
||||
private val defaultDigestService = DigestService.sha2_256
|
||||
private val customDigestService = DigestService("SHA256-BLAKE2S256-TEST")
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
DigestAlgorithmFactory.registerClass(SHA256BLAKE2s256DigestAlgorithm::class.java.name)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `component nonces are correct for custom preimage resistant hash algo`() {
|
||||
val wireTransaction = WireTransaction(componentGroups = componentGroupsFourInputs, privacySalt = privacySalt, digestService = customDigestService)
|
||||
val expected = componentGroupsFourInputs.associate {
|
||||
it.groupIndex to it.components.mapIndexed { componentIndexInGroup, _ ->
|
||||
customDigestService.computeNonce(privacySalt, it.groupIndex, componentIndexInGroup)
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(expected, wireTransaction.accessAvailableComponentNonces())
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `component nonces are correct for default SHA256 hash algo`() {
|
||||
val wireTransaction = WireTransaction(componentGroups = componentGroupsFourInputs, privacySalt = privacySalt)
|
||||
val expected = componentGroupsFourInputs.associate {
|
||||
it.groupIndex to it.components.mapIndexed { componentIndexInGroup, componentBytes ->
|
||||
defaultDigestService.componentHash(componentBytes, privacySalt, it.groupIndex, componentIndexInGroup)
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(expected, wireTransaction.accessAvailableComponentNonces())
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `custom algorithm transaction pads leaf in single component component group`() {
|
||||
val wtx = WireTransaction(componentGroups = componentGroupsSingle, privacySalt = privacySalt, digestService = customDigestService)
|
||||
|
||||
val inputsTreeLeaves: List<SecureHash> = wtx.accessAvailableComponentHashes()[INPUTS_GROUP.ordinal]!!
|
||||
val expected = customDigestService.hash(inputsTreeLeaves[0].bytes + customDigestService.zeroHash.bytes)
|
||||
|
||||
assertEquals(expected, wtx.accessGroupMerkleRoots()[INPUTS_GROUP.ordinal]!!)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `default algorithm transaction does not pad leaf in single component component group`() {
|
||||
val wtx = WireTransaction(componentGroups = componentGroupsSingle, privacySalt = privacySalt, digestService = defaultDigestService)
|
||||
|
||||
val inputsTreeLeaves: List<SecureHash> = wtx.accessAvailableComponentHashes()[INPUTS_GROUP.ordinal]!!
|
||||
val expected = inputsTreeLeaves[0]
|
||||
|
||||
assertEquals(expected, wtx.accessGroupMerkleRoots()[INPUTS_GROUP.ordinal]!!)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `custom algorithm transaction has expected root for four components component group tree`() {
|
||||
val wtx = WireTransaction(componentGroups = componentGroupsFourInputs, privacySalt = privacySalt, digestService = customDigestService)
|
||||
|
||||
val inputsTreeLeaves: List<SecureHash> = wtx.accessAvailableComponentHashes()[INPUTS_GROUP.ordinal]!!
|
||||
val h1 = customDigestService.hash(inputsTreeLeaves[0].bytes + inputsTreeLeaves[1].bytes)
|
||||
val h2 = customDigestService.hash(inputsTreeLeaves[2].bytes + inputsTreeLeaves[3].bytes)
|
||||
val expected = customDigestService.hash(h1.bytes + h2.bytes)
|
||||
|
||||
assertEquals(expected, wtx.accessGroupMerkleRoots()[INPUTS_GROUP.ordinal]!!)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `default algorithm transaction has expected root for four components component group tree`() {
|
||||
val wtx = WireTransaction(componentGroups = componentGroupsFourInputs, privacySalt = privacySalt, digestService = defaultDigestService)
|
||||
|
||||
val inputsTreeLeaves: List<SecureHash> = wtx.accessAvailableComponentHashes()[INPUTS_GROUP.ordinal]!!
|
||||
val h1 = defaultDigestService.hash(inputsTreeLeaves[0].bytes + inputsTreeLeaves[1].bytes)
|
||||
val h2 = defaultDigestService.hash(inputsTreeLeaves[2].bytes + inputsTreeLeaves[3].bytes)
|
||||
val expected = defaultDigestService.hash(h1.bytes + h2.bytes)
|
||||
|
||||
assertEquals(expected, wtx.accessGroupMerkleRoots()[INPUTS_GROUP.ordinal]!!)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `custom algorithm transaction has expected root for three components component group tree`() {
|
||||
val wtx = WireTransaction(componentGroups = componentGroupsThreeInputs, privacySalt = privacySalt, digestService = customDigestService)
|
||||
|
||||
val inputsTreeLeaves: List<SecureHash> = wtx.accessAvailableComponentHashes()[INPUTS_GROUP.ordinal]!!
|
||||
val h1 = customDigestService.hash(inputsTreeLeaves[0].bytes + inputsTreeLeaves[1].bytes)
|
||||
val h2 = customDigestService.hash(inputsTreeLeaves[2].bytes + customDigestService.zeroHash.bytes)
|
||||
val expected = customDigestService.hash(h1.bytes + h2.bytes)
|
||||
|
||||
assertEquals(expected, wtx.accessGroupMerkleRoots()[INPUTS_GROUP.ordinal]!!)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `default algorithm transaction has expected root for three components component group tree`() {
|
||||
val wtx = WireTransaction(componentGroups = componentGroupsThreeInputs, privacySalt = privacySalt, digestService = defaultDigestService)
|
||||
|
||||
val inputsTreeLeaves: List<SecureHash> = wtx.accessAvailableComponentHashes()[INPUTS_GROUP.ordinal]!!
|
||||
val h1 = defaultDigestService.hash(inputsTreeLeaves[0].bytes + inputsTreeLeaves[1].bytes)
|
||||
val h2 = defaultDigestService.hash(inputsTreeLeaves[2].bytes + defaultDigestService.zeroHash.bytes)
|
||||
val expected = defaultDigestService.hash(h1.bytes + h2.bytes)
|
||||
|
||||
assertEquals(expected, wtx.accessGroupMerkleRoots()[INPUTS_GROUP.ordinal]!!)
|
||||
}
|
||||
}
|
@ -3,7 +3,16 @@ package net.corda.coretests.transactions
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.Command
|
||||
import net.corda.core.contracts.ContractAttachment
|
||||
import net.corda.core.contracts.HashAttachmentConstraint
|
||||
import net.corda.core.contracts.PrivacySalt
|
||||
import net.corda.core.contracts.SignatureAttachmentConstraint
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.contracts.TransactionState
|
||||
import net.corda.core.contracts.TransactionVerificationException
|
||||
import net.corda.core.cordapp.CordappProvider
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.DigestService
|
||||
@ -20,11 +29,16 @@ import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.node.services.NetworkParametersService
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.coretesting.internal.rigorousMock
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.contracts.DummyState
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.coretesting.internal.rigorousMock
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||
import net.corda.testing.core.DummyCommandData
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.core.TestIdentity
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.Assert.assertFalse
|
||||
@ -35,6 +49,7 @@ import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class TransactionBuilderTest {
|
||||
@Rule
|
||||
@ -299,4 +314,22 @@ class TransactionBuilderTest {
|
||||
HashAgility.init()
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `toWireTransaction fails if no scheme is registered with schemeId`() {
|
||||
val outputState = TransactionState(
|
||||
data = DummyState(),
|
||||
contract = DummyContract.PROGRAM_ID,
|
||||
notary = notary,
|
||||
constraint = HashAttachmentConstraint(contractAttachmentId)
|
||||
)
|
||||
val builder = TransactionBuilder()
|
||||
.addOutputState(outputState)
|
||||
.addCommand(DummyCommandData, notary.owningKey)
|
||||
|
||||
val schemeId = 7
|
||||
assertFailsWith<UnsupportedOperationException>("Could not find custom serialization scheme with SchemeId = $schemeId.") {
|
||||
builder.toWireTransaction(services, schemeId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import net.corda.testing.internal.createWireTransaction
|
||||
import net.corda.testing.internal.fakeAttachment
|
||||
import net.corda.coretesting.internal.rigorousMock
|
||||
import net.corda.testing.internal.TestingNamedCacheFactory
|
||||
import org.assertj.core.api.Assertions.fail
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@ -36,6 +37,7 @@ import kotlin.test.assertNotEquals
|
||||
@RunWith(Parameterized::class)
|
||||
class TransactionTests(private val digestService : DigestService) {
|
||||
private companion object {
|
||||
const val ISOLATED_JAR = "isolated-4.0.jar"
|
||||
val DUMMY_KEY_1 = generateKeyPair()
|
||||
val DUMMY_KEY_2 = generateKeyPair()
|
||||
val DUMMY_CASH_ISSUER_KEY = entropyToKeyPair(BigInteger.valueOf(10))
|
||||
@ -200,15 +202,15 @@ class TransactionTests(private val digestService : DigestService) {
|
||||
|
||||
val outputs = listOf(outState)
|
||||
val commands = emptyList<CommandWithParties<CommandData>>()
|
||||
val attachments = listOf(object : AbstractAttachment({
|
||||
AttachmentsClassLoaderTests::class.java.getResource("isolated-4.0.jar").openStream().readBytes()
|
||||
val attachments = listOf(ContractAttachment(object : AbstractAttachment({
|
||||
(AttachmentsClassLoaderTests::class.java.getResource(ISOLATED_JAR) ?: fail("Missing $ISOLATED_JAR")).openStream().readBytes()
|
||||
}, TESTDSL_UPLOADER) {
|
||||
@Suppress("OverridingDeprecatedMember")
|
||||
override val signers: List<Party> = emptyList()
|
||||
override val signerKeys: List<PublicKey> = emptyList()
|
||||
override val size: Int = 1234
|
||||
override val id: SecureHash = SecureHash.zeroHash
|
||||
})
|
||||
}, DummyContract.PROGRAM_ID))
|
||||
val id = digestService.randomHash()
|
||||
val timeWindow: TimeWindow? = null
|
||||
val privacySalt = PrivacySalt(digestService.digestLength)
|
||||
|
@ -12,6 +12,10 @@ description 'Corda core'
|
||||
// required by DJVM and Avian JVM (for running inside the SGX enclave) which only supports Java 8.
|
||||
targetCompatibility = VERSION_1_8
|
||||
|
||||
sourceSets {
|
||||
obfuscator
|
||||
}
|
||||
|
||||
configurations {
|
||||
integrationTestCompile.extendsFrom testCompile
|
||||
integrationTestRuntimeOnly.extendsFrom testRuntimeOnly
|
||||
@ -22,6 +26,9 @@ configurations {
|
||||
|
||||
dependencies {
|
||||
|
||||
obfuscatorImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
|
||||
testImplementation sourceSets.obfuscator.output
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}"
|
||||
testImplementation "junit:junit:$junit_version"
|
||||
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:${junit_vintage_version}"
|
||||
@ -110,7 +117,16 @@ configurations {
|
||||
}
|
||||
|
||||
|
||||
test{
|
||||
processTestResources {
|
||||
inputs.files(jar)
|
||||
into("zip") {
|
||||
from(jar) {
|
||||
rename { "core.jar" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
maxParallelForks = (System.env.CORDA_CORE_TESTING_FORKS == null) ? 1 : "$System.env.CORDA_CORE_TESTING_FORKS".toInteger()
|
||||
}
|
||||
|
||||
@ -163,3 +179,10 @@ scanApi {
|
||||
publish {
|
||||
name jar.baseName
|
||||
}
|
||||
|
||||
tasks.register("writeTestResources", JavaExec) {
|
||||
classpath sourceSets.obfuscator.output
|
||||
classpath sourceSets.obfuscator.runtimeClasspath
|
||||
main 'net.corda.core.internal.utilities.TestResourceWriter'
|
||||
args new File(sourceSets.test.resources.srcDirs.first(), "zip").toString()
|
||||
}
|
||||
|
@ -343,7 +343,11 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S
|
||||
"You will need to manually install the CorDapp to whitelist it for use.")
|
||||
|
||||
@KeepForDJVM
|
||||
class UnsupportedHashTypeException(txId: SecureHash) : TransactionVerificationException(txId, "The transaction Id is defined by an unsupported hash type", null);
|
||||
class UnsupportedHashTypeException(txId: SecureHash) : TransactionVerificationException(txId, "The transaction Id is defined by an unsupported hash type", null)
|
||||
|
||||
@KeepForDJVM
|
||||
class AttachmentTooBigException(txId: SecureHash) : TransactionVerificationException(
|
||||
txId, "The transaction attachments are too large and exceed both max transaction size and the maximum allowed compression ratio", null)
|
||||
|
||||
/*
|
||||
If you add a new class extending [TransactionVerificationException], please add a test in `TransactionVerificationExceptionSerializationTests`
|
||||
|
@ -25,8 +25,16 @@ interface DigestAlgorithm {
|
||||
fun digest(bytes: ByteArray): ByteArray
|
||||
|
||||
/**
|
||||
* Computes the digest of the [ByteArray] which is resistant to pre-image attacks.
|
||||
* Computes the digest of the [ByteArray] which is resistant to pre-image attacks. Only used to calculate the hash of the leaves of the
|
||||
* ComponentGroup Merkle tree, starting from its serialized components.
|
||||
* Default implementation provides double hashing, but can it be changed to single hashing or something else for better performance.
|
||||
*/
|
||||
fun preImageResistantDigest(bytes: ByteArray): ByteArray = digest(digest(bytes))
|
||||
fun componentDigest(bytes: ByteArray): ByteArray = digest(digest(bytes))
|
||||
|
||||
/**
|
||||
* Computes the digest of the [ByteArray] which is resistant to pre-image attacks. Only used to calculate the nonces for the leaves of
|
||||
* the ComponentGroup Merkle tree.
|
||||
* Default implementation provides double hashing, but can it be changed to single hashing or something else for better performance.
|
||||
*/
|
||||
fun nonceDigest(bytes: ByteArray): ByteArray = digest(digest(bytes))
|
||||
}
|
||||
|
@ -75,23 +75,20 @@ data class DigestService(val hashAlgorithm: String) {
|
||||
val zeroHash: SecureHash
|
||||
get() = SecureHash.zeroHashFor(hashAlgorithm)
|
||||
|
||||
// val privacySalt: PrivacySalt
|
||||
// get() = PrivacySalt.createFor(hashAlgorithm)
|
||||
|
||||
/**
|
||||
* Compute the hash of each serialised component so as to be used as Merkle tree leaf. The resultant output (leaf) is
|
||||
* calculated using the service's hash algorithm, thus HASH(HASH(nonce || serializedComponent)) for SHA2-256 and other
|
||||
* algorithms loaded via JCA [MessageDigest], or DigestAlgorithm.preImageResistantDigest(nonce || serializedComponent)
|
||||
* algorithms loaded via JCA [MessageDigest], or DigestAlgorithm.componentDigest(nonce || serializedComponent)
|
||||
* otherwise, where nonce is computed from [computeNonce].
|
||||
*/
|
||||
fun componentHash(opaqueBytes: OpaqueBytes, privacySalt: PrivacySalt, componentGroupIndex: Int, internalIndex: Int): SecureHash =
|
||||
componentHash(computeNonce(privacySalt, componentGroupIndex, internalIndex), opaqueBytes)
|
||||
|
||||
/** Return the HASH(HASH(nonce || serializedComponent)) for SHA2-256 and other algorithms loaded via JCA [MessageDigest],
|
||||
* otherwise it's defined by DigestAlgorithm.preImageResistantDigest(nonce || serializedComponent). */
|
||||
* otherwise it's defined by DigestAlgorithm.componentDigest(nonce || serializedComponent). */
|
||||
fun componentHash(nonce: SecureHash, opaqueBytes: OpaqueBytes): SecureHash {
|
||||
val data = nonce.bytes + opaqueBytes.bytes
|
||||
return SecureHash.preImageResistantHashAs(hashAlgorithm, data)
|
||||
return SecureHash.componentHashAs(hashAlgorithm, data)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -109,11 +106,11 @@ data class DigestService(val hashAlgorithm: String) {
|
||||
* @param groupIndex the fixed index (ordinal) of this component group.
|
||||
* @param internalIndex the internal index of this object in its corresponding components list.
|
||||
* @return HASH(HASH(privacySalt || groupIndex || internalIndex)) for SHA2-256 and other algorithms loaded via JCA [MessageDigest],
|
||||
* otherwise it's defined by DigestAlgorithm.preImageResistantDigest(privacySalt || groupIndex || internalIndex).
|
||||
* otherwise it's defined by DigestAlgorithm.nonceDigest(privacySalt || groupIndex || internalIndex).
|
||||
*/
|
||||
fun computeNonce(privacySalt: PrivacySalt, groupIndex: Int, internalIndex: Int) : SecureHash {
|
||||
val data = (privacySalt.bytes + ByteBuffer.allocate(NONCE_SIZE).putInt(groupIndex).putInt(internalIndex).array())
|
||||
return SecureHash.preImageResistantHashAs(hashAlgorithm, data)
|
||||
return SecureHash.nonceHashAs(hashAlgorithm, data)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -37,19 +37,19 @@ sealed class MerkleTree {
|
||||
require(algorithms.size == 1) {
|
||||
"Cannot build Merkle tree with multiple hash algorithms: $algorithms"
|
||||
}
|
||||
val leaves = padWithZeros(allLeavesHashes).map { Leaf(it) }
|
||||
val leaves = padWithZeros(allLeavesHashes, nodeDigestService.hashAlgorithm == SecureHash.SHA2_256).map { Leaf(it) }
|
||||
return buildMerkleTree(leaves, nodeDigestService)
|
||||
}
|
||||
|
||||
// If number of leaves in the tree is not a power of 2, we need to pad it with zero hashes.
|
||||
private fun padWithZeros(allLeavesHashes: List<SecureHash>): List<SecureHash> {
|
||||
private fun padWithZeros(allLeavesHashes: List<SecureHash>, singleLeafWithoutPadding: Boolean): List<SecureHash> {
|
||||
var n = allLeavesHashes.size
|
||||
if (isPow2(n)) return allLeavesHashes
|
||||
if (isPow2(n) && (n > 1 || singleLeafWithoutPadding)) return allLeavesHashes
|
||||
val paddedHashes = ArrayList(allLeavesHashes)
|
||||
val zeroHash = SecureHash.zeroHashFor(paddedHashes[0].algorithm)
|
||||
while (!isPow2(n++)) {
|
||||
do {
|
||||
paddedHashes.add(zeroHash)
|
||||
}
|
||||
} while (!isPow2(++n))
|
||||
return paddedHashes
|
||||
}
|
||||
|
||||
|
@ -216,13 +216,31 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
* @param bytes The [ByteArray] to hash.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun preImageResistantHashAs(algorithm: String, bytes: ByteArray): SecureHash {
|
||||
fun componentHashAs(algorithm: String, bytes: ByteArray): SecureHash {
|
||||
return if (algorithm == SHA2_256) {
|
||||
sha256Twice(bytes)
|
||||
} else {
|
||||
val digest = digestFor(algorithm).get()
|
||||
val firstHash = digest.preImageResistantDigest(bytes)
|
||||
HASH(algorithm, digest.digest(firstHash))
|
||||
val hash = digest.componentDigest(bytes)
|
||||
HASH(algorithm, hash)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the digest of the [ByteArray] which is resistant to pre-image attacks.
|
||||
* It computes the hash of the hash for SHA2-256 and other algorithms loaded via JCA [MessageDigest].
|
||||
* For custom algorithms the strategy can be modified via [DigestAlgorithm].
|
||||
* @param algorithm The [MessageDigest] algorithm to use.
|
||||
* @param bytes The [ByteArray] to hash.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun nonceHashAs(algorithm: String, bytes: ByteArray): SecureHash {
|
||||
return if (algorithm == SHA2_256) {
|
||||
sha256Twice(bytes)
|
||||
} else {
|
||||
val digest = digestFor(algorithm).get()
|
||||
val hash = digest.nonceDigest(bytes)
|
||||
HASH(algorithm, hash)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,9 +28,7 @@ sealed class DigestAlgorithmFactory {
|
||||
}
|
||||
|
||||
private class CustomAlgorithmFactory(className: String) : DigestAlgorithmFactory() {
|
||||
val constructor: Constructor<out DigestAlgorithm> = javaClass
|
||||
.classLoader
|
||||
.loadClass(className)
|
||||
val constructor: Constructor<out DigestAlgorithm> = Class.forName(className, false, javaClass.classLoader)
|
||||
.asSubclass(DigestAlgorithm::class.java)
|
||||
.getConstructor()
|
||||
override val algorithm: String = constructor.newInstance().algorithm
|
||||
|
@ -11,9 +11,9 @@ import kotlin.concurrent.withLock
|
||||
private val pooledScanMutex = ReentrantLock()
|
||||
|
||||
/**
|
||||
* Use this rather than the built in implementation of [scan] on [ClassGraph]. The built in implementation of [scan] creates
|
||||
* a thread pool every time resulting in too many threads. This one uses a mutex to restrict concurrency.
|
||||
* Use this rather than the built-in implementation of [ClassGraph.scan]. The built-in implementation creates
|
||||
* a thread pool every time, resulting in too many threads. This one uses a mutex to restrict concurrency.
|
||||
*/
|
||||
fun ClassGraph.pooledScan(): ScanResult {
|
||||
return pooledScanMutex.withLock { this@pooledScan.scan() }
|
||||
return pooledScanMutex.withLock(::scan)
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ import net.corda.core.serialization.internal.AttachmentURLStreamHandlerFactory.a
|
||||
fun <T: Any> createInstancesOfClassesImplementing(classloader: ClassLoader, clazz: Class<T>,
|
||||
classVersionRange: IntRange? = null): Set<T> {
|
||||
return getNamesOfClassesImplementing(classloader, clazz, classVersionRange)
|
||||
.map { classloader.loadClass(it).asSubclass(clazz) }
|
||||
.map { Class.forName(it, false, classloader).asSubclass(clazz) }
|
||||
.mapTo(LinkedHashSet()) { it.kotlin.objectOrNewInstance() }
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@ typealias Version = Int
|
||||
* Attention: this value affects consensus, so it requires a minimum platform version bump in order to be changed.
|
||||
*/
|
||||
const val MAX_NUMBER_OF_KEYS_IN_SIGNATURE_CONSTRAINT = 20
|
||||
private const val DJVM_SANDBOX_PREFIX = "sandbox."
|
||||
|
||||
private val log = loggerFor<AttachmentConstraint>()
|
||||
|
||||
@ -29,10 +30,14 @@ val Attachment.contractVersion: Version get() = if (this is ContractAttachment)
|
||||
val ContractState.requiredContractClassName: String? get() {
|
||||
val annotation = javaClass.getAnnotation(BelongsToContract::class.java)
|
||||
if (annotation != null) {
|
||||
return annotation.value.java.typeName
|
||||
return annotation.value.java.typeName.removePrefix(DJVM_SANDBOX_PREFIX)
|
||||
}
|
||||
val enclosingClass = javaClass.enclosingClass ?: return null
|
||||
return if (Contract::class.java.isAssignableFrom(enclosingClass)) enclosingClass.typeName else null
|
||||
return if (Contract::class.java.isAssignableFrom(enclosingClass)) {
|
||||
enclosingClass.typeName.removePrefix(DJVM_SANDBOX_PREFIX)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -28,7 +28,7 @@ import java.util.jar.JarInputStream
|
||||
|
||||
// *Internal* Corda-specific utilities.
|
||||
|
||||
const val PLATFORM_VERSION = 9
|
||||
const val PLATFORM_VERSION = 10
|
||||
|
||||
fun ServicesForResolution.ensureMinimumPlatformVersion(requiredMinPlatformVersion: Int, feature: String) {
|
||||
checkMinimumPlatformVersion(networkParameters.minimumPlatformVersion, requiredMinPlatformVersion, feature)
|
||||
|
@ -56,7 +56,9 @@ import java.security.cert.TrustAnchor
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Duration
|
||||
import java.time.temporal.Temporal
|
||||
import java.util.*
|
||||
import java.util.Collections
|
||||
import java.util.PrimitiveIterator
|
||||
import java.util.Spliterator
|
||||
import java.util.Spliterator.DISTINCT
|
||||
import java.util.Spliterator.IMMUTABLE
|
||||
import java.util.Spliterator.NONNULL
|
||||
@ -64,6 +66,7 @@ import java.util.Spliterator.ORDERED
|
||||
import java.util.Spliterator.SIZED
|
||||
import java.util.Spliterator.SORTED
|
||||
import java.util.Spliterator.SUBSIZED
|
||||
import java.util.Spliterators
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.stream.Collectors
|
||||
|
@ -16,6 +16,6 @@ object PlatformVersionSwitches {
|
||||
const val LIMIT_KEYS_IN_SIGNATURE_CONSTRAINTS = 5
|
||||
const val BATCH_DOWNLOAD_COUNTERPARTY_BACKCHAIN = 6
|
||||
const val ENABLE_P2P_COMPRESSION = 7
|
||||
const val CERTIFICATE_ROTATION = 9
|
||||
const val RESTRICTED_DATABASE_OPERATIONS = 7
|
||||
const val CERTIFICATE_ROTATION = 9
|
||||
}
|
@ -54,7 +54,7 @@ fun combinedHash(components: Iterable<SecureHash>, digestService: DigestService)
|
||||
components.forEach {
|
||||
stream.write(it.bytes)
|
||||
}
|
||||
return digestService.hash(stream.toByteArray());
|
||||
return digestService.hash(stream.toByteArray())
|
||||
}
|
||||
|
||||
/**
|
||||
@ -114,14 +114,14 @@ fun deserialiseCommands(
|
||||
componentGroups: List<ComponentGroup>,
|
||||
forceDeserialize: Boolean = false,
|
||||
factory: SerializationFactory = SerializationFactory.defaultFactory,
|
||||
@Suppress("UNUSED_PARAMETER") context: SerializationContext = factory.defaultContext,
|
||||
context: SerializationContext = factory.defaultContext,
|
||||
digestService: DigestService = DigestService.sha2_256
|
||||
): List<Command<*>> {
|
||||
// TODO: we could avoid deserialising unrelated signers.
|
||||
// However, current approach ensures the transaction is not malformed
|
||||
// and it will throw if any of the signers objects is not List of public keys).
|
||||
val signersList: List<List<PublicKey>> = uncheckedCast(deserialiseComponentGroup(componentGroups, List::class, ComponentGroupEnum.SIGNERS_GROUP, forceDeserialize))
|
||||
val commandDataList: List<CommandData> = deserialiseComponentGroup(componentGroups, CommandData::class, ComponentGroupEnum.COMMANDS_GROUP, forceDeserialize)
|
||||
val signersList: List<List<PublicKey>> = uncheckedCast(deserialiseComponentGroup(componentGroups, List::class, ComponentGroupEnum.SIGNERS_GROUP, forceDeserialize, factory, context))
|
||||
val commandDataList: List<CommandData> = deserialiseComponentGroup(componentGroups, CommandData::class, ComponentGroupEnum.COMMANDS_GROUP, forceDeserialize, factory, context)
|
||||
val group = componentGroups.firstOrNull { it.groupIndex == ComponentGroupEnum.COMMANDS_GROUP.ordinal }
|
||||
return if (group is FilteredComponentGroup) {
|
||||
check(commandDataList.size <= signersList.size) {
|
||||
@ -154,7 +154,9 @@ fun createComponentGroups(inputs: List<StateRef>,
|
||||
timeWindow: TimeWindow?,
|
||||
references: List<StateRef>,
|
||||
networkParametersHash: SecureHash?): List<ComponentGroup> {
|
||||
val serialize = { value: Any, _: Int -> value.serialize() }
|
||||
val serializationFactory = SerializationFactory.defaultFactory
|
||||
val serializationContext = serializationFactory.defaultContext
|
||||
val serialize = { value: Any, _: Int -> value.serialize(serializationFactory, serializationContext) }
|
||||
val componentGroupMap: MutableList<ComponentGroup> = mutableListOf()
|
||||
if (inputs.isNotEmpty()) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.INPUTS_GROUP.ordinal, inputs.lazyMapped(serialize)))
|
||||
if (references.isNotEmpty()) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.REFERENCES_GROUP.ordinal, references.lazyMapped(serialize)))
|
||||
@ -177,7 +179,11 @@ fun createComponentGroups(inputs: List<StateRef>,
|
||||
*/
|
||||
@KeepForDJVM
|
||||
data class SerializedStateAndRef(val serializedState: SerializedBytes<TransactionState<ContractState>>, val ref: StateRef) {
|
||||
fun toStateAndRef(): StateAndRef<ContractState> = StateAndRef(serializedState.deserialize(), ref)
|
||||
fun toStateAndRef(factory: SerializationFactory, context: SerializationContext) = StateAndRef(serializedState.deserialize(factory, context), ref)
|
||||
fun toStateAndRef(): StateAndRef<ContractState> {
|
||||
val factory = SerializationFactory.defaultFactory
|
||||
return toStateAndRef(factory, factory.defaultContext)
|
||||
}
|
||||
}
|
||||
|
||||
/** Check that network parameters hash on this transaction is the current hash for the network. */
|
||||
|
@ -3,14 +3,40 @@ package net.corda.core.internal
|
||||
import net.corda.core.DeleteForDJVM
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.contracts.Contract
|
||||
import net.corda.core.contracts.ContractAttachment
|
||||
import net.corda.core.contracts.ContractClassName
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.HashAttachmentConstraint
|
||||
import net.corda.core.contracts.SignatureAttachmentConstraint
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TransactionState
|
||||
import net.corda.core.contracts.TransactionVerificationException
|
||||
import net.corda.core.contracts.TransactionVerificationException.ConflictingAttachmentsRejection
|
||||
import net.corda.core.contracts.TransactionVerificationException.ConstraintPropagationRejection
|
||||
import net.corda.core.contracts.TransactionVerificationException.ContractConstraintRejection
|
||||
import net.corda.core.contracts.TransactionVerificationException.ContractCreationError
|
||||
import net.corda.core.contracts.TransactionVerificationException.ContractRejection
|
||||
import net.corda.core.contracts.TransactionVerificationException.Direction
|
||||
import net.corda.core.contracts.TransactionVerificationException.DuplicateAttachmentsRejection
|
||||
import net.corda.core.contracts.TransactionVerificationException.InvalidConstraintRejection
|
||||
import net.corda.core.contracts.TransactionVerificationException.MissingAttachmentRejection
|
||||
import net.corda.core.contracts.TransactionVerificationException.NotaryChangeInWrongTransactionType
|
||||
import net.corda.core.contracts.TransactionVerificationException.TransactionContractConflictException
|
||||
import net.corda.core.contracts.TransactionVerificationException.TransactionDuplicateEncumbranceException
|
||||
import net.corda.core.contracts.TransactionVerificationException.TransactionMissingEncumbranceException
|
||||
import net.corda.core.contracts.TransactionVerificationException.TransactionNonMatchingEncumbranceException
|
||||
import net.corda.core.contracts.TransactionVerificationException.TransactionNotaryMismatchEncumbranceException
|
||||
import net.corda.core.contracts.TransactionVerificationException.TransactionRequiredContractUnspecifiedException
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.internal.rules.StateContractValidationEnforcementRule
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import java.util.function.Function
|
||||
import java.util.function.Supplier
|
||||
|
||||
@DeleteForDJVM
|
||||
interface TransactionVerifierServiceInternal {
|
||||
@ -22,16 +48,54 @@ interface TransactionVerifierServiceInternal {
|
||||
*/
|
||||
fun LedgerTransaction.prepareVerify(attachments: List<Attachment>) = internalPrepareVerify(attachments)
|
||||
|
||||
interface Verifier {
|
||||
|
||||
/**
|
||||
* Placeholder function for the verification logic.
|
||||
*/
|
||||
fun verify()
|
||||
}
|
||||
|
||||
// This class allows us unit-test transaction verification more easily.
|
||||
abstract class AbstractVerifier(
|
||||
protected val ltx: LedgerTransaction,
|
||||
protected val transactionClassLoader: ClassLoader
|
||||
) : Verifier {
|
||||
protected abstract val transaction: Supplier<LedgerTransaction>
|
||||
|
||||
protected companion object {
|
||||
@JvmField
|
||||
val logger = loggerFor<Verifier>()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the transaction is internally consistent, and then check that it is
|
||||
* contract-valid by running verify() for each input and output state contract.
|
||||
* If any contract fails to verify, the whole transaction is considered to be invalid.
|
||||
*
|
||||
* Note: Reference states are not verified.
|
||||
*/
|
||||
final override fun verify() {
|
||||
try {
|
||||
TransactionVerifier(transactionClassLoader).apply(transaction)
|
||||
} catch (e: TransactionVerificationException) {
|
||||
logger.error("Error validating transaction ${ltx.id}.", e.cause)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Because we create a separate [LedgerTransaction] onto which we need to perform verification, it becomes important we don't verify the
|
||||
* wrong object instance. This class helps avoid that.
|
||||
*/
|
||||
abstract class Verifier(val ltx: LedgerTransaction, protected val transactionClassLoader: ClassLoader) {
|
||||
private val inputStates: List<TransactionState<*>> = ltx.inputs.map { it.state }
|
||||
private val allStates: List<TransactionState<*>> = inputStates + ltx.references.map { it.state } + ltx.outputs
|
||||
@KeepForDJVM
|
||||
private class Validator(private val ltx: LedgerTransaction, private val transactionClassLoader: ClassLoader) {
|
||||
private val inputStates: List<TransactionState<*>> = ltx.inputs.map(StateAndRef<ContractState>::state)
|
||||
private val allStates: List<TransactionState<*>> = inputStates + ltx.references.map(StateAndRef<ContractState>::state) + ltx.outputs
|
||||
|
||||
companion object {
|
||||
val logger = contextLogger()
|
||||
private companion object {
|
||||
private val logger = loggerFor<Validator>()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -39,9 +103,9 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
||||
*
|
||||
* It is a critical piece of the security of the platform.
|
||||
*
|
||||
* @throws TransactionVerificationException
|
||||
* @throws net.corda.core.contracts.TransactionVerificationException
|
||||
*/
|
||||
fun verify() {
|
||||
fun validate() {
|
||||
// checkNoNotaryChange and checkEncumbrancesValid are called here, and not in the c'tor, as they need access to the "outputs"
|
||||
// list, the contents of which need to be deserialized under the correct classloader.
|
||||
checkNoNotaryChange()
|
||||
@ -68,8 +132,7 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
||||
// 4. Check that the [TransactionState] objects are correctly formed.
|
||||
validateStatesAgainstContract()
|
||||
|
||||
// 5. Final step is to run the contract code. After the first 4 steps we are now sure that we are running the correct code.
|
||||
verifyContracts()
|
||||
// 5. Final step will be to run the contract code.
|
||||
}
|
||||
|
||||
private fun checkTransactionWithTimeWindowIsNotarised() {
|
||||
@ -81,11 +144,12 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
||||
* It makes sure there is one and only one.
|
||||
* This is an important piece of the security of transactions.
|
||||
*/
|
||||
@Suppress("ThrowsCount")
|
||||
private fun getUniqueContractAttachmentsByContract(): Map<ContractClassName, ContractAttachment> {
|
||||
val contractClasses = allStates.map { it.contract }.toSet()
|
||||
val contractClasses = allStates.mapTo(LinkedHashSet(), TransactionState<*>::contract)
|
||||
|
||||
// Check that there are no duplicate attachments added.
|
||||
if (ltx.attachments.size != ltx.attachments.toSet().size) throw TransactionVerificationException.DuplicateAttachmentsRejection(ltx.id, ltx.attachments.groupBy { it }.filterValues { it.size > 1 }.keys.first())
|
||||
if (ltx.attachments.size != ltx.attachments.toSet().size) throw DuplicateAttachmentsRejection(ltx.id, ltx.attachments.groupBy { it }.filterValues { it.size > 1 }.keys.first())
|
||||
|
||||
// For each attachment this finds all the relevant state contracts that it provides.
|
||||
// And then maps them to the attachment.
|
||||
@ -103,12 +167,12 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
||||
.groupBy { it.first } // Group by contract.
|
||||
.filter { (_, attachments) -> attachments.size > 1 } // And only keep contracts that are in multiple attachments. It's guaranteed that attachments were unique by a previous check.
|
||||
.keys.firstOrNull() // keep the first one - if any - to throw a meaningful exception.
|
||||
if (contractWithMultipleAttachments != null) throw TransactionVerificationException.ConflictingAttachmentsRejection(ltx.id, contractWithMultipleAttachments)
|
||||
if (contractWithMultipleAttachments != null) throw ConflictingAttachmentsRejection(ltx.id, contractWithMultipleAttachments)
|
||||
|
||||
val result = contractAttachmentsPerContract.toMap()
|
||||
|
||||
// Check that there is an attachment for each contract.
|
||||
if (result.keys != contractClasses) throw TransactionVerificationException.MissingAttachmentRejection(ltx.id, contractClasses.minus(result.keys).first())
|
||||
if (result.keys != contractClasses) throw MissingAttachmentRejection(ltx.id, contractClasses.minus(result.keys).first())
|
||||
|
||||
return result
|
||||
}
|
||||
@ -124,7 +188,7 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
||||
if (ltx.notary != null && (ltx.inputs.isNotEmpty() || ltx.references.isNotEmpty())) {
|
||||
ltx.outputs.forEach {
|
||||
if (it.notary != ltx.notary) {
|
||||
throw TransactionVerificationException.NotaryChangeInWrongTransactionType(ltx.id, ltx.notary, it.notary)
|
||||
throw NotaryChangeInWrongTransactionType(ltx.id, ltx.notary, it.notary)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -156,10 +220,10 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
||||
it.ref.txhash == ref.txhash && it.ref.index == state.encumbrance
|
||||
}
|
||||
if (!encumbranceStateExists) {
|
||||
throw TransactionVerificationException.TransactionMissingEncumbranceException(
|
||||
throw TransactionMissingEncumbranceException(
|
||||
ltx.id,
|
||||
state.encumbrance!!,
|
||||
TransactionVerificationException.Direction.INPUT
|
||||
Direction.INPUT
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -185,6 +249,7 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
||||
// b -> c and c -> b
|
||||
// c -> a b -> a
|
||||
// and form a full cycle, meaning that the bi-directionality property is satisfied.
|
||||
@Suppress("ThrowsCount")
|
||||
private fun checkBidirectionalOutputEncumbrances(statesAndEncumbrance: List<Pair<Int, Int>>) {
|
||||
// [Set] of "from" (encumbered states).
|
||||
val encumberedSet = mutableSetOf<Int>()
|
||||
@ -194,15 +259,15 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
||||
statesAndEncumbrance.forEach { (statePosition, encumbrance) ->
|
||||
// Check it does not refer to itself.
|
||||
if (statePosition == encumbrance || encumbrance >= ltx.outputs.size) {
|
||||
throw TransactionVerificationException.TransactionMissingEncumbranceException(
|
||||
throw TransactionMissingEncumbranceException(
|
||||
ltx.id,
|
||||
encumbrance,
|
||||
TransactionVerificationException.Direction.OUTPUT
|
||||
Direction.OUTPUT
|
||||
)
|
||||
} else {
|
||||
encumberedSet.add(statePosition) // Guaranteed to have unique elements.
|
||||
if (!encumbranceSet.add(encumbrance)) {
|
||||
throw TransactionVerificationException.TransactionDuplicateEncumbranceException(ltx.id, encumbrance)
|
||||
throw TransactionDuplicateEncumbranceException(ltx.id, encumbrance)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -211,7 +276,7 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
||||
val symmetricDifference = (encumberedSet union encumbranceSet).subtract(encumberedSet intersect encumbranceSet)
|
||||
if (symmetricDifference.isNotEmpty()) {
|
||||
// At least one encumbered state is not in the [encumbranceSet] and vice versa.
|
||||
throw TransactionVerificationException.TransactionNonMatchingEncumbranceException(ltx.id, symmetricDifference)
|
||||
throw TransactionNonMatchingEncumbranceException(ltx.id, symmetricDifference)
|
||||
}
|
||||
}
|
||||
|
||||
@ -235,7 +300,7 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
||||
if (indicesAlreadyChecked.add(index)) {
|
||||
val encumbranceIndex = ltx.outputs[index].encumbrance!!
|
||||
if (ltx.outputs[index].notary != ltx.outputs[encumbranceIndex].notary) {
|
||||
throw TransactionVerificationException.TransactionNotaryMismatchEncumbranceException(
|
||||
throw TransactionNotaryMismatchEncumbranceException(
|
||||
ltx.id,
|
||||
index,
|
||||
encumbranceIndex,
|
||||
@ -263,7 +328,7 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
||||
val shouldEnforce = StateContractValidationEnforcementRule.shouldEnforce(state.data)
|
||||
|
||||
val requiredContractClassName = state.data.requiredContractClassName
|
||||
?: if (shouldEnforce) throw TransactionVerificationException.TransactionRequiredContractUnspecifiedException(ltx.id, state) else return
|
||||
?: if (shouldEnforce) throw TransactionRequiredContractUnspecifiedException(ltx.id, state) else return
|
||||
|
||||
if (state.contract != requiredContractClassName)
|
||||
if (shouldEnforce) {
|
||||
@ -281,6 +346,7 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
||||
* - Constraints should be one of the valid supported ones.
|
||||
* - Constraints should propagate correctly if not marked otherwise (in that case it is the responsibility of the contract to ensure that the output states are created properly).
|
||||
*/
|
||||
@Suppress("NestedBlockDepth")
|
||||
private fun verifyConstraintsValidity(contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>) {
|
||||
|
||||
// First check that the constraints are valid.
|
||||
@ -310,7 +376,7 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
||||
outputConstraints.forEach { outputConstraint ->
|
||||
inputConstraints.forEach { inputConstraint ->
|
||||
if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, contractAttachment))) {
|
||||
throw TransactionVerificationException.ConstraintPropagationRejection(
|
||||
throw ConstraintPropagationRejection(
|
||||
ltx.id,
|
||||
contractClassName,
|
||||
inputConstraint,
|
||||
@ -331,7 +397,7 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
||||
@Suppress("NestedBlockDepth", "MagicNumber")
|
||||
private fun verifyConstraints(contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>) {
|
||||
// For each contract/constraint pair check that the relevant attachment is valid.
|
||||
allStates.map { it.contract to it.constraint }.toSet().forEach { (contract, constraint) ->
|
||||
allStates.mapTo(LinkedHashSet()) { it.contract to it.constraint }.forEach { (contract, constraint) ->
|
||||
if (constraint is SignatureAttachmentConstraint) {
|
||||
/**
|
||||
* Support for signature constraints has been added on
|
||||
@ -346,9 +412,9 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
||||
"Signature constraints"
|
||||
)
|
||||
val constraintKey = constraint.key
|
||||
if (ltx.networkParameters?.minimumPlatformVersion ?: 1 >= PlatformVersionSwitches.LIMIT_KEYS_IN_SIGNATURE_CONSTRAINTS) {
|
||||
if ((ltx.networkParameters?.minimumPlatformVersion ?: 1) >= PlatformVersionSwitches.LIMIT_KEYS_IN_SIGNATURE_CONSTRAINTS) {
|
||||
if (constraintKey is CompositeKey && constraintKey.leafKeys.size > MAX_NUMBER_OF_KEYS_IN_SIGNATURE_CONSTRAINT) {
|
||||
throw TransactionVerificationException.InvalidConstraintRejection(ltx.id, contract,
|
||||
throw InvalidConstraintRejection(ltx.id, contract,
|
||||
"Signature constraint contains composite key with ${constraintKey.leafKeys.size} leaf keys, " +
|
||||
"which is more than the maximum allowed number of keys " +
|
||||
"($MAX_NUMBER_OF_KEYS_IN_SIGNATURE_CONSTRAINT).")
|
||||
@ -364,39 +430,18 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
||||
if (HashAttachmentConstraint.disableHashConstraints && constraint is HashAttachmentConstraint)
|
||||
logger.warnOnce("Skipping hash constraints verification.")
|
||||
else if (!constraint.isSatisfiedBy(constraintAttachment))
|
||||
throw TransactionVerificationException.ContractConstraintRejection(ltx.id, contract)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder function for the contract verification logic.
|
||||
*/
|
||||
abstract fun verifyContracts()
|
||||
}
|
||||
|
||||
class BasicVerifier(ltx: LedgerTransaction, transactionClassLoader: ClassLoader) : Verifier(ltx, transactionClassLoader) {
|
||||
/**
|
||||
* Check the transaction is contract-valid by running the verify() for each input and output state contract.
|
||||
* If any contract fails to verify, the whole transaction is considered to be invalid.
|
||||
*
|
||||
* Note: Reference states are not verified.
|
||||
*/
|
||||
override fun verifyContracts() {
|
||||
try {
|
||||
ContractVerifier(transactionClassLoader).apply(ltx)
|
||||
} catch (e: TransactionVerificationException.ContractRejection) {
|
||||
logger.error("Error validating transaction ${ltx.id}.", e.cause)
|
||||
throw e
|
||||
throw ContractConstraintRejection(ltx.id, contract)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify all of the contracts on the given [LedgerTransaction].
|
||||
* Verify the given [LedgerTransaction]. This includes validating
|
||||
* its contents, as well as executing all of its smart contracts.
|
||||
*/
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
@KeepForDJVM
|
||||
class ContractVerifier(private val transactionClassLoader: ClassLoader) : Function<LedgerTransaction, Unit> {
|
||||
class TransactionVerifier(private val transactionClassLoader: ClassLoader) : Function<Supplier<LedgerTransaction>, Unit> {
|
||||
// This constructor is used inside the DJVM's sandbox.
|
||||
@Suppress("unused")
|
||||
constructor() : this(ClassLoader.getSystemClassLoader())
|
||||
@ -406,34 +451,62 @@ class ContractVerifier(private val transactionClassLoader: ClassLoader) : Functi
|
||||
return try {
|
||||
Class.forName(contractClassName, false, transactionClassLoader).asSubclass(Contract::class.java)
|
||||
} catch (e: Exception) {
|
||||
throw TransactionVerificationException.ContractCreationError(id, contractClassName, e)
|
||||
throw ContractCreationError(id, contractClassName, e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun apply(ltx: LedgerTransaction) {
|
||||
val contractClassNames = (ltx.inputs.map(StateAndRef<ContractState>::state) + ltx.outputs)
|
||||
private fun generateContracts(ltx: LedgerTransaction): List<Contract> {
|
||||
return (ltx.inputs.map(StateAndRef<ContractState>::state) + ltx.outputs)
|
||||
.mapTo(LinkedHashSet(), TransactionState<*>::contract)
|
||||
|
||||
contractClassNames.associateBy(
|
||||
{ it }, { createContractClass(ltx.id, it) }
|
||||
).map { (contractClassName, contractClass) ->
|
||||
try {
|
||||
/**
|
||||
* This function must execute with the DJVM's sandbox, which does not
|
||||
* permit user code to invoke [java.lang.Class.getDeclaredConstructor].
|
||||
*
|
||||
* [Class.newInstance] is deprecated as of Java 9.
|
||||
*/
|
||||
@Suppress("deprecation")
|
||||
contractClass.newInstance()
|
||||
} catch (e: Exception) {
|
||||
throw TransactionVerificationException.ContractCreationError(ltx.id, contractClassName, e)
|
||||
.map { contractClassName ->
|
||||
createContractClass(ltx.id, contractClassName)
|
||||
}.map { contractClass ->
|
||||
try {
|
||||
/**
|
||||
* This function must execute within the DJVM's sandbox, which does not
|
||||
* permit user code to invoke [java.lang.reflect.Constructor.newInstance].
|
||||
* (This would be fixable now, provided the constructor is public.)
|
||||
*
|
||||
* [Class.newInstance] is deprecated as of Java 9.
|
||||
*/
|
||||
@Suppress("deprecation")
|
||||
contractClass.newInstance()
|
||||
} catch (e: Exception) {
|
||||
throw ContractCreationError(ltx.id, contractClass.name, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateTransaction(ltx: LedgerTransaction) {
|
||||
Validator(ltx, transactionClassLoader).validate()
|
||||
}
|
||||
|
||||
override fun apply(transactionFactory: Supplier<LedgerTransaction>) {
|
||||
var firstLtx: LedgerTransaction? = null
|
||||
|
||||
transactionFactory.get().let { ltx ->
|
||||
firstLtx = ltx
|
||||
|
||||
/**
|
||||
* Check that this transaction is correctly formed.
|
||||
* We only need to run these checks once.
|
||||
*/
|
||||
validateTransaction(ltx)
|
||||
|
||||
/**
|
||||
* Generate the list of unique contracts
|
||||
* within this transaction.
|
||||
*/
|
||||
generateContracts(ltx)
|
||||
}.forEach { contract ->
|
||||
val ltx = firstLtx ?: transactionFactory.get()
|
||||
firstLtx = null
|
||||
try {
|
||||
// Final step is to run the contract code. Having validated the
|
||||
// transaction, we are now sure that we are running the correct code.
|
||||
contract.verify(ltx)
|
||||
} catch (e: Exception) {
|
||||
throw TransactionVerificationException.ContractRejection(ltx.id, contract, e)
|
||||
throw ContractRejection(ltx.id, contract, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,14 @@ abstract class NotaryService : SingletonSerializeAsToken() {
|
||||
abstract val services: ServiceHub
|
||||
abstract val notaryIdentityKey: PublicKey
|
||||
|
||||
/**
|
||||
* Mapping between @InitiatingFlow classes and factory methods that produce responder flows.
|
||||
* Can be overridden in case of advanced notary service that serves both custom and standard flows.
|
||||
*/
|
||||
open val initiatingFlows = mapOf(
|
||||
NotaryFlow.Client::class to ::createServiceFlow
|
||||
)
|
||||
|
||||
/**
|
||||
* Interfaces for the request and result formats of queries supported by notary services. To
|
||||
* implement a new query, you must:
|
||||
|
@ -28,7 +28,11 @@ import java.time.Duration
|
||||
* @param etaThreshold If the ETA for processing the request, according to the service, is greater than this, notify the client.
|
||||
*/
|
||||
// See AbstractStateReplacementFlow.Acceptor for why it's Void?
|
||||
abstract class NotaryServiceFlow(val otherSideSession: FlowSession, val service: SinglePartyNotaryService, private val etaThreshold: Duration) : FlowLogic<Void?>() {
|
||||
abstract class NotaryServiceFlow(
|
||||
val otherSideSession: FlowSession,
|
||||
val service: SinglePartyNotaryService,
|
||||
private val etaThreshold: Duration
|
||||
) : FlowLogic<Void?>() {
|
||||
companion object {
|
||||
// TODO: Determine an appropriate limit and also enforce in the network parameters and the transaction builder.
|
||||
private const val maxAllowedInputsAndReferences = 10_000
|
||||
|
@ -0,0 +1,63 @@
|
||||
package net.corda.core.internal.utilities
|
||||
|
||||
import java.io.FilterInputStream
|
||||
import java.io.InputStream
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
object ZipBombDetector {
|
||||
|
||||
private class CounterInputStream(source : InputStream) : FilterInputStream(source) {
|
||||
private var byteCount : Long = 0
|
||||
|
||||
val count : Long
|
||||
get() = byteCount
|
||||
|
||||
override fun read(): Int {
|
||||
return super.read().also { byte ->
|
||||
if(byte >= 0) byteCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
override fun read(b: ByteArray): Int {
|
||||
return super.read(b).also { bytesRead ->
|
||||
if(bytesRead > 0) byteCount += bytesRead
|
||||
}
|
||||
}
|
||||
|
||||
override fun read(b: ByteArray, off: Int, len: Int): Int {
|
||||
return super.read(b, off, len).also { bytesRead ->
|
||||
if(bytesRead > 0) byteCount += bytesRead
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a zip file is a potential malicious zip bomb
|
||||
* @param source the zip archive file content
|
||||
* @param maxUncompressedSize the maximum allowable uncompressed archive size
|
||||
* @param maxCompressionRatio the maximum allowable compression ratio
|
||||
* @return true if the zip file total uncompressed size exceeds [maxUncompressedSize] and the
|
||||
* average entry compression ratio is larger than [maxCompressionRatio], false otherwise
|
||||
*/
|
||||
@Suppress("NestedBlockDepth")
|
||||
fun scanZip(source : InputStream, maxUncompressedSize : Long, maxCompressionRatio : Float = 10.0f) : Boolean {
|
||||
val counterInputStream = CounterInputStream(source)
|
||||
var uncompressedByteCount : Long = 0
|
||||
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||
ZipInputStream(counterInputStream).use { zipInputStream ->
|
||||
while(true) {
|
||||
zipInputStream.nextEntry ?: break
|
||||
while(true) {
|
||||
val read = zipInputStream.read(buffer)
|
||||
if(read <= 0) break
|
||||
uncompressedByteCount += read
|
||||
if(uncompressedByteCount > maxUncompressedSize &&
|
||||
uncompressedByteCount.toFloat() / counterInputStream.count.toFloat() > maxCompressionRatio) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
@ -13,6 +13,8 @@ import net.corda.core.utilities.days
|
||||
import java.security.PublicKey
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.Collections.unmodifiableList
|
||||
import java.util.Collections.unmodifiableMap
|
||||
|
||||
// DOCSTART 1
|
||||
/**
|
||||
@ -166,6 +168,38 @@ data class NetworkParameters(
|
||||
epoch=$epoch
|
||||
}"""
|
||||
}
|
||||
|
||||
fun toImmutable(): NetworkParameters {
|
||||
return NetworkParameters(
|
||||
minimumPlatformVersion = minimumPlatformVersion,
|
||||
notaries = unmodifiable(notaries),
|
||||
maxMessageSize = maxMessageSize,
|
||||
maxTransactionSize = maxTransactionSize,
|
||||
modifiedTime = modifiedTime,
|
||||
epoch = epoch,
|
||||
whitelistedContractImplementations = unmodifiable(whitelistedContractImplementations) { entry ->
|
||||
unmodifiableList(entry.value)
|
||||
},
|
||||
eventHorizon = eventHorizon,
|
||||
packageOwnership = unmodifiable(packageOwnership)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> unmodifiable(list: List<T>): List<T> {
|
||||
return if (list.isEmpty()) {
|
||||
emptyList()
|
||||
} else {
|
||||
unmodifiableList(list)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <K, V> unmodifiable(map: Map<K, V>, transform: (Map.Entry<K, V>) -> V = Map.Entry<K, V>::value): Map<K, V> {
|
||||
return if (map.isEmpty()) {
|
||||
emptyMap()
|
||||
} else {
|
||||
unmodifiableMap(map.mapValues(transform))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -64,7 +64,7 @@ interface ServicesForResolution {
|
||||
/**
|
||||
* Given a [Set] of [StateRef]'s loads the referenced transaction and looks up the specified output [ContractState].
|
||||
*
|
||||
* @throws TransactionResolutionException if [stateRef] points to a non-existent transaction.
|
||||
* @throws TransactionResolutionException if any of the [stateRefs] point to a non-existent transaction.
|
||||
*/
|
||||
// TODO: future implementation to use a Vault state ref -> contract state BLOB table and perform single query bulk load
|
||||
// as the existing transaction store will become encrypted at some point
|
||||
|
@ -1,3 +1,5 @@
|
||||
@file:Suppress("LongParameterList")
|
||||
|
||||
package net.corda.core.node.services
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
@ -197,8 +199,7 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
|
||||
* 4) Status types used in this query: [StateStatus.UNCONSUMED], [StateStatus.CONSUMED], [StateStatus.ALL].
|
||||
* 5) Other results as a [List] of any type (eg. aggregate function results with/without group by).
|
||||
*
|
||||
* Note: currently otherResults are used only for Aggregate Functions (in which case, the states and statesMetadata
|
||||
* results will be empty).
|
||||
* Note: currently [otherResults] is used only for aggregate functions (in which case, [states] and [statesMetadata] will be empty).
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class Page<out T : ContractState>(val states: List<StateAndRef<T>>,
|
||||
@ -213,11 +214,11 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
|
||||
val contractStateClassName: String,
|
||||
val recordedTime: Instant,
|
||||
val consumedTime: Instant?,
|
||||
val status: Vault.StateStatus,
|
||||
val status: StateStatus,
|
||||
val notary: AbstractParty?,
|
||||
val lockId: String?,
|
||||
val lockUpdateTime: Instant?,
|
||||
val relevancyStatus: Vault.RelevancyStatus? = null,
|
||||
val relevancyStatus: RelevancyStatus? = null,
|
||||
val constraintInfo: ConstraintInfo? = null
|
||||
) {
|
||||
fun copy(
|
||||
@ -225,7 +226,7 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
|
||||
contractStateClassName: String = this.contractStateClassName,
|
||||
recordedTime: Instant = this.recordedTime,
|
||||
consumedTime: Instant? = this.consumedTime,
|
||||
status: Vault.StateStatus = this.status,
|
||||
status: StateStatus = this.status,
|
||||
notary: AbstractParty? = this.notary,
|
||||
lockId: String? = this.lockId,
|
||||
lockUpdateTime: Instant? = this.lockUpdateTime
|
||||
@ -237,11 +238,11 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
|
||||
contractStateClassName: String = this.contractStateClassName,
|
||||
recordedTime: Instant = this.recordedTime,
|
||||
consumedTime: Instant? = this.consumedTime,
|
||||
status: Vault.StateStatus = this.status,
|
||||
status: StateStatus = this.status,
|
||||
notary: AbstractParty? = this.notary,
|
||||
lockId: String? = this.lockId,
|
||||
lockUpdateTime: Instant? = this.lockUpdateTime,
|
||||
relevancyStatus: Vault.RelevancyStatus?
|
||||
relevancyStatus: RelevancyStatus?
|
||||
): StateMetadata {
|
||||
return StateMetadata(ref, contractStateClassName, recordedTime, consumedTime, status, notary, lockId, lockUpdateTime, relevancyStatus, ConstraintInfo(AlwaysAcceptAttachmentConstraint))
|
||||
}
|
||||
@ -249,9 +250,9 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
|
||||
|
||||
companion object {
|
||||
@Deprecated("No longer used. The vault does not emit empty updates")
|
||||
val NoUpdate = Update(emptySet(), emptySet(), type = Vault.UpdateType.GENERAL, references = emptySet())
|
||||
val NoUpdate = Update(emptySet(), emptySet(), type = UpdateType.GENERAL, references = emptySet())
|
||||
@Deprecated("No longer used. The vault does not emit empty updates")
|
||||
val NoNotaryUpdate = Vault.Update(emptySet(), emptySet(), type = Vault.UpdateType.NOTARY_CHANGE, references = emptySet())
|
||||
val NoNotaryUpdate = Update(emptySet(), emptySet(), type = UpdateType.NOTARY_CHANGE, references = emptySet())
|
||||
}
|
||||
}
|
||||
|
||||
@ -302,7 +303,7 @@ interface VaultService {
|
||||
fun whenConsumed(ref: StateRef): CordaFuture<Vault.Update<ContractState>> {
|
||||
val query = QueryCriteria.VaultQueryCriteria(
|
||||
stateRefs = listOf(ref),
|
||||
status = Vault.StateStatus.CONSUMED
|
||||
status = StateStatus.CONSUMED
|
||||
)
|
||||
val result = trackBy<ContractState>(query)
|
||||
val snapshot = result.snapshot.states
|
||||
@ -358,8 +359,8 @@ interface VaultService {
|
||||
/**
|
||||
* Helper function to determine spendable states and soft locking them.
|
||||
* Currently performance will be worse than for the hand optimised version in
|
||||
* [Cash.unconsumedCashStatesForSpending]. However, this is fully generic and can operate with custom [FungibleState]
|
||||
* and [FungibleAsset] states.
|
||||
* [net.corda.finance.workflows.asset.selection.AbstractCashSelection.unconsumedCashStatesForSpending]. However, this is fully generic
|
||||
* and can operate with custom [FungibleState] and [FungibleAsset] states.
|
||||
* @param lockId The [FlowLogic.runId]'s [UUID] of the current flow used to soft lock the states.
|
||||
* @param eligibleStatesQuery A custom query object that selects down to the appropriate subset of all states of the
|
||||
* [contractStateType]. e.g. by selecting on account, issuer, etc. The query is internally augmented with the
|
||||
|
@ -0,0 +1,35 @@
|
||||
package net.corda.core.serialization
|
||||
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import java.io.NotSerializableException
|
||||
|
||||
/***
|
||||
* Implement this interface to add your own Serialization Scheme. This is an experimental feature. All methods in this class MUST be
|
||||
* thread safe i.e. methods from the same instance of this class can be called in different threads simultaneously.
|
||||
*/
|
||||
interface CustomSerializationScheme {
|
||||
/**
|
||||
* This method must return an id used to uniquely identify the Scheme. This should be unique within a network as serialized data might
|
||||
* be sent over the wire.
|
||||
*/
|
||||
fun getSchemeId(): Int
|
||||
|
||||
/**
|
||||
* This method must deserialize the data stored [bytes] into an instance of [T].
|
||||
*
|
||||
* @param bytes the serialized data.
|
||||
* @param clazz the class to instantiate.
|
||||
* @param context used to pass information about how the object should be deserialized.
|
||||
*/
|
||||
@Throws(NotSerializableException::class)
|
||||
fun <T : Any> deserialize(bytes: ByteSequence, clazz: Class<T>, context: SerializationSchemeContext): T
|
||||
|
||||
/**
|
||||
* This method must be able to serialize any object [T] into a ByteSequence.
|
||||
*
|
||||
* @param obj the object to be serialized.
|
||||
* @param context used to pass information about how the object should be serialized.
|
||||
*/
|
||||
@Throws(NotSerializableException::class)
|
||||
fun <T : Any> serialize(obj: T, context: SerializationSchemeContext): ByteSequence
|
||||
}
|
@ -13,6 +13,10 @@ import net.corda.core.utilities.sequence
|
||||
import java.io.NotSerializableException
|
||||
import java.sql.Blob
|
||||
|
||||
const val DESERIALIZATION_CACHE_PROPERTY = "DESERIALIZATION_CACHE"
|
||||
const val AMQP_ENVELOPE_CACHE_PROPERTY = "AMQP_ENVELOPE_CACHE"
|
||||
const val AMQP_ENVELOPE_CACHE_INITIAL_CAPACITY = 256
|
||||
|
||||
data class ObjectWithCompatibleContext<out T : Any>(val obj: T, val context: SerializationContext)
|
||||
|
||||
/**
|
||||
@ -65,12 +69,16 @@ abstract class SerializationFactory {
|
||||
* Change the current context inside the block to that supplied.
|
||||
*/
|
||||
fun <T> withCurrentContext(context: SerializationContext?, block: () -> T): T {
|
||||
val priorContext = _currentContext.get()
|
||||
if (context != null) _currentContext.set(context)
|
||||
try {
|
||||
return block()
|
||||
} finally {
|
||||
if (context != null) _currentContext.set(priorContext)
|
||||
return if (context == null) {
|
||||
block()
|
||||
} else {
|
||||
val priorContext = _currentContext.get()
|
||||
_currentContext.set(context)
|
||||
try {
|
||||
block()
|
||||
} finally {
|
||||
_currentContext.set(priorContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -134,7 +142,7 @@ interface SerializationContext {
|
||||
*/
|
||||
val encodingWhitelist: EncodingWhitelist
|
||||
/**
|
||||
* A map of any addition properties specific to the particular use case.
|
||||
* A map of any additional properties specific to the particular use case.
|
||||
*/
|
||||
val properties: Map<Any, Any>
|
||||
/**
|
||||
@ -178,6 +186,11 @@ interface SerializationContext {
|
||||
*/
|
||||
fun withProperty(property: Any, value: Any): SerializationContext
|
||||
|
||||
/**
|
||||
* Helper method to return a new context based on this context with the extra properties added.
|
||||
*/
|
||||
fun withProperties(extraProperties: Map<Any, Any>): SerializationContext
|
||||
|
||||
/**
|
||||
* Helper method to return a new context based on this context with object references disabled.
|
||||
*/
|
||||
|
@ -0,0 +1,30 @@
|
||||
package net.corda.core.serialization
|
||||
|
||||
import net.corda.core.DoNotImplement
|
||||
|
||||
/**
|
||||
* This is used to pass information into [CustomSerializationScheme] about how the object should be (de)serialized.
|
||||
* This context can change depending on the specific circumstances in the node when (de)serialization occurs.
|
||||
*/
|
||||
@DoNotImplement
|
||||
interface SerializationSchemeContext {
|
||||
/**
|
||||
* The class loader to use for deserialization. This is guaranteed to be able to load all the required classes passed into
|
||||
* [CustomSerializationScheme.deserialize].
|
||||
*/
|
||||
val deserializationClassLoader: ClassLoader
|
||||
/**
|
||||
* A whitelist that contains (mostly for security purposes) which classes are authorised to be deserialized.
|
||||
* A secure implementation will not instantiate any object which is not either whitelisted or annotated with [CordaSerializable] when
|
||||
* deserializing. To catch classes missing from the whitelist as early as possible it is HIGHLY recommended to also check this
|
||||
* whitelist when serializing (as well as deserializing) objects.
|
||||
*/
|
||||
val whitelist: ClassWhitelist
|
||||
/**
|
||||
* A map of any additional properties specific to the particular use case. If these properties are set via
|
||||
* [toWireTransaction][net.corda.core.transactions.TransactionBuilder.toWireTransaction] then they might not be available when
|
||||
* deserializing. If the properties are required when deserializing, they can be added into the blob when serializing and read back
|
||||
* when deserializing.
|
||||
*/
|
||||
val properties: Map<Any, Any>
|
||||
}
|
@ -9,21 +9,46 @@ import net.corda.core.contracts.TransactionVerificationException
|
||||
import net.corda.core.contracts.TransactionVerificationException.OverlappingAttachmentsException
|
||||
import net.corda.core.contracts.TransactionVerificationException.PackageOwnershipException
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.internal.JDK1_2_CLASS_FILE_FORMAT_MAJOR_VERSION
|
||||
import net.corda.core.internal.JDK8_CLASS_FILE_FORMAT_MAJOR_VERSION
|
||||
import net.corda.core.internal.JarSignatureCollector
|
||||
import net.corda.core.internal.NamedCacheFactory
|
||||
import net.corda.core.internal.PlatformVersionSwitches
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import net.corda.core.internal.cordapp.targetPlatformVersion
|
||||
import net.corda.core.internal.createInstancesOfClassesImplementing
|
||||
import net.corda.core.internal.createSimpleCache
|
||||
import net.corda.core.internal.toSynchronised
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.serialization.AMQP_ENVELOPE_CACHE_INITIAL_CAPACITY
|
||||
import net.corda.core.serialization.AMQP_ENVELOPE_CACHE_PROPERTY
|
||||
import net.corda.core.serialization.DESERIALIZATION_CACHE_PROPERTY
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationCustomSerializer
|
||||
import net.corda.core.serialization.SerializationFactory
|
||||
import net.corda.core.serialization.SerializationWhitelist
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.serialization.internal.AttachmentURLStreamHandlerFactory.toUrl
|
||||
import net.corda.core.serialization.withWhitelist
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import java.io.ByteArrayOutputStream
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.lang.ref.ReferenceQueue
|
||||
import java.lang.ref.WeakReference
|
||||
import java.net.*
|
||||
import java.net.URL
|
||||
import java.net.URLClassLoader
|
||||
import java.net.URLConnection
|
||||
import java.net.URLStreamHandler
|
||||
import java.net.URLStreamHandlerFactory
|
||||
import java.security.MessageDigest
|
||||
import java.security.Permission
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
import java.util.ServiceLoader
|
||||
import java.util.WeakHashMap
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import java.util.function.Function
|
||||
|
||||
/**
|
||||
@ -51,12 +76,15 @@ class AttachmentsClassLoader(attachments: List<Attachment>,
|
||||
init {
|
||||
// Apply our own URLStreamHandlerFactory to resolve attachments
|
||||
setOrDecorateURLStreamHandlerFactory()
|
||||
|
||||
// Allow AttachmentsClassLoader to be used concurrently.
|
||||
registerAsParallelCapable()
|
||||
}
|
||||
|
||||
// Jolokia and Json-simple are dependencies that were bundled by mistake within contract jars.
|
||||
// In the AttachmentsClassLoader we just block any class in those 2 packages.
|
||||
private val ignoreDirectories = listOf("org/jolokia/", "org/json/simple/")
|
||||
private val ignorePackages = ignoreDirectories.map { it.replace("/", ".") }
|
||||
private val ignorePackages = ignoreDirectories.map { it.replace('/', '.') }
|
||||
|
||||
/**
|
||||
* Apply our custom factory either directly, if `URL.setURLStreamHandlerFactory` has not been called yet,
|
||||
@ -128,6 +156,20 @@ class AttachmentsClassLoader(attachments: List<Attachment>,
|
||||
checkAttachments(attachments)
|
||||
}
|
||||
|
||||
private class AttachmentHashContext(
|
||||
val txId: SecureHash,
|
||||
val buffer: ByteArray = ByteArray(DEFAULT_BUFFER_SIZE))
|
||||
|
||||
private fun hash(inputStream : InputStream, ctx : AttachmentHashContext) : SecureHash.SHA256 {
|
||||
val md = MessageDigest.getInstance(SecureHash.SHA2_256)
|
||||
while(true) {
|
||||
val read = inputStream.read(ctx.buffer)
|
||||
if(read <= 0) break
|
||||
md.update(ctx.buffer, 0, read)
|
||||
}
|
||||
return SecureHash.SHA256(md.digest())
|
||||
}
|
||||
|
||||
private fun isZipOrJar(attachment: Attachment) = attachment.openAsJAR().use { jar ->
|
||||
jar.nextEntry != null
|
||||
}
|
||||
@ -146,10 +188,10 @@ class AttachmentsClassLoader(attachments: List<Attachment>,
|
||||
// TODO - investigate potential exploits.
|
||||
private fun shouldCheckForNoOverlap(path: String, targetPlatformVersion: Int): Boolean {
|
||||
require(path.toLowerCase() == path)
|
||||
require(!path.contains("\\"))
|
||||
require(!path.contains('\\'))
|
||||
|
||||
return when {
|
||||
path.endsWith("/") -> false // Directories (packages) can overlap.
|
||||
path.endsWith('/') -> false // Directories (packages) can overlap.
|
||||
targetPlatformVersion < PlatformVersionSwitches.IGNORE_JOLOKIA_JSON_SIMPLE_IN_CORDAPPS &&
|
||||
ignoreDirectories.any { path.startsWith(it) } -> false // Ignore jolokia and json-simple for old cordapps.
|
||||
path.endsWith(".class") -> true // All class files need to be unique.
|
||||
@ -160,6 +202,7 @@ class AttachmentsClassLoader(attachments: List<Attachment>,
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("ThrowsCount", "ComplexMethod", "NestedBlockDepth")
|
||||
private fun checkAttachments(attachments: List<Attachment>) {
|
||||
require(attachments.isNotEmpty()) { "attachments list is empty" }
|
||||
|
||||
@ -188,7 +231,8 @@ class AttachmentsClassLoader(attachments: List<Attachment>,
|
||||
// attacks on externally connected systems that only consider type names, we allow people to formally
|
||||
// claim their parts of the Java package namespace via registration with the zone operator.
|
||||
|
||||
val classLoaderEntries = mutableMapOf<String, SecureHash.SHA256>()
|
||||
val classLoaderEntries = mutableMapOf<String, SecureHash>()
|
||||
val ctx = AttachmentHashContext(sampleTxId)
|
||||
for (attachment in attachments) {
|
||||
// We may have been given an attachment loaded from the database in which case, important info like
|
||||
// signers is already calculated.
|
||||
@ -206,10 +250,12 @@ class AttachmentsClassLoader(attachments: List<Attachment>,
|
||||
// signed by the owners of the packages, even if it's not. We'd eventually discover that fact
|
||||
// when trying to read the class file to use it, but if we'd made any decisions based on
|
||||
// perceived correctness of the signatures or package ownership already, that would be too late.
|
||||
attachment.openAsJAR().use { JarSignatureCollector.collectSigners(it) }
|
||||
attachment.openAsJAR().use(JarSignatureCollector::collectSigners)
|
||||
}
|
||||
|
||||
// Now open it again to compute the overlap and package ownership data.
|
||||
attachment.openAsJAR().use { jar ->
|
||||
|
||||
val targetPlatformVersion = jar.manifest?.targetPlatformVersion ?: 1
|
||||
while (true) {
|
||||
val entry = jar.nextJarEntry ?: break
|
||||
@ -250,13 +296,9 @@ class AttachmentsClassLoader(attachments: List<Attachment>,
|
||||
if (!shouldCheckForNoOverlap(path, targetPlatformVersion)) continue
|
||||
|
||||
// This calculates the hash of the current entry because the JarInputStream returns only the current entry.
|
||||
fun entryHash() = ByteArrayOutputStream().use {
|
||||
jar.copyTo(it)
|
||||
it.toByteArray()
|
||||
}.sha256()
|
||||
val currentHash = hash(jar, ctx)
|
||||
|
||||
// If 2 entries are identical, it means the same file is present in both attachments, so that is ok.
|
||||
val currentHash = entryHash()
|
||||
val previousFileHash = classLoaderEntries[path]
|
||||
when {
|
||||
previousFileHash == null -> {
|
||||
@ -279,11 +321,11 @@ class AttachmentsClassLoader(attachments: List<Attachment>,
|
||||
* Required to prevent classes that were excluded from the no-overlap check from being loaded by contract code.
|
||||
* As it can lead to non-determinism.
|
||||
*/
|
||||
override fun loadClass(name: String?): Class<*> {
|
||||
if (ignorePackages.any { name!!.startsWith(it) }) {
|
||||
override fun loadClass(name: String, resolve: Boolean): Class<*>? {
|
||||
if (ignorePackages.any { name.startsWith(it) }) {
|
||||
throw ClassNotFoundException(name)
|
||||
}
|
||||
return super.loadClass(name)
|
||||
return super.loadClass(name, resolve)
|
||||
}
|
||||
}
|
||||
|
||||
@ -293,7 +335,8 @@ class AttachmentsClassLoader(attachments: List<Attachment>,
|
||||
*/
|
||||
@VisibleForTesting
|
||||
object AttachmentsClassLoaderBuilder {
|
||||
const val CACHE_SIZE = 16
|
||||
private const val CACHE_SIZE = 16
|
||||
private const val STRONG_REFERENCE_TO_CACHED_SERIALIZATION_CONTEXT = "cachedSerializationContext"
|
||||
|
||||
private val fallBackCache: AttachmentsClassLoaderCache = AttachmentsClassLoaderSimpleCacheImpl(CACHE_SIZE)
|
||||
|
||||
@ -309,20 +352,19 @@ object AttachmentsClassLoaderBuilder {
|
||||
isAttachmentTrusted: (Attachment) -> Boolean,
|
||||
parent: ClassLoader = ClassLoader.getSystemClassLoader(),
|
||||
attachmentsClassLoaderCache: AttachmentsClassLoaderCache?,
|
||||
block: (ClassLoader) -> T): T {
|
||||
val attachmentIds = attachments.map(Attachment::id).toSet()
|
||||
block: (SerializationContext) -> T): T {
|
||||
val attachmentIds = attachments.mapTo(LinkedHashSet(), Attachment::id)
|
||||
|
||||
val cache = attachmentsClassLoaderCache ?: fallBackCache
|
||||
val serializationContext = cache.computeIfAbsent(AttachmentsClassLoaderKey(attachmentIds, params), Function {
|
||||
val cachedSerializationContext = cache.computeIfAbsent(AttachmentsClassLoaderKey(attachmentIds, params), Function { key ->
|
||||
// Create classloader and load serializers, whitelisted classes
|
||||
val transactionClassLoader = AttachmentsClassLoader(attachments, params, txId, isAttachmentTrusted, parent)
|
||||
val transactionClassLoader = AttachmentsClassLoader(attachments, key.params, txId, isAttachmentTrusted, parent)
|
||||
val serializers = try {
|
||||
createInstancesOfClassesImplementing(transactionClassLoader, SerializationCustomSerializer::class.java,
|
||||
JDK1_2_CLASS_FILE_FORMAT_MAJOR_VERSION..JDK8_CLASS_FILE_FORMAT_MAJOR_VERSION)
|
||||
}
|
||||
catch(ex: UnsupportedClassVersionError) {
|
||||
throw TransactionVerificationException.UnsupportedClassVersionError(txId, ex.message!!, ex)
|
||||
}
|
||||
} catch (ex: UnsupportedClassVersionError) {
|
||||
throw TransactionVerificationException.UnsupportedClassVersionError(txId, ex.message!!, ex)
|
||||
}
|
||||
val whitelistedClasses = ServiceLoader.load(SerializationWhitelist::class.java, transactionClassLoader)
|
||||
.flatMap(SerializationWhitelist::whitelist)
|
||||
|
||||
@ -338,9 +380,20 @@ object AttachmentsClassLoaderBuilder {
|
||||
.withoutCarpenter()
|
||||
})
|
||||
|
||||
val serializationContext = cachedSerializationContext.withProperties(mapOf<Any, Any>(
|
||||
// Duplicate the SerializationContext from the cache and give
|
||||
// it these extra properties, just for this transaction.
|
||||
// However, keep a strong reference to the cached SerializationContext so we can
|
||||
// leverage the power of WeakReferences in the AttachmentsClassLoaderCacheImpl to figure
|
||||
// out when all these have gone out of scope by the BasicVerifier going out of scope.
|
||||
AMQP_ENVELOPE_CACHE_PROPERTY to HashMap<Any, Any>(AMQP_ENVELOPE_CACHE_INITIAL_CAPACITY),
|
||||
DESERIALIZATION_CACHE_PROPERTY to HashMap<Any, Any>(),
|
||||
STRONG_REFERENCE_TO_CACHED_SERIALIZATION_CONTEXT to cachedSerializationContext
|
||||
))
|
||||
|
||||
// Deserialize all relevant classes in the transaction classloader.
|
||||
return SerializationFactory.defaultFactory.withCurrentContext(serializationContext) {
|
||||
block(serializationContext.deserializationClassLoader)
|
||||
block(serializationContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -352,6 +405,8 @@ object AttachmentsClassLoaderBuilder {
|
||||
object AttachmentURLStreamHandlerFactory : URLStreamHandlerFactory {
|
||||
internal const val attachmentScheme = "attachment"
|
||||
|
||||
private val uniqueness = AtomicLong(0)
|
||||
|
||||
private val loadedAttachments: AttachmentsHolder = AttachmentsHolderImpl()
|
||||
|
||||
override fun createURLStreamHandler(protocol: String): URLStreamHandler? {
|
||||
@ -362,14 +417,9 @@ object AttachmentURLStreamHandlerFactory : URLStreamHandlerFactory {
|
||||
|
||||
@Synchronized
|
||||
fun toUrl(attachment: Attachment): URL {
|
||||
val proposedURL = URL(attachmentScheme, "", -1, attachment.id.toString(), AttachmentURLStreamHandler)
|
||||
val existingURL = loadedAttachments.getKey(proposedURL)
|
||||
return if (existingURL == null) {
|
||||
loadedAttachments[proposedURL] = attachment
|
||||
proposedURL
|
||||
} else {
|
||||
existingURL
|
||||
}
|
||||
val uniqueURL = URL(attachmentScheme, "", -1, attachment.id.toString()+ "?" + uniqueness.getAndIncrement(), AttachmentURLStreamHandler)
|
||||
loadedAttachments[uniqueURL] = attachment
|
||||
return uniqueURL
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@ -427,9 +477,52 @@ interface AttachmentsClassLoaderCache {
|
||||
@DeleteForDJVM
|
||||
class AttachmentsClassLoaderCacheImpl(cacheFactory: NamedCacheFactory) : SingletonSerializeAsToken(), AttachmentsClassLoaderCache {
|
||||
|
||||
private val cache: Cache<AttachmentsClassLoaderKey, SerializationContext> = cacheFactory.buildNamed(Caffeine.newBuilder(), "AttachmentsClassLoader_cache")
|
||||
private class ToBeClosed(
|
||||
serializationContext: SerializationContext,
|
||||
val classLoaderToClose: AutoCloseable,
|
||||
val cacheKey: AttachmentsClassLoaderKey,
|
||||
queue: ReferenceQueue<SerializationContext>
|
||||
) : WeakReference<SerializationContext>(serializationContext, queue)
|
||||
|
||||
private val logger = loggerFor<AttachmentsClassLoaderCacheImpl>()
|
||||
private val toBeClosed = ConcurrentHashMap.newKeySet<ToBeClosed>()
|
||||
private val expiryQueue = ReferenceQueue<SerializationContext>()
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
private fun purgeExpiryQueue() {
|
||||
// Close the AttachmentsClassLoader for every SerializationContext
|
||||
// that has already been garbage-collected.
|
||||
while (true) {
|
||||
val head = expiryQueue.poll() as? ToBeClosed ?: break
|
||||
if (!toBeClosed.remove(head)) {
|
||||
logger.warn("Reaped unexpected serialization context for {}", head.cacheKey)
|
||||
}
|
||||
|
||||
try {
|
||||
head.classLoaderToClose.close()
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Error destroying serialization context for ${head.cacheKey}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val cache: Cache<AttachmentsClassLoaderKey, SerializationContext> = cacheFactory.buildNamed(
|
||||
// Schedule for closing the deserialization classloaders when we evict them
|
||||
// to release any resources they may be holding.
|
||||
Caffeine.newBuilder().removalListener { key, context, _ ->
|
||||
(context?.deserializationClassLoader as? AutoCloseable)?.also { autoCloseable ->
|
||||
// ClassLoader to be closed once the BasicVerifier, which has a strong
|
||||
// reference chain to this SerializationContext, has gone out of scope.
|
||||
toBeClosed += ToBeClosed(context, autoCloseable, key!!, expiryQueue)
|
||||
}
|
||||
|
||||
// Reap any entries which have been garbage-collected.
|
||||
purgeExpiryQueue()
|
||||
}, "AttachmentsClassLoader_cache"
|
||||
)
|
||||
|
||||
override fun computeIfAbsent(key: AttachmentsClassLoaderKey, mappingFunction: Function<in AttachmentsClassLoaderKey, out SerializationContext>): SerializationContext {
|
||||
purgeExpiryQueue()
|
||||
return cache.get(key, mappingFunction) ?: throw NullPointerException("null returned from cache mapping function")
|
||||
}
|
||||
}
|
||||
@ -465,4 +558,4 @@ private class AttachmentURLConnection(url: URL, private val attachment: Attachme
|
||||
override fun connect() {
|
||||
connected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,28 @@
|
||||
package net.corda.core.serialization.internal
|
||||
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.serialization.SerializationMagic
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
class CustomSerializationSchemeUtils {
|
||||
|
||||
@KeepForDJVM
|
||||
companion object {
|
||||
|
||||
private const val SERIALIZATION_SCHEME_ID_SIZE = 4
|
||||
private val PREFIX = "CUS".toByteArray()
|
||||
|
||||
fun getCustomSerializationMagicFromSchemeId(schemeId: Int) : SerializationMagic {
|
||||
return SerializationMagic.of(PREFIX + ByteBuffer.allocate(SERIALIZATION_SCHEME_ID_SIZE).putInt(schemeId).array())
|
||||
}
|
||||
|
||||
fun getSchemeIdIfCustomSerializationMagic(magic: SerializationMagic): Int? {
|
||||
return if (magic.take(PREFIX.size) != ByteSequence.of(PREFIX)) {
|
||||
null
|
||||
} else {
|
||||
return magic.slice(start = PREFIX.size).int
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -145,7 +145,7 @@ data class ContractUpgradeWireTransaction(
|
||||
|
||||
private fun upgradedContract(className: ContractClassName, classLoader: ClassLoader): UpgradedContract<ContractState, ContractState> = try {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
classLoader.loadClass(className).asSubclass(UpgradedContract::class.java).getDeclaredConstructor().newInstance() as UpgradedContract<ContractState, ContractState>
|
||||
Class.forName(className, false, classLoader).asSubclass(UpgradedContract::class.java).getDeclaredConstructor().newInstance() as UpgradedContract<ContractState, ContractState>
|
||||
} catch (e: Exception) {
|
||||
throw TransactionVerificationException.ContractCreationError(id, className, e)
|
||||
}
|
||||
@ -166,9 +166,9 @@ data class ContractUpgradeWireTransaction(
|
||||
params,
|
||||
id,
|
||||
{ (services as ServiceHubCoreInternal).attachmentTrustCalculator.calculate(it) },
|
||||
attachmentsClassLoaderCache = (services as ServiceHubCoreInternal).attachmentsClassLoaderCache) { transactionClassLoader ->
|
||||
attachmentsClassLoaderCache = (services as ServiceHubCoreInternal).attachmentsClassLoaderCache) { serializationContext ->
|
||||
val resolvedInput = binaryInput.deserialize()
|
||||
val upgradedContract = upgradedContract(upgradedContractClassName, transactionClassLoader)
|
||||
val upgradedContract = upgradedContract(upgradedContractClassName, serializationContext.deserializationClassLoader)
|
||||
val outputState = calculateUpgradedState(resolvedInput, upgradedContract, upgradedAttachment)
|
||||
outputState.serialize()
|
||||
}
|
||||
@ -311,8 +311,7 @@ private constructor(
|
||||
@CordaInternal
|
||||
internal fun loadUpgradedContract(upgradedContractClassName: ContractClassName, classLoader: ClassLoader): UpgradedContract<ContractState, *> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return classLoader
|
||||
.loadClass(upgradedContractClassName)
|
||||
return Class.forName(upgradedContractClassName, false, classLoader)
|
||||
.asSubclass(Contract::class.java)
|
||||
.getConstructor()
|
||||
.newInstance() as UpgradedContract<ContractState, *>
|
||||
|
@ -18,21 +18,25 @@ import net.corda.core.crypto.DigestService
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.BasicVerifier
|
||||
import net.corda.core.internal.AbstractVerifier
|
||||
import net.corda.core.internal.SerializedStateAndRef
|
||||
import net.corda.core.internal.Verifier
|
||||
import net.corda.core.internal.castIfPossible
|
||||
import net.corda.core.internal.deserialiseCommands
|
||||
import net.corda.core.internal.deserialiseComponentGroup
|
||||
import net.corda.core.internal.eagerDeserialise
|
||||
import net.corda.core.internal.isUploaderTrusted
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationFactory
|
||||
import net.corda.core.serialization.internal.AttachmentsClassLoaderCache
|
||||
import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import java.util.Collections.unmodifiableList
|
||||
import java.util.function.Predicate
|
||||
import java.util.function.Supplier
|
||||
|
||||
/**
|
||||
* A LedgerTransaction is derived from a [WireTransaction]. It is the result of doing the following operations:
|
||||
@ -90,7 +94,7 @@ private constructor(
|
||||
private val serializedInputs: List<SerializedStateAndRef>?,
|
||||
private val serializedReferences: List<SerializedStateAndRef>?,
|
||||
private val isAttachmentTrusted: (Attachment) -> Boolean,
|
||||
private val verifierFactory: (LedgerTransaction, ClassLoader) -> Verifier,
|
||||
private val verifierFactory: (LedgerTransaction, SerializationContext) -> Verifier,
|
||||
private val attachmentsClassLoaderCache: AttachmentsClassLoaderCache?,
|
||||
val digestService: DigestService
|
||||
) : FullTransaction() {
|
||||
@ -100,22 +104,23 @@ private constructor(
|
||||
*/
|
||||
@DeprecatedConstructorForDeserialization(1)
|
||||
private constructor(
|
||||
inputs: List<StateAndRef<ContractState>>,
|
||||
outputs: List<TransactionState<ContractState>>,
|
||||
commands: List<CommandWithParties<CommandData>>,
|
||||
attachments: List<Attachment>,
|
||||
id: SecureHash,
|
||||
notary: Party?,
|
||||
timeWindow: TimeWindow?,
|
||||
privacySalt: PrivacySalt,
|
||||
networkParameters: NetworkParameters?,
|
||||
references: List<StateAndRef<ContractState>>,
|
||||
componentGroups: List<ComponentGroup>?,
|
||||
serializedInputs: List<SerializedStateAndRef>?,
|
||||
serializedReferences: List<SerializedStateAndRef>?,
|
||||
isAttachmentTrusted: (Attachment) -> Boolean,
|
||||
verifierFactory: (LedgerTransaction, ClassLoader) -> Verifier,
|
||||
attachmentsClassLoaderCache: AttachmentsClassLoaderCache?) : this(
|
||||
inputs: List<StateAndRef<ContractState>>,
|
||||
outputs: List<TransactionState<ContractState>>,
|
||||
commands: List<CommandWithParties<CommandData>>,
|
||||
attachments: List<Attachment>,
|
||||
id: SecureHash,
|
||||
notary: Party?,
|
||||
timeWindow: TimeWindow?,
|
||||
privacySalt: PrivacySalt,
|
||||
networkParameters: NetworkParameters?,
|
||||
references: List<StateAndRef<ContractState>>,
|
||||
componentGroups: List<ComponentGroup>?,
|
||||
serializedInputs: List<SerializedStateAndRef>?,
|
||||
serializedReferences: List<SerializedStateAndRef>?,
|
||||
isAttachmentTrusted: (Attachment) -> Boolean,
|
||||
verifierFactory: (LedgerTransaction, SerializationContext) -> Verifier,
|
||||
attachmentsClassLoaderCache: AttachmentsClassLoaderCache?
|
||||
) : this(
|
||||
inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt,
|
||||
networkParameters, references, componentGroups, serializedInputs, serializedReferences,
|
||||
isAttachmentTrusted, verifierFactory, attachmentsClassLoaderCache, DigestService.sha2_256)
|
||||
@ -124,8 +129,8 @@ private constructor(
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
|
||||
private fun <T> protect(list: List<T>?): List<T>? {
|
||||
return list?.run {
|
||||
private fun <T> protect(list: List<T>): List<T> {
|
||||
return list.run {
|
||||
if (isEmpty()) {
|
||||
emptyList()
|
||||
} else {
|
||||
@ -134,6 +139,8 @@ private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> protectOrNull(list: List<T>?): List<T>? = list?.let(::protect)
|
||||
|
||||
@CordaInternal
|
||||
internal fun create(
|
||||
inputs: List<StateAndRef<ContractState>>,
|
||||
@ -164,9 +171,9 @@ private constructor(
|
||||
privacySalt = privacySalt,
|
||||
networkParameters = networkParameters,
|
||||
references = references,
|
||||
componentGroups = protect(componentGroups),
|
||||
serializedInputs = protect(serializedInputs),
|
||||
serializedReferences = protect(serializedReferences),
|
||||
componentGroups = protectOrNull(componentGroups),
|
||||
serializedInputs = protectOrNull(serializedInputs),
|
||||
serializedReferences = protectOrNull(serializedReferences),
|
||||
isAttachmentTrusted = isAttachmentTrusted,
|
||||
verifierFactory = ::BasicVerifier,
|
||||
attachmentsClassLoaderCache = attachmentsClassLoaderCache,
|
||||
@ -176,10 +183,11 @@ private constructor(
|
||||
|
||||
/**
|
||||
* This factory function will create an instance of [LedgerTransaction]
|
||||
* that will be used inside the DJVM sandbox.
|
||||
* that will be used for contract verification. See [BasicVerifier] and
|
||||
* [DeterministicVerifier][net.corda.node.internal.djvm.DeterministicVerifier].
|
||||
*/
|
||||
@CordaInternal
|
||||
fun createForSandbox(
|
||||
fun createForContractVerify(
|
||||
inputs: List<StateAndRef<ContractState>>,
|
||||
outputs: List<TransactionState<ContractState>>,
|
||||
commands: List<CommandWithParties<CommandData>>,
|
||||
@ -188,28 +196,31 @@ private constructor(
|
||||
notary: Party?,
|
||||
timeWindow: TimeWindow?,
|
||||
privacySalt: PrivacySalt,
|
||||
networkParameters: NetworkParameters,
|
||||
networkParameters: NetworkParameters?,
|
||||
references: List<StateAndRef<ContractState>>,
|
||||
digestService: DigestService): LedgerTransaction {
|
||||
return LedgerTransaction(
|
||||
inputs = inputs,
|
||||
outputs = outputs,
|
||||
commands = commands,
|
||||
attachments = attachments,
|
||||
inputs = protect(inputs),
|
||||
outputs = protect(outputs),
|
||||
commands = protect(commands),
|
||||
attachments = protect(attachments),
|
||||
id = id,
|
||||
notary = notary,
|
||||
timeWindow = timeWindow,
|
||||
privacySalt = privacySalt,
|
||||
networkParameters = networkParameters,
|
||||
references = references,
|
||||
references = protect(references),
|
||||
componentGroups = null,
|
||||
serializedInputs = null,
|
||||
serializedReferences = null,
|
||||
isAttachmentTrusted = { true },
|
||||
verifierFactory = ::BasicVerifier,
|
||||
verifierFactory = ::NoOpVerifier,
|
||||
attachmentsClassLoaderCache = null,
|
||||
digestService = digestService
|
||||
)
|
||||
// This check accesses input states and must run on the LedgerTransaction
|
||||
// instance that is verified, not on the outer LedgerTransaction shell.
|
||||
// All states must also deserialize using the correct SerializationContext.
|
||||
).also(LedgerTransaction::checkBaseInvariants)
|
||||
}
|
||||
}
|
||||
|
||||
@ -251,11 +262,17 @@ private constructor(
|
||||
getParamsWithGoo(),
|
||||
id,
|
||||
isAttachmentTrusted = isAttachmentTrusted,
|
||||
attachmentsClassLoaderCache = attachmentsClassLoaderCache) { transactionClassLoader ->
|
||||
// Create a copy of the outer LedgerTransaction which deserializes all fields inside the [transactionClassLoader].
|
||||
attachmentsClassLoaderCache = attachmentsClassLoaderCache) { serializationContext ->
|
||||
|
||||
// Legacy check - warns if the LedgerTransaction was created incorrectly.
|
||||
checkLtxForVerification()
|
||||
|
||||
// Create a copy of the outer LedgerTransaction which deserializes all fields using
|
||||
// the serialization context (or its deserializationClassloader).
|
||||
// Only the copy will be used for verification, and the outer shell will be discarded.
|
||||
// This artifice is required to preserve backwards compatibility.
|
||||
verifierFactory(createLtxForVerification(), transactionClassLoader)
|
||||
// NOTE: The Verifier creates the copies of the LedgerTransaction object now.
|
||||
verifierFactory(this, serializationContext)
|
||||
}
|
||||
}
|
||||
|
||||
@ -272,7 +289,7 @@ private constructor(
|
||||
* Node without changing either the wire format or any public APIs.
|
||||
*/
|
||||
@CordaInternal
|
||||
fun specialise(alternateVerifier: (LedgerTransaction, ClassLoader) -> Verifier): LedgerTransaction = LedgerTransaction(
|
||||
fun specialise(alternateVerifier: (LedgerTransaction, SerializationContext) -> Verifier): LedgerTransaction = LedgerTransaction(
|
||||
inputs = inputs,
|
||||
outputs = outputs,
|
||||
commands = commands,
|
||||
@ -287,7 +304,11 @@ private constructor(
|
||||
serializedInputs = serializedInputs,
|
||||
serializedReferences = serializedReferences,
|
||||
isAttachmentTrusted = isAttachmentTrusted,
|
||||
verifierFactory = alternateVerifier,
|
||||
verifierFactory = if (verifierFactory == ::NoOpVerifier) {
|
||||
throw IllegalStateException("Cannot specialise transaction while verifying contracts")
|
||||
} else {
|
||||
alternateVerifier
|
||||
},
|
||||
attachmentsClassLoaderCache = attachmentsClassLoaderCache,
|
||||
digestService = digestService
|
||||
)
|
||||
@ -319,58 +340,12 @@ private constructor(
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the [LedgerTransaction] instance that will be used by contract verification.
|
||||
*
|
||||
* This method needs to run in the special transaction attachments classloader context.
|
||||
*/
|
||||
private fun createLtxForVerification(): LedgerTransaction {
|
||||
val serializedInputs = this.serializedInputs
|
||||
val serializedReferences = this.serializedReferences
|
||||
val componentGroups = this.componentGroups
|
||||
|
||||
val transaction= if (serializedInputs != null && serializedReferences != null && componentGroups != null) {
|
||||
// Deserialize all relevant classes in the transaction classloader.
|
||||
val deserializedInputs = serializedInputs.map { it.toStateAndRef() }
|
||||
val deserializedReferences = serializedReferences.map { it.toStateAndRef() }
|
||||
val deserializedOutputs = deserialiseComponentGroup(componentGroups, TransactionState::class, ComponentGroupEnum.OUTPUTS_GROUP, forceDeserialize = true)
|
||||
val deserializedCommands = deserialiseCommands(componentGroups, forceDeserialize = true, digestService = digestService)
|
||||
val authenticatedDeserializedCommands = deserializedCommands.map { cmd ->
|
||||
@Suppress("DEPRECATION") // Deprecated feature.
|
||||
val parties = commands.find { it.value.javaClass.name == cmd.value.javaClass.name }!!.signingParties
|
||||
CommandWithParties(cmd.signers, parties, cmd.value)
|
||||
}
|
||||
|
||||
LedgerTransaction(
|
||||
inputs = deserializedInputs,
|
||||
outputs = deserializedOutputs,
|
||||
commands = authenticatedDeserializedCommands,
|
||||
attachments = this.attachments,
|
||||
id = this.id,
|
||||
notary = this.notary,
|
||||
timeWindow = this.timeWindow,
|
||||
privacySalt = this.privacySalt,
|
||||
networkParameters = this.networkParameters,
|
||||
references = deserializedReferences,
|
||||
componentGroups = componentGroups,
|
||||
serializedInputs = serializedInputs,
|
||||
serializedReferences = serializedReferences,
|
||||
isAttachmentTrusted = isAttachmentTrusted,
|
||||
verifierFactory = verifierFactory,
|
||||
attachmentsClassLoaderCache = attachmentsClassLoaderCache,
|
||||
digestService = digestService
|
||||
)
|
||||
} else {
|
||||
// This branch is only present for backwards compatibility.
|
||||
private fun checkLtxForVerification() {
|
||||
if (serializedInputs == null || serializedReferences == null || componentGroups == null) {
|
||||
logger.warn("The LedgerTransaction should not be instantiated directly from client code. Please use WireTransaction.toLedgerTransaction." +
|
||||
"The result of the verify method might not be accurate.")
|
||||
this
|
||||
}
|
||||
|
||||
// This check accesses input states and must be run in this context.
|
||||
// It must run on the instance that is verified, not on the outer LedgerTransaction shell.
|
||||
transaction.checkBaseInvariants()
|
||||
|
||||
return transaction
|
||||
}
|
||||
|
||||
/**
|
||||
@ -740,7 +715,7 @@ private constructor(
|
||||
componentGroups = null,
|
||||
serializedInputs = null,
|
||||
serializedReferences = null,
|
||||
isAttachmentTrusted = { it.isUploaderTrusted() },
|
||||
isAttachmentTrusted = Attachment::isUploaderTrusted,
|
||||
verifierFactory = ::BasicVerifier,
|
||||
attachmentsClassLoaderCache = null
|
||||
)
|
||||
@ -770,7 +745,7 @@ private constructor(
|
||||
componentGroups = null,
|
||||
serializedInputs = null,
|
||||
serializedReferences = null,
|
||||
isAttachmentTrusted = { it.isUploaderTrusted() },
|
||||
isAttachmentTrusted = Attachment::isUploaderTrusted,
|
||||
verifierFactory = ::BasicVerifier,
|
||||
attachmentsClassLoaderCache = null
|
||||
)
|
||||
@ -838,3 +813,80 @@ private constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the default [Verifier] that configures Corda
|
||||
* to execute [Contract.verify(LedgerTransaction)].
|
||||
*
|
||||
* THIS CLASS IS NOT PUBLIC API, AND IS DELIBERATELY PRIVATE!
|
||||
*/
|
||||
@CordaInternal
|
||||
private class BasicVerifier(
|
||||
ltx: LedgerTransaction,
|
||||
private val serializationContext: SerializationContext
|
||||
) : AbstractVerifier(ltx, serializationContext.deserializationClassLoader) {
|
||||
|
||||
init {
|
||||
// This is a sanity check: We should only instantiate this
|
||||
// class from [LedgerTransaction.internalPrepareVerify].
|
||||
require(serializationContext === SerializationFactory.defaultFactory.currentContext) {
|
||||
"BasicVerifier for TX ${ltx.id} created outside its SerializationContext"
|
||||
}
|
||||
|
||||
// Fetch these commands' signing parties from the database.
|
||||
// Corda forbids database access during contract verification,
|
||||
// and so we must load the commands here eagerly instead.
|
||||
// THIS ALSO DESERIALISES THE COMMANDS USING THE WRONG CONTEXT
|
||||
// BECAUSE THAT CONTEXT WAS CHOSEN WHEN THE LAZY MAP WAS CREATED,
|
||||
// AND CHANGING THE DEFAULT CONTEXT HERE DOES NOT AFFECT IT.
|
||||
ltx.commands.eagerDeserialise()
|
||||
}
|
||||
|
||||
override val transaction: Supplier<LedgerTransaction>
|
||||
get() = Supplier(::createTransaction)
|
||||
|
||||
private fun createTransaction(): LedgerTransaction {
|
||||
// Deserialize all relevant classes using the serializationContext.
|
||||
return SerializationFactory.defaultFactory.withCurrentContext(serializationContext) {
|
||||
ltx.transform { componentGroups, serializedInputs, serializedReferences ->
|
||||
val deserializedInputs = serializedInputs.map(SerializedStateAndRef::toStateAndRef)
|
||||
val deserializedReferences = serializedReferences.map(SerializedStateAndRef::toStateAndRef)
|
||||
val deserializedOutputs = deserialiseComponentGroup(componentGroups, TransactionState::class, ComponentGroupEnum.OUTPUTS_GROUP, forceDeserialize = true)
|
||||
val deserializedCommands = deserialiseCommands(componentGroups, forceDeserialize = true, digestService = ltx.digestService)
|
||||
val authenticatedDeserializedCommands = deserializedCommands.mapIndexed { idx, cmd ->
|
||||
// Requires ltx.commands to have been deserialized already.
|
||||
@Suppress("DEPRECATION") // Deprecated feature.
|
||||
val parties = ltx.commands[idx].signingParties
|
||||
CommandWithParties(cmd.signers, parties, cmd.value)
|
||||
}
|
||||
|
||||
LedgerTransaction.createForContractVerify(
|
||||
inputs = deserializedInputs,
|
||||
outputs = deserializedOutputs,
|
||||
commands = authenticatedDeserializedCommands,
|
||||
attachments = ltx.attachments,
|
||||
id = ltx.id,
|
||||
notary = ltx.notary,
|
||||
timeWindow = ltx.timeWindow,
|
||||
privacySalt = ltx.privacySalt,
|
||||
networkParameters = ltx.networkParameters,
|
||||
references = deserializedReferences,
|
||||
digestService = ltx.digestService
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A "do nothing" [Verifier] installed for contract verification.
|
||||
*
|
||||
* THIS CLASS IS NOT PUBLIC API, AND IS DELIBERATELY PRIVATE!
|
||||
*/
|
||||
@Suppress("unused_parameter")
|
||||
@CordaInternal
|
||||
private class NoOpVerifier(ltx: LedgerTransaction, serializationContext: SerializationContext) : Verifier {
|
||||
// Invoking LedgerTransaction.verify() from Contract.verify(LedgerTransaction)
|
||||
// will execute this function. But why would anyone do that?!
|
||||
override fun verify() {}
|
||||
}
|
||||
|
@ -16,14 +16,18 @@ import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.node.ZoneVersionTooLowException
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.node.services.KeyManagementService
|
||||
import net.corda.core.serialization.CustomSerializationScheme
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.core.serialization.SerializationFactory
|
||||
import net.corda.core.serialization.SerializationMagic
|
||||
import net.corda.core.serialization.SerializationSchemeContext
|
||||
import net.corda.core.serialization.internal.CustomSerializationSchemeUtils.Companion.getCustomSerializationMagicFromSchemeId
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import java.security.PublicKey
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.ArrayDeque
|
||||
import java.util.UUID
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.collections.component1
|
||||
@ -140,6 +144,41 @@ open class TransactionBuilder(
|
||||
fun toWireTransaction(services: ServicesForResolution): WireTransaction = toWireTransactionWithContext(services, null)
|
||||
.apply { checkSupportedHashType() }
|
||||
|
||||
/**
|
||||
* Generates a [WireTransaction] from this builder, resolves any [AutomaticPlaceholderConstraint], and selects the attachments to use for this transaction.
|
||||
*
|
||||
* @param [schemeId] is used to specify the [CustomSerializationScheme] used to serialize each component of the componentGroups of the [WireTransaction].
|
||||
* This is an experimental feature.
|
||||
*
|
||||
* @returns A new [WireTransaction] that will be unaffected by further changes to this [TransactionBuilder].
|
||||
*
|
||||
* @throws [ZoneVersionTooLowException] if there are reference states and the zone minimum platform version is less than 4.
|
||||
*/
|
||||
@Throws(MissingContractAttachments::class)
|
||||
fun toWireTransaction(services: ServicesForResolution, schemeId: Int): WireTransaction {
|
||||
return toWireTransaction(services, schemeId, emptyMap()).apply { checkSupportedHashType() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a [WireTransaction] from this builder, resolves any [AutomaticPlaceholderConstraint], and selects the attachments to use for this transaction.
|
||||
*
|
||||
* @param [schemeId] is used to specify the [CustomSerializationScheme] used to serialize each component of the componentGroups of the [WireTransaction].
|
||||
* This is an experimental feature.
|
||||
*
|
||||
* @param [properties] a list of properties to add to the [SerializationSchemeContext] these properties can be accessed in [CustomSerializationScheme.serialize]
|
||||
* when serializing the componentGroups of the wire transaction but might not be available when deserializing.
|
||||
*
|
||||
* @returns A new [WireTransaction] that will be unaffected by further changes to this [TransactionBuilder].
|
||||
*
|
||||
* @throws [ZoneVersionTooLowException] if there are reference states and the zone minimum platform version is less than 4.
|
||||
*/
|
||||
@Throws(MissingContractAttachments::class)
|
||||
fun toWireTransaction(services: ServicesForResolution, schemeId: Int, properties: Map<Any, Any>): WireTransaction {
|
||||
val magic: SerializationMagic = getCustomSerializationMagicFromSchemeId(schemeId)
|
||||
val serializationContext = SerializationDefaults.P2P_CONTEXT.withPreferredSerializationVersion(magic).withProperties(properties)
|
||||
return toWireTransactionWithContext(services, serializationContext).apply { checkSupportedHashType() }
|
||||
}
|
||||
|
||||
@CordaInternal
|
||||
internal fun toWireTransactionWithContext(
|
||||
services: ServicesForResolution,
|
||||
|
@ -15,6 +15,7 @@ import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
||||
import net.corda.core.serialization.SerializationFactory
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.internal.AttachmentsClassLoaderCache
|
||||
import net.corda.core.serialization.serialize
|
||||
@ -154,7 +155,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
resolveAttachment,
|
||||
{ stateRef -> resolveStateRef(stateRef)?.serialize() },
|
||||
{ null },
|
||||
{ it.isUploaderTrusted() },
|
||||
Attachment::isUploaderTrusted,
|
||||
null
|
||||
)
|
||||
}
|
||||
@ -187,19 +188,26 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
): LedgerTransaction {
|
||||
// Look up public keys to authenticated identities.
|
||||
val authenticatedCommands = commands.lazyMapped { cmd, _ ->
|
||||
val parties = cmd.signers.mapNotNull { pk -> resolveIdentity(pk) }
|
||||
val parties = cmd.signers.mapNotNull(resolveIdentity)
|
||||
CommandWithParties(cmd.signers, parties, cmd.value)
|
||||
}
|
||||
|
||||
// Ensure that the lazy mappings will use the correct SerializationContext.
|
||||
val serializationFactory = SerializationFactory.defaultFactory
|
||||
val serializationContext = serializationFactory.defaultContext
|
||||
val toStateAndRef = { ssar: SerializedStateAndRef, _: Int ->
|
||||
ssar.toStateAndRef(serializationFactory, serializationContext)
|
||||
}
|
||||
|
||||
val serializedResolvedInputs = inputs.map { ref ->
|
||||
SerializedStateAndRef(resolveStateRefAsSerialized(ref) ?: throw TransactionResolutionException(ref.txhash), ref)
|
||||
}
|
||||
val resolvedInputs = serializedResolvedInputs.lazyMapped { star, _ -> star.toStateAndRef() }
|
||||
val resolvedInputs = serializedResolvedInputs.lazyMapped(toStateAndRef)
|
||||
|
||||
val serializedResolvedReferences = references.map { ref ->
|
||||
SerializedStateAndRef(resolveStateRefAsSerialized(ref) ?: throw TransactionResolutionException(ref.txhash), ref)
|
||||
}
|
||||
val resolvedReferences = serializedResolvedReferences.lazyMapped { star, _ -> star.toStateAndRef() }
|
||||
val resolvedReferences = serializedResolvedReferences.lazyMapped(toStateAndRef)
|
||||
|
||||
val resolvedAttachments = attachments.lazyMapped { att, _ -> resolveAttachment(att) ?: throw AttachmentResolutionException(att) }
|
||||
|
||||
@ -214,7 +222,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
notary,
|
||||
timeWindow,
|
||||
privacySalt,
|
||||
resolvedNetworkParameters,
|
||||
resolvedNetworkParameters.toImmutable(),
|
||||
resolvedReferences,
|
||||
componentGroups,
|
||||
serializedResolvedInputs,
|
||||
@ -318,7 +326,11 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
* nothing about the rest.
|
||||
*/
|
||||
internal val availableComponentNonces: Map<Int, List<SecureHash>> by lazy {
|
||||
componentGroups.associate { it.groupIndex to it.components.mapIndexed { internalIndex, internalIt -> digestService.componentHash(internalIt, privacySalt, it.groupIndex, internalIndex) } }
|
||||
if(digestService.hashAlgorithm == SecureHash.SHA2_256) {
|
||||
componentGroups.associate { it.groupIndex to it.components.mapIndexed { internalIndex, internalIt -> digestService.componentHash(internalIt, privacySalt, it.groupIndex, internalIndex) } }
|
||||
} else {
|
||||
componentGroups.associate { it.groupIndex to it.components.mapIndexed { internalIndex, _ -> digestService.computeNonce(privacySalt, it.groupIndex, internalIndex) } }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,54 @@
|
||||
package net.corda.core.internal.utilities
|
||||
|
||||
import net.corda.core.obfuscator.XorOutputStream
|
||||
import java.net.URL
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
object TestResourceWriter {
|
||||
|
||||
private val externalZipBombUrls = arrayOf(
|
||||
URL("https://www.bamsoftware.com/hacks/zipbomb/zbsm.zip"),
|
||||
URL("https://www.bamsoftware.com/hacks/zipbomb/zblg.zip"),
|
||||
URL("https://www.bamsoftware.com/hacks/zipbomb/zbxl.zip")
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
@Suppress("NestedBlockDepth", "MagicNumber")
|
||||
fun main(vararg args : String) {
|
||||
for(arg in args) {
|
||||
/**
|
||||
* Download zip bombs
|
||||
*/
|
||||
for(url in externalZipBombUrls) {
|
||||
url.openStream().use { inputStream ->
|
||||
val destination = Paths.get(arg).resolve(Paths.get(url.path + ".xor").fileName)
|
||||
Files.newOutputStream(destination).buffered().let(::XorOutputStream).use { outputStream ->
|
||||
inputStream.copyTo(outputStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Create a jar archive with a huge manifest file, used in unit tests to check that it is also identified as a zip bomb.
|
||||
* This is because {@link java.util.jar.JarInputStream}
|
||||
* <a href="https://github.com/openjdk/jdk/blob/4dedba9ebe11750f4b39c41feb4a4314ccdb3a08/src/java.base/share/classes/java/util/jar/JarInputStream.java#L95">eagerly loads the manifest file in memory</a>
|
||||
* which would make such a jar dangerous if used as an attachment
|
||||
*/
|
||||
val destination = Paths.get(arg).resolve(Paths.get("big-manifest.jar.xor").fileName)
|
||||
ZipOutputStream(XorOutputStream((Files.newOutputStream(destination).buffered()))).use { zos ->
|
||||
val zipEntry = ZipEntry("MANIFEST.MF")
|
||||
zipEntry.method = ZipEntry.DEFLATED
|
||||
zos.putNextEntry(zipEntry)
|
||||
val buffer = ByteArray(0x100000) { 0x0 }
|
||||
var written = 0L
|
||||
while(written < 10_000_000_000) {
|
||||
zos.write(buffer)
|
||||
written += buffer.size
|
||||
}
|
||||
zos.closeEntry()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package net.corda.core.obfuscator
|
||||
|
||||
import java.io.FilterInputStream
|
||||
import java.io.InputStream
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
class XorInputStream(private val source : InputStream) : FilterInputStream(source) {
|
||||
var prev : Int = 0
|
||||
|
||||
override fun read(): Int {
|
||||
prev = source.read() xor prev
|
||||
return prev - 0x80
|
||||
}
|
||||
|
||||
override fun read(buffer: ByteArray): Int {
|
||||
return read(buffer, 0, buffer.size)
|
||||
}
|
||||
|
||||
override fun read(buffer: ByteArray, off: Int, len: Int): Int {
|
||||
var read = 0
|
||||
while(true) {
|
||||
val b = source.read()
|
||||
if(b < 0) break
|
||||
buffer[off + read++] = ((b xor prev) - 0x80).toByte()
|
||||
prev = b
|
||||
if(read == len) break
|
||||
}
|
||||
return read
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package net.corda.core.obfuscator
|
||||
|
||||
import java.io.FilterOutputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
class XorOutputStream(private val destination : OutputStream) : FilterOutputStream(destination) {
|
||||
var prev : Int = 0
|
||||
|
||||
override fun write(byte: Int) {
|
||||
val b = (byte + 0x80) xor prev
|
||||
destination.write(b)
|
||||
prev = b
|
||||
}
|
||||
|
||||
override fun write(buffer: ByteArray) {
|
||||
write(buffer, 0, buffer.size)
|
||||
}
|
||||
|
||||
override fun write(buffer: ByteArray, off: Int, len: Int) {
|
||||
var written = 0
|
||||
while(true) {
|
||||
val b = (buffer[written] + 0x80) xor prev
|
||||
destination.write(b)
|
||||
prev = b
|
||||
++written
|
||||
if(written == len) break
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,18 @@ The Corda core module defines a lot of types and helpers that can only be exerci
|
||||
the context of a node. However, as everything else depends on the core module, we cannot pull the node into
|
||||
this module. Therefore, any tests that require further Corda dependencies need to be defined in the module
|
||||
`core-tests`, which has the full set of dependencies including `node-driver`.
|
||||
|
||||
|
||||
# ZipBomb tests
|
||||
|
||||
There is a unit test that checks the zip bomb detector in `net.corda.core.internal.utilities.ZipBombDetector` works correctly.
|
||||
This test (`core/src/test/kotlin/net/corda/core/internal/utilities/ZipBombDetectorTest.kt`) uses real zip bombs, provided by `https://www.bamsoftware.com/hacks/zipbomb/`.
|
||||
As it is undesirable to have unit test depends on external internet resources we do not control, those files are included as resources in
|
||||
`core/src/test/resources/zip/`, however some Windows antivirus software correctly identifies those files as zip bombs,
|
||||
raising an alert to the user. To mitigate this, those files have been obfuscated using `net.corda.core.obfuscator.XorOutputStream`
|
||||
(which simply XORs every byte of the file with the previous one, except for the first byte that is XORed with zero)
|
||||
to prevent antivirus software from detecting them as zip bombs and are de-obfuscated on the fly in unit tests using
|
||||
`net.corda.core.obfuscator.XorInputStream`.
|
||||
|
||||
There is a dedicated Gradle task to re-download and re-obfuscate all the test resource files named `writeTestResources`,
|
||||
its source code is in `core/src/obfuscator/kotlin/net/corda/core/internal/utilities/TestResourceWriter.kt`
|
||||
|
@ -1,33 +1,19 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.crypto.internal.DigestAlgorithmFactory
|
||||
import org.bouncycastle.crypto.digests.Blake2sDigest
|
||||
import net.corda.core.internal.BLAKE2s256DigestAlgorithm
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class Blake2s256DigestServiceTest {
|
||||
class BLAKE2s256DigestService : DigestAlgorithm {
|
||||
override val algorithm = "BLAKE_TEST"
|
||||
|
||||
override val digestLength = 32
|
||||
|
||||
override fun digest(bytes: ByteArray): ByteArray {
|
||||
val blake2s256 = Blake2sDigest(null, digestLength, null, "12345678".toByteArray())
|
||||
blake2s256.reset()
|
||||
blake2s256.update(bytes, 0, bytes.size)
|
||||
val hash = ByteArray(digestLength)
|
||||
blake2s256.doFinal(hash, 0)
|
||||
return hash
|
||||
}
|
||||
}
|
||||
|
||||
private val service = DigestService("BLAKE_TEST")
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
DigestAlgorithmFactory.registerClass(BLAKE2s256DigestService::class.java.name)
|
||||
DigestAlgorithmFactory.registerClass(BLAKE2s256DigestAlgorithm::class.java.name)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
|
@ -0,0 +1,36 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
import net.corda.core.crypto.DigestAlgorithm
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import org.bouncycastle.crypto.digests.Blake2sDigest
|
||||
|
||||
/**
|
||||
* A set of custom hash algorithms
|
||||
*/
|
||||
|
||||
open class BLAKE2s256DigestAlgorithm : DigestAlgorithm {
|
||||
override val algorithm = "BLAKE_TEST"
|
||||
|
||||
override val digestLength = 32
|
||||
|
||||
protected fun blake2sHash(bytes: ByteArray): ByteArray {
|
||||
val blake2s256 = Blake2sDigest(null, digestLength, null, "12345678".toByteArray())
|
||||
blake2s256.reset()
|
||||
blake2s256.update(bytes, 0, bytes.size)
|
||||
val hash = ByteArray(digestLength)
|
||||
blake2s256.doFinal(hash, 0)
|
||||
return hash
|
||||
}
|
||||
|
||||
override fun digest(bytes: ByteArray): ByteArray = blake2sHash(bytes)
|
||||
}
|
||||
|
||||
class SHA256BLAKE2s256DigestAlgorithm : BLAKE2s256DigestAlgorithm() {
|
||||
override val algorithm = "SHA256-BLAKE2S256-TEST"
|
||||
|
||||
override fun digest(bytes: ByteArray): ByteArray = SecureHash.hashAs(SecureHash.SHA2_256, bytes).bytes
|
||||
|
||||
override fun componentDigest(bytes: ByteArray): ByteArray = blake2sHash(bytes)
|
||||
|
||||
override fun nonceDigest(bytes: ByteArray): ByteArray = blake2sHash(bytes)
|
||||
}
|
@ -5,10 +5,12 @@ import net.corda.core.crypto.DigestService
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.internal.AttachmentsClassLoaderCache
|
||||
import net.corda.core.transactions.ComponentGroup
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import java.util.function.Supplier
|
||||
|
||||
/**
|
||||
* A set of functions in core:test that allows testing of core internal classes in the core-tests project.
|
||||
@ -18,6 +20,7 @@ fun WireTransaction.accessGroupHashes() = this.groupHashes
|
||||
|
||||
fun WireTransaction.accessGroupMerkleRoots() = this.groupsMerkleRoots
|
||||
fun WireTransaction.accessAvailableComponentHashes() = this.availableComponentHashes
|
||||
fun WireTransaction.accessAvailableComponentNonces() = this.availableComponentNonces
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
fun createLedgerTransaction(
|
||||
@ -37,7 +40,17 @@ fun createLedgerTransaction(
|
||||
isAttachmentTrusted: (Attachment) -> Boolean,
|
||||
attachmentsClassLoaderCache: AttachmentsClassLoaderCache,
|
||||
digestService: DigestService = DigestService.default
|
||||
): LedgerTransaction = LedgerTransaction.create(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, networkParameters, references, componentGroups, serializedInputs, serializedReferences, isAttachmentTrusted, attachmentsClassLoaderCache, digestService)
|
||||
): LedgerTransaction = LedgerTransaction.create(
|
||||
inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, networkParameters, references, componentGroups, serializedInputs, serializedReferences, isAttachmentTrusted, attachmentsClassLoaderCache, digestService
|
||||
).specialise(::PassthroughVerifier)
|
||||
|
||||
fun createContractCreationError(txId: SecureHash, contractClass: String, cause: Throwable) = TransactionVerificationException.ContractCreationError(txId, contractClass, cause)
|
||||
fun createContractRejection(txId: SecureHash, contract: Contract, cause: Throwable) = TransactionVerificationException.ContractRejection(txId, contract, cause)
|
||||
|
||||
/**
|
||||
* Verify the [LedgerTransaction] we already have.
|
||||
*/
|
||||
private class PassthroughVerifier(ltx: LedgerTransaction, context: SerializationContext) : AbstractVerifier(ltx, context.deserializationClassLoader) {
|
||||
override val transaction: Supplier<LedgerTransaction>
|
||||
get() = Supplier { ltx }
|
||||
}
|
||||
|
@ -0,0 +1,70 @@
|
||||
package net.corda.core.internal.utilities
|
||||
|
||||
import net.corda.core.obfuscator.XorInputStream
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class ZipBombDetectorTest(private val case : TestCase) {
|
||||
|
||||
enum class TestCase(
|
||||
val description : String,
|
||||
val zipResource : String,
|
||||
val maxUncompressedSize : Long,
|
||||
val maxCompressionRatio : Float,
|
||||
val expectedOutcome : Boolean
|
||||
) {
|
||||
LEGIT_JAR("This project's jar file", "zip/core.jar", 128_000, 10f, false),
|
||||
|
||||
// This is not detected as a zip bomb as ZipInputStream is unable to read all of its entries
|
||||
// (https://stackoverflow.com/questions/69286786/zipinputstream-cannot-parse-a-281-tb-zip-bomb),
|
||||
// so the total uncompressed size doesn't exceed maxUncompressedSize
|
||||
SMALL_BOMB(
|
||||
"A large (5.5 GB) zip archive",
|
||||
"zip/zbsm.zip.xor", 64_000_000, 10f, false),
|
||||
|
||||
// Decreasing maxUncompressedSize leads to a successful detection
|
||||
SMALL_BOMB2(
|
||||
"A large (5.5 GB) zip archive, with 1MB maxUncompressedSize",
|
||||
"zip/zbsm.zip.xor", 1_000_000, 10f, true),
|
||||
|
||||
// ZipInputStream is also unable to read all entries of zblg.zip, but since the first one is already bigger than 4GB,
|
||||
// that is enough to exceed maxUncompressedSize
|
||||
LARGE_BOMB(
|
||||
"A huge (281 TB) Zip bomb, this is the biggest possible non-recursive non-Zip64 archive",
|
||||
"zip/zblg.zip.xor", 64_000_000, 10f, true),
|
||||
|
||||
//Same for this, but its entries are 22GB each
|
||||
EXTRA_LARGE_BOMB(
|
||||
"A humongous (4.5 PB) Zip64 bomb",
|
||||
"zip/zbxl.zip.xor", 64_000_000, 10f, true),
|
||||
|
||||
//This is a jar file containing a single 10GB manifest
|
||||
BIG_MANIFEST(
|
||||
"A jar file with a huge manifest",
|
||||
"zip/big-manifest.jar.xor", 64_000_000, 10f, true);
|
||||
|
||||
override fun toString() = description
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
fun generateTestCases(): Collection<*> {
|
||||
return TestCase.values().toList()
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=10_000)
|
||||
fun test() {
|
||||
(javaClass.classLoader.getResourceAsStream(case.zipResource) ?:
|
||||
throw IllegalStateException("Missing test resource file ${case.zipResource}"))
|
||||
.buffered()
|
||||
.let(::XorInputStream)
|
||||
.let {
|
||||
Assert.assertEquals(case.expectedOutcome, ZipBombDetector.scanZip(it, case.maxUncompressedSize, case.maxCompressionRatio))
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package net.corda.core.obfuscator
|
||||
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.security.DigestInputStream
|
||||
import java.security.DigestOutputStream
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class XorStreamTest(private val size : Int) {
|
||||
private val random = Random(0)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
fun generateTestCases(): Collection<*> {
|
||||
return listOf(0, 16, 31, 127, 1000, 1024)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 5000)
|
||||
fun test() {
|
||||
val baos = ByteArrayOutputStream(size)
|
||||
val md = MessageDigest.getInstance("MD5")
|
||||
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||
DigestOutputStream(XorOutputStream(baos), md).use { outputStream ->
|
||||
var written = 0
|
||||
while(written < size) {
|
||||
random.nextBytes(buffer)
|
||||
val bytesToWrite = (size - written).coerceAtMost(buffer.size)
|
||||
outputStream.write(buffer, 0, bytesToWrite)
|
||||
written += bytesToWrite
|
||||
}
|
||||
}
|
||||
val digest = md.digest()
|
||||
md.reset()
|
||||
DigestInputStream(XorInputStream(ByteArrayInputStream(baos.toByteArray())), md).use { inputStream ->
|
||||
while(true) {
|
||||
val read = inputStream.read(buffer)
|
||||
if(read <= 0) break
|
||||
}
|
||||
}
|
||||
Assert.assertArrayEquals(digest, md.digest())
|
||||
}
|
||||
}
|
BIN
core/src/test/resources/zip/big-manifest.jar.xor
Normal file
BIN
core/src/test/resources/zip/big-manifest.jar.xor
Normal file
Binary file not shown.
BIN
core/src/test/resources/zip/zblg.zip.xor
Normal file
BIN
core/src/test/resources/zip/zblg.zip.xor
Normal file
Binary file not shown.
BIN
core/src/test/resources/zip/zbsm.zip.xor
Normal file
BIN
core/src/test/resources/zip/zbsm.zip.xor
Normal file
Binary file not shown.
BIN
core/src/test/resources/zip/zbxl.zip.xor
Normal file
BIN
core/src/test/resources/zip/zbxl.zip.xor
Normal file
Binary file not shown.
@ -1162,6 +1162,7 @@
|
||||
<ID>MatchingDeclarationName:NamedCache.kt$net.corda.core.internal.NamedCache.kt</ID>
|
||||
<ID>MatchingDeclarationName:NetParams.kt$net.corda.netparams.NetParams.kt</ID>
|
||||
<ID>MatchingDeclarationName:NetworkParametersServiceInternal.kt$net.corda.core.internal.NetworkParametersServiceInternal.kt</ID>
|
||||
<ID>MatchingDeclarationName:NotaryQueries.kt$net.corda.nodeapi.notary.NotaryQueries.kt</ID>
|
||||
<ID>MatchingDeclarationName:OGSwapPricingCcpExample.kt$net.corda.vega.analytics.example.OGSwapPricingCcpExample.kt</ID>
|
||||
<ID>MatchingDeclarationName:OGSwapPricingExample.kt$net.corda.vega.analytics.example.OGSwapPricingExample.kt</ID>
|
||||
<ID>MatchingDeclarationName:PlatformSecureRandom.kt$net.corda.core.crypto.internal.PlatformSecureRandom.kt</ID>
|
||||
|
@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
NODE_LIST=("dockerNode1" "dockerNode2" "dockerNode3")
|
||||
NETWORK_NAME=mininet
|
||||
CORDAPP_VERSION="4.6-SNAPSHOT"
|
||||
DOCKER_IMAGE_VERSION="corda-zulu-4.6-snapshot"
|
||||
CORDAPP_VERSION="4.8-SNAPSHOT"
|
||||
DOCKER_IMAGE_VERSION="corda-zulu-4.8-snapshot"
|
||||
|
||||
mkdir cordapps
|
||||
rm -f cordapps/*
|
||||
|
@ -1,10 +1,11 @@
|
||||
FROM azul/zulu-openjdk:8u192
|
||||
FROM azul/zulu-openjdk:8u312
|
||||
|
||||
## Remove Azul Zulu repo, as it is gone by now
|
||||
RUN rm -rf /etc/apt/sources.list.d/zulu.list
|
||||
|
||||
## Add packages, clean cache, create dirs, create corda user and change ownership
|
||||
RUN apt-get update && \
|
||||
apt-mark hold zulu8-jdk && \
|
||||
apt-get -y upgrade && \
|
||||
apt-get -y install bash curl unzip && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
|
@ -1,7 +1,8 @@
|
||||
FROM azul/zulu-openjdk:8u192
|
||||
FROM azul/zulu-openjdk:8u312
|
||||
|
||||
## Add packages, clean cache, create dirs, create corda user and change ownership
|
||||
RUN apt-get update && \
|
||||
apt-mark hold zulu8-jdk && \
|
||||
apt-get -y upgrade && \
|
||||
apt-get -y install bash curl unzip netstat lsof telnet netcat && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
|
@ -5,6 +5,10 @@ apply plugin: 'net.corda.plugins.publish-utils'
|
||||
apply plugin: 'maven-publish'
|
||||
apply plugin: 'com.jfrog.artifactory'
|
||||
|
||||
dependencies {
|
||||
compile rootProject
|
||||
}
|
||||
|
||||
def internalPackagePrefixes(sourceDirs) {
|
||||
def prefixes = []
|
||||
// Kotlin allows packages to deviate from the directory structure, but let's assume they don't:
|
||||
@ -36,10 +40,13 @@ task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) {
|
||||
}
|
||||
|
||||
[dokka, dokkaJavadoc].collect {
|
||||
it.configure {
|
||||
it.configuration {
|
||||
moduleName = 'corda'
|
||||
processConfigurations = ['compile']
|
||||
sourceDirs = dokkaSourceDirs
|
||||
dokkaSourceDirs.collect { sourceDir ->
|
||||
sourceRoot {
|
||||
path = sourceDir.path
|
||||
}
|
||||
}
|
||||
includes = ['packages.md']
|
||||
jdkVersion = 8
|
||||
externalDocumentationLink {
|
||||
@ -52,7 +59,7 @@ task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) {
|
||||
url = new URL("https://www.bouncycastle.org/docs/docs1.5on/")
|
||||
}
|
||||
internalPackagePrefixes.collect { packagePrefix ->
|
||||
packageOptions {
|
||||
perPackageOption {
|
||||
prefix = packagePrefix
|
||||
suppress = true
|
||||
}
|
||||
|
@ -21,14 +21,29 @@ 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.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme
|
||||
import net.corda.coretesting.internal.NettyTestClient
|
||||
import net.corda.coretesting.internal.NettyTestHandler
|
||||
import net.corda.coretesting.internal.NettyTestServer
|
||||
import net.corda.coretesting.internal.stubs.CertificateStoreStubs
|
||||
import net.corda.nodeapi.internal.config.MutualSslConfiguration
|
||||
import net.corda.nodeapi.internal.createDevNodeCa
|
||||
import net.corda.nodeapi.internal.crypto.CertificateType
|
||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities.DEFAULT_IDENTITY_SIGNATURE_SCHEME
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME
|
||||
import net.corda.nodeapi.internal.crypto.checkValidity
|
||||
import net.corda.nodeapi.internal.crypto.getSupportedKey
|
||||
import net.corda.nodeapi.internal.crypto.loadOrCreateKeyStore
|
||||
import net.corda.nodeapi.internal.crypto.save
|
||||
import net.corda.nodeapi.internal.crypto.toBc
|
||||
import net.corda.nodeapi.internal.crypto.x509
|
||||
import net.corda.nodeapi.internal.crypto.x509Certificates
|
||||
import net.corda.nodeapi.internal.installDevNodeCaCertPath
|
||||
import net.corda.nodeapi.internal.protonwrapper.netty.init
|
||||
import net.corda.nodeapi.internal.protonwrapper.netty.keyManagerFactory
|
||||
import net.corda.nodeapi.internal.protonwrapper.netty.trustManagerFactory
|
||||
import net.corda.nodeapi.internal.registerDevP2pCertificates
|
||||
import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme
|
||||
import net.corda.serialization.internal.AllWhitelist
|
||||
import net.corda.serialization.internal.SerializationContextImpl
|
||||
import net.corda.serialization.internal.SerializationFactoryImpl
|
||||
@ -37,25 +52,16 @@ 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.coretesting.internal.NettyTestClient
|
||||
import net.corda.coretesting.internal.NettyTestHandler
|
||||
import net.corda.coretesting.internal.NettyTestServer
|
||||
import net.corda.testing.internal.createDevIntermediateCaCertPath
|
||||
import net.corda.coretesting.internal.stubs.CertificateStoreStubs
|
||||
import net.corda.nodeapi.internal.crypto.CertificateType
|
||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import net.corda.nodeapi.internal.crypto.checkValidity
|
||||
import net.corda.nodeapi.internal.crypto.getSupportedKey
|
||||
import net.corda.nodeapi.internal.crypto.loadOrCreateKeyStore
|
||||
import net.corda.nodeapi.internal.crypto.save
|
||||
import net.corda.nodeapi.internal.crypto.toBc
|
||||
import net.corda.nodeapi.internal.crypto.x509
|
||||
import net.corda.nodeapi.internal.crypto.x509Certificates
|
||||
import net.corda.testing.internal.IS_OPENJ9
|
||||
import net.corda.testing.internal.createDevIntermediateCaCertPath
|
||||
import net.i2p.crypto.eddsa.EdDSAPrivateKey
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.bouncycastle.asn1.x509.*
|
||||
import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier
|
||||
import org.bouncycastle.asn1.x509.BasicConstraints
|
||||
import org.bouncycastle.asn1.x509.CRLDistPoint
|
||||
import org.bouncycastle.asn1.x509.Extension
|
||||
import org.bouncycastle.asn1.x509.KeyUsage
|
||||
import org.bouncycastle.asn1.x509.SubjectKeyIdentifier
|
||||
import org.bouncycastle.jcajce.provider.asymmetric.edec.BCEdDSAPrivateKey
|
||||
import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PrivateKey
|
||||
import org.junit.Assume
|
||||
@ -74,10 +80,19 @@ import java.security.PrivateKey
|
||||
import java.security.cert.CertPath
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
import javax.net.ssl.*
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.SSLParameters
|
||||
import javax.net.ssl.SSLServerSocket
|
||||
import javax.net.ssl.SSLSocket
|
||||
import javax.security.auth.x500.X500Principal
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.test.*
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.fail
|
||||
|
||||
class X509UtilitiesTest {
|
||||
private companion object {
|
||||
@ -295,15 +310,10 @@ class X509UtilitiesTest {
|
||||
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 context = SSLContext.getInstance("TLS")
|
||||
val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
|
||||
keyManagerFactory.init(keyStore)
|
||||
val keyManagerFactory = keyManagerFactory(sslConfig.keyStore.get())
|
||||
val keyManagers = keyManagerFactory.keyManagers
|
||||
val trustMgrFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
trustMgrFactory.init(trustStore)
|
||||
val trustMgrFactory = trustManagerFactory(sslConfig.trustStore.get())
|
||||
val trustManagers = trustMgrFactory.trustManagers
|
||||
context.init(keyManagers, trustManagers, newSecureRandom())
|
||||
|
||||
@ -388,15 +398,8 @@ class X509UtilitiesTest {
|
||||
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 keyManagerFactory = keyManagerFactory(sslConfig.keyStore.get())
|
||||
val trustManagerFactory = trustManagerFactory(sslConfig.trustStore.get())
|
||||
|
||||
val sslServerContext = SslContextBuilder
|
||||
.forServer(keyManagerFactory)
|
||||
|
@ -29,7 +29,7 @@ dependencies {
|
||||
|
||||
// SQL connection pooling library
|
||||
compile "com.zaxxer:HikariCP:$hikari_version"
|
||||
|
||||
|
||||
// ClassGraph: classpath scanning
|
||||
compile "io.github.classgraph:classgraph:$class_graph_version"
|
||||
|
||||
@ -54,6 +54,9 @@ dependencies {
|
||||
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:${junit_vintage_version}"
|
||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junit_jupiter_version}"
|
||||
testRuntimeOnly "org.junit.platform:junit-platform-launcher:${junit_platform_version}"
|
||||
|
||||
testCompile project(':node-driver')
|
||||
|
||||
// Unit testing helpers.
|
||||
testCompile "org.assertj:assertj-core:$assertj_version"
|
||||
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||
|
@ -5,7 +5,6 @@ import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_P2P_USER
|
||||
import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.p2pConnectorTcpTransport
|
||||
import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.p2pConnectorTcpTransportFromList
|
||||
import net.corda.nodeapi.internal.config.MessagingServerConnectionConfiguration
|
||||
import net.corda.nodeapi.internal.config.MutualSslConfiguration
|
||||
import org.apache.activemq.artemis.api.core.client.*
|
||||
@ -25,7 +24,9 @@ class ArtemisMessagingClient(private val config: MutualSslConfiguration,
|
||||
private val confirmationWindowSize: Int = -1,
|
||||
private val messagingServerConnectionConfig: MessagingServerConnectionConfiguration? = null,
|
||||
private val backupServerAddressPool: List<NetworkHostAndPort> = emptyList(),
|
||||
private val failoverCallback: ((FailoverEventType) -> Unit)? = null
|
||||
private val failoverCallback: ((FailoverEventType) -> Unit)? = null,
|
||||
private val threadPoolName: String = "ArtemisClient",
|
||||
private val trace: Boolean = false
|
||||
) : ArtemisSessionProvider {
|
||||
companion object {
|
||||
private val log = loggerFor<ArtemisMessagingClient>()
|
||||
@ -40,8 +41,10 @@ class ArtemisMessagingClient(private val config: MutualSslConfiguration,
|
||||
|
||||
override fun start(): Started = synchronized(this) {
|
||||
check(started == null) { "start can't be called twice" }
|
||||
val tcpTransport = p2pConnectorTcpTransport(serverAddress, config)
|
||||
val backupTransports = p2pConnectorTcpTransportFromList(backupServerAddressPool, config)
|
||||
val tcpTransport = p2pConnectorTcpTransport(serverAddress, config, threadPoolName = threadPoolName, trace = trace)
|
||||
val backupTransports = backupServerAddressPool.mapIndexed { index, address ->
|
||||
p2pConnectorTcpTransport(address, config, threadPoolName = "$threadPoolName-backup${index+1}", trace = trace)
|
||||
}
|
||||
|
||||
log.info("Connecting to message broker: $serverAddress")
|
||||
if (backupTransports.isNotEmpty()) {
|
||||
@ -50,8 +53,6 @@ class ArtemisMessagingClient(private val config: MutualSslConfiguration,
|
||||
// If back-up artemis addresses are configured, the locator will be created using HA mode.
|
||||
@Suppress("SpreadOperator")
|
||||
val locator = ActiveMQClient.createServerLocator(backupTransports.isNotEmpty(), *(listOf(tcpTransport) + backupTransports).toTypedArray()).apply {
|
||||
// Never time out on our loopback Artemis connections. If we switch back to using the InVM transport this
|
||||
// would be the default and the two lines below can be deleted.
|
||||
connectionTTL = 60000
|
||||
clientFailureCheckPeriod = 30000
|
||||
callFailoverTimeout = java.lang.Long.getLong(CORDA_ARTEMIS_CALL_TIMEOUT_PROP_NAME, CORDA_ARTEMIS_CALL_TIMEOUT_DEFAULT)
|
||||
|
@ -1,19 +1,20 @@
|
||||
@file:Suppress("LongParameterList")
|
||||
|
||||
package net.corda.nodeapi.internal
|
||||
|
||||
import net.corda.core.messaging.ClientRpcSslOptions
|
||||
import net.corda.core.serialization.internal.nodeSerializationEnv
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.nodeapi.BrokerRpcSslOptions
|
||||
import net.corda.nodeapi.internal.config.CertificateStore
|
||||
import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier
|
||||
import net.corda.nodeapi.internal.config.DEFAULT_SSL_HANDSHAKE_TIMEOUT
|
||||
import net.corda.nodeapi.internal.config.MutualSslConfiguration
|
||||
import net.corda.nodeapi.internal.config.SslConfiguration
|
||||
import net.corda.nodeapi.internal.protonwrapper.netty.trustManagerFactory
|
||||
import org.apache.activemq.artemis.api.core.TransportConfiguration
|
||||
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory
|
||||
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants
|
||||
import java.nio.file.Path
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
|
||||
// This avoids internal types from leaking in the public API. The "external" ArtemisTcpTransport delegates to this internal one.
|
||||
@Suppress("LongParameterList")
|
||||
class ArtemisTcpTransport {
|
||||
companion object {
|
||||
val CIPHER_SUITES = listOf(
|
||||
@ -23,65 +24,52 @@ class ArtemisTcpTransport {
|
||||
|
||||
val TLS_VERSIONS = listOf("TLSv1.2")
|
||||
|
||||
internal fun defaultArtemisOptions(hostAndPort: NetworkHostAndPort) = mapOf(
|
||||
const val SSL_HANDSHAKE_TIMEOUT_NAME = "Corda-SSLHandshakeTimeout"
|
||||
const val TRUST_MANAGER_FACTORY_NAME = "Corda-TrustManagerFactory"
|
||||
const val TRACE_NAME = "Corda-Trace"
|
||||
const val THREAD_POOL_NAME_NAME = "Corda-ThreadPoolName"
|
||||
|
||||
// Turn on AMQP support, which needs the protocol jar on the classpath.
|
||||
// Unfortunately we cannot disable core protocol as artemis only uses AMQP for interop.
|
||||
// It does not use AMQP messages for its own messages e.g. topology and heartbeats.
|
||||
private const val P2P_PROTOCOLS = "CORE,AMQP"
|
||||
private const val RPC_PROTOCOLS = "CORE"
|
||||
|
||||
private fun defaultArtemisOptions(hostAndPort: NetworkHostAndPort, protocols: String) = mapOf(
|
||||
// Basic TCP target details.
|
||||
TransportConstants.HOST_PROP_NAME to hostAndPort.host,
|
||||
TransportConstants.PORT_PROP_NAME to hostAndPort.port,
|
||||
|
||||
// Turn on AMQP support, which needs the protocol jar on the classpath.
|
||||
// Unfortunately we cannot disable core protocol as artemis only uses AMQP for interop.
|
||||
// It does not use AMQP messages for its own messages e.g. topology and heartbeats.
|
||||
// TODO further investigate how to ensure we use a well defined wire level protocol for Node to Node communications.
|
||||
TransportConstants.PROTOCOLS_PROP_NAME to "CORE,AMQP",
|
||||
TransportConstants.PROTOCOLS_PROP_NAME to protocols,
|
||||
TransportConstants.USE_GLOBAL_WORKER_POOL_PROP_NAME to (nodeSerializationEnv != null),
|
||||
TransportConstants.REMOTING_THREADS_PROPNAME to (if (nodeSerializationEnv != null) -1 else 1),
|
||||
// turn off direct delivery in Artemis - this is latency optimisation that can lead to
|
||||
//hick-ups under high load (CORDA-1336)
|
||||
TransportConstants.DIRECT_DELIVER to false)
|
||||
|
||||
internal val defaultSSLOptions = mapOf(
|
||||
TransportConstants.ENABLED_CIPHER_SUITES_PROP_NAME to CIPHER_SUITES.joinToString(","),
|
||||
TransportConstants.ENABLED_PROTOCOLS_PROP_NAME to TLS_VERSIONS.joinToString(","))
|
||||
|
||||
private fun SslConfiguration.toTransportOptions(): Map<String, Any> {
|
||||
|
||||
val options = mutableMapOf<String, Any>()
|
||||
(keyStore to trustStore).addToTransportOptions(options)
|
||||
return options
|
||||
}
|
||||
|
||||
private fun Pair<FileBasedCertificateStoreSupplier?, FileBasedCertificateStoreSupplier?>.addToTransportOptions(options: MutableMap<String, Any>) {
|
||||
|
||||
val keyStore = first
|
||||
val trustStore = second
|
||||
private fun SslConfiguration.addToTransportOptions(options: MutableMap<String, Any>) {
|
||||
if (keyStore != null || trustStore != null) {
|
||||
options[TransportConstants.SSL_ENABLED_PROP_NAME] = true
|
||||
options[TransportConstants.NEED_CLIENT_AUTH_PROP_NAME] = true
|
||||
}
|
||||
keyStore?.let {
|
||||
with (it) {
|
||||
path.requireOnDefaultFileSystem()
|
||||
options.putAll(get().toKeyStoreTransportOptions(path))
|
||||
options[TransportConstants.KEYSTORE_PROVIDER_PROP_NAME] = "JKS"
|
||||
options[TransportConstants.KEYSTORE_PATH_PROP_NAME] = path
|
||||
options[TransportConstants.KEYSTORE_PASSWORD_PROP_NAME] = get().password
|
||||
}
|
||||
}
|
||||
trustStore?.let {
|
||||
with (it) {
|
||||
path.requireOnDefaultFileSystem()
|
||||
options.putAll(get().toTrustStoreTransportOptions(path))
|
||||
options[TransportConstants.TRUSTSTORE_PROVIDER_PROP_NAME] = "JKS"
|
||||
options[TransportConstants.TRUSTSTORE_PATH_PROP_NAME] = path
|
||||
options[TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME] = get().password
|
||||
}
|
||||
}
|
||||
options[TransportConstants.SSL_PROVIDER] = if (useOpenSsl) TransportConstants.OPENSSL_PROVIDER else TransportConstants.DEFAULT_SSL_PROVIDER
|
||||
options[SSL_HANDSHAKE_TIMEOUT_NAME] = handshakeTimeout ?: DEFAULT_SSL_HANDSHAKE_TIMEOUT
|
||||
}
|
||||
|
||||
private fun CertificateStore.toKeyStoreTransportOptions(path: Path) = mapOf(
|
||||
TransportConstants.SSL_ENABLED_PROP_NAME to true,
|
||||
TransportConstants.KEYSTORE_PROVIDER_PROP_NAME to "JKS",
|
||||
TransportConstants.KEYSTORE_PATH_PROP_NAME to path,
|
||||
TransportConstants.KEYSTORE_PASSWORD_PROP_NAME to password,
|
||||
TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to true)
|
||||
|
||||
private fun CertificateStore.toTrustStoreTransportOptions(path: Path) = mapOf(
|
||||
TransportConstants.SSL_ENABLED_PROP_NAME to true,
|
||||
TransportConstants.TRUSTSTORE_PROVIDER_PROP_NAME to "JKS",
|
||||
TransportConstants.TRUSTSTORE_PATH_PROP_NAME to path,
|
||||
TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME to password,
|
||||
TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to true)
|
||||
|
||||
private fun ClientRpcSslOptions.toTransportOptions() = mapOf(
|
||||
TransportConstants.SSL_ENABLED_PROP_NAME to true,
|
||||
TransportConstants.TRUSTSTORE_PROVIDER_PROP_NAME to trustStoreProvider,
|
||||
@ -95,86 +83,164 @@ class ArtemisTcpTransport {
|
||||
TransportConstants.KEYSTORE_PASSWORD_PROP_NAME to keyStorePassword,
|
||||
TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to false)
|
||||
|
||||
internal val acceptorFactoryClassName = "org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptorFactory"
|
||||
internal val connectorFactoryClassName = NettyConnectorFactory::class.java.name
|
||||
|
||||
fun p2pAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: MutualSslConfiguration?, enableSSL: Boolean = true): TransportConfiguration {
|
||||
|
||||
return p2pAcceptorTcpTransport(hostAndPort, config?.keyStore, config?.trustStore, enableSSL = enableSSL, useOpenSsl = config?.useOpenSsl ?: false)
|
||||
}
|
||||
|
||||
fun p2pConnectorTcpTransport(hostAndPort: NetworkHostAndPort, config: MutualSslConfiguration?, enableSSL: Boolean = true, keyStoreProvider: String? = null): TransportConfiguration {
|
||||
|
||||
return p2pConnectorTcpTransport(hostAndPort, config?.keyStore, config?.trustStore, enableSSL = enableSSL, useOpenSsl = config?.useOpenSsl ?: false, keyStoreProvider = keyStoreProvider)
|
||||
}
|
||||
|
||||
fun p2pAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, keyStore: FileBasedCertificateStoreSupplier?, trustStore: FileBasedCertificateStoreSupplier?, enableSSL: Boolean = true, useOpenSsl: Boolean = false): TransportConfiguration {
|
||||
|
||||
val options = defaultArtemisOptions(hostAndPort).toMutableMap()
|
||||
fun p2pAcceptorTcpTransport(hostAndPort: NetworkHostAndPort,
|
||||
config: MutualSslConfiguration?,
|
||||
trustManagerFactory: TrustManagerFactory? = config?.trustStore?.get()?.let(::trustManagerFactory),
|
||||
enableSSL: Boolean = true,
|
||||
threadPoolName: String = "P2PServer",
|
||||
trace: Boolean = false,
|
||||
remotingThreads: Int? = null): TransportConfiguration {
|
||||
val options = mutableMapOf<String, Any>()
|
||||
if (enableSSL) {
|
||||
options.putAll(defaultSSLOptions)
|
||||
(keyStore to trustStore).addToTransportOptions(options)
|
||||
options[TransportConstants.SSL_PROVIDER] = if (useOpenSsl) TransportConstants.OPENSSL_PROVIDER else TransportConstants.DEFAULT_SSL_PROVIDER
|
||||
config?.addToTransportOptions(options)
|
||||
}
|
||||
options[TransportConstants.HANDSHAKE_TIMEOUT] = 0 // Suppress core.server.lambda$channelActive$0 - AMQ224088 error from load balancer type connections
|
||||
return TransportConfiguration(acceptorFactoryClassName, options)
|
||||
return createAcceptorTransport(
|
||||
hostAndPort,
|
||||
P2P_PROTOCOLS,
|
||||
options,
|
||||
trustManagerFactory,
|
||||
enableSSL,
|
||||
threadPoolName,
|
||||
trace,
|
||||
remotingThreads
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
fun p2pConnectorTcpTransport(hostAndPort: NetworkHostAndPort, keyStore: FileBasedCertificateStoreSupplier?, trustStore: FileBasedCertificateStoreSupplier?, enableSSL: Boolean = true, useOpenSsl: Boolean = false, keyStoreProvider: String? = null): TransportConfiguration {
|
||||
|
||||
val options = defaultArtemisOptions(hostAndPort).toMutableMap()
|
||||
fun p2pConnectorTcpTransport(hostAndPort: NetworkHostAndPort,
|
||||
config: MutualSslConfiguration?,
|
||||
enableSSL: Boolean = true,
|
||||
threadPoolName: String = "P2PClient",
|
||||
trace: Boolean = false,
|
||||
remotingThreads: Int? = null): TransportConfiguration {
|
||||
val options = mutableMapOf<String, Any>()
|
||||
if (enableSSL) {
|
||||
options.putAll(defaultSSLOptions)
|
||||
(keyStore to trustStore).addToTransportOptions(options)
|
||||
options[TransportConstants.SSL_PROVIDER] = if (useOpenSsl) TransportConstants.OPENSSL_PROVIDER else TransportConstants.DEFAULT_SSL_PROVIDER
|
||||
keyStoreProvider?.let { options.put(TransportConstants.KEYSTORE_PROVIDER_PROP_NAME, keyStoreProvider) }
|
||||
config?.addToTransportOptions(options)
|
||||
}
|
||||
return TransportConfiguration(connectorFactoryClassName, options)
|
||||
return createConnectorTransport(hostAndPort, P2P_PROTOCOLS, options, enableSSL, threadPoolName, trace, remotingThreads)
|
||||
}
|
||||
|
||||
fun p2pConnectorTcpTransportFromList(hostAndPortList: List<NetworkHostAndPort>, config: MutualSslConfiguration?, enableSSL: Boolean = true, keyStoreProvider: String? = null): List<TransportConfiguration> = hostAndPortList.map {
|
||||
p2pConnectorTcpTransport(it, config, enableSSL, keyStoreProvider)
|
||||
}
|
||||
|
||||
fun rpcAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: BrokerRpcSslOptions?, enableSSL: Boolean = true): TransportConfiguration {
|
||||
val options = defaultArtemisOptions(hostAndPort).toMutableMap()
|
||||
|
||||
fun rpcAcceptorTcpTransport(hostAndPort: NetworkHostAndPort,
|
||||
config: BrokerRpcSslOptions?,
|
||||
enableSSL: Boolean = true,
|
||||
threadPoolName: String = "RPCServer",
|
||||
trace: Boolean = false,
|
||||
remotingThreads: Int? = null): TransportConfiguration {
|
||||
val options = mutableMapOf<String, Any>()
|
||||
if (config != null && enableSSL) {
|
||||
config.keyStorePath.requireOnDefaultFileSystem()
|
||||
options.putAll(config.toTransportOptions())
|
||||
options.putAll(defaultSSLOptions)
|
||||
}
|
||||
options[TransportConstants.HANDSHAKE_TIMEOUT] = 0 // Suppress core.server.lambda$channelActive$0 - AMQ224088 error from load balancer type connections
|
||||
return TransportConfiguration(acceptorFactoryClassName, options)
|
||||
return createAcceptorTransport(hostAndPort, RPC_PROTOCOLS, options, null, enableSSL, threadPoolName, trace, remotingThreads)
|
||||
}
|
||||
|
||||
fun rpcConnectorTcpTransport(hostAndPort: NetworkHostAndPort, config: ClientRpcSslOptions?, enableSSL: Boolean = true): TransportConfiguration {
|
||||
val options = defaultArtemisOptions(hostAndPort).toMutableMap()
|
||||
|
||||
fun rpcConnectorTcpTransport(hostAndPort: NetworkHostAndPort,
|
||||
config: ClientRpcSslOptions?,
|
||||
enableSSL: Boolean = true,
|
||||
trace: Boolean = false,
|
||||
remotingThreads: Int? = null): TransportConfiguration {
|
||||
val options = mutableMapOf<String, Any>()
|
||||
if (config != null && enableSSL) {
|
||||
config.trustStorePath.requireOnDefaultFileSystem()
|
||||
options.putAll(config.toTransportOptions())
|
||||
options.putAll(defaultSSLOptions)
|
||||
}
|
||||
return TransportConfiguration(connectorFactoryClassName, options)
|
||||
return createConnectorTransport(hostAndPort, RPC_PROTOCOLS, options, enableSSL, "RPCClient", trace, remotingThreads)
|
||||
}
|
||||
|
||||
fun rpcConnectorTcpTransportsFromList(hostAndPortList: List<NetworkHostAndPort>, config: ClientRpcSslOptions?, enableSSL: Boolean = true): List<TransportConfiguration> = hostAndPortList.map {
|
||||
rpcConnectorTcpTransport(it, config, enableSSL)
|
||||
fun rpcInternalClientTcpTransport(hostAndPort: NetworkHostAndPort,
|
||||
config: SslConfiguration,
|
||||
threadPoolName: String = "Internal-RPCClient",
|
||||
trace: Boolean = false): TransportConfiguration {
|
||||
val options = mutableMapOf<String, Any>()
|
||||
config.addToTransportOptions(options)
|
||||
return createConnectorTransport(hostAndPort, RPC_PROTOCOLS, options, true, threadPoolName, trace, null)
|
||||
}
|
||||
|
||||
fun rpcInternalClientTcpTransport(hostAndPort: NetworkHostAndPort, config: SslConfiguration, keyStoreProvider: String? = null): TransportConfiguration {
|
||||
return TransportConfiguration(connectorFactoryClassName, defaultArtemisOptions(hostAndPort) + defaultSSLOptions + config.toTransportOptions() + asMap(keyStoreProvider))
|
||||
fun rpcInternalAcceptorTcpTransport(hostAndPort: NetworkHostAndPort,
|
||||
config: SslConfiguration,
|
||||
threadPoolName: String = "Internal-RPCServer",
|
||||
trace: Boolean = false,
|
||||
remotingThreads: Int? = null): TransportConfiguration {
|
||||
val options = mutableMapOf<String, Any>()
|
||||
config.addToTransportOptions(options)
|
||||
return createAcceptorTransport(
|
||||
hostAndPort,
|
||||
RPC_PROTOCOLS,
|
||||
options,
|
||||
trustManagerFactory(requireNotNull(config.trustStore).get()),
|
||||
true,
|
||||
threadPoolName,
|
||||
trace,
|
||||
remotingThreads
|
||||
)
|
||||
}
|
||||
|
||||
fun rpcInternalAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: SslConfiguration, keyStoreProvider: String? = null): TransportConfiguration {
|
||||
return TransportConfiguration(acceptorFactoryClassName, defaultArtemisOptions(hostAndPort) + defaultSSLOptions +
|
||||
config.toTransportOptions() + (TransportConstants.HANDSHAKE_TIMEOUT to 0) + asMap(keyStoreProvider))
|
||||
private fun createAcceptorTransport(hostAndPort: NetworkHostAndPort,
|
||||
protocols: String,
|
||||
options: MutableMap<String, Any>,
|
||||
trustManagerFactory: TrustManagerFactory?,
|
||||
enableSSL: Boolean,
|
||||
threadPoolName: String,
|
||||
trace: Boolean,
|
||||
remotingThreads: Int?): TransportConfiguration {
|
||||
// Suppress core.server.lambda$channelActive$0 - AMQ224088 error from load balancer type connections
|
||||
options[TransportConstants.HANDSHAKE_TIMEOUT] = 0
|
||||
if (trustManagerFactory != null) {
|
||||
// NettyAcceptor only creates default TrustManagerFactorys with the provided trust store details. However, we need to use
|
||||
// more customised instances which use our revocation checkers, which we pass directly into NodeNettyAcceptorFactory.
|
||||
//
|
||||
// This, however, requires copying a lot of code from NettyAcceptor into NodeNettyAcceptor. The version of Artemis in
|
||||
// Corda 4.9 solves this problem by introducing a "trustManagerFactoryPlugin" config option.
|
||||
options[TRUST_MANAGER_FACTORY_NAME] = trustManagerFactory
|
||||
}
|
||||
return createTransport(
|
||||
"net.corda.node.services.messaging.NodeNettyAcceptorFactory",
|
||||
hostAndPort,
|
||||
protocols,
|
||||
options,
|
||||
enableSSL,
|
||||
threadPoolName,
|
||||
trace,
|
||||
remotingThreads
|
||||
)
|
||||
}
|
||||
|
||||
private fun asMap(keyStoreProvider: String?): Map<String, String> {
|
||||
return keyStoreProvider?.let {mutableMapOf(TransportConstants.KEYSTORE_PROVIDER_PROP_NAME to it)} ?: emptyMap()
|
||||
private fun createConnectorTransport(hostAndPort: NetworkHostAndPort,
|
||||
protocols: String,
|
||||
options: MutableMap<String, Any>,
|
||||
enableSSL: Boolean,
|
||||
threadPoolName: String,
|
||||
trace: Boolean,
|
||||
remotingThreads: Int?): TransportConfiguration {
|
||||
return createTransport(
|
||||
CordaNettyConnectorFactory::class.java.name,
|
||||
hostAndPort,
|
||||
protocols,
|
||||
options,
|
||||
enableSSL,
|
||||
threadPoolName,
|
||||
trace,
|
||||
remotingThreads
|
||||
)
|
||||
}
|
||||
|
||||
private fun createTransport(className: String,
|
||||
hostAndPort: NetworkHostAndPort,
|
||||
protocols: String,
|
||||
options: MutableMap<String, Any>,
|
||||
enableSSL: Boolean,
|
||||
threadPoolName: String,
|
||||
trace: Boolean,
|
||||
remotingThreads: Int?): TransportConfiguration {
|
||||
options += defaultArtemisOptions(hostAndPort, protocols)
|
||||
if (enableSSL) {
|
||||
options[TransportConstants.ENABLED_CIPHER_SUITES_PROP_NAME] = CIPHER_SUITES.joinToString(",")
|
||||
options[TransportConstants.ENABLED_PROTOCOLS_PROP_NAME] = TLS_VERSIONS.joinToString(",")
|
||||
}
|
||||
// By default, use only one remoting thread in tests (https://github.com/corda/corda/pull/2357)
|
||||
options[TransportConstants.REMOTING_THREADS_PROPNAME] = remotingThreads ?: if (nodeSerializationEnv == null) 1 else -1
|
||||
options[THREAD_POOL_NAME_NAME] = threadPoolName
|
||||
options[TRACE_NAME] = trace
|
||||
return TransportConfiguration(className, options)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,14 @@
|
||||
@file:JvmName("ArtemisUtils")
|
||||
package net.corda.nodeapi.internal
|
||||
|
||||
import net.corda.core.internal.declaredField
|
||||
import org.apache.activemq.artemis.utils.actors.ProcessorBase
|
||||
import java.nio.file.FileSystems
|
||||
import java.nio.file.Path
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.ThreadFactory
|
||||
import java.util.concurrent.ThreadPoolExecutor
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
/**
|
||||
* Require that the [Path] is on a default file system, and therefore is one that Artemis is willing to use.
|
||||
@ -16,3 +22,29 @@ fun requireMessageSize(messageSize: Int, limit: Int) {
|
||||
require(messageSize <= limit) { "Message exceeds maxMessageSize network parameter, maxMessageSize: [$limit], message size: [$messageSize]" }
|
||||
}
|
||||
|
||||
val Executor.rootExecutor: Executor get() {
|
||||
var executor: Executor = this
|
||||
while (executor is ProcessorBase<*>) {
|
||||
executor = executor.declaredField<Executor>("delegate").value
|
||||
}
|
||||
return executor
|
||||
}
|
||||
|
||||
fun Executor.setThreadPoolName(threadPoolName: String) {
|
||||
(rootExecutor as? ThreadPoolExecutor)?.let { it.threadFactory = NamedThreadFactory(threadPoolName, it.threadFactory) }
|
||||
}
|
||||
|
||||
private class NamedThreadFactory(poolName: String, private val delegate: ThreadFactory) : ThreadFactory {
|
||||
companion object {
|
||||
private val poolId = AtomicInteger(0)
|
||||
}
|
||||
|
||||
private val prefix = "$poolName-${poolId.incrementAndGet()}-"
|
||||
private val nextId = AtomicInteger(0)
|
||||
|
||||
override fun newThread(r: Runnable): Thread {
|
||||
val thread = delegate.newThread(r)
|
||||
thread.name = "$prefix${nextId.incrementAndGet()}"
|
||||
return thread
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,73 @@
|
||||
package net.corda.nodeapi.internal
|
||||
|
||||
import io.netty.channel.ChannelPipeline
|
||||
import io.netty.handler.logging.LogLevel
|
||||
import io.netty.handler.logging.LoggingHandler
|
||||
import org.apache.activemq.artemis.core.protocol.core.impl.ActiveMQClientProtocolManager
|
||||
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnector
|
||||
import org.apache.activemq.artemis.spi.core.remoting.BufferHandler
|
||||
import org.apache.activemq.artemis.spi.core.remoting.ClientConnectionLifeCycleListener
|
||||
import org.apache.activemq.artemis.spi.core.remoting.ClientProtocolManager
|
||||
import org.apache.activemq.artemis.spi.core.remoting.Connector
|
||||
import org.apache.activemq.artemis.spi.core.remoting.ConnectorFactory
|
||||
import org.apache.activemq.artemis.utils.ConfigurationHelper
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
|
||||
class CordaNettyConnectorFactory : ConnectorFactory {
|
||||
override fun createConnector(configuration: MutableMap<String, Any>?,
|
||||
handler: BufferHandler?,
|
||||
listener: ClientConnectionLifeCycleListener?,
|
||||
closeExecutor: Executor,
|
||||
threadPool: Executor,
|
||||
scheduledThreadPool: ScheduledExecutorService,
|
||||
protocolManager: ClientProtocolManager?): Connector {
|
||||
val threadPoolName = ConfigurationHelper.getStringProperty(ArtemisTcpTransport.THREAD_POOL_NAME_NAME, "Connector", configuration)
|
||||
setThreadPoolName(threadPool, closeExecutor, scheduledThreadPool, threadPoolName)
|
||||
val trace = ConfigurationHelper.getBooleanProperty(ArtemisTcpTransport.TRACE_NAME, false, configuration)
|
||||
return NettyConnector(
|
||||
configuration,
|
||||
handler,
|
||||
listener,
|
||||
closeExecutor,
|
||||
threadPool,
|
||||
scheduledThreadPool,
|
||||
MyClientProtocolManager("$threadPoolName-netty", trace)
|
||||
)
|
||||
}
|
||||
|
||||
override fun isReliable(): Boolean = false
|
||||
|
||||
override fun getDefaults(): Map<String?, Any?> = NettyConnector.DEFAULT_CONFIG
|
||||
|
||||
private fun setThreadPoolName(threadPool: Executor, closeExecutor: Executor, scheduledThreadPool: ScheduledExecutorService, name: String) {
|
||||
threadPool.setThreadPoolName("$name-artemis")
|
||||
// Artemis will actually wrap the same backing Executor to create multiple "OrderedExecutors". In this scenerio both the threadPool
|
||||
// and the closeExecutor are the same when it comes to the pool names. If however they are different then given them separate names.
|
||||
if (threadPool.rootExecutor !== closeExecutor.rootExecutor) {
|
||||
closeExecutor.setThreadPoolName("$name-artemis-closer")
|
||||
}
|
||||
// The scheduler is separate
|
||||
scheduledThreadPool.setThreadPoolName("$name-artemis-scheduler")
|
||||
}
|
||||
|
||||
|
||||
private class MyClientProtocolManager(private val threadPoolName: String, private val trace: Boolean) : ActiveMQClientProtocolManager() {
|
||||
override fun addChannelHandlers(pipeline: ChannelPipeline) {
|
||||
applyThreadPoolName()
|
||||
super.addChannelHandlers(pipeline)
|
||||
if (trace) {
|
||||
pipeline.addLast("logger", LoggingHandler(LogLevel.INFO))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [NettyConnector.start] does not provide a way to configure the thread pool name, so we modify the thread name accordingly.
|
||||
*/
|
||||
private fun applyThreadPoolName() {
|
||||
with(Thread.currentThread()) {
|
||||
name = name.replace("nioEventLoopGroup", threadPoolName) // pool and thread numbers are preserved
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
@file:Suppress("LongParameterList", "MagicNumber")
|
||||
|
||||
package net.corda.nodeapi.internal
|
||||
|
||||
import io.netty.util.concurrent.DefaultThreadFactory
|
||||
import net.corda.core.utilities.seconds
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.BlockingQueue
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import java.util.concurrent.ThreadPoolExecutor
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Creates a [ThreadPoolExecutor] which will use a maximum of [maxPoolSize] threads at any given time and will by default idle down to 0
|
||||
* threads.
|
||||
*/
|
||||
fun namedThreadPoolExecutor(maxPoolSize: Int,
|
||||
corePoolSize: Int = 0,
|
||||
idleKeepAlive: Duration = 30.seconds,
|
||||
workQueue: BlockingQueue<Runnable> = LinkedBlockingQueue(),
|
||||
poolName: String = "pool",
|
||||
daemonThreads: Boolean = false,
|
||||
threadPriority: Int = Thread.NORM_PRIORITY): ThreadPoolExecutor {
|
||||
return ThreadPoolExecutor(
|
||||
corePoolSize,
|
||||
maxPoolSize,
|
||||
idleKeepAlive.toNanos(),
|
||||
TimeUnit.NANOSECONDS,
|
||||
workQueue,
|
||||
DefaultThreadFactory(poolName, daemonThreads, threadPriority)
|
||||
)
|
||||
}
|
@ -5,22 +5,24 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder
|
||||
import io.netty.channel.EventLoop
|
||||
import io.netty.channel.EventLoopGroup
|
||||
import io.netty.channel.nio.NioEventLoopGroup
|
||||
import io.netty.util.concurrent.DefaultThreadFactory
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.nodeapi.internal.ArtemisConstants.MESSAGE_ID_KEY
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingClient
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_P2P_USER
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2PMessagingHeaders
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.RemoteInboxAddress.Companion.translateLocalQueueToInboxAddress
|
||||
import net.corda.nodeapi.internal.ArtemisSessionProvider
|
||||
import net.corda.nodeapi.internal.ArtemisConstants.MESSAGE_ID_KEY
|
||||
import net.corda.nodeapi.internal.config.CertificateStore
|
||||
import net.corda.nodeapi.internal.protonwrapper.messages.MessageStatus
|
||||
import net.corda.nodeapi.internal.protonwrapper.netty.AMQPClient
|
||||
import net.corda.nodeapi.internal.protonwrapper.netty.AMQPConfiguration
|
||||
import net.corda.nodeapi.internal.protonwrapper.netty.ProxyConfig
|
||||
import net.corda.nodeapi.internal.protonwrapper.netty.RevocationConfig
|
||||
import net.corda.nodeapi.internal.protonwrapper.netty.sslDelegatedTaskExecutor
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQObjectClosedException
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE
|
||||
@ -29,6 +31,8 @@ import org.apache.activemq.artemis.api.core.client.ClientMessage
|
||||
import org.apache.activemq.artemis.api.core.client.ClientSession
|
||||
import org.slf4j.MDC
|
||||
import rx.Subscription
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
import java.util.concurrent.ScheduledFuture
|
||||
@ -51,10 +55,10 @@ open class AMQPBridgeManager(keyStore: CertificateStore,
|
||||
maxMessageSize: Int,
|
||||
revocationConfig: RevocationConfig,
|
||||
enableSNI: Boolean,
|
||||
private val artemisMessageClientFactory: () -> ArtemisSessionProvider,
|
||||
private val artemisMessageClientFactory: (String) -> ArtemisSessionProvider,
|
||||
private val bridgeMetricsService: BridgeMetricsService? = null,
|
||||
trace: Boolean,
|
||||
sslHandshakeTimeout: Long?,
|
||||
sslHandshakeTimeout: Duration?,
|
||||
private val bridgeConnectionTTLSeconds: Int) : BridgeManager {
|
||||
|
||||
private val lock = ReentrantLock()
|
||||
@ -69,16 +73,18 @@ open class AMQPBridgeManager(keyStore: CertificateStore,
|
||||
override val enableSNI: Boolean,
|
||||
override val sourceX500Name: String? = null,
|
||||
override val trace: Boolean,
|
||||
private val _sslHandshakeTimeout: Long?) : AMQPConfiguration {
|
||||
override val sslHandshakeTimeout: Long
|
||||
private val _sslHandshakeTimeout: Duration?) : AMQPConfiguration {
|
||||
override val sslHandshakeTimeout: Duration
|
||||
get() = _sslHandshakeTimeout ?: super.sslHandshakeTimeout
|
||||
}
|
||||
|
||||
private val amqpConfig: AMQPConfiguration = AMQPConfigurationImpl(keyStore, trustStore, proxyConfig, maxMessageSize, revocationConfig,useOpenSSL, enableSNI, trace = trace, _sslHandshakeTimeout = sslHandshakeTimeout)
|
||||
private var sharedEventLoopGroup: EventLoopGroup? = null
|
||||
private var sslDelegatedTaskExecutor: ExecutorService? = null
|
||||
private var artemis: ArtemisSessionProvider? = null
|
||||
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
|
||||
private const val CORDA_NUM_BRIDGE_THREADS_PROP_NAME = "net.corda.nodeapi.amqpbridgemanager.NumBridgeThreads"
|
||||
|
||||
@ -95,18 +101,11 @@ open class AMQPBridgeManager(keyStore: CertificateStore,
|
||||
* however Artemis and the remote Corda instanced will deduplicate these messages.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
private class AMQPBridge(val sourceX500Name: String,
|
||||
val queueName: String,
|
||||
val targets: List<NetworkHostAndPort>,
|
||||
val legalNames: Set<CordaX500Name>,
|
||||
private val amqpConfig: AMQPConfiguration,
|
||||
sharedEventGroup: EventLoopGroup,
|
||||
private val artemis: ArtemisSessionProvider,
|
||||
private val bridgeMetricsService: BridgeMetricsService?,
|
||||
private val bridgeConnectionTTLSeconds: Int) {
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
}
|
||||
private inner class AMQPBridge(val sourceX500Name: String,
|
||||
val queueName: String,
|
||||
val targets: List<NetworkHostAndPort>,
|
||||
val allowedRemoteLegalNames: Set<CordaX500Name>,
|
||||
private val amqpConfig: AMQPConfiguration) {
|
||||
|
||||
private fun withMDC(block: () -> Unit) {
|
||||
val oldMDC = MDC.getCopyOfContextMap() ?: emptyMap<String, String>()
|
||||
@ -114,7 +113,7 @@ open class AMQPBridgeManager(keyStore: CertificateStore,
|
||||
MDC.put("queueName", queueName)
|
||||
MDC.put("source", amqpConfig.sourceX500Name)
|
||||
MDC.put("targets", targets.joinToString(separator = ";") { it.toString() })
|
||||
MDC.put("legalNames", legalNames.joinToString(separator = ";") { it.toString() })
|
||||
MDC.put("allowedRemoteLegalNames", allowedRemoteLegalNames.joinToString(separator = ";") { it.toString() })
|
||||
MDC.put("maxMessageSize", amqpConfig.maxMessageSize.toString())
|
||||
block()
|
||||
} finally {
|
||||
@ -132,13 +131,18 @@ open class AMQPBridgeManager(keyStore: CertificateStore,
|
||||
|
||||
private fun logWarnWithMDC(msg: String) = withMDC { log.warn(msg) }
|
||||
|
||||
val amqpClient = AMQPClient(targets, legalNames, amqpConfig, sharedThreadPool = sharedEventGroup)
|
||||
val amqpClient = AMQPClient(
|
||||
targets,
|
||||
allowedRemoteLegalNames,
|
||||
amqpConfig,
|
||||
AMQPClient.NettyThreading.Shared(sharedEventLoopGroup!!, sslDelegatedTaskExecutor!!)
|
||||
)
|
||||
private var session: ClientSession? = null
|
||||
private var consumer: ClientConsumer? = null
|
||||
private var connectedSubscription: Subscription? = null
|
||||
@Volatile
|
||||
private var messagesReceived: Boolean = false
|
||||
private val eventLoop: EventLoop = sharedEventGroup.next()
|
||||
private val eventLoop: EventLoop = sharedEventLoopGroup!!.next()
|
||||
private var artemisState: ArtemisState = ArtemisState.STOPPED
|
||||
set(value) {
|
||||
logDebugWithMDC { "State change $field to $value" }
|
||||
@ -150,32 +154,9 @@ open class AMQPBridgeManager(keyStore: CertificateStore,
|
||||
private var scheduledExecutorService: ScheduledExecutorService
|
||||
= Executors.newSingleThreadScheduledExecutor(ThreadFactoryBuilder().setNameFormat("bridge-connection-reset-%d").build())
|
||||
|
||||
@Suppress("ClassNaming")
|
||||
private sealed class ArtemisState {
|
||||
object STARTING : ArtemisState()
|
||||
data class STARTED(override val pending: ScheduledFuture<Unit>) : ArtemisState()
|
||||
|
||||
object CHECKING : ArtemisState()
|
||||
object RESTARTED : ArtemisState()
|
||||
object RECEIVING : ArtemisState()
|
||||
|
||||
object AMQP_STOPPED : ArtemisState()
|
||||
object AMQP_STARTING : ArtemisState()
|
||||
object AMQP_STARTED : ArtemisState()
|
||||
object AMQP_RESTARTED : ArtemisState()
|
||||
|
||||
object STOPPING : ArtemisState()
|
||||
object STOPPED : ArtemisState()
|
||||
data class STOPPED_AMQP_START_SCHEDULED(override val pending: ScheduledFuture<Unit>) : ArtemisState()
|
||||
|
||||
open val pending: ScheduledFuture<Unit>? = null
|
||||
|
||||
override fun toString(): String = javaClass.simpleName
|
||||
}
|
||||
|
||||
private fun artemis(inProgress: ArtemisState, block: (precedingState: ArtemisState) -> ArtemisState) {
|
||||
val runnable = {
|
||||
synchronized(artemis) {
|
||||
synchronized(artemis!!) {
|
||||
try {
|
||||
val precedingState = artemisState
|
||||
artemisState.pending?.cancel(false)
|
||||
@ -229,7 +210,7 @@ open class AMQPBridgeManager(keyStore: CertificateStore,
|
||||
}
|
||||
ArtemisState.STOPPING
|
||||
}
|
||||
bridgeMetricsService?.bridgeDisconnected(targets, legalNames)
|
||||
bridgeMetricsService?.bridgeDisconnected(targets, allowedRemoteLegalNames)
|
||||
connectedSubscription?.unsubscribe()
|
||||
connectedSubscription = null
|
||||
// Do this last because we already scheduled the Artemis stop, so it's okay to unsubscribe onConnected first.
|
||||
@ -241,7 +222,7 @@ open class AMQPBridgeManager(keyStore: CertificateStore,
|
||||
if (connected) {
|
||||
logInfoWithMDC("Bridge Connected")
|
||||
|
||||
bridgeMetricsService?.bridgeConnected(targets, legalNames)
|
||||
bridgeMetricsService?.bridgeConnected(targets, allowedRemoteLegalNames)
|
||||
if (bridgeConnectionTTLSeconds > 0) {
|
||||
// AMQP outbound connection will be restarted periodically with bridgeConnectionTTLSeconds interval
|
||||
amqpRestartEvent = scheduledArtemisInExecutor(bridgeConnectionTTLSeconds.toLong(), TimeUnit.SECONDS,
|
||||
@ -251,7 +232,7 @@ open class AMQPBridgeManager(keyStore: CertificateStore,
|
||||
}
|
||||
}
|
||||
artemis(ArtemisState.STARTING) {
|
||||
val startedArtemis = artemis.started
|
||||
val startedArtemis = artemis!!.started
|
||||
if (startedArtemis == null) {
|
||||
logInfoWithMDC("Bridge Connected but Artemis is disconnected")
|
||||
ArtemisState.STOPPED
|
||||
@ -284,7 +265,7 @@ open class AMQPBridgeManager(keyStore: CertificateStore,
|
||||
logInfoWithMDC("Bridge Disconnected")
|
||||
amqpRestartEvent?.cancel(false)
|
||||
if (artemisState != ArtemisState.AMQP_STARTING && artemisState != ArtemisState.STOPPED) {
|
||||
bridgeMetricsService?.bridgeDisconnected(targets, legalNames)
|
||||
bridgeMetricsService?.bridgeDisconnected(targets, allowedRemoteLegalNames)
|
||||
}
|
||||
artemis(ArtemisState.STOPPING) { precedingState: ArtemisState ->
|
||||
logInfoWithMDC("Stopping Artemis because AMQP bridge disconnected")
|
||||
@ -416,10 +397,10 @@ open class AMQPBridgeManager(keyStore: CertificateStore,
|
||||
properties[key] = value
|
||||
}
|
||||
}
|
||||
logDebugWithMDC { "Bridged Send to ${legalNames.first()} uuid: ${artemisMessage.getObjectProperty(MESSAGE_ID_KEY)}" }
|
||||
logDebugWithMDC { "Bridged Send to ${allowedRemoteLegalNames.first()} uuid: ${artemisMessage.getObjectProperty(MESSAGE_ID_KEY)}" }
|
||||
val peerInbox = translateLocalQueueToInboxAddress(queueName)
|
||||
val sendableMessage = amqpClient.createMessage(artemisMessage.payload(), peerInbox,
|
||||
legalNames.first().toString(),
|
||||
allowedRemoteLegalNames.first().toString(),
|
||||
properties)
|
||||
sendableMessage.onComplete.then {
|
||||
logDebugWithMDC { "Bridge ACK ${sendableMessage.onComplete.get()}" }
|
||||
@ -455,6 +436,29 @@ open class AMQPBridgeManager(keyStore: CertificateStore,
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("ClassNaming")
|
||||
private sealed class ArtemisState {
|
||||
object STARTING : ArtemisState()
|
||||
data class STARTED(override val pending: ScheduledFuture<Unit>) : ArtemisState()
|
||||
|
||||
object CHECKING : ArtemisState()
|
||||
object RESTARTED : ArtemisState()
|
||||
object RECEIVING : ArtemisState()
|
||||
|
||||
object AMQP_STOPPED : ArtemisState()
|
||||
object AMQP_STARTING : ArtemisState()
|
||||
object AMQP_STARTED : ArtemisState()
|
||||
object AMQP_RESTARTED : ArtemisState()
|
||||
|
||||
object STOPPING : ArtemisState()
|
||||
object STOPPED : ArtemisState()
|
||||
data class STOPPED_AMQP_START_SCHEDULED(override val pending: ScheduledFuture<Unit>) : ArtemisState()
|
||||
|
||||
open val pending: ScheduledFuture<Unit>? = null
|
||||
|
||||
override fun toString(): String = javaClass.simpleName
|
||||
}
|
||||
|
||||
override fun deployBridge(sourceX500Name: String, queueName: String, targets: List<NetworkHostAndPort>, legalNames: Set<CordaX500Name>) {
|
||||
lock.withLock {
|
||||
val bridges = queueNamesToBridgesMap.getOrPut(queueName) { mutableListOf() }
|
||||
@ -465,8 +469,7 @@ open class AMQPBridgeManager(keyStore: CertificateStore,
|
||||
}
|
||||
val newAMQPConfig = with(amqpConfig) { AMQPConfigurationImpl(keyStore, trustStore, proxyConfig, maxMessageSize,
|
||||
revocationConfig, useOpenSsl, enableSNI, sourceX500Name, trace, sslHandshakeTimeout) }
|
||||
val newBridge = AMQPBridge(sourceX500Name, queueName, targets, legalNames, newAMQPConfig, sharedEventLoopGroup!!, artemis!!,
|
||||
bridgeMetricsService, bridgeConnectionTTLSeconds)
|
||||
val newBridge = AMQPBridge(sourceX500Name, queueName, targets, legalNames, newAMQPConfig)
|
||||
bridges += newBridge
|
||||
bridgeMetricsService?.bridgeCreated(targets, legalNames)
|
||||
newBridge
|
||||
@ -484,7 +487,7 @@ open class AMQPBridgeManager(keyStore: CertificateStore,
|
||||
queueNamesToBridgesMap.remove(queueName)
|
||||
}
|
||||
bridge.stop()
|
||||
bridgeMetricsService?.bridgeDestroyed(bridge.targets, bridge.legalNames)
|
||||
bridgeMetricsService?.bridgeDestroyed(bridge.targets, bridge.allowedRemoteLegalNames)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -495,15 +498,16 @@ open class AMQPBridgeManager(keyStore: CertificateStore,
|
||||
// queueNamesToBridgesMap returns a mutable list, .toList converts it to a immutable list so it won't be changed by the [destroyBridge] method.
|
||||
val bridges = queueNamesToBridgesMap[queueName]?.toList()
|
||||
destroyBridge(queueName, bridges?.flatMap { it.targets } ?: emptyList())
|
||||
bridges?.map {
|
||||
it.sourceX500Name to BridgeEntry(it.queueName, it.targets, it.legalNames.toList(), serviceAddress = false)
|
||||
}?.toMap() ?: emptyMap()
|
||||
bridges?.associate {
|
||||
it.sourceX500Name to BridgeEntry(it.queueName, it.targets, it.allowedRemoteLegalNames.toList(), serviceAddress = false)
|
||||
} ?: emptyMap()
|
||||
}
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
sharedEventLoopGroup = NioEventLoopGroup(NUM_BRIDGE_THREADS)
|
||||
val artemis = artemisMessageClientFactory()
|
||||
sharedEventLoopGroup = NioEventLoopGroup(NUM_BRIDGE_THREADS, DefaultThreadFactory("NettyBridge", Thread.MAX_PRIORITY))
|
||||
sslDelegatedTaskExecutor = sslDelegatedTaskExecutor("NettyBridge")
|
||||
val artemis = artemisMessageClientFactory("ArtemisBridge")
|
||||
this.artemis = artemis
|
||||
artemis.start()
|
||||
}
|
||||
@ -520,6 +524,8 @@ open class AMQPBridgeManager(keyStore: CertificateStore,
|
||||
sharedEventLoopGroup = null
|
||||
queueNamesToBridgesMap.clear()
|
||||
artemis?.stop()
|
||||
sslDelegatedTaskExecutor?.shutdown()
|
||||
sslDelegatedTaskExecutor = null
|
||||
}
|
||||
}
|
||||
}
|
@ -5,16 +5,13 @@ import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingClient
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.BRIDGE_CONTROL
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.BRIDGE_NOTIFY
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX
|
||||
import net.corda.nodeapi.internal.ArtemisSessionProvider
|
||||
import net.corda.nodeapi.internal.config.CertificateStore
|
||||
import net.corda.nodeapi.internal.config.MutualSslConfiguration
|
||||
import net.corda.nodeapi.internal.crypto.x509
|
||||
import net.corda.nodeapi.internal.protonwrapper.netty.ProxyConfig
|
||||
import net.corda.nodeapi.internal.protonwrapper.netty.RevocationConfig
|
||||
@ -27,6 +24,7 @@ import org.apache.activemq.artemis.api.core.client.ClientMessage
|
||||
import org.apache.activemq.artemis.api.core.client.ClientSession
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
|
||||
class BridgeControlListener(private val keyStore: CertificateStore,
|
||||
@ -36,10 +34,10 @@ class BridgeControlListener(private val keyStore: CertificateStore,
|
||||
maxMessageSize: Int,
|
||||
revocationConfig: RevocationConfig,
|
||||
enableSNI: Boolean,
|
||||
private val artemisMessageClientFactory: () -> ArtemisSessionProvider,
|
||||
private val artemisMessageClientFactory: (String) -> ArtemisSessionProvider,
|
||||
bridgeMetricsService: BridgeMetricsService? = null,
|
||||
trace: Boolean = false,
|
||||
sslHandshakeTimeout: Long? = null,
|
||||
sslHandshakeTimeout: Duration? = null,
|
||||
bridgeConnectionTTLSeconds: Int = 0) : AutoCloseable {
|
||||
private val bridgeId: String = UUID.randomUUID().toString()
|
||||
private var bridgeControlQueue = "$BRIDGE_CONTROL.$bridgeId"
|
||||
@ -57,13 +55,6 @@ class BridgeControlListener(private val keyStore: CertificateStore,
|
||||
private var controlConsumer: ClientConsumer? = null
|
||||
private var notifyConsumer: ClientConsumer? = null
|
||||
|
||||
constructor(config: MutualSslConfiguration,
|
||||
p2pAddress: NetworkHostAndPort,
|
||||
maxMessageSize: Int,
|
||||
revocationConfig: RevocationConfig,
|
||||
enableSNI: Boolean,
|
||||
proxy: ProxyConfig? = null) : this(config.keyStore.get(), config.trustStore.get(), config.useOpenSsl, proxy, maxMessageSize, revocationConfig, enableSNI, { ArtemisMessagingClient(config, p2pAddress, maxMessageSize) })
|
||||
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
}
|
||||
@ -88,7 +79,7 @@ class BridgeControlListener(private val keyStore: CertificateStore,
|
||||
bridgeNotifyQueue = "$BRIDGE_NOTIFY.$queueDisambiguityId"
|
||||
|
||||
bridgeManager.start()
|
||||
val artemis = artemisMessageClientFactory()
|
||||
val artemis = artemisMessageClientFactory("BridgeControl")
|
||||
this.artemis = artemis
|
||||
artemis.start()
|
||||
val artemisClient = artemis.started!!
|
||||
|
@ -23,6 +23,7 @@ import org.apache.activemq.artemis.api.core.client.ClientMessage
|
||||
import org.apache.activemq.artemis.api.core.client.ClientProducer
|
||||
import org.apache.activemq.artemis.api.core.client.ClientSession
|
||||
import org.slf4j.MDC
|
||||
import java.time.Duration
|
||||
|
||||
/**
|
||||
* The LoopbackBridgeManager holds the list of independent LoopbackBridge objects that actively loopback messages to local Artemis
|
||||
@ -36,11 +37,11 @@ class LoopbackBridgeManager(keyStore: CertificateStore,
|
||||
maxMessageSize: Int,
|
||||
revocationConfig: RevocationConfig,
|
||||
enableSNI: Boolean,
|
||||
private val artemisMessageClientFactory: () -> ArtemisSessionProvider,
|
||||
private val artemisMessageClientFactory: (String) -> ArtemisSessionProvider,
|
||||
private val bridgeMetricsService: BridgeMetricsService? = null,
|
||||
private val isLocalInbox: (String) -> Boolean,
|
||||
trace: Boolean,
|
||||
sslHandshakeTimeout: Long? = null,
|
||||
sslHandshakeTimeout: Duration? = null,
|
||||
bridgeConnectionTTLSeconds: Int = 0) : AMQPBridgeManager(keyStore, trustStore, useOpenSSL, proxyConfig,
|
||||
maxMessageSize, revocationConfig, enableSNI,
|
||||
artemisMessageClientFactory, bridgeMetricsService,
|
||||
@ -203,7 +204,7 @@ class LoopbackBridgeManager(keyStore: CertificateStore,
|
||||
|
||||
override fun start() {
|
||||
super.start()
|
||||
val artemis = artemisMessageClientFactory()
|
||||
val artemis = artemisMessageClientFactory("LoopbackBridge")
|
||||
this.artemis = artemis
|
||||
artemis.start()
|
||||
}
|
||||
|
@ -1,16 +1,20 @@
|
||||
package net.corda.nodeapi.internal.config
|
||||
|
||||
import net.corda.core.utilities.seconds
|
||||
import java.time.Duration
|
||||
|
||||
interface SslConfiguration {
|
||||
|
||||
val keyStore: FileBasedCertificateStoreSupplier?
|
||||
val trustStore: FileBasedCertificateStoreSupplier?
|
||||
val useOpenSsl: Boolean
|
||||
val handshakeTimeout: Duration?
|
||||
|
||||
companion object {
|
||||
|
||||
fun mutual(keyStore: FileBasedCertificateStoreSupplier, trustStore: FileBasedCertificateStoreSupplier): MutualSslConfiguration {
|
||||
|
||||
return MutualSslOptions(keyStore, trustStore)
|
||||
fun mutual(keyStore: FileBasedCertificateStoreSupplier,
|
||||
trustStore: FileBasedCertificateStoreSupplier,
|
||||
handshakeTimeout: Duration? = null): MutualSslConfiguration {
|
||||
return MutualSslOptions(keyStore, trustStore, handshakeTimeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -21,9 +25,10 @@ interface MutualSslConfiguration : SslConfiguration {
|
||||
}
|
||||
|
||||
private class MutualSslOptions(override val keyStore: FileBasedCertificateStoreSupplier,
|
||||
override val trustStore: FileBasedCertificateStoreSupplier) : MutualSslConfiguration {
|
||||
override val trustStore: FileBasedCertificateStoreSupplier,
|
||||
override val handshakeTimeout: Duration?) : MutualSslConfiguration {
|
||||
override val useOpenSsl: Boolean = false
|
||||
}
|
||||
|
||||
const val DEFAULT_SSL_HANDSHAKE_TIMEOUT_MILLIS = 60000L // Set at least 3 times higher than sun.security.provider.certpath.URICertStore.DEFAULT_CRL_CONNECT_TIMEOUT which is 15 sec
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
val DEFAULT_SSL_HANDSHAKE_TIMEOUT: Duration = 60.seconds // Set at least 3 times higher than sun.security.provider.certpath.URICertStore.DEFAULT_CRL_CONNECT_TIMEOUT which is 15 sec
|
||||
|
@ -1,16 +1,41 @@
|
||||
@file:Suppress("MagicNumber", "TooGenericExceptionCaught")
|
||||
|
||||
package net.corda.nodeapi.internal.crypto
|
||||
|
||||
import net.corda.core.CordaOID
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.newSecureRandom
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.internal.CertRole
|
||||
import net.corda.core.internal.SignedDataWithCert
|
||||
import net.corda.core.internal.reader
|
||||
import net.corda.core.internal.signWithCert
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.internal.validate
|
||||
import net.corda.core.internal.writer
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.core.utilities.millis
|
||||
import org.bouncycastle.asn1.*
|
||||
import net.corda.core.utilities.toHex
|
||||
import net.corda.nodeapi.internal.protonwrapper.netty.distributionPointsToString
|
||||
import org.bouncycastle.asn1.ASN1EncodableVector
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier
|
||||
import org.bouncycastle.asn1.ASN1Sequence
|
||||
import org.bouncycastle.asn1.DERSequence
|
||||
import org.bouncycastle.asn1.DERUTF8String
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x500.style.BCStyle
|
||||
import org.bouncycastle.asn1.x509.*
|
||||
import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier
|
||||
import org.bouncycastle.asn1.x509.BasicConstraints
|
||||
import org.bouncycastle.asn1.x509.CRLDistPoint
|
||||
import org.bouncycastle.asn1.x509.DistributionPoint
|
||||
import org.bouncycastle.asn1.x509.DistributionPointName
|
||||
import org.bouncycastle.asn1.x509.Extension
|
||||
import org.bouncycastle.asn1.x509.GeneralName
|
||||
import org.bouncycastle.asn1.x509.GeneralNames
|
||||
import org.bouncycastle.asn1.x509.KeyPurposeId
|
||||
import org.bouncycastle.asn1.x509.KeyUsage
|
||||
import org.bouncycastle.asn1.x509.NameConstraints
|
||||
import org.bouncycastle.asn1.x509.SubjectKeyIdentifier
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
import org.bouncycastle.cert.X509v3CertificateBuilder
|
||||
import org.bouncycastle.cert.bc.BcX509ExtensionUtils
|
||||
@ -28,8 +53,13 @@ import java.nio.file.Path
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
import java.security.cert.*
|
||||
import java.security.cert.CertPath
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.TrustAnchor
|
||||
import java.security.cert.X509CRL
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
@ -355,7 +385,7 @@ object X509Utilities {
|
||||
|
||||
private fun addCrlInfo(builder: X509v3CertificateBuilder, crlDistPoint: String?, crlIssuer: X500Name?) {
|
||||
if (crlDistPoint != null) {
|
||||
val distPointName = DistributionPointName(GeneralNames(GeneralName(GeneralName.uniformResourceIdentifier, crlDistPoint)))
|
||||
val distPointName = DistributionPointName(toGeneralNames(crlDistPoint, GeneralName.uniformResourceIdentifier))
|
||||
val crlIssuerGeneralNames = crlIssuer?.let {
|
||||
GeneralNames(GeneralName(crlIssuer))
|
||||
}
|
||||
@ -368,7 +398,6 @@ object X509Utilities {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun generateCertificateSerialNumber(): BigInteger {
|
||||
val bytes = ByteArray(CERTIFICATE_SERIAL_NUMBER_LENGTH)
|
||||
newSecureRandom().nextBytes(bytes)
|
||||
@ -376,6 +405,8 @@ object X509Utilities {
|
||||
bytes[0] = bytes[0].and(0x3F).or(0x40)
|
||||
return BigInteger(bytes)
|
||||
}
|
||||
|
||||
fun toGeneralNames(string: String, tag: Int = GeneralName.directoryName): GeneralNames = GeneralNames(GeneralName(tag, string))
|
||||
}
|
||||
|
||||
// Assuming cert type to role is 1:1
|
||||
@ -408,6 +439,29 @@ fun PKCS10CertificationRequest.isSignatureValid(): Boolean {
|
||||
return this.isSignatureValid(JcaContentVerifierProviderBuilder().build(this.subjectPublicKeyInfo))
|
||||
}
|
||||
|
||||
fun X509Certificate.toSimpleString(): String {
|
||||
val bcCert = toBc()
|
||||
val keyIdentifier = try {
|
||||
SubjectKeyIdentifier.getInstance(bcCert.getExtension(Extension.subjectKeyIdentifier).parsedValue).keyIdentifier.toHex()
|
||||
} catch (e: Exception) {
|
||||
"null"
|
||||
}
|
||||
val authorityKeyIdentifier = try {
|
||||
AuthorityKeyIdentifier.getInstance(bcCert.getExtension(Extension.authorityKeyIdentifier).parsedValue).keyIdentifier.toHex()
|
||||
} catch (e: Exception) {
|
||||
"null"
|
||||
}
|
||||
val subject = bcCert.subject
|
||||
val issuer = bcCert.issuer
|
||||
val role = CertRole.extract(this)
|
||||
return "$subject[$keyIdentifier] issued by $issuer[$authorityKeyIdentifier] $role $serialNumber [${distributionPointsToString()}]"
|
||||
}
|
||||
|
||||
fun X509CRL.toSimpleString(): String {
|
||||
val revokedSerialNumbers = revokedCertificates?.map { it.serialNumber }
|
||||
return "$issuerX500Principal ${thisUpdate.toInstant()} ${nextUpdate.toInstant()} ${revokedSerialNumbers ?: "[]"}"
|
||||
}
|
||||
|
||||
/**
|
||||
* Check certificate validity or print warning if expiry is within 30 days
|
||||
*/
|
||||
@ -438,6 +492,8 @@ class X509CertificateFactory {
|
||||
fun generateCertPath(vararg certificates: X509Certificate): CertPath = generateCertPath(certificates.asList())
|
||||
|
||||
fun generateCertPath(certificates: List<X509Certificate>): CertPath = delegate.generateCertPath(certificates)
|
||||
|
||||
fun generateCRL(input: InputStream): X509CRL = delegate.generateCRL(input) as X509CRL
|
||||
}
|
||||
|
||||
enum class CertificateType(val keyUsage: KeyUsage, vararg val purposes: KeyPurposeId, val isCA: Boolean, val role: CertRole?) {
|
||||
|
@ -115,11 +115,10 @@ internal class AMQPChannelHandler(private val serverMode: Boolean,
|
||||
val transport = connection.transport as ProtonJTransport
|
||||
transport.protocolTracer = object : ProtocolTracer {
|
||||
override fun sentFrame(transportFrame: TransportFrame) {
|
||||
logInfoWithMDC { "${transportFrame.body}" }
|
||||
logInfoWithMDC { "sentFrame: ${transportFrame.body}" }
|
||||
}
|
||||
|
||||
override fun receivedFrame(transportFrame: TransportFrame) {
|
||||
logInfoWithMDC { "${transportFrame.body}" }
|
||||
logInfoWithMDC { "receivedFrame: ${transportFrame.body}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -186,7 +185,7 @@ internal class AMQPChannelHandler(private val serverMode: Boolean,
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("OverridingDeprecatedMember")
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
|
||||
logWarnWithMDC("Closing channel due to nonrecoverable exception ${cause.message}")
|
||||
if (log.isTraceEnabled) {
|
||||
@ -298,16 +297,15 @@ internal class AMQPChannelHandler(private val serverMode: Boolean,
|
||||
cause is ClosedChannelException -> logWarnWithMDC("SSL Handshake closed early.")
|
||||
cause is SslHandshakeTimeoutException -> logWarnWithMDC("SSL Handshake timed out")
|
||||
// Sadly the exception thrown by Netty wrapper requires that we check the message.
|
||||
cause is SSLException && (cause.message?.contains("close_notify") == true)
|
||||
-> logWarnWithMDC("Received close_notify during handshake")
|
||||
cause is SSLException && (cause.message?.contains("close_notify") == true) -> logWarnWithMDC("Received close_notify during handshake")
|
||||
// io.netty.handler.ssl.SslHandler.setHandshakeFailureTransportFailure()
|
||||
cause is SSLException && (cause.message?.contains("writing TLS control frames") == true) -> logWarnWithMDC(cause.message!!)
|
||||
|
||||
else -> badCert = true
|
||||
}
|
||||
logWarnWithMDC("Handshake failure: ${evt.cause().message}")
|
||||
if (log.isTraceEnabled) {
|
||||
withMDC { log.trace("Handshake failure", evt.cause()) }
|
||||
withMDC { log.trace("Handshake failure", cause) }
|
||||
} else {
|
||||
logWarnWithMDC("Handshake failure: ${cause.message}")
|
||||
}
|
||||
ctx.close()
|
||||
}
|
||||
|
@ -1,7 +1,11 @@
|
||||
package net.corda.nodeapi.internal.protonwrapper.netty
|
||||
|
||||
import io.netty.bootstrap.Bootstrap
|
||||
import io.netty.channel.*
|
||||
import io.netty.channel.Channel
|
||||
import io.netty.channel.ChannelFutureListener
|
||||
import io.netty.channel.ChannelHandler
|
||||
import io.netty.channel.ChannelInitializer
|
||||
import io.netty.channel.EventLoopGroup
|
||||
import io.netty.channel.nio.NioEventLoopGroup
|
||||
import io.netty.channel.socket.SocketChannel
|
||||
import io.netty.channel.socket.nio.NioSocketChannel
|
||||
@ -11,6 +15,7 @@ import io.netty.handler.proxy.HttpProxyHandler
|
||||
import io.netty.handler.proxy.Socks4ProxyHandler
|
||||
import io.netty.handler.proxy.Socks5ProxyHandler
|
||||
import io.netty.resolver.NoopAddressResolverGroup
|
||||
import io.netty.util.concurrent.DefaultThreadFactory
|
||||
import io.netty.util.internal.logging.InternalLoggerFactory
|
||||
import io.netty.util.internal.logging.Slf4JLoggerFactory
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
@ -22,14 +27,16 @@ import net.corda.nodeapi.internal.protonwrapper.messages.SendableMessage
|
||||
import net.corda.nodeapi.internal.protonwrapper.messages.impl.SendableMessageImpl
|
||||
import net.corda.nodeapi.internal.protonwrapper.netty.AMQPChannelHandler.Companion.PROXY_LOGGER_NAME
|
||||
import net.corda.nodeapi.internal.requireMessageSize
|
||||
import net.corda.nodeapi.internal.revocation.CertDistPointCrlSource
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import java.lang.Long.min
|
||||
import java.net.InetSocketAddress
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.ThreadPoolExecutor
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import javax.net.ssl.KeyManagerFactory
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
enum class ProxyVersion {
|
||||
@ -53,10 +60,11 @@ data class ProxyConfig(val version: ProxyVersion, val proxyAddress: NetworkHostA
|
||||
* otherwise it creates a self-contained Netty thraed pool and socket objects.
|
||||
* Once connected it can accept application packets to send via the AMQP protocol.
|
||||
*/
|
||||
class AMQPClient(val targets: List<NetworkHostAndPort>,
|
||||
class AMQPClient(private val targets: List<NetworkHostAndPort>,
|
||||
val allowedRemoteLegalNames: Set<CordaX500Name>,
|
||||
private val configuration: AMQPConfiguration,
|
||||
private val sharedThreadPool: EventLoopGroup? = null) : AutoCloseable {
|
||||
private val nettyThreading: NettyThreading = NettyThreading.NonShared("AMQPClient"),
|
||||
private val distPointCrlSource: CertDistPointCrlSource = CertDistPointCrlSource.SINGLETON) : AutoCloseable {
|
||||
companion object {
|
||||
init {
|
||||
InternalLoggerFactory.setDefaultFactory(Slf4JLoggerFactory.INSTANCE)
|
||||
@ -75,7 +83,6 @@ class AMQPClient(val targets: List<NetworkHostAndPort>,
|
||||
private val lock = ReentrantLock()
|
||||
@Volatile
|
||||
private var started: Boolean = false
|
||||
private var workerGroup: EventLoopGroup? = null
|
||||
@Volatile
|
||||
private var clientChannel: Channel? = null
|
||||
// Offset into the list of targets, so that we can implement round-robin reconnect logic.
|
||||
@ -109,24 +116,22 @@ class AMQPClient(val targets: List<NetworkHostAndPort>,
|
||||
retryInterval = min(MAX_RETRY_INTERVAL, retryInterval * BACKOFF_MULTIPLIER)
|
||||
}
|
||||
|
||||
private val connectListener = object : ChannelFutureListener {
|
||||
override fun operationComplete(future: ChannelFuture) {
|
||||
amqpActive = false
|
||||
if (!future.isSuccess) {
|
||||
log.info("Failed to connect to $currentTarget", future.cause())
|
||||
private val connectListener = ChannelFutureListener { future ->
|
||||
amqpActive = false
|
||||
if (!future.isSuccess) {
|
||||
log.info("Failed to connect to $currentTarget", future.cause())
|
||||
|
||||
if (started) {
|
||||
workerGroup?.schedule({
|
||||
nextTarget()
|
||||
restart()
|
||||
}, retryInterval, TimeUnit.MILLISECONDS)
|
||||
}
|
||||
} else {
|
||||
// Connection established successfully
|
||||
clientChannel = future.channel()
|
||||
clientChannel?.closeFuture()?.addListener(closeListener)
|
||||
log.info("Connected to $currentTarget, Local address: $localAddressString")
|
||||
if (started) {
|
||||
nettyThreading.eventLoopGroup.schedule({
|
||||
nextTarget()
|
||||
restart()
|
||||
}, retryInterval, TimeUnit.MILLISECONDS)
|
||||
}
|
||||
} else {
|
||||
// Connection established successfully
|
||||
clientChannel = future.channel()
|
||||
clientChannel?.closeFuture()?.addListener(closeListener)
|
||||
log.info("Connected to $currentTarget, Local address: $localAddressString")
|
||||
}
|
||||
}
|
||||
|
||||
@ -136,7 +141,7 @@ class AMQPClient(val targets: List<NetworkHostAndPort>,
|
||||
clientChannel = null
|
||||
if (started && !amqpActive) {
|
||||
log.debug { "Scheduling restart of $currentTarget (AMQP inactive)" }
|
||||
workerGroup?.schedule({
|
||||
nettyThreading.eventLoopGroup.schedule({
|
||||
nextTarget()
|
||||
restart()
|
||||
}, retryInterval, TimeUnit.MILLISECONDS)
|
||||
@ -144,17 +149,16 @@ class AMQPClient(val targets: List<NetworkHostAndPort>,
|
||||
}
|
||||
|
||||
private class ClientChannelInitializer(val parent: AMQPClient) : ChannelInitializer<SocketChannel>() {
|
||||
private val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
|
||||
private val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
private val keyManagerFactory = keyManagerFactory(parent.configuration.keyStore)
|
||||
private val trustManagerFactory = trustManagerFactoryWithRevocation(
|
||||
parent.configuration.trustStore,
|
||||
parent.configuration.revocationConfig,
|
||||
parent.distPointCrlSource
|
||||
)
|
||||
private val conf = parent.configuration
|
||||
@Volatile
|
||||
private lateinit var amqpChannelHandler: AMQPChannelHandler
|
||||
|
||||
init {
|
||||
keyManagerFactory.init(conf.keyStore)
|
||||
trustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(conf.trustStore, conf.revocationConfig))
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
override fun initChannel(ch: SocketChannel) {
|
||||
val pipeline = ch.pipeline()
|
||||
@ -194,14 +198,28 @@ class AMQPClient(val targets: List<NetworkHostAndPort>,
|
||||
val wrappedKeyManagerFactory = CertHoldingKeyManagerFactoryWrapper(keyManagerFactory, parent.configuration)
|
||||
val target = parent.currentTarget
|
||||
val handler = if (parent.configuration.useOpenSsl) {
|
||||
createClientOpenSslHandler(target, parent.allowedRemoteLegalNames, wrappedKeyManagerFactory, trustManagerFactory, ch.alloc())
|
||||
createClientOpenSslHandler(
|
||||
target,
|
||||
parent.allowedRemoteLegalNames,
|
||||
wrappedKeyManagerFactory,
|
||||
trustManagerFactory,
|
||||
ch.alloc(),
|
||||
parent.nettyThreading.sslDelegatedTaskExecutor
|
||||
)
|
||||
} else {
|
||||
createClientSslHelper(target, parent.allowedRemoteLegalNames, wrappedKeyManagerFactory, trustManagerFactory)
|
||||
createClientSslHandler(
|
||||
target,
|
||||
parent.allowedRemoteLegalNames,
|
||||
wrappedKeyManagerFactory,
|
||||
trustManagerFactory,
|
||||
parent.nettyThreading.sslDelegatedTaskExecutor
|
||||
)
|
||||
}
|
||||
handler.handshakeTimeoutMillis = conf.sslHandshakeTimeout
|
||||
handler.handshakeTimeoutMillis = conf.sslHandshakeTimeout.toMillis()
|
||||
pipeline.addLast("sslHandler", handler)
|
||||
if (conf.trace) pipeline.addLast("logger", LoggingHandler(LogLevel.INFO))
|
||||
amqpChannelHandler = AMQPChannelHandler(false,
|
||||
amqpChannelHandler = AMQPChannelHandler(
|
||||
false,
|
||||
parent.allowedRemoteLegalNames,
|
||||
// Single entry, key can be anything.
|
||||
mapOf(DEFAULT to wrappedKeyManagerFactory),
|
||||
@ -209,37 +227,41 @@ class AMQPClient(val targets: List<NetworkHostAndPort>,
|
||||
conf.password,
|
||||
conf.trace,
|
||||
false,
|
||||
onOpen = { _, change ->
|
||||
parent.run {
|
||||
amqpActive = true
|
||||
retryInterval = MIN_RETRY_INTERVAL // reset to fast reconnect if we connect properly
|
||||
_onConnection.onNext(change)
|
||||
}
|
||||
},
|
||||
onClose = { _, change ->
|
||||
if (parent.amqpChannelHandler == amqpChannelHandler) {
|
||||
parent.run {
|
||||
_onConnection.onNext(change)
|
||||
if (change.badCert) {
|
||||
log.error("Blocking future connection attempts to $target due to bad certificate on endpoint")
|
||||
badCertTargets += target
|
||||
}
|
||||
|
||||
if (started && amqpActive) {
|
||||
log.debug { "Scheduling restart of $currentTarget (AMQP active)" }
|
||||
workerGroup?.schedule({
|
||||
nextTarget()
|
||||
restart()
|
||||
}, retryInterval, TimeUnit.MILLISECONDS)
|
||||
}
|
||||
amqpActive = false
|
||||
}
|
||||
}
|
||||
},
|
||||
onReceive = { rcv -> parent._onReceive.onNext(rcv) })
|
||||
onOpen = { _, change -> onChannelOpen(change) },
|
||||
onClose = { _, change -> onChannelClose(change, target) },
|
||||
onReceive = parent._onReceive::onNext
|
||||
)
|
||||
parent.amqpChannelHandler = amqpChannelHandler
|
||||
pipeline.addLast(amqpChannelHandler)
|
||||
}
|
||||
|
||||
private fun onChannelOpen(change: ConnectionChange) {
|
||||
parent.run {
|
||||
amqpActive = true
|
||||
retryInterval = MIN_RETRY_INTERVAL // reset to fast reconnect if we connect properly
|
||||
_onConnection.onNext(change)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onChannelClose(change: ConnectionChange, target: NetworkHostAndPort) {
|
||||
if (parent.amqpChannelHandler != amqpChannelHandler) return
|
||||
parent.run {
|
||||
_onConnection.onNext(change)
|
||||
if (change.badCert) {
|
||||
log.error("Blocking future connection attempts to $target due to bad certificate on endpoint")
|
||||
badCertTargets += target
|
||||
}
|
||||
|
||||
if (started && amqpActive) {
|
||||
log.debug { "Scheduling restart of $currentTarget (AMQP active)" }
|
||||
nettyThreading.eventLoopGroup.schedule({
|
||||
nextTarget()
|
||||
restart()
|
||||
}, retryInterval, TimeUnit.MILLISECONDS)
|
||||
}
|
||||
amqpActive = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun start() {
|
||||
@ -249,7 +271,7 @@ class AMQPClient(val targets: List<NetworkHostAndPort>,
|
||||
return
|
||||
}
|
||||
log.info("Connect to: $currentTarget")
|
||||
workerGroup = sharedThreadPool ?: NioEventLoopGroup(NUM_CLIENT_THREADS)
|
||||
(nettyThreading as? NettyThreading.NonShared)?.start()
|
||||
started = true
|
||||
restart()
|
||||
}
|
||||
@ -261,7 +283,7 @@ class AMQPClient(val targets: List<NetworkHostAndPort>,
|
||||
}
|
||||
val bootstrap = Bootstrap()
|
||||
// TODO Needs more configuration control when we profile. e.g. to use EPOLL on Linux
|
||||
bootstrap.group(workerGroup).channel(NioSocketChannel::class.java).handler(ClientChannelInitializer(this))
|
||||
bootstrap.group(nettyThreading.eventLoopGroup).channel(NioSocketChannel::class.java).handler(ClientChannelInitializer(this))
|
||||
// Delegate DNS Resolution to the proxy side, if we are using proxy.
|
||||
if (configuration.proxyConfig != null) {
|
||||
bootstrap.resolver(NoopAddressResolverGroup.INSTANCE)
|
||||
@ -275,14 +297,12 @@ class AMQPClient(val targets: List<NetworkHostAndPort>,
|
||||
lock.withLock {
|
||||
log.info("Stopping connection to: $currentTarget, Local address: $localAddressString")
|
||||
started = false
|
||||
if (sharedThreadPool == null) {
|
||||
workerGroup?.shutdownGracefully()
|
||||
workerGroup?.terminationFuture()?.sync()
|
||||
if (nettyThreading is NettyThreading.NonShared) {
|
||||
nettyThreading.stop()
|
||||
} else {
|
||||
clientChannel?.close()?.sync()
|
||||
}
|
||||
clientChannel = null
|
||||
workerGroup = null
|
||||
log.info("Stopped connection to $currentTarget")
|
||||
}
|
||||
}
|
||||
@ -323,4 +343,36 @@ class AMQPClient(val targets: List<NetworkHostAndPort>,
|
||||
private val _onConnection = PublishSubject.create<ConnectionChange>().toSerialized()
|
||||
val onConnection: Observable<ConnectionChange>
|
||||
get() = _onConnection
|
||||
}
|
||||
|
||||
|
||||
sealed class NettyThreading {
|
||||
abstract val eventLoopGroup: EventLoopGroup
|
||||
abstract val sslDelegatedTaskExecutor: Executor
|
||||
|
||||
class Shared(override val eventLoopGroup: EventLoopGroup,
|
||||
override val sslDelegatedTaskExecutor: ExecutorService = sslDelegatedTaskExecutor("AMQPClient")) : NettyThreading()
|
||||
|
||||
class NonShared(val threadPoolName: String) : NettyThreading() {
|
||||
private var _eventLoopGroup: NioEventLoopGroup? = null
|
||||
override val eventLoopGroup: EventLoopGroup get() = checkNotNull(_eventLoopGroup)
|
||||
|
||||
private var _sslDelegatedTaskExecutor: ThreadPoolExecutor? = null
|
||||
override val sslDelegatedTaskExecutor: ExecutorService get() = checkNotNull(_sslDelegatedTaskExecutor)
|
||||
|
||||
fun start() {
|
||||
check(_eventLoopGroup == null)
|
||||
check(_sslDelegatedTaskExecutor == null)
|
||||
_eventLoopGroup = NioEventLoopGroup(NUM_CLIENT_THREADS, DefaultThreadFactory(threadPoolName, Thread.MAX_PRIORITY))
|
||||
_sslDelegatedTaskExecutor = sslDelegatedTaskExecutor(threadPoolName)
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
eventLoopGroup.shutdownGracefully()
|
||||
eventLoopGroup.terminationFuture().sync()
|
||||
sslDelegatedTaskExecutor.shutdown()
|
||||
_eventLoopGroup = null
|
||||
_sslDelegatedTaskExecutor = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,8 @@ package net.corda.nodeapi.internal.protonwrapper.netty
|
||||
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent
|
||||
import net.corda.nodeapi.internal.config.CertificateStore
|
||||
import net.corda.nodeapi.internal.config.DEFAULT_SSL_HANDSHAKE_TIMEOUT_MILLIS
|
||||
import net.corda.nodeapi.internal.config.DEFAULT_SSL_HANDSHAKE_TIMEOUT
|
||||
import java.time.Duration
|
||||
|
||||
interface AMQPConfiguration {
|
||||
/**
|
||||
@ -67,8 +68,8 @@ interface AMQPConfiguration {
|
||||
get() = false
|
||||
|
||||
@JvmDefault
|
||||
val sslHandshakeTimeout: Long
|
||||
get() = DEFAULT_SSL_HANDSHAKE_TIMEOUT_MILLIS // Aligned with sun.security.provider.certpath.URICertStore.DEFAULT_CRL_CONNECT_TIMEOUT
|
||||
val sslHandshakeTimeout: Duration
|
||||
get() = DEFAULT_SSL_HANDSHAKE_TIMEOUT // Aligned with sun.security.provider.certpath.URICertStore.DEFAULT_CRL_CONNECT_TIMEOUT
|
||||
|
||||
/**
|
||||
* An optional Health Check Phrase which if passed through the channel will cause AMQP Server to echo it back instead of doing normal pipeline processing
|
||||
|
@ -11,6 +11,7 @@ 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.util.concurrent.DefaultThreadFactory
|
||||
import io.netty.util.internal.logging.InternalLoggerFactory
|
||||
import io.netty.util.internal.logging.Slf4JLoggerFactory
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
@ -20,15 +21,15 @@ import net.corda.nodeapi.internal.protonwrapper.messages.ReceivedMessage
|
||||
import net.corda.nodeapi.internal.protonwrapper.messages.SendableMessage
|
||||
import net.corda.nodeapi.internal.protonwrapper.messages.impl.SendableMessageImpl
|
||||
import net.corda.nodeapi.internal.requireMessageSize
|
||||
import net.corda.nodeapi.internal.revocation.CertDistPointCrlSource
|
||||
import org.apache.qpid.proton.engine.Delivery
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import java.net.BindException
|
||||
import java.net.InetSocketAddress
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import javax.net.ssl.KeyManagerFactory
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
/**
|
||||
@ -36,37 +37,35 @@ import kotlin.concurrent.withLock
|
||||
*/
|
||||
class AMQPServer(val hostName: String,
|
||||
val port: Int,
|
||||
private val configuration: AMQPConfiguration) : AutoCloseable {
|
||||
|
||||
private val configuration: AMQPConfiguration,
|
||||
private val threadPoolName: String = "AMQPServer",
|
||||
private val distPointCrlSource: CertDistPointCrlSource = CertDistPointCrlSource.SINGLETON,
|
||||
private val remotingThreads: Int? = null) : AutoCloseable {
|
||||
companion object {
|
||||
init {
|
||||
InternalLoggerFactory.setDefaultFactory(Slf4JLoggerFactory.INSTANCE)
|
||||
}
|
||||
|
||||
private const val CORDA_AMQP_NUM_SERVER_THREAD_PROP_NAME = "net.corda.nodeapi.amqpserver.NumServerThreads"
|
||||
|
||||
private val log = contextLogger()
|
||||
private val NUM_SERVER_THREADS = Integer.getInteger(CORDA_AMQP_NUM_SERVER_THREAD_PROP_NAME, 4)
|
||||
private val DEFAULT_REMOTING_THREADS = Integer.getInteger("net.corda.nodeapi.amqpserver.NumServerThreads", 4)
|
||||
}
|
||||
|
||||
private val lock = ReentrantLock()
|
||||
@Volatile
|
||||
private var stopping: Boolean = false
|
||||
private var bossGroup: EventLoopGroup? = null
|
||||
private var workerGroup: EventLoopGroup? = null
|
||||
private var serverChannel: Channel? = null
|
||||
private var sslDelegatedTaskExecutor: ExecutorService? = null
|
||||
private val clientChannels = ConcurrentHashMap<InetSocketAddress, SocketChannel>()
|
||||
|
||||
private class ServerChannelInitializer(val parent: AMQPServer) : ChannelInitializer<SocketChannel>() {
|
||||
private val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
|
||||
private val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
private val keyManagerFactory = keyManagerFactory(parent.configuration.keyStore)
|
||||
private val trustManagerFactory = trustManagerFactoryWithRevocation(
|
||||
parent.configuration.trustStore,
|
||||
parent.configuration.revocationConfig,
|
||||
parent.distPointCrlSource
|
||||
)
|
||||
private val conf = parent.configuration
|
||||
|
||||
init {
|
||||
keyManagerFactory.init(conf.keyStore.value.internal, conf.keyStore.entryPassword.toCharArray())
|
||||
trustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(conf.trustStore, conf.revocationConfig))
|
||||
}
|
||||
|
||||
override fun initChannel(ch: SocketChannel) {
|
||||
val amqpConfiguration = parent.configuration
|
||||
val pipeline = ch.pipeline()
|
||||
@ -75,7 +74,8 @@ class AMQPServer(val hostName: String,
|
||||
pipeline.addLast("sslHandler", sslHandler)
|
||||
if (conf.trace) pipeline.addLast("logger", LoggingHandler(LogLevel.INFO))
|
||||
val suppressLogs = ch.remoteAddress()?.hostString in amqpConfiguration.silencedIPs
|
||||
pipeline.addLast(AMQPChannelHandler(true,
|
||||
pipeline.addLast(AMQPChannelHandler(
|
||||
true,
|
||||
null,
|
||||
// Passing a mapping of legal names to key managers to be able to pick the correct one after
|
||||
// SNI completion event is fired up.
|
||||
@ -84,36 +84,42 @@ class AMQPServer(val hostName: String,
|
||||
conf.password,
|
||||
conf.trace,
|
||||
suppressLogs,
|
||||
onOpen = { channel, change ->
|
||||
parent.run {
|
||||
clientChannels[channel.remoteAddress()] = channel
|
||||
_onConnection.onNext(change)
|
||||
}
|
||||
},
|
||||
onClose = { channel, change ->
|
||||
parent.run {
|
||||
val remoteAddress = channel.remoteAddress()
|
||||
clientChannels.remove(remoteAddress)
|
||||
_onConnection.onNext(change)
|
||||
}
|
||||
},
|
||||
onReceive = { rcv -> parent._onReceive.onNext(rcv) }))
|
||||
onOpen = ::onChannelOpen,
|
||||
onClose = ::onChannelClose,
|
||||
onReceive = parent._onReceive::onNext
|
||||
))
|
||||
}
|
||||
|
||||
private fun onChannelOpen(channel: SocketChannel, change: ConnectionChange) {
|
||||
parent.run {
|
||||
clientChannels[channel.remoteAddress()] = channel
|
||||
_onConnection.onNext(change)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onChannelClose(channel: SocketChannel, change: ConnectionChange) {
|
||||
parent.run {
|
||||
val remoteAddress = channel.remoteAddress()
|
||||
clientChannels.remove(remoteAddress)
|
||||
_onConnection.onNext(change)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSSLHandler(amqpConfig: AMQPConfiguration, ch: SocketChannel): Pair<ChannelHandler, Map<String, CertHoldingKeyManagerFactoryWrapper>> {
|
||||
return if (amqpConfig.useOpenSsl && amqpConfig.enableSNI && amqpConfig.keyStore.aliases().size > 1) {
|
||||
val keyManagerFactoriesMap = splitKeystore(amqpConfig)
|
||||
// SNI matching needed only when multiple nodes exist behind the server.
|
||||
Pair(createServerSNIOpenSslHandler(keyManagerFactoriesMap, trustManagerFactory), keyManagerFactoriesMap)
|
||||
Pair(createServerSNIOpenSniHandler(keyManagerFactoriesMap, trustManagerFactory), keyManagerFactoriesMap)
|
||||
} else {
|
||||
val keyManagerFactory = CertHoldingKeyManagerFactoryWrapper(keyManagerFactory, amqpConfig)
|
||||
val delegatedTaskExecutor = checkNotNull(parent.sslDelegatedTaskExecutor)
|
||||
val handler = if (amqpConfig.useOpenSsl) {
|
||||
createServerOpenSslHandler(keyManagerFactory, trustManagerFactory, ch.alloc())
|
||||
createServerOpenSslHandler(keyManagerFactory, trustManagerFactory, ch.alloc(), delegatedTaskExecutor)
|
||||
} else {
|
||||
// For javaSSL, SNI matching is handled at key manager level.
|
||||
createServerSslHandler(amqpConfig.keyStore, keyManagerFactory, trustManagerFactory)
|
||||
createServerSslHandler(amqpConfig.keyStore, keyManagerFactory, trustManagerFactory, delegatedTaskExecutor)
|
||||
}
|
||||
handler.handshakeTimeoutMillis = amqpConfig.sslHandshakeTimeout
|
||||
handler.handshakeTimeoutMillis = amqpConfig.sslHandshakeTimeout.toMillis()
|
||||
Pair(handler, mapOf(DEFAULT to keyManagerFactory))
|
||||
}
|
||||
}
|
||||
@ -123,8 +129,13 @@ class AMQPServer(val hostName: String,
|
||||
lock.withLock {
|
||||
stop()
|
||||
|
||||
bossGroup = NioEventLoopGroup(1)
|
||||
workerGroup = NioEventLoopGroup(NUM_SERVER_THREADS)
|
||||
sslDelegatedTaskExecutor = sslDelegatedTaskExecutor(threadPoolName)
|
||||
|
||||
bossGroup = NioEventLoopGroup(1, DefaultThreadFactory("$threadPoolName-boss", Thread.MAX_PRIORITY))
|
||||
workerGroup = NioEventLoopGroup(
|
||||
remotingThreads ?: DEFAULT_REMOTING_THREADS,
|
||||
DefaultThreadFactory("$threadPoolName-worker", Thread.MAX_PRIORITY)
|
||||
)
|
||||
|
||||
val server = ServerBootstrap()
|
||||
// TODO Needs more configuration control when we profile. e.g. to use EPOLL on Linux
|
||||
@ -145,22 +156,19 @@ class AMQPServer(val hostName: String,
|
||||
|
||||
fun stop() {
|
||||
lock.withLock {
|
||||
try {
|
||||
stopping = true
|
||||
serverChannel?.apply { close() }
|
||||
serverChannel = null
|
||||
serverChannel?.close()
|
||||
serverChannel = null
|
||||
|
||||
workerGroup?.shutdownGracefully()
|
||||
workerGroup?.terminationFuture()?.sync()
|
||||
workerGroup?.shutdownGracefully()
|
||||
workerGroup?.terminationFuture()?.sync()
|
||||
workerGroup = null
|
||||
|
||||
bossGroup?.shutdownGracefully()
|
||||
bossGroup?.terminationFuture()?.sync()
|
||||
bossGroup?.shutdownGracefully()
|
||||
bossGroup?.terminationFuture()?.sync()
|
||||
bossGroup = null
|
||||
|
||||
workerGroup = null
|
||||
bossGroup = null
|
||||
} finally {
|
||||
stopping = false
|
||||
}
|
||||
sslDelegatedTaskExecutor?.shutdown()
|
||||
sslDelegatedTaskExecutor = null
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,7 @@ object AllowAllRevocationChecker : PKIXRevocationChecker() {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(AllowAllRevocationChecker::class.java)
|
||||
|
||||
override fun check(cert: Certificate?, unresolvedCritExts: MutableCollection<String>?) {
|
||||
override fun check(cert: Certificate, unresolvedCritExts: Collection<String>) {
|
||||
logger.debug {"Passing certificate check for: $cert"}
|
||||
// Nothing to do
|
||||
}
|
||||
@ -20,7 +20,7 @@ object AllowAllRevocationChecker : PKIXRevocationChecker() {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun getSupportedExtensions(): MutableSet<String>? {
|
||||
override fun getSupportedExtensions(): Set<String>? {
|
||||
return null
|
||||
}
|
||||
|
||||
@ -28,7 +28,9 @@ object AllowAllRevocationChecker : PKIXRevocationChecker() {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
override fun getSoftFailExceptions(): MutableList<CertPathValidatorException> {
|
||||
return LinkedList()
|
||||
override fun getSoftFailExceptions(): List<CertPathValidatorException> {
|
||||
return Collections.emptyList()
|
||||
}
|
||||
|
||||
override fun clone(): AllowAllRevocationChecker = this
|
||||
}
|
@ -3,10 +3,11 @@ package net.corda.nodeapi.internal.protonwrapper.netty
|
||||
import java.security.cert.X509CRL
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
interface ExternalCrlSource {
|
||||
@FunctionalInterface
|
||||
interface CrlSource {
|
||||
|
||||
/**
|
||||
* Given certificate provides a set of CRLs, potentially performing remote communication.
|
||||
*/
|
||||
fun fetch(certificate: X509Certificate) : Set<X509CRL>
|
||||
}
|
||||
fun fetch(certificate: X509Certificate): Set<X509CRL>
|
||||
}
|
@ -26,7 +26,7 @@ interface RevocationConfig {
|
||||
|
||||
/**
|
||||
* CRLs are obtained from external source
|
||||
* @see ExternalCrlSource
|
||||
* @see CrlSource
|
||||
*/
|
||||
EXTERNAL_SOURCE,
|
||||
|
||||
@ -39,14 +39,9 @@ interface RevocationConfig {
|
||||
val mode: Mode
|
||||
|
||||
/**
|
||||
* Optional `ExternalCrlSource` which only makes sense with `mode` = `EXTERNAL_SOURCE`
|
||||
* Optional [CrlSource] which only makes sense with `mode` = `EXTERNAL_SOURCE`
|
||||
*/
|
||||
val externalCrlSource: ExternalCrlSource?
|
||||
|
||||
/**
|
||||
* Creates a copy of `RevocationConfig` with ExternalCrlSource enriched
|
||||
*/
|
||||
fun enrichExternalCrlSource(sourceFunc: (() -> ExternalCrlSource)?): RevocationConfig
|
||||
val externalCrlSource: CrlSource?
|
||||
}
|
||||
|
||||
/**
|
||||
@ -54,16 +49,7 @@ interface RevocationConfig {
|
||||
*/
|
||||
fun Boolean.toRevocationConfig() = if(this) RevocationConfigImpl(RevocationConfig.Mode.SOFT_FAIL) else RevocationConfigImpl(RevocationConfig.Mode.HARD_FAIL)
|
||||
|
||||
data class RevocationConfigImpl(override val mode: RevocationConfig.Mode, override val externalCrlSource: ExternalCrlSource? = null) : RevocationConfig {
|
||||
override fun enrichExternalCrlSource(sourceFunc: (() -> ExternalCrlSource)?): RevocationConfig {
|
||||
return if(mode != RevocationConfig.Mode.EXTERNAL_SOURCE) {
|
||||
this
|
||||
} else {
|
||||
assert(sourceFunc != null) { "There should be a way to obtain ExternalCrlSource" }
|
||||
copy(externalCrlSource = sourceFunc!!())
|
||||
}
|
||||
}
|
||||
}
|
||||
data class RevocationConfigImpl(override val mode: RevocationConfig.Mode, override val externalCrlSource: CrlSource? = null) : RevocationConfig
|
||||
|
||||
class RevocationConfigParser : ConfigParser<RevocationConfig> {
|
||||
override fun parse(config: Config): RevocationConfig {
|
||||
@ -80,4 +66,4 @@ class RevocationConfigParser : ConfigParser<RevocationConfig> {
|
||||
else -> throw IllegalArgumentException("Unsupported mode : '$mode'")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
@file:Suppress("ComplexMethod", "LongParameterList")
|
||||
|
||||
package net.corda.nodeapi.internal.protonwrapper.netty
|
||||
|
||||
import io.netty.buffer.ByteBufAllocator
|
||||
@ -13,31 +15,41 @@ import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.toHex
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.nodeapi.internal.ArtemisTcpTransport
|
||||
import net.corda.nodeapi.internal.config.CertificateStore
|
||||
import net.corda.nodeapi.internal.crypto.toBc
|
||||
import net.corda.nodeapi.internal.crypto.toSimpleString
|
||||
import net.corda.nodeapi.internal.crypto.x509
|
||||
import net.corda.nodeapi.internal.protonwrapper.netty.revocation.ExternalSourceRevocationChecker
|
||||
import net.corda.nodeapi.internal.namedThreadPoolExecutor
|
||||
import net.corda.nodeapi.internal.revocation.CordaRevocationChecker
|
||||
import org.bouncycastle.asn1.ASN1InputStream
|
||||
import org.bouncycastle.asn1.ASN1Primitive
|
||||
import org.bouncycastle.asn1.DERIA5String
|
||||
import org.bouncycastle.asn1.DEROctetString
|
||||
import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x509.CRLDistPoint
|
||||
import org.bouncycastle.asn1.x509.DistributionPointName
|
||||
import org.bouncycastle.asn1.x509.Extension
|
||||
import org.bouncycastle.asn1.x509.GeneralName
|
||||
import org.bouncycastle.asn1.x509.GeneralNames
|
||||
import org.bouncycastle.asn1.x509.SubjectKeyIdentifier
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.net.Socket
|
||||
import java.net.URI
|
||||
import java.security.KeyStore
|
||||
import java.security.cert.*
|
||||
import java.util.*
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.PKIXBuilderParameters
|
||||
import java.security.cert.X509CertSelector
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.concurrent.Executor
|
||||
import javax.net.ssl.*
|
||||
import kotlin.system.measureTimeMillis
|
||||
import java.util.concurrent.ThreadPoolExecutor
|
||||
import javax.net.ssl.CertPathTrustManagerParameters
|
||||
import javax.net.ssl.KeyManagerFactory
|
||||
import javax.net.ssl.SNIHostName
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.SSLEngine
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import javax.net.ssl.X509ExtendedTrustManager
|
||||
import javax.security.auth.x500.X500Principal
|
||||
|
||||
private const val HOSTNAME_FORMAT = "%s.corda.net"
|
||||
internal const val DEFAULT = "default"
|
||||
@ -46,65 +58,73 @@ internal const val DP_DEFAULT_ANSWER = "NO CRLDP ext"
|
||||
|
||||
internal val logger = LoggerFactory.getLogger("net.corda.nodeapi.internal.protonwrapper.netty.SSLHelper")
|
||||
|
||||
fun X509Certificate.distributionPoints() : Set<String>? {
|
||||
logger.debug("Checking CRLDPs for $subjectX500Principal")
|
||||
/**
|
||||
* Returns all the CRL distribution points in the certificate as [URI]s along with the CRL issuer names, if any.
|
||||
*/
|
||||
fun X509Certificate.distributionPoints(): Map<URI, List<X500Principal>?> {
|
||||
logger.debug { "Checking CRLDPs for $subjectX500Principal" }
|
||||
|
||||
val crldpExtBytes = getExtensionValue(Extension.cRLDistributionPoints.id)
|
||||
if (crldpExtBytes == null) {
|
||||
logger.debug(DP_DEFAULT_ANSWER)
|
||||
return emptySet()
|
||||
return emptyMap()
|
||||
}
|
||||
|
||||
val derObjCrlDP = ASN1InputStream(ByteArrayInputStream(crldpExtBytes)).readObject()
|
||||
val derObjCrlDP = crldpExtBytes.toAsn1Object()
|
||||
val dosCrlDP = derObjCrlDP as? DEROctetString
|
||||
if (dosCrlDP == null) {
|
||||
logger.error("Expected to have DEROctetString, actual type: ${derObjCrlDP.javaClass}")
|
||||
return emptySet()
|
||||
return emptyMap()
|
||||
}
|
||||
val crldpExtOctetsBytes = dosCrlDP.octets
|
||||
val dpObj = ASN1InputStream(ByteArrayInputStream(crldpExtOctetsBytes)).readObject()
|
||||
val distPoint = CRLDistPoint.getInstance(dpObj)
|
||||
if (distPoint == null) {
|
||||
val dpObj = dosCrlDP.octets.toAsn1Object()
|
||||
val crlDistPoint = CRLDistPoint.getInstance(dpObj)
|
||||
if (crlDistPoint == null) {
|
||||
logger.error("Could not instantiate CRLDistPoint, from: $dpObj")
|
||||
return emptySet()
|
||||
return emptyMap()
|
||||
}
|
||||
|
||||
val dpNames = distPoint.distributionPoints.mapNotNull { it.distributionPoint }.filter { it.type == DistributionPointName.FULL_NAME }
|
||||
val generalNames = dpNames.flatMap { GeneralNames.getInstance(it.name).names.asList() }
|
||||
return generalNames.filter { it.tagNo == GeneralName.uniformResourceIdentifier}.map { DERIA5String.getInstance(it.name).string }.toSet()
|
||||
}
|
||||
|
||||
fun X509Certificate.distributionPointsToString() : String {
|
||||
return with(distributionPoints()) {
|
||||
if(this == null || isEmpty()) {
|
||||
DP_DEFAULT_ANSWER
|
||||
} else {
|
||||
sorted().joinToString()
|
||||
val dpMap = HashMap<URI, List<X500Principal>?>()
|
||||
for (distributionPoint in crlDistPoint.distributionPoints) {
|
||||
val distributionPointName = distributionPoint.distributionPoint
|
||||
if (distributionPointName?.type != DistributionPointName.FULL_NAME) continue
|
||||
val issuerNames = distributionPoint.crlIssuer?.names?.mapNotNull {
|
||||
if (it.tagNo == GeneralName.directoryName) {
|
||||
X500Principal(X500Name.getInstance(it.name).encoded)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
for (generalName in GeneralNames.getInstance(distributionPointName.name).names) {
|
||||
if (generalName.tagNo == GeneralName.uniformResourceIdentifier) {
|
||||
val uri = URI(DERIA5String.getInstance(generalName.name).string)
|
||||
dpMap[uri] = issuerNames
|
||||
}
|
||||
}
|
||||
}
|
||||
return dpMap
|
||||
}
|
||||
|
||||
fun X509Certificate.distributionPointsToString(): String {
|
||||
return with(distributionPoints().keys) {
|
||||
if (isEmpty()) DP_DEFAULT_ANSWER else sorted().joinToString()
|
||||
}
|
||||
}
|
||||
|
||||
fun ByteArray.toAsn1Object(): ASN1Primitive = ASN1InputStream(this).readObject()
|
||||
|
||||
fun certPathToString(certPath: Array<out X509Certificate>?): String {
|
||||
if (certPath == null) {
|
||||
return "<empty certpath>"
|
||||
}
|
||||
val certs = certPath.map {
|
||||
val bcCert = it.toBc()
|
||||
val subject = bcCert.subject.toString()
|
||||
val issuer = bcCert.issuer.toString()
|
||||
val keyIdentifier = try {
|
||||
SubjectKeyIdentifier.getInstance(bcCert.getExtension(Extension.subjectKeyIdentifier).parsedValue).keyIdentifier.toHex()
|
||||
} catch (ex: Exception) {
|
||||
"null"
|
||||
}
|
||||
val authorityKeyIdentifier = try {
|
||||
AuthorityKeyIdentifier.getInstance(bcCert.getExtension(Extension.authorityKeyIdentifier).parsedValue).keyIdentifier.toHex()
|
||||
} catch (ex: Exception) {
|
||||
"null"
|
||||
}
|
||||
" $subject[$keyIdentifier] issued by $issuer[$authorityKeyIdentifier] [${it.distributionPointsToString()}]"
|
||||
}
|
||||
return certs.joinToString("\r\n")
|
||||
return certPath.joinToString(System.lineSeparator()) { " ${it.toSimpleString()}" }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an executor for processing SSL handshake tasks asynchronously (see [SSLEngine.getDelegatedTask]). The max number of threads is 3,
|
||||
* which is the typical number of CRLs expected in a Corda TLS cert path. The executor needs to be passed to the [SslHandler] constructor.
|
||||
*/
|
||||
fun sslDelegatedTaskExecutor(parentPoolName: String): ThreadPoolExecutor {
|
||||
return namedThreadPoolExecutor(maxPoolSize = 3, poolName = "$parentPoolName-ssltask")
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@ -117,7 +137,7 @@ class LoggingTrustManagerWrapper(val wrapped: X509ExtendedTrustManager) : X509Ex
|
||||
if (chain == null) {
|
||||
return "<empty certpath>"
|
||||
}
|
||||
return chain.map { it.toString() }.joinToString(", ")
|
||||
return chain.joinToString(", ") { it.toString() }
|
||||
}
|
||||
|
||||
private fun logErrors(chain: Array<out X509Certificate>?, block: () -> Unit) {
|
||||
@ -169,37 +189,11 @@ class LoggingTrustManagerWrapper(val wrapped: X509ExtendedTrustManager) : X509Ex
|
||||
|
||||
}
|
||||
|
||||
private object LoggingImmediateExecutor : Executor {
|
||||
|
||||
override fun execute(command: Runnable?) {
|
||||
val log = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
if (command == null) {
|
||||
log.error("SSL handler executor called with a null command")
|
||||
throw NullPointerException("command")
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught", "MagicNumber") // log and rethrow all exceptions
|
||||
try {
|
||||
val commandName = command::class.qualifiedName?.let { "[$it]" } ?: ""
|
||||
log.debug("Entering SSL command $commandName")
|
||||
val elapsedTime = measureTimeMillis { command.run() }
|
||||
log.debug("Exiting SSL command $elapsedTime millis")
|
||||
if (elapsedTime > 100) {
|
||||
log.info("Command: $commandName took $elapsedTime millis to execute")
|
||||
}
|
||||
}
|
||||
catch (ex: Exception) {
|
||||
log.error("Caught exception in SSL handler executor", ex)
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun createClientSslHelper(target: NetworkHostAndPort,
|
||||
expectedRemoteLegalNames: Set<CordaX500Name>,
|
||||
keyManagerFactory: KeyManagerFactory,
|
||||
trustManagerFactory: TrustManagerFactory): SslHandler {
|
||||
internal fun createClientSslHandler(target: NetworkHostAndPort,
|
||||
expectedRemoteLegalNames: Set<CordaX500Name>,
|
||||
keyManagerFactory: KeyManagerFactory,
|
||||
trustManagerFactory: TrustManagerFactory,
|
||||
delegateTaskExecutor: Executor): SslHandler {
|
||||
val sslContext = createAndInitSslContext(keyManagerFactory, trustManagerFactory)
|
||||
val sslEngine = sslContext.createSSLEngine(target.host, target.port)
|
||||
sslEngine.useClientMode = true
|
||||
@ -211,15 +205,15 @@ internal fun createClientSslHelper(target: NetworkHostAndPort,
|
||||
sslParameters.serverNames = listOf(SNIHostName(x500toHostName(expectedRemoteLegalNames.single())))
|
||||
sslEngine.sslParameters = sslParameters
|
||||
}
|
||||
@Suppress("DEPRECATION")
|
||||
return SslHandler(sslEngine, false, LoggingImmediateExecutor)
|
||||
return SslHandler(sslEngine, false, delegateTaskExecutor)
|
||||
}
|
||||
|
||||
internal fun createClientOpenSslHandler(target: NetworkHostAndPort,
|
||||
expectedRemoteLegalNames: Set<CordaX500Name>,
|
||||
keyManagerFactory: KeyManagerFactory,
|
||||
trustManagerFactory: TrustManagerFactory,
|
||||
alloc: ByteBufAllocator): SslHandler {
|
||||
alloc: ByteBufAllocator,
|
||||
delegateTaskExecutor: Executor): SslHandler {
|
||||
val sslContext = SslContextBuilder.forClient().sslProvider(SslProvider.OPENSSL).keyManager(keyManagerFactory).trustManager(LoggingTrustManagerFactoryWrapper(trustManagerFactory)).build()
|
||||
val sslEngine = sslContext.newEngine(alloc, target.host, target.port)
|
||||
sslEngine.enabledProtocols = ArtemisTcpTransport.TLS_VERSIONS.toTypedArray()
|
||||
@ -229,13 +223,13 @@ internal fun createClientOpenSslHandler(target: NetworkHostAndPort,
|
||||
sslParameters.serverNames = listOf(SNIHostName(x500toHostName(expectedRemoteLegalNames.single())))
|
||||
sslEngine.sslParameters = sslParameters
|
||||
}
|
||||
@Suppress("DEPRECATION")
|
||||
return SslHandler(sslEngine, false, LoggingImmediateExecutor)
|
||||
return SslHandler(sslEngine, false, delegateTaskExecutor)
|
||||
}
|
||||
|
||||
internal fun createServerSslHandler(keyStore: CertificateStore,
|
||||
keyManagerFactory: KeyManagerFactory,
|
||||
trustManagerFactory: TrustManagerFactory): SslHandler {
|
||||
trustManagerFactory: TrustManagerFactory,
|
||||
delegateTaskExecutor: Executor): SslHandler {
|
||||
val sslContext = createAndInitSslContext(keyManagerFactory, trustManagerFactory)
|
||||
val sslEngine = sslContext.createSSLEngine()
|
||||
sslEngine.useClientMode = false
|
||||
@ -246,65 +240,34 @@ internal fun createServerSslHandler(keyStore: CertificateStore,
|
||||
val sslParameters = sslEngine.sslParameters
|
||||
sslParameters.sniMatchers = listOf(ServerSNIMatcher(keyStore))
|
||||
sslEngine.sslParameters = sslParameters
|
||||
@Suppress("DEPRECATION")
|
||||
return SslHandler(sslEngine, false, LoggingImmediateExecutor)
|
||||
}
|
||||
|
||||
fun createAndInitSslContext(keyManagerFactory: KeyManagerFactory, trustManagerFactory: TrustManagerFactory): SSLContext {
|
||||
val sslContext = SSLContext.getInstance("TLS")
|
||||
val keyManagers = keyManagerFactory.keyManagers
|
||||
val trustManagers = trustManagerFactory.trustManagers.filterIsInstance(X509ExtendedTrustManager::class.java)
|
||||
.map { LoggingTrustManagerWrapper(it) }.toTypedArray()
|
||||
sslContext.init(keyManagers, trustManagers, newSecureRandom())
|
||||
return sslContext
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun initialiseTrustStoreAndEnableCrlChecking(trustStore: CertificateStore, revocationConfig: RevocationConfig): ManagerFactoryParameters {
|
||||
val pkixParams = PKIXBuilderParameters(trustStore.value.internal, X509CertSelector())
|
||||
val revocationChecker = when (revocationConfig.mode) {
|
||||
RevocationConfig.Mode.OFF -> AllowAllRevocationChecker // Custom PKIXRevocationChecker skipping CRL check
|
||||
RevocationConfig.Mode.EXTERNAL_SOURCE -> {
|
||||
require(revocationConfig.externalCrlSource != null) { "externalCrlSource must not be null" }
|
||||
ExternalSourceRevocationChecker(revocationConfig.externalCrlSource!!) { Date() } // Custom PKIXRevocationChecker which uses `externalCrlSource`
|
||||
}
|
||||
else -> {
|
||||
val certPathBuilder = CertPathBuilder.getInstance("PKIX")
|
||||
val pkixRevocationChecker = certPathBuilder.revocationChecker as PKIXRevocationChecker
|
||||
pkixRevocationChecker.options = EnumSet.of(
|
||||
// Prefer CRL over OCSP
|
||||
PKIXRevocationChecker.Option.PREFER_CRLS,
|
||||
// Don't fall back to OCSP checking
|
||||
PKIXRevocationChecker.Option.NO_FALLBACK)
|
||||
if (revocationConfig.mode == RevocationConfig.Mode.SOFT_FAIL) {
|
||||
// Allow revocation check to succeed if the revocation status cannot be determined for one of
|
||||
// the following reasons: The CRL or OCSP response cannot be obtained because of a network error.
|
||||
pkixRevocationChecker.options = pkixRevocationChecker.options + PKIXRevocationChecker.Option.SOFT_FAIL
|
||||
}
|
||||
pkixRevocationChecker
|
||||
}
|
||||
}
|
||||
pkixParams.addCertPathChecker(revocationChecker)
|
||||
return CertPathTrustManagerParameters(pkixParams)
|
||||
return SslHandler(sslEngine, false, delegateTaskExecutor)
|
||||
}
|
||||
|
||||
internal fun createServerOpenSslHandler(keyManagerFactory: KeyManagerFactory,
|
||||
trustManagerFactory: TrustManagerFactory,
|
||||
alloc: ByteBufAllocator): SslHandler {
|
||||
|
||||
alloc: ByteBufAllocator,
|
||||
delegateTaskExecutor: Executor): SslHandler {
|
||||
val sslContext = getServerSslContextBuilder(keyManagerFactory, trustManagerFactory).build()
|
||||
val sslEngine = sslContext.newEngine(alloc)
|
||||
sslEngine.useClientMode = false
|
||||
@Suppress("DEPRECATION")
|
||||
return SslHandler(sslEngine, false, LoggingImmediateExecutor)
|
||||
return SslHandler(sslEngine, false, delegateTaskExecutor)
|
||||
}
|
||||
|
||||
fun createAndInitSslContext(keyManagerFactory: KeyManagerFactory, trustManagerFactory: TrustManagerFactory?): SSLContext {
|
||||
val sslContext = SSLContext.getInstance("TLS")
|
||||
val trustManagers = trustManagerFactory
|
||||
?.trustManagers
|
||||
?.map { if (it is X509ExtendedTrustManager) LoggingTrustManagerWrapper(it) else it }
|
||||
?.toTypedArray()
|
||||
sslContext.init(keyManagerFactory.keyManagers, trustManagers, newSecureRandom())
|
||||
return sslContext
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a special SNI handler used only when openSSL is used for AMQPServer
|
||||
*/
|
||||
internal fun createServerSNIOpenSslHandler(keyManagerFactoriesMap: Map<String, KeyManagerFactory>,
|
||||
internal fun createServerSNIOpenSniHandler(keyManagerFactoriesMap: Map<String, KeyManagerFactory>,
|
||||
trustManagerFactory: TrustManagerFactory): SniHandler {
|
||||
|
||||
// Default value can be any in the map.
|
||||
val sslCtxBuilder = getServerSslContextBuilder(keyManagerFactoriesMap.values.first(), trustManagerFactory)
|
||||
val mapping = DomainWildcardMappingBuilder(sslCtxBuilder.build())
|
||||
@ -314,20 +277,19 @@ internal fun createServerSNIOpenSslHandler(keyManagerFactoriesMap: Map<String, K
|
||||
return SniHandler(mapping.build())
|
||||
}
|
||||
|
||||
@Suppress("SpreadOperator")
|
||||
private fun getServerSslContextBuilder(keyManagerFactory: KeyManagerFactory, trustManagerFactory: TrustManagerFactory): SslContextBuilder {
|
||||
return SslContextBuilder.forServer(keyManagerFactory)
|
||||
.sslProvider(SslProvider.OPENSSL)
|
||||
.trustManager(LoggingTrustManagerFactoryWrapper(trustManagerFactory))
|
||||
.clientAuth(ClientAuth.REQUIRE)
|
||||
.ciphers(ArtemisTcpTransport.CIPHER_SUITES)
|
||||
.protocols(*ArtemisTcpTransport.TLS_VERSIONS.toTypedArray())
|
||||
.protocols(ArtemisTcpTransport.TLS_VERSIONS)
|
||||
}
|
||||
|
||||
internal fun splitKeystore(config: AMQPConfiguration): Map<String, CertHoldingKeyManagerFactoryWrapper> {
|
||||
val keyStore = config.keyStore.value.internal
|
||||
val password = config.keyStore.entryPassword.toCharArray()
|
||||
return keyStore.aliases().toList().map { alias ->
|
||||
return keyStore.aliases().toList().associate { alias ->
|
||||
val key = keyStore.getKey(alias, password)
|
||||
val certs = keyStore.getCertificateChain(alias)
|
||||
val x500Name = keyStore.getCertificate(alias).x509.subjectX500Principal
|
||||
@ -338,14 +300,45 @@ internal fun splitKeystore(config: AMQPConfiguration): Map<String, CertHoldingKe
|
||||
val newKeyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
|
||||
newKeyManagerFactory.init(newKeyStore, password)
|
||||
x500toHostName(cordaX500Name) to CertHoldingKeyManagerFactoryWrapper(newKeyManagerFactory, config)
|
||||
}.toMap()
|
||||
}
|
||||
}
|
||||
|
||||
// As per Javadoc in: https://docs.oracle.com/javase/8/docs/api/javax/net/ssl/KeyManagerFactory.html `init` method
|
||||
// 2nd parameter `password` - the password for recovering keys in the KeyStore
|
||||
fun KeyManagerFactory.init(keyStore: CertificateStore) = init(keyStore.value.internal, keyStore.entryPassword.toCharArray())
|
||||
|
||||
fun TrustManagerFactory.init(trustStore: CertificateStore) = init(trustStore.value.internal)
|
||||
fun keyManagerFactory(keyStore: CertificateStore): KeyManagerFactory {
|
||||
val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
|
||||
keyManagerFactory.init(keyStore)
|
||||
return keyManagerFactory
|
||||
}
|
||||
|
||||
fun trustManagerFactory(trustStore: CertificateStore): TrustManagerFactory {
|
||||
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
trustManagerFactory.init(trustStore.value.internal)
|
||||
return trustManagerFactory
|
||||
}
|
||||
|
||||
fun trustManagerFactoryWithRevocation(trustStore: CertificateStore,
|
||||
revocationConfig: RevocationConfig,
|
||||
crlSource: CrlSource): TrustManagerFactory {
|
||||
val revocationChecker = when (revocationConfig.mode) {
|
||||
RevocationConfig.Mode.OFF -> AllowAllRevocationChecker
|
||||
RevocationConfig.Mode.EXTERNAL_SOURCE -> {
|
||||
val externalCrlSource = requireNotNull(revocationConfig.externalCrlSource) {
|
||||
"externalCrlSource must be specfied for EXTERNAL_SOURCE"
|
||||
}
|
||||
CordaRevocationChecker(externalCrlSource, softFail = true)
|
||||
}
|
||||
RevocationConfig.Mode.SOFT_FAIL -> CordaRevocationChecker(crlSource, softFail = true)
|
||||
RevocationConfig.Mode.HARD_FAIL -> CordaRevocationChecker(crlSource, softFail = false)
|
||||
}
|
||||
val pkixParams = PKIXBuilderParameters(trustStore.value.internal, X509CertSelector())
|
||||
pkixParams.addCertPathChecker(revocationChecker)
|
||||
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
trustManagerFactory.init(CertPathTrustManagerParameters(pkixParams))
|
||||
return trustManagerFactory
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that converts a [CordaX500Name] to a a valid hostname (RFC-1035). It's used for SNI to indicate the target
|
||||
|
@ -1,88 +0,0 @@
|
||||
package net.corda.nodeapi.internal.protonwrapper.netty.revocation
|
||||
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.nodeapi.internal.protonwrapper.netty.ExternalCrlSource
|
||||
import org.bouncycastle.asn1.x509.Extension
|
||||
import java.security.cert.CRLReason
|
||||
import java.security.cert.CertPathValidatorException
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.CertificateRevokedException
|
||||
import java.security.cert.PKIXRevocationChecker
|
||||
import java.security.cert.X509CRL
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Implementation of [PKIXRevocationChecker] which determines whether certificate is revoked using [externalCrlSource] which knows how to
|
||||
* obtain a set of CRLs for a given certificate from an external source
|
||||
*/
|
||||
class ExternalSourceRevocationChecker(private val externalCrlSource: ExternalCrlSource, private val dateSource: () -> Date) : PKIXRevocationChecker() {
|
||||
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
}
|
||||
|
||||
override fun check(cert: Certificate, unresolvedCritExts: MutableCollection<String>?) {
|
||||
val x509Certificate = cert as X509Certificate
|
||||
checkApprovedCRLs(x509Certificate, externalCrlSource.fetch(x509Certificate))
|
||||
}
|
||||
|
||||
/**
|
||||
* Borrowed from `RevocationChecker.checkApprovedCRLs()`
|
||||
*/
|
||||
@Suppress("NestedBlockDepth")
|
||||
@Throws(CertPathValidatorException::class)
|
||||
private fun checkApprovedCRLs(cert: X509Certificate, approvedCRLs: Set<X509CRL>) {
|
||||
// See if the cert is in the set of approved crls.
|
||||
logger.debug("ExternalSourceRevocationChecker.checkApprovedCRLs() cert SN: ${cert.serialNumber}")
|
||||
|
||||
for (crl in approvedCRLs) {
|
||||
val entry = crl.getRevokedCertificate(cert)
|
||||
if (entry != null) {
|
||||
logger.debug("ExternalSourceRevocationChecker.checkApprovedCRLs() CRL entry: $entry")
|
||||
|
||||
/*
|
||||
* Abort CRL validation and throw exception if there are any
|
||||
* unrecognized critical CRL entry extensions (see section
|
||||
* 5.3 of RFC 5280).
|
||||
*/
|
||||
val unresCritExts = entry.criticalExtensionOIDs
|
||||
if (unresCritExts != null && !unresCritExts.isEmpty()) {
|
||||
/* remove any that we will process */
|
||||
unresCritExts.remove(Extension.cRLDistributionPoints.id)
|
||||
unresCritExts.remove(Extension.certificateIssuer.id)
|
||||
if (!unresCritExts.isEmpty()) {
|
||||
throw CertPathValidatorException(
|
||||
"Unrecognized critical extension(s) in revoked CRL entry: $unresCritExts")
|
||||
}
|
||||
}
|
||||
|
||||
val reasonCode = entry.revocationReason ?: CRLReason.UNSPECIFIED
|
||||
val revocationDate = entry.revocationDate
|
||||
if (revocationDate.before(dateSource())) {
|
||||
val t = CertificateRevokedException(
|
||||
revocationDate, reasonCode,
|
||||
crl.issuerX500Principal, mutableMapOf())
|
||||
throw CertPathValidatorException(
|
||||
t.message, t, null, -1, CertPathValidatorException.BasicReason.REVOKED)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun isForwardCheckingSupported(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun getSupportedExtensions(): MutableSet<String>? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun init(forward: Boolean) {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
override fun getSoftFailExceptions(): MutableList<CertPathValidatorException> {
|
||||
return LinkedList()
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user