mirror of
https://github.com/corda/corda.git
synced 2025-01-16 01:40:17 +00:00
Merge branch 'master' into colljos-merge-release3-dp-master
This commit is contained in:
commit
24bf6df868
2
.gitignore
vendored
2
.gitignore
vendored
@ -37,6 +37,7 @@ lib/quasar.jar
|
|||||||
.idea/markdown-navigator
|
.idea/markdown-navigator
|
||||||
.idea/runConfigurations
|
.idea/runConfigurations
|
||||||
.idea/dictionaries
|
.idea/dictionaries
|
||||||
|
.idea/codeStyles/
|
||||||
/gradle-plugins/.idea/
|
/gradle-plugins/.idea/
|
||||||
|
|
||||||
# Include the -parameters compiler option by default in IntelliJ required for serialization.
|
# Include the -parameters compiler option by default in IntelliJ required for serialization.
|
||||||
@ -88,6 +89,7 @@ crashlytics-build.properties
|
|||||||
|
|
||||||
# docs related
|
# docs related
|
||||||
docs/virtualenv/
|
docs/virtualenv/
|
||||||
|
virtualenv/
|
||||||
|
|
||||||
# bft-smart
|
# bft-smart
|
||||||
**/config/currentView
|
**/config/currentView
|
||||||
|
@ -12,11 +12,20 @@ import java.util.jar.JarInputStream
|
|||||||
* An attachment is a ZIP (or an optionally signed JAR) that contains one or more files. Attachments are meant to
|
* An attachment is a ZIP (or an optionally signed JAR) that contains one or more files. Attachments are meant to
|
||||||
* contain public static data which can be referenced from transactions and utilised from contracts. Good examples
|
* contain public static data which can be referenced from transactions and utilised from contracts. Good examples
|
||||||
* of how attachments are meant to be used include:
|
* of how attachments are meant to be used include:
|
||||||
|
*
|
||||||
* - Calendar data
|
* - Calendar data
|
||||||
* - Fixes (e.g. LIBOR)
|
* - Fixes (e.g. LIBOR)
|
||||||
* - Smart contract code
|
* - Smart contract code
|
||||||
* - Legal documents
|
* - Legal documents
|
||||||
* - Facts generated by oracles which might be reused a lot
|
* - Facts generated by oracles which might be reused a lot.
|
||||||
|
*
|
||||||
|
* At the moment, non-ZIP attachments are not supported. Support may come in a future release. Using ZIP files for
|
||||||
|
* attachments makes it easy to ensure data on the ledger is compressed, which is useful considering that attachments
|
||||||
|
* may be widely replicated around the network. It also allows the jarsigner tool to be used to sign an attachment
|
||||||
|
* using ordinary certificates of the kind that many organisations already have, and improves the efficiency of
|
||||||
|
* attachment resolution in cases where the attachment is logically made up of many small files - e.g. is bytecode.
|
||||||
|
* Finally, using ZIPs ensures files have a timestamp associated with them, and enables informational attachments
|
||||||
|
* to be password protected (although in current releases password protected ZIPs are likely to fail to work).
|
||||||
*/
|
*/
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
interface Attachment : NamedByHash {
|
interface Attachment : NamedByHash {
|
||||||
|
@ -407,7 +407,7 @@ object Crypto {
|
|||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||||
fun doSign(privateKey: PrivateKey, clearData: ByteArray) = doSign(findSignatureScheme(privateKey), privateKey, clearData)
|
fun doSign(privateKey: PrivateKey, clearData: ByteArray): ByteArray = doSign(findSignatureScheme(privateKey), privateKey, clearData)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic way to sign [ByteArray] data with a [PrivateKey] and a known schemeCodeName [String].
|
* Generic way to sign [ByteArray] data with a [PrivateKey] and a known schemeCodeName [String].
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
package net.corda.core.internal
|
||||||
|
|
||||||
|
import net.corda.core.crypto.DigitalSignature
|
||||||
|
import net.corda.core.crypto.SignedData
|
||||||
|
import net.corda.core.crypto.verify
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
import net.corda.core.serialization.SerializedBytes
|
||||||
|
import net.corda.core.serialization.deserialize
|
||||||
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
|
||||||
|
// TODO: Rename this to DigitalSignature.WithCert once we're happy for it to be public API. The methods will need documentation
|
||||||
|
// and the correct exceptions will be need to be annotated
|
||||||
|
/** A digital signature with attached certificate of the public key. */
|
||||||
|
class DigitalSignatureWithCert(val by: X509Certificate, bytes: ByteArray) : DigitalSignature(bytes) {
|
||||||
|
fun verify(content: ByteArray): Boolean = by.publicKey.verify(content, this)
|
||||||
|
fun verify(content: OpaqueBytes): Boolean = verify(content.bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Similar to [SignedData] but instead of just attaching the public key, the certificate for the key is attached instead. */
|
||||||
|
@CordaSerializable
|
||||||
|
class SignedDataWithCert<T : Any>(val raw: SerializedBytes<T>, val sig: DigitalSignatureWithCert) {
|
||||||
|
fun verified(): T {
|
||||||
|
sig.verify(raw)
|
||||||
|
return uncheckedCast(raw.deserialize<Any>())
|
||||||
|
}
|
||||||
|
}
|
@ -3,11 +3,14 @@
|
|||||||
package net.corda.core.internal
|
package net.corda.core.internal
|
||||||
|
|
||||||
import net.corda.core.cordapp.CordappProvider
|
import net.corda.core.cordapp.CordappProvider
|
||||||
|
import net.corda.core.crypto.Crypto
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.sha256
|
import net.corda.core.crypto.sha256
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.node.ServicesForResolution
|
import net.corda.core.node.ServicesForResolution
|
||||||
import net.corda.core.serialization.SerializationContext
|
import net.corda.core.serialization.SerializationContext
|
||||||
|
import net.corda.core.serialization.deserialize
|
||||||
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.core.transactions.WireTransaction
|
import net.corda.core.transactions.WireTransaction
|
||||||
import org.bouncycastle.asn1.x500.X500Name
|
import org.bouncycastle.asn1.x500.X500Name
|
||||||
@ -27,6 +30,8 @@ import java.nio.charset.Charset
|
|||||||
import java.nio.charset.StandardCharsets.UTF_8
|
import java.nio.charset.StandardCharsets.UTF_8
|
||||||
import java.nio.file.*
|
import java.nio.file.*
|
||||||
import java.nio.file.attribute.FileAttribute
|
import java.nio.file.attribute.FileAttribute
|
||||||
|
import java.security.PrivateKey
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.temporal.Temporal
|
import java.time.temporal.Temporal
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -308,6 +313,19 @@ fun TransactionBuilder.toLedgerTransaction(services: ServicesForResolution, seri
|
|||||||
val KClass<*>.packageName: String get() = java.`package`.name
|
val KClass<*>.packageName: String get() = java.`package`.name
|
||||||
|
|
||||||
fun URL.openHttpConnection(): HttpURLConnection = openConnection() as HttpURLConnection
|
fun URL.openHttpConnection(): HttpURLConnection = openConnection() as HttpURLConnection
|
||||||
|
|
||||||
|
fun HttpURLConnection.checkOkResponse() {
|
||||||
|
if (responseCode != 200) {
|
||||||
|
val message = errorStream.use { it.reader().readText() }
|
||||||
|
throw IOException("Response Code $responseCode: $message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T : Any> HttpURLConnection.responseAs(): T {
|
||||||
|
checkOkResponse()
|
||||||
|
return inputStream.use { it.readBytes() }.deserialize()
|
||||||
|
}
|
||||||
|
|
||||||
/** Analogous to [Thread.join]. */
|
/** Analogous to [Thread.join]. */
|
||||||
fun ExecutorService.join() {
|
fun ExecutorService.join() {
|
||||||
shutdown() // Do not change to shutdownNow, tests use this method to assert the executor has no more tasks.
|
shutdown() // Do not change to shutdownNow, tests use this method to assert the executor has no more tasks.
|
||||||
@ -336,3 +354,9 @@ val CordaX500Name.x500Name: X500Name
|
|||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
val CordaX500Name.Companion.unspecifiedCountry
|
val CordaX500Name.Companion.unspecifiedCountry
|
||||||
get() = "ZZ"
|
get() = "ZZ"
|
||||||
|
|
||||||
|
fun <T : Any> T.signWithCert(privateKey: PrivateKey, certificate: X509Certificate): SignedDataWithCert<T> {
|
||||||
|
val serialised = serialize()
|
||||||
|
val signature = Crypto.doSign(privateKey, serialised.bytes)
|
||||||
|
return SignedDataWithCert(serialised, DigitalSignatureWithCert(certificate, signature))
|
||||||
|
}
|
||||||
|
@ -4,7 +4,9 @@ import net.corda.core.contracts.*
|
|||||||
import net.corda.core.identity.AbstractParty
|
import net.corda.core.identity.AbstractParty
|
||||||
import org.hibernate.annotations.Type
|
import org.hibernate.annotations.Type
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.persistence.*
|
import javax.persistence.Column
|
||||||
|
import javax.persistence.MappedSuperclass
|
||||||
|
import javax.persistence.Transient
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JPA representation of the common schema entities
|
* JPA representation of the common schema entities
|
||||||
|
@ -6,6 +6,9 @@ from previous releases. Please refer to :doc:`upgrade-notes` for detailed instru
|
|||||||
|
|
||||||
UNRELEASED
|
UNRELEASED
|
||||||
----------
|
----------
|
||||||
|
* JPA Mapping annotations for States extending ``CommonSchemaV1.LinearState`` and ``CommonSchemaV1.FungibleState`` on the
|
||||||
|
`participants` collection need to be moved to the actual class. This allows to properly specify the unique table name per a collection.
|
||||||
|
See: DummyDealStateSchemaV1.PersistentDummyDealState
|
||||||
|
|
||||||
* JPA Mapping annotations for States extending ``CommonSchemaV1.LinearState`` and ``CommonSchemaV1.FungibleState`` on the
|
* JPA Mapping annotations for States extending ``CommonSchemaV1.LinearState`` and ``CommonSchemaV1.FungibleState`` on the
|
||||||
`participants` collection need to be moved to the actual State class. This allows developers to properly specify
|
`participants` collection need to be moved to the actual State class. This allows developers to properly specify
|
||||||
|
BIN
docs/source/design/hadr/HA deployment - Hot-Cold.png
Normal file
BIN
docs/source/design/hadr/HA deployment - Hot-Cold.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 376 KiB |
BIN
docs/source/design/hadr/HA deployment - Hot-Hot.png
Normal file
BIN
docs/source/design/hadr/HA deployment - Hot-Hot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 423 KiB |
BIN
docs/source/design/hadr/HA deployment - Hot-Warm.png
Normal file
BIN
docs/source/design/hadr/HA deployment - Hot-Warm.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 247 KiB |
BIN
docs/source/design/hadr/HA deployment - No HA.png
Normal file
BIN
docs/source/design/hadr/HA deployment - No HA.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 280 KiB |
52
docs/source/design/hadr/decisions/crash-shell.md
Normal file
52
docs/source/design/hadr/decisions/crash-shell.md
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
![Corda](https://www.corda.net/wp-content/uploads/2016/11/fg005_corda_b.png)
|
||||||
|
|
||||||
|
--------------------------------------------
|
||||||
|
Design Decision: Node starting & stopping
|
||||||
|
============================================
|
||||||
|
|
||||||
|
## Background / Context
|
||||||
|
|
||||||
|
The potential use of a crash shell is relevant to [high availability](../design.md) capabilities of nodes.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Options Analysis
|
||||||
|
|
||||||
|
### 1. Use crash shell
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
1. Already built into the node.
|
||||||
|
2. Potentially add custom commands.
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
|
||||||
|
1. Won’t reliably work if the node is in an unstable state
|
||||||
|
2. Not practical for running hundreds of nodes as our customers arealready trying to do.
|
||||||
|
3. Doesn’t mesh with the user access controls of the organisation.
|
||||||
|
4. Doesn’t interface to the existing monitoring andcontrol systems i.e. Nagios, Geneos ITRS, Docker Swarm, etc.
|
||||||
|
|
||||||
|
### 2. Delegate to external tools
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
1. Doesn’t require change from our customers
|
||||||
|
2. Will work even if node is completely stuck
|
||||||
|
3. Allows scripted node restart schedules
|
||||||
|
4. Doesn’t raise questions about access controllists and audit
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
|
||||||
|
1. More uncertainty about what customers do.
|
||||||
|
2. Might be more requirements on us to interact nicely with lots of different products.
|
||||||
|
3. Might mean we get blamed for faults in other people’s control software.
|
||||||
|
4. Doesn’t coordinate with the node for graceful shutdown.
|
||||||
|
5. Doesn’t address any crypto features that target protecting the AMQP headers.
|
||||||
|
|
||||||
|
## Recommendation and justification
|
||||||
|
|
||||||
|
Proceed with Option 2: Delegate to external tools
|
||||||
|
|
||||||
|
## Decision taken
|
||||||
|
|
||||||
|
**[DRB meeting, 16/11/2017:](./drb-meeting-20171116.md)** Restarts should be handled by polite shutdown, followed by a hard clear. (RGB, JC, MH agreed)
|
47
docs/source/design/hadr/decisions/db-msg-store.md
Normal file
47
docs/source/design/hadr/decisions/db-msg-store.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
![Corda](https://www.corda.net/wp-content/uploads/2016/11/fg005_corda_b.png)
|
||||||
|
|
||||||
|
--------------------------------------------
|
||||||
|
Design Decision: Message storage
|
||||||
|
============================================
|
||||||
|
|
||||||
|
## Background / Context
|
||||||
|
|
||||||
|
Storage of messages by the message broker has implications for replication technologies which can be used to ensure both [high availability](../design.md) and disaster recovery of Corda nodes.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Options Analysis
|
||||||
|
|
||||||
|
### 1. Storage in the file system
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
1. Out of the box configuration.
|
||||||
|
2. Recommended Artemis setup
|
||||||
|
3. Faster
|
||||||
|
4. Less likely to have interaction with DB Blob rules
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
|
||||||
|
1. Unaligned capture time of journal data compared to DB checkpointing.
|
||||||
|
2. Replication options on Azure are limited. Currently we may be forced to the ‘Azure Files’ SMB mount, rather than the ‘Azure Data Disk’ option. This is still being evaluated
|
||||||
|
|
||||||
|
### 2. Storage in node database
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
1. Single point of data capture and backup
|
||||||
|
2. Consistent solution between VM and physical box solutions
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
|
||||||
|
1. Doesn’t work on H2, or SQL Server. From my own testing LargeObject support is broken. The current Artemis code base does allow somepluggability, but not of the large object implementation, only of the SQLstatements. We should lobby for someone to fix the implementations for SQLServer and H2.
|
||||||
|
2. Probably much slower, although this needs measuring.
|
||||||
|
|
||||||
|
## Recommendation and justification
|
||||||
|
|
||||||
|
Continue with Option 1: Storage in the file system
|
||||||
|
|
||||||
|
## Decision taken
|
||||||
|
|
||||||
|
[DRB meeting, 16/11/2017:](./drb-meeting-20171116.md) Use storage in the file system (for now)
|
126
docs/source/design/hadr/decisions/drb-meeting-20171116.md
Normal file
126
docs/source/design/hadr/decisions/drb-meeting-20171116.md
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
![Corda](https://www.corda.net/wp-content/uploads/2016/11/fg005_corda_b.png)
|
||||||
|
|
||||||
|
--------------------------------------------
|
||||||
|
Design Review Board Meeting Minutes
|
||||||
|
============================================
|
||||||
|
|
||||||
|
**Date / Time:** 16/11/2017, 16:30
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Attendees
|
||||||
|
|
||||||
|
- Mark Oldfield (MO)
|
||||||
|
- Matthew Nesbit (MN)
|
||||||
|
- Richard Gendal Brown (RGB)
|
||||||
|
- James Carlyle (JC)
|
||||||
|
- Mike Hearn (MH)
|
||||||
|
- Jose Coll (JoC)
|
||||||
|
- Rick Parker (RP)
|
||||||
|
- Andrey Bozhko (AB)
|
||||||
|
- Dave Hudson (DH)
|
||||||
|
- Nick Arini (NA)
|
||||||
|
- Ben Abineri (BA)
|
||||||
|
- Jonathan Sartin (JS)
|
||||||
|
- David Lee (DL)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## **Minutes**
|
||||||
|
|
||||||
|
The meeting re-opened following prior discussion of the float design.
|
||||||
|
|
||||||
|
MN introduced the design for high availability, clarifying that the design did not include support for DR-implied features (asynchronous replication etc.).
|
||||||
|
|
||||||
|
MN highlighted limitations in testability: Azure had confirmed support for geo replication but with limited control by the user and no testing facility; all R3 can do is test for impact on performance.
|
||||||
|
|
||||||
|
The design was noted to be dependent on a lot on external dependencies for replication, with R3's testing capability limited to Azure. Agent banks may want to use SAN across dark fiber sites, redundant switches etc. not available to R3.
|
||||||
|
|
||||||
|
MN noted that certain databases are not yet officially supported in Corda.
|
||||||
|
|
||||||
|
### [Near-term-target](./near-term-target.md), [Medium-term target](./medium-term-target.md)
|
||||||
|
|
||||||
|
Outlining the hot-cold design, MN highlighted importance of ensuring only one node is active at one time. MN argued for having a tested hot-cold solution as a ‘backstop’. MN confirmed the work involved was to develop DB/SAN exclusion checkers and test appropriately.
|
||||||
|
|
||||||
|
JC queried whether unknowns exist for hot-cold. MN described limitations of Azure file replication.
|
||||||
|
|
||||||
|
JC noted there was optionality around both the replication mechanisms and the on-premises vs. cloud deployment.
|
||||||
|
|
||||||
|
### [Message storage](./db-msg-store.md)
|
||||||
|
|
||||||
|
Lack of support for storing Artemis messages via JDBC was raised, and the possibility for RedHat to provide an enhancement was discussed.
|
||||||
|
|
||||||
|
MH raised the alternative of using Artemis’ inbuilt replication protocol - MN confirmed this was in scope for hot-warm, but not hot-cold.
|
||||||
|
|
||||||
|
JC posited that file system/SAN replication should be OK for banks
|
||||||
|
|
||||||
|
**DECISION AGREED**: Use storage in the file system (for now)
|
||||||
|
|
||||||
|
AB questioned about protections against corruption; RGB highlighted the need for testing on this. MH described previous testing activity, arguing for a performance cluster that repeatedly runs load tests, kills nodes,checking they come back etc.
|
||||||
|
|
||||||
|
MN could not comment on testing status of current code. MH noted the notary hasn't been tested.
|
||||||
|
|
||||||
|
AB queried how basic node recovery would work. MN explained, highlighting the limitation for RPC callbacks.
|
||||||
|
|
||||||
|
JC proposed these limitations should be noted and explained to Finastra; move on.
|
||||||
|
|
||||||
|
There was discussion of how RPC observables could be made to persist across node outages. MN argued that for most applications, a clear signal of the outage that triggered clients to resubscribe was preferable. This was agreed.
|
||||||
|
|
||||||
|
JC argued for using Kafka.
|
||||||
|
|
||||||
|
MN presented the Hot-warm solution as a target for March-April and provide clarifications on differences vs. hot-cold and hot-hot.
|
||||||
|
|
||||||
|
JC highlighted that the clustered artemis was an important intermediate step. MN highlighted other important features
|
||||||
|
|
||||||
|
MO noted that different banks may opt for different solutions.
|
||||||
|
|
||||||
|
JoC raised the question of multi-IP per node.
|
||||||
|
|
||||||
|
MN described the Hot-hot solution, highlighting that flows remained 'sticky' to a particular instance but could be picked up by another when needed.
|
||||||
|
|
||||||
|
AB preferred the hot-hot solution. MN noted the many edge cases to be worked through.
|
||||||
|
|
||||||
|
AB Queried the DR story. MO stated this was out of scope at present.
|
||||||
|
|
||||||
|
There was discussion of the implications of not having synchronous replication.
|
||||||
|
|
||||||
|
MH questioned the need for a backup strategy that allows winding back the clock. MO stated this was out of scope at present.
|
||||||
|
|
||||||
|
MO drew attention to the expectation that Corda would be considered part of larger solutions with controlled restore procedures under BCP.
|
||||||
|
|
||||||
|
JC noted the variability in many elements as a challenge.
|
||||||
|
|
||||||
|
MO argued for providing a 'shrink-wrapped' solution based around equipment R3 could test (e.g. Azure)
|
||||||
|
|
||||||
|
JC argued for the need to manage testing of banks' infrastructure choices in order to reduce time to implementation.
|
||||||
|
|
||||||
|
There was discussion around the semantic difference between HA and DR. MH argued for a definition based around rolling backups. MN and MO shared banks' view of what DR is. MH contrasted this with Google definitions. AB noted HA and DR have different SLAs.
|
||||||
|
|
||||||
|
**DECISION AGREED:** Near-term target: Hot Cold; Medium-term target: Hot-warm (RGB, JC, MH agreed)
|
||||||
|
|
||||||
|
RGB queried why Artemis couldn't be run in clustered mode now. MN explained.
|
||||||
|
|
||||||
|
AB queried what Finastra asked for. MO implied nothing specific; MH maintained this would be needed anyway.
|
||||||
|
|
||||||
|
### [Broker separation](./external-broker.md)
|
||||||
|
|
||||||
|
MN outlined his rationale for Broker separation.
|
||||||
|
|
||||||
|
JC queried whether this would affect demos.
|
||||||
|
|
||||||
|
MN gave an assumption that HA was for enterprise only; RGB, JC: pointed out that Enterprise might still be made available for non-production use.
|
||||||
|
|
||||||
|
**DECISION AGREED**: The broker should only be separated if required by other features (e.g. the float), otherwise not. (RGB, JC, MH agreed).
|
||||||
|
|
||||||
|
### [Load balancers and multi-IP](./ip-addressing.md)
|
||||||
|
|
||||||
|
The topic was discussed.
|
||||||
|
|
||||||
|
**DECISION AGREED**: The design can allow for optional load balancers to be implemented by clients.
|
||||||
|
|
||||||
|
### [Crash shell](./crash-shell.md)
|
||||||
|
|
||||||
|
MN provided outline explanation.
|
||||||
|
|
||||||
|
**DECISION AGREED**: Restarts should be handled by polite shutdown, followed by a hard clear. (RGB, JC, MH agreed)
|
||||||
|
|
49
docs/source/design/hadr/decisions/external-broker.md
Normal file
49
docs/source/design/hadr/decisions/external-broker.md
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
![Corda](https://www.corda.net/wp-content/uploads/2016/11/fg005_corda_b.png)
|
||||||
|
|
||||||
|
--------------------------------------------
|
||||||
|
Design Decision: Broker separation
|
||||||
|
============================================
|
||||||
|
|
||||||
|
## Background / Context
|
||||||
|
|
||||||
|
A decision of whether to extract the Artemis message broker as a separate component has implications for the design of [high availability](../design.md) for nodes.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Options Analysis
|
||||||
|
|
||||||
|
### 1. No change (leave broker embedded)
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
1. Least change
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
|
||||||
|
1. Means that starting/stopping Corda is tightly coupled to starting/stopping Artemis instances.
|
||||||
|
2. Risks resource leaks from one system component affecting other components.
|
||||||
|
3. Not pluggable if we wish to have an alternative broker.
|
||||||
|
|
||||||
|
## 2. External broker
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
1. Separates concerns
|
||||||
|
2. Allows future pluggability and standardisation on AMQP
|
||||||
|
3. Separates life cycles of the components
|
||||||
|
4. Makes Artemis deployment much more out of the box.
|
||||||
|
5. Allows easier tuning of VM resources for Flow processing workloads vs broker type workloads.
|
||||||
|
6. Allows later encrypted version to be an enterprise feature that can interoperate with OS versions.
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
|
||||||
|
1. More work
|
||||||
|
2. Requires creating a protocol to control external bridge formation.
|
||||||
|
|
||||||
|
## Recommendation and justification
|
||||||
|
|
||||||
|
Proceed with Option 2: External broker
|
||||||
|
|
||||||
|
## Decision taken
|
||||||
|
|
||||||
|
**[DRB meeting, 16/11/2017:](./drb-meeting-20171116.md)** The broker should only be separated if required by other features (e.g. the float), otherwise not. (RGB, JC, MH agreed).
|
48
docs/source/design/hadr/decisions/ip-addressing.md
Normal file
48
docs/source/design/hadr/decisions/ip-addressing.md
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
![Corda](https://www.corda.net/wp-content/uploads/2016/11/fg005_corda_b.png)
|
||||||
|
|
||||||
|
--------------------------------------------
|
||||||
|
Design Decision: IP addressing mechanism (near-term)
|
||||||
|
============================================
|
||||||
|
|
||||||
|
## Background / Context
|
||||||
|
|
||||||
|
End-to-end encryption is a desirable potential design feature for the [float](../design.md).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Options Analysis
|
||||||
|
|
||||||
|
### 1. Via load balancer
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
1. Standard technology in banks and on clouds, often for non-HA purposes.
|
||||||
|
2. Intended to allow us to wait for completion of network map work.
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
|
||||||
|
1. We do need to support multiple IP address advertisements in network map long term.
|
||||||
|
2. Might involve small amount of code if we find Artemis doesn’t like the health probes. So far though testing of the Azure Load balancer doesn’t need this.
|
||||||
|
3. Won’t work over very large data centre separations, but that doesn’t work for HA/DR either
|
||||||
|
|
||||||
|
### 2. Via IP list in Network Map
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
1. More flexible
|
||||||
|
2. More deployment options
|
||||||
|
3. We will need it one day
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
|
||||||
|
1. Have to write code to support it.
|
||||||
|
2. Configuration more complicated and now the nodesare non-equivalent, so you can’t just copy the config to the backup.
|
||||||
|
3. Artemis has round robin and automatic failover, so we may have to expose a vendor specific config flag in the network map.
|
||||||
|
|
||||||
|
## Recommendation and justification
|
||||||
|
|
||||||
|
Proceed with Option 1: Via Load Balancer
|
||||||
|
|
||||||
|
## Decision taken
|
||||||
|
|
||||||
|
**[DRB meeting, 16/11/2017:](./drb-meeting-20171116.md)** The design can allow for optional load balancers to be implemented by clients. (RGB, JC, MH agreed)
|
49
docs/source/design/hadr/decisions/medium-term-target.md
Normal file
49
docs/source/design/hadr/decisions/medium-term-target.md
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
![Corda](https://www.corda.net/wp-content/uploads/2016/11/fg005_corda_b.png)
|
||||||
|
|
||||||
|
------
|
||||||
|
|
||||||
|
# Design Decision: Medium-term target for node HA
|
||||||
|
|
||||||
|
## Background / Context
|
||||||
|
|
||||||
|
Designing for high availability is a complex task which can only be delivered over an operationally-significant timeline. It is therefore important to determine whether an intermediate state design (deliverable for around March 2018) is desirable as a precursor to longer term outcomes.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Options Analysis
|
||||||
|
|
||||||
|
### 1. Hot-warm as interim state (see [HA design doc](../design.md))
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
1. Simpler master/slave election logic
|
||||||
|
2. Less edge cases with respect to messages being consumed by flows.
|
||||||
|
3. Naive solution of just stopping/starting the node code is simple to implement.
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
|
||||||
|
1. Still probably requires the Artemis MQ outside of the node in a cluster.
|
||||||
|
2. May actually turn out more risky than hot-hot, because shutting down code is always prone to deadlocks and resource leakages.
|
||||||
|
3. Some work would have to be thrown away when we create a full hot-hot solution.
|
||||||
|
|
||||||
|
### 2. Progress immediately to Hot-hot (see [HA design doc](../design.md))
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
1. Horizontal scalability is what all our customers want.
|
||||||
|
2. It simplifies many deployments as nodes in a cluster are all equivalent.
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
|
||||||
|
1. More complicated especially regarding message routing.
|
||||||
|
2. Riskier to do this big-bang style.
|
||||||
|
3. Might not meet deadlines.
|
||||||
|
|
||||||
|
## Recommendation and justification
|
||||||
|
|
||||||
|
Proceed with Option 1: Hot-warm as interim state.
|
||||||
|
|
||||||
|
## Decision taken
|
||||||
|
|
||||||
|
**[DRB meeting, 16/11/2017:](./drb-meeting-20171116.md)** Adopt option 1: Medium-term target: Hot Warm (RGB, JC, MH agreed)
|
||||||
|
|
46
docs/source/design/hadr/decisions/near-term-target.md
Normal file
46
docs/source/design/hadr/decisions/near-term-target.md
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
![Corda](https://www.corda.net/wp-content/uploads/2016/11/fg005_corda_b.png)
|
||||||
|
|
||||||
|
--------------------------------------------
|
||||||
|
Design Decision: Near-term target for node HA
|
||||||
|
============================================
|
||||||
|
|
||||||
|
## Background / Context
|
||||||
|
|
||||||
|
Designing for high availability is a complex task which can only be delivered over an operationally-significant timeline. It is therefore important to determine the target state in the near term as a precursor to longer term outcomes.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Options Analysis
|
||||||
|
|
||||||
|
### 1. No HA
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
1. Reduces developer distractions.
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
|
||||||
|
1. No backstop if we miss our targets for fuller HA.
|
||||||
|
2. No answer at all for simple DR modes.
|
||||||
|
|
||||||
|
### 2. Hot-cold (see [HA design doc](../design.md))
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
1. Flushes out lots of basic deployment issues that will be of benefit later.
|
||||||
|
2. If stuff slips we at least have a backstop position with hot-cold.
|
||||||
|
3. For now, the only DR story we have is essentially a continuation of this mode
|
||||||
|
4. The intent of decisions such as using a loadbalancer is to minimise code changes
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
|
||||||
|
1. Distracts from the work for more complete forms of HA.
|
||||||
|
2. Involves creating a few components that are not much use later, for instance the mutual exclusion lock.
|
||||||
|
|
||||||
|
## Recommendation and justification
|
||||||
|
|
||||||
|
Proceed with Option 2: Hot-cold.
|
||||||
|
|
||||||
|
## Decision taken
|
||||||
|
|
||||||
|
**[DRB meeting, 16/11/2017:](./drb-meeting-20171116.md)** Adopt option 2: Near-term target: Hot Cold (RGB, JC, MH agreed)
|
236
docs/source/design/hadr/design.md
Normal file
236
docs/source/design/hadr/design.md
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
![Corda](https://www.corda.net/wp-content/uploads/2016/11/fg005_corda_b.png)
|
||||||
|
|
||||||
|
# High Availability Support for Corda: A Phased Approach
|
||||||
|
|
||||||
|
-------------------
|
||||||
|
DOCUMENT MANAGEMENT
|
||||||
|
===================
|
||||||
|
|
||||||
|
## Document Control
|
||||||
|
|
||||||
|
* High Availability and Disaster Recovery for Corda: A Phased Approach
|
||||||
|
* Date: 13th November 2017
|
||||||
|
* Author: Matthew Nesbit
|
||||||
|
* Distribution: Design Review Board, Product Management, Services - Technical (Consulting), Platform Delivery
|
||||||
|
* Corda target version: Enterprise
|
||||||
|
|
||||||
|
## Document Sign-off
|
||||||
|
|
||||||
|
* Author: David Lee
|
||||||
|
* Reviewers(s): TBD
|
||||||
|
* Final approver(s): TBD
|
||||||
|
|
||||||
|
## Document History
|
||||||
|
|
||||||
|
--------------------------------------------
|
||||||
|
HIGH LEVEL DESIGN
|
||||||
|
============================================
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
### Background
|
||||||
|
|
||||||
|
The term high availability (HA) is used in this document to refer to the ability to rapidly handle any single component failure, whether due to physical issues (e.g. hard drive failure), network connectivity loss, or software faults.
|
||||||
|
|
||||||
|
Expectations of HA in modern enterprise systems are for systems to recover normal operation in a few minutes at most, while ensuring minimal/zero data loss. Whilst overall reliability is the overriding objective, it is desirable for Corda to offer HA mechanisms which are both highly automated and transparent to node operators. HA mechanism must not involve any configuration changes that require more than an appropriate admin tool, or a simple start/stop of a process as that would need an Emergency Change Request.
|
||||||
|
|
||||||
|
HA naturally grades into requirements for Disaster Recovery (DR), which requires that there is a tested procedure to handle large scale multi-component failures e.g. due to data centre flooding, acts of terrorism. DR processes are permitted to involve significant manual intervention, although the complications of actually invoking a Business Continuity Plan (BCP) mean that the less manual intervention, the more competitive Corda will be in the modern vendor market.
|
||||||
|
For modern financial institutions, maintaining comprehensive and effective BCP procedures are a legal requirement which is generally tested at least once a year.
|
||||||
|
|
||||||
|
However, until Corda is the system of record, or the primary system for transactions we are unlikely to be required to have any kind of fully automatic DR. In fact, we are likely to be restarted only once BCP has restored the most critical systems.
|
||||||
|
In contrast, typical financial institutions maintain large, complex technology landscapes in which individual component failures can occur, such as:
|
||||||
|
|
||||||
|
* Small scale software failures
|
||||||
|
* Mandatory data centre power cycles
|
||||||
|
* Operating system patching and restarts
|
||||||
|
* Short lived network outages
|
||||||
|
* Middleware queue build-up
|
||||||
|
* Machine failures
|
||||||
|
|
||||||
|
Thus, HA is essential for enterprise Corda and providing help to administrators necessary for rapid fault diagnosis.
|
||||||
|
|
||||||
|
### Current node topology
|
||||||
|
|
||||||
|
![Current (single process)](./HA%20deployment%20-%20No%20HA.png)
|
||||||
|
|
||||||
|
The current solution has a single integrated process running in one JVM including
|
||||||
|
Artemis, H2 database, Flow State Machine, P2P bridging. All storage is on the local file system. There is no HA capability other than manual restart of the node following failure.
|
||||||
|
|
||||||
|
#### Limitations
|
||||||
|
|
||||||
|
- All sub-systems must be started and stopped together.
|
||||||
|
- Unable to handle partial failure e.g. Artemis.
|
||||||
|
- Artemis cannot use its in-built HA capability (clustered slave mode) as it is embedded.
|
||||||
|
- Cannot run the node with the flow state machine suspended.
|
||||||
|
- Cannot use alternative message brokers.
|
||||||
|
- Cannot run multiple nodes against the same broker.
|
||||||
|
- Cannot use alternative databases to H2.
|
||||||
|
- Cannot share the database across Corda nodes.
|
||||||
|
- RPC clients do have automatic reconnect but there is no clear solution for resynchronising on reconnect.
|
||||||
|
- The backup strategy is unclear.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
### Goals
|
||||||
|
* A logical Corda node should continue to function in the event of an individual component failure or (e.g.) restart.
|
||||||
|
* No loss, corruption or duplication of data on the ledger due to component outages
|
||||||
|
* Ensure continuity of flows throughout any disruption
|
||||||
|
* Support software upgrades in a live network
|
||||||
|
|
||||||
|
### Goals (out of scope for this design document)
|
||||||
|
* Be able to distribute a node over more than two datacenters.
|
||||||
|
* Be able to distribute a node between datacenters that are very far apart latency-wise (unless you don't care about performance).
|
||||||
|
* Be able to tolerate arbitrary byzantine failures within a node cluster.
|
||||||
|
* DR, specifically in the case of the complete failure of a site/datacentre/cluster or region will require a different solution to that specified here. For now DR is only supported where performant synchronous replication is feasible i.e. sites only a few miles apart.
|
||||||
|
|
||||||
|
## Timeline
|
||||||
|
|
||||||
|
This design document outlines a range of topologies which will be enabled through progressive enhancements from the short to long term.
|
||||||
|
|
||||||
|
On the timescales available for the current production pilot deployments we clearly do not have time to reach the ideal of a highly fault tolerant, horizontally scaled Corda.
|
||||||
|
|
||||||
|
Instead, I suggest that we can only achieve the simplest state of a standby Corda installation only by January 5th and even this is contingent on other enterprise features, such as external database and network map stabilisation being completed on this timescale, plus any issues raised by testing.
|
||||||
|
|
||||||
|
For the March 31st timeline, I hope that we can achieve a more fully automatic node failover state, with the Artemis broker running as a cluster too. I include a diagram of a fully scaled Corda for completeness and so that I can discuss what work is re-usable/throw away.
|
||||||
|
|
||||||
|
With regards to DR it is unclear how this would work where synchronous replication is not feasible. At this point we can only investigate approaches as an aside to the main thrust of work for HA support. In the synchronous replication mode it is assumed that the file and database replication can be used to ensure a cold DR backup.
|
||||||
|
|
||||||
|
## Design Decisions
|
||||||
|
|
||||||
|
The following design decisions are assumed by this design:
|
||||||
|
|
||||||
|
1. [Near-term-target](./decisions/near-term-target.md): Hot-Cold HA (see below)
|
||||||
|
2. [Medium-term target](./decisions/medium-term-target.md): Hot-Warm HA (see below)
|
||||||
|
3. [External broker](./decisions/external-broker.md): Yes
|
||||||
|
4. [Database message store](./decisions/db-msg-store.md): No
|
||||||
|
5. [IP addressing mechanism](./decisions/ip-addressing.md): Load balancer
|
||||||
|
6. [Crash shell start/stop](./decisions/crash-shell.md): No
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Target Solution
|
||||||
|
|
||||||
|
|
||||||
|
### Hot-Cold (minimum requirement)
|
||||||
|
![Hot-Cold (minimum requirement)](./HA%20deployment%20-%20Hot-Cold.png)
|
||||||
|
|
||||||
|
Small scale software failures on a node are recovered from locally via restarting/re-setting the offending component by the external (to JVM) "Health Watchdog" (HW) process. The HW process (eg a shell script or similar) would monitor parameters for java processes by periodically query them (sleep period a few seconds). This may require introduction of a few monitoring 'hooks' into Corda codebase or a "health" CorDapp the HW script can interface with. There would be a back-off logic to prevent continues restarts in the case of persistent failure.
|
||||||
|
|
||||||
|
We would provide a fully-functional sample HW script for Linux/Unix deployment platforms.
|
||||||
|
|
||||||
|
The hot-cold design provides a backup VM and Corda deployment instance that can be manually started if the primary is stopped. The failed primary must be killed to ensure it is fully stopped.
|
||||||
|
|
||||||
|
For single-node deployment scenarios the simplest supported way to recover from failures is to re-start the entire set of Corda Node processes or reboot the node OS.
|
||||||
|
|
||||||
|
For a 2-node HA deployment scenario a load balancer determines which node is active and routes traffic to that node.
|
||||||
|
The load balancer will need to monitor the health of the primary and secondary nodes and automatically route traffic from the public IP address to the only active end-point. An external solution is required for the load balancer and health monitor. In the case of Azure cloud deployments, no custom code needs to be developed to support the health monitor.
|
||||||
|
|
||||||
|
An additional component will be written to prevent accidental dual running which is likely to make use of a database heartbeat table. Code size should be minimal.
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
- This approach minimises the need for new code so can be deployed quickly.
|
||||||
|
- Use of a load balancer in the short term avoids the need for new code and configuration management to support the alternative approach of multiple advertised addresses for a single legal identity.
|
||||||
|
- Configuration of the inactive mode should be a simple mirror of the primary.
|
||||||
|
- Assumes external monitoring and management of the nodes e.g. ability to identify node failure and that Corda watchdog code will not be required (customer developed).
|
||||||
|
|
||||||
|
#### Limitations
|
||||||
|
|
||||||
|
- Slow failover as this is manually controlled.
|
||||||
|
- Requires external solutions for replication of database and Artemis journal data.
|
||||||
|
- Replication mechanism on agent banks with real servers not tested.
|
||||||
|
- Replication mechanism on Azure is under test but may prove to be too slow.
|
||||||
|
- Compatibility with external load balancers not tested. Only Azure configuration tested.
|
||||||
|
- Contingent on completion of database support and testing of replication.
|
||||||
|
- Failure of database (loss of connection) may not be supported or may require additional code.
|
||||||
|
- RPC clients assumed to make short lived RPC requests e.g. from Rest server so no support for long term clients operating across failover.
|
||||||
|
- Replication time point of the database and Artemis message data are independent and may not fully synchronise (may work subject to testing) .
|
||||||
|
- Health reporting and process controls need to be developed by the customer.
|
||||||
|
|
||||||
|
### Hot-Warm (Medium-term solution)
|
||||||
|
![Hot-Warm (Medium-term solution)](./HA%20deployment%20-%20Hot-Warm.png)
|
||||||
|
|
||||||
|
Hot-warm aims to automate failover and provide failover of individual major components e.g. Artemis.
|
||||||
|
|
||||||
|
It involves Two key changes to the hot-cold design:
|
||||||
|
1) Separation and clustering of the Artemis broker.
|
||||||
|
2) Start and stop of flow processing without JVM exit.
|
||||||
|
|
||||||
|
The consequences of these changes are that peer to peer bridging is separated from the node and a bridge control protocol must be developed.
|
||||||
|
A leader election component is a pre-cursor to load balancing – likely to be a combination of custom code and standard library and, in the short term, is likely to be via the database.
|
||||||
|
Cleaner handling of disconnects from the external components (Artemis and the database) will also be needed.
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
- Faster failover as no manual intervention.
|
||||||
|
- We can use Artemis replication protocol to replicate the message store.
|
||||||
|
- The approach is integrated with preliminary steps for the float.
|
||||||
|
- Able to handle loss of network connectivity to the database from one node.
|
||||||
|
- Extraction of Artemis server allows a more standard Artemis deployment.
|
||||||
|
- Provides protection against resource leakage in Artemis or Node from affecting the other component.
|
||||||
|
- VMs can be tuned to address different work load patterns of broker and node.
|
||||||
|
- Bridge work allows chance to support multiple IP addresses without a load balancer.
|
||||||
|
|
||||||
|
#### Limitations
|
||||||
|
|
||||||
|
- This approach will require careful testing of resource management on partial shutdown.
|
||||||
|
- No horizontal scaling support.
|
||||||
|
- Deployment of master and slave may not be completely symmetric.
|
||||||
|
- Care must be taken with upgrades to ensure master/slave election operates across updates.
|
||||||
|
- Artemis clustering does require a designated master at start-up of its cluster hence any restart involving changing the primary node will require configuration management.
|
||||||
|
- The development effort is much more significant than the hot-cold configuration.
|
||||||
|
|
||||||
|
### Hot-Hot (Long-term strategic solution)
|
||||||
|
![Hot-Hot (Long-term strategic solution)](./HA%20deployment%20-%20Hot-Hot.png)
|
||||||
|
|
||||||
|
In this configuration, all nodes are actively processing work and share a clustered database. A mechanism for sharding or distributing the work load will need to be developed.
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
- Faster failover as flows are picked up by other active nodes.
|
||||||
|
- Rapid scaling by adding additional nodes.
|
||||||
|
- Node deployment is symmetric.
|
||||||
|
- Any broker that can support AMQP can be used.
|
||||||
|
- RPC can gracefully handle failover because responsibility for the flow can be migrated across nodes without the client being aware.
|
||||||
|
|
||||||
|
#### Limitations
|
||||||
|
|
||||||
|
- Very significant work with many edge cases during failure.
|
||||||
|
- Will require handling of more states than just checkpoints e.g. soft locks and RPC subscriptions.
|
||||||
|
- Single flows will not be active on multiple nodes without future development work.
|
||||||
|
|
||||||
|
--------------------------------------------
|
||||||
|
IMPLEMENTATION PLAN
|
||||||
|
============================================
|
||||||
|
|
||||||
|
## Transitioning from Corda 2.0 to Manually Activated HA
|
||||||
|
|
||||||
|
The current Corda is built to run as a fully contained single process with the Flow logic, H2 database and Artemis broker all bundled together. This limits the options for automatic replication, or subsystem failure. Thus, we must use external mechanisms to replicate the data in the case of failure. We also should ensure that accidental dual start is not possible in case of mistakes, or slow shutdown of the primary.
|
||||||
|
|
||||||
|
Based on this situation, I suggest the following minimum development tasks are required for a tested HA deployment:
|
||||||
|
|
||||||
|
1. Complete and merge JDBC support for an external clustered database. Azure SQL Server has been identified as the most likely initial deployment. With this we should be able to point at an HA database instance for Ledger and Checkpoint data.
|
||||||
|
2. I am suggesting that for the near term we just use the Azure Load Balancer to hide the multiple machine addresses. This does require allowing a health monitoring link to the Artemis broker, but so far testing indicates that this operates without issue. Longer term we need to ensure that the network map and configuration support exists for the system to work with multiple TCP/IP endpoints advertised to external nodes. Ideally this should be rolled into the work for AMQP bridges and Floats.
|
||||||
|
3. Implement a very simple mutual exclusion feature, so that an enterprise node cannot start if another is running onto the same database. This can be via a simple heartbeat update in the database, or possibly some other library. This feature should be enabled only when specified by configuration.
|
||||||
|
4. The replication of the Artemis Message Queues will have to be via an external mechanism. On Azure we believe that the only practical solution is the 'Azure Files' approach which maps a virtual Samba drive. This we are testing in-case it is too slow to work. The mounting of separate Data Disks is possible, but they can only be mounted to one VM at a time, so they would not be compatible with the goal of no change requests for HA.
|
||||||
|
5. Improve health monitoring to better indicate fault failure. Extending the existing JMX and logging support should achieve this, although we probably need to create watchdog CordApp that verifies that the State Machine and Artemis messaging are able to process new work and to monitor flow latency.
|
||||||
|
6. Test the checkpointing mechanism and confirm that failures don't corrupt the data by deploying an HA setup on Azure and driving flows through the system as we stop the node randomly and switch to the other node. If this reveals any issues we will have to fix them.
|
||||||
|
7. Confirm that the behaviour of the RPC Client API is stable through these restarts, from the perspective of a stateless REST server calling through to RPC. The RPC API should provide positive feedback to the application, so that it can respond in a controlled fashion when disconnected.
|
||||||
|
8. Work on flow hospital tools where needed
|
||||||
|
|
||||||
|
## Moving Towards Automatic Failover HA
|
||||||
|
|
||||||
|
To move towards more automatic failover handling we need to ensure that the node can be partially active i.e. live monitoring the health status and perhaps keeping major data structures in sync for faster activation, but not actually processing flows. This needs to be reversible without leakage, or destabilising the node as it is common to use manually driven master changes to help with software upgrades and to carry out regular node shutdown and maintenance. Also, to reduce the risks associated with the uncoupled replication of the Artemis message data and the database I would recommend that we move the Artemis broker out of the node to allow us to create a failover cluster. This is also in line with the goal of creating a AMQP bridges and Floats.
|
||||||
|
|
||||||
|
To this end I would suggest packages of work that include:
|
||||||
|
|
||||||
|
1. Move the broker out of the node, which will require having a protocol that can be used to signal bridge creation and which decouples the network map. This is in line with the Flow work anyway.
|
||||||
|
2. Create a mastering solution, probably using Atomix.IO although this might require a solution with a minimum of three nodes to avoid split brain issues. Ideally this service should be extensible in the future to lead towards an eventual state with Flow level sharding. Alternatively, we may be able to add a quick enterprise adaptor to ZooKeeper as master selector if time is tight. This will inevitably impact upon configuration and deployment support.
|
||||||
|
3. Test the leakage when we repeated start-stop the Node class and fix any resource leaks, or deadlocks that occur at shutdown.
|
||||||
|
4. Switch the Artemis client code to be able to use the HA mode connection type and thus take advantage of the rapid failover code. Also, ensure that we can support multiple public IP addresses reported in the network map.
|
||||||
|
5. Implement proper detection and handling of disconnect from the external database and/or Artemis broker, which should immediately drop the master status of the node and flush any incomplete flows.
|
||||||
|
6. We should start looking at how to make RPC proxies recover from disconnect/failover, although this is probably not a top priority. However, it would be good to capture the missed results of completed flows and ensure the API allows clients to unregister/re-register Observables.
|
||||||
|
|
||||||
|
## The Future
|
||||||
|
|
||||||
|
Hopefully, most of the work from the automatic failover mode can be modified when we move to a full hot-hot sharding of flows across nodes. The mastering solution will need to be modified to negotiate finer grained claim on individual flows, rather than stopping the whole of Node. Also, the routing of messages will have to be thought about so that they go to the correct node for processing, but failover if the node dies. However, most of the other health monitoring and operational aspects should be reusable.
|
||||||
|
|
||||||
|
We also need to look at DR issues and in particular how we might handle asynchronous replication and possibly alternative recovery/reconciliation mechanisms.
|
@ -12,7 +12,7 @@ rpcAddress : "my-corda-node:10003"
|
|||||||
webAddress : "localhost:10004"
|
webAddress : "localhost:10004"
|
||||||
useHTTPS : false
|
useHTTPS : false
|
||||||
rpcUsers : [
|
rpcUsers : [
|
||||||
{ username=user1, password=letmein, permissions=[ StartProtocol.net.corda.protocols.CashProtocol ] }
|
{ username=user1, password=letmein, permissions=[ StartFlow.net.corda.protocols.CashProtocol ] }
|
||||||
]
|
]
|
||||||
devMode : true
|
devMode : true
|
||||||
// certificateSigningService : "https://testnet.certificate.corda.net"
|
// certificateSigningService : "https://testnet.certificate.corda.net"
|
||||||
|
@ -51,7 +51,7 @@ correctly interoperate with each other. If the node is using the HTTP network ma
|
|||||||
download the signed network parameters, cache it in a ``network-parameters`` file and apply them on the node.
|
download the signed network parameters, cache it in a ``network-parameters`` file and apply them on the node.
|
||||||
|
|
||||||
.. warning:: If the ``network-parameters`` file is changed and no longer matches what the network map service is advertising
|
.. warning:: If the ``network-parameters`` file is changed and no longer matches what the network map service is advertising
|
||||||
then the node will automatically shutdown. Resolution to this is to delete the incorrect file and restart the node so
|
then the node will automatically shutdown. Resolution to this is to delete the incorrect file and restart the node so
|
||||||
that the parameters can be downloaded again.
|
that the parameters can be downloaded again.
|
||||||
|
|
||||||
.. note:: A future release will support the notion of network parameters changes.
|
.. note:: A future release will support the notion of network parameters changes.
|
||||||
@ -63,15 +63,15 @@ also distributes the node info files to the node directories. More information c
|
|||||||
The current set of network parameters:
|
The current set of network parameters:
|
||||||
|
|
||||||
:minimumPlatformVersion: The minimum platform version that the nodes must be running. Any node which is below this will
|
:minimumPlatformVersion: The minimum platform version that the nodes must be running. Any node which is below this will
|
||||||
not start.
|
not start.
|
||||||
:notaries: List of identity and validation type (either validating or non-validating) of the notaries which are permitted
|
:notaries: List of identity and validation type (either validating or non-validating) of the notaries which are permitted
|
||||||
in the compatibility zone.
|
in the compatibility zone.
|
||||||
:maxMessageSize: Maximum allowed P2P message size sent over the wire in bytes. Any message larger than this will be
|
:maxMessageSize: Maximum allowed P2P message size sent over the wire in bytes. Any message larger than this will be
|
||||||
split up.
|
split up.
|
||||||
:maxTransactionSize: Maximum permitted transaction size in bytes.
|
:maxTransactionSize: Maximum permitted transaction size in bytes.
|
||||||
:modifiedTime: The time when the network parameters were last modified by the compatibility zone operator.
|
:modifiedTime: The time when the network parameters were last modified by the compatibility zone operator.
|
||||||
:epoch: Version number of the network parameters. Starting from 1, this will always increment whenever any of the
|
:epoch: Version number of the network parameters. Starting from 1, this will always increment whenever any of the
|
||||||
parameters change.
|
parameters change.
|
||||||
|
|
||||||
.. note:: ``maxTransactionSize`` is currently not enforced in the node, but will be in a later release.
|
.. note:: ``maxTransactionSize`` is currently not enforced in the node, but will be in a later release.
|
||||||
|
|
||||||
|
@ -4,6 +4,13 @@ Release notes
|
|||||||
Unreleased
|
Unreleased
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
* **Enum Class Evolution**
|
||||||
|
With the addition of AMQP serialization Corda now supports enum constant evolution.
|
||||||
|
|
||||||
|
That is the ability to alter an enum constant and, as long as certain rules are followed and the correct
|
||||||
|
annotations applied, have older and newer instances of that enumeration be understood.
|
||||||
|
|
||||||
|
|
||||||
R3 Corda 3.0 Developer Preview
|
R3 Corda 3.0 Developer Preview
|
||||||
------------------------------
|
------------------------------
|
||||||
This Developer Preview takes us towards the launch of R3 Corda, R3's commercially supported enterprise blockchain platform.
|
This Developer Preview takes us towards the launch of R3 Corda, R3's commercially supported enterprise blockchain platform.
|
||||||
|
@ -56,7 +56,7 @@ was a compelling use case for the definition and development of a custom format
|
|||||||
|
|
||||||
#. A desire to have a schema describing what has been serialized along-side the actual data:
|
#. A desire to have a schema describing what has been serialized along-side the actual data:
|
||||||
|
|
||||||
#. To assist with versioning, both in terms of being able to interpret long ago archivEd data (e.g. trades from
|
#. To assist with versioning, both in terms of being able to interpret long ago archived data (e.g. trades from
|
||||||
a decade ago, long after the code has changed) and between differing code versions.
|
a decade ago, long after the code has changed) and between differing code versions.
|
||||||
#. To make it easier to write user interfaces that can navigate the serialized form of data.
|
#. To make it easier to write user interfaces that can navigate the serialized form of data.
|
||||||
#. To support cross platform (non-JVM) interaction, where the format of a class file is not so easily interpreted.
|
#. To support cross platform (non-JVM) interaction, where the format of a class file is not so easily interpreted.
|
||||||
|
@ -140,6 +140,26 @@ Applies to both gradle deployNodes tasks and/or corda node configuration (node.c
|
|||||||
|
|
||||||
notary = [validating : true]
|
notary = [validating : true]
|
||||||
|
|
||||||
|
* For existing contract ORM schemas that extend from `CommonSchemaV1.LinearState` or `CommonSchemaV1.FungibleState`,
|
||||||
|
you will need to explicitly map the `participants` collection to a database table. Previously this mapping was done in the
|
||||||
|
superclass, but that makes it impossible to properly configure the table name.
|
||||||
|
The required change is to add the ``override var participants: MutableSet<AbstractParty>? = null`` field to your class, and
|
||||||
|
add JPA mappings. For ex., see this example:
|
||||||
|
|
||||||
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "cash_states_v2",
|
||||||
|
indexes = arrayOf(Index(name = "ccy_code_idx2", columnList = "ccy_code")))
|
||||||
|
class PersistentCashState(
|
||||||
|
|
||||||
|
@ElementCollection
|
||||||
|
@Column(name = "participants")
|
||||||
|
@CollectionTable(name="cash_states_v2_participants", joinColumns = arrayOf(
|
||||||
|
JoinColumn(name = "output_index", referencedColumnName = "output_index"),
|
||||||
|
JoinColumn(name = "transaction_id", referencedColumnName = "transaction_id")))
|
||||||
|
override var participants: MutableSet<AbstractParty>? = null,
|
||||||
|
|
||||||
Testing
|
Testing
|
||||||
^^^^^^^
|
^^^^^^^
|
||||||
|
|
||||||
|
@ -134,8 +134,8 @@ networkMapConfig {
|
|||||||
|
|
||||||
### 1. Create keystore for local signer
|
### 1. Create keystore for local signer
|
||||||
|
|
||||||
If local signer is enabled, the server will look for keystores in the certificate folder on start up.
|
If local signer is enabled, the server will look for key stores in the certificate folder on start up.
|
||||||
The keystores can be created using `--mode` flag.
|
The key stores can be created using `--mode` flag.
|
||||||
```
|
```
|
||||||
java -jar doorman-<version>.jar --mode ROOT_KEYGEN
|
java -jar doorman-<version>.jar --mode ROOT_KEYGEN
|
||||||
```
|
```
|
||||||
@ -180,8 +180,8 @@ networkMapConfig {
|
|||||||
Save the parameters to `network-parameters.conf`
|
Save the parameters to `network-parameters.conf`
|
||||||
|
|
||||||
### 5. Load initial network parameters file for network map service
|
### 5. Load initial network parameters file for network map service
|
||||||
A network parameters file is required to start the network map service for the first time. The initial network parameters file can be loaded using the `--update-network-parameter` flag.
|
A network parameters file is required to start the network map service for the first time. The initial network parameters file can be loaded using the `--update-network-parameters` flag.
|
||||||
We can now restart the network management server with both doorman and network map service.
|
We can now restart the network management server with both doorman and network map service.
|
||||||
```
|
```
|
||||||
java -jar doorman-<version>.jar --update-network-parameter network-parameters.conf
|
java -jar doorman-<version>.jar --update-network-parameters network-parameters.conf
|
||||||
```
|
```
|
||||||
|
@ -15,6 +15,7 @@ import net.corda.core.utilities.getOrThrow
|
|||||||
import net.corda.core.utilities.seconds
|
import net.corda.core.utilities.seconds
|
||||||
import net.corda.finance.DOLLARS
|
import net.corda.finance.DOLLARS
|
||||||
import net.corda.finance.flows.CashIssueAndPaymentFlow
|
import net.corda.finance.flows.CashIssueAndPaymentFlow
|
||||||
|
import net.corda.nodeapi.internal.createDevNetworkMapCa
|
||||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||||
import net.corda.nodeapi.internal.network.NetworkParameters
|
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||||
@ -60,15 +61,17 @@ class NodeRegistrationTest : IntegrationTest() {
|
|||||||
private val dbId = random63BitValue().toString()
|
private val dbId = random63BitValue().toString()
|
||||||
|
|
||||||
private lateinit var rootCaCert: X509Certificate
|
private lateinit var rootCaCert: X509Certificate
|
||||||
private lateinit var intermediateCa: CertificateAndKeyPair
|
private lateinit var csrCa: CertificateAndKeyPair
|
||||||
|
private lateinit var networkMapCa: CertificateAndKeyPair
|
||||||
|
|
||||||
private var server: NetworkManagementServer? = null
|
private var server: NetworkManagementServer? = null
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun init() {
|
fun init() {
|
||||||
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
|
val (rootCa, doormanCa) = createDevIntermediateCaCertPath()
|
||||||
rootCaCert = rootCa.certificate
|
rootCaCert = rootCa.certificate
|
||||||
this.intermediateCa = intermediateCa
|
this.csrCa = doormanCa
|
||||||
|
networkMapCa = createDevNetworkMapCa(rootCa)
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
@ -138,10 +141,15 @@ class NodeRegistrationTest : IntegrationTest() {
|
|||||||
start(
|
start(
|
||||||
serverAddress,
|
serverAddress,
|
||||||
configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(runMigration = true)),
|
configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(runMigration = true)),
|
||||||
LocalSigner(intermediateCa.keyPair, arrayOf(intermediateCa.certificate, rootCaCert)),
|
LocalSigner(csrCa.keyPair, arrayOf(csrCa.certificate, rootCaCert)),
|
||||||
networkParameters,
|
DoormanConfig(approveAll = true, jiraConfig = null, approveInterval = timeoutMillis),
|
||||||
networkParameters?.let { NetworkMapConfig(cacheTimeout = timeoutMillis, signInterval = timeoutMillis) },
|
networkParameters?.let {
|
||||||
DoormanConfig(approveAll = true, jiraConfig = null, approveInterval = timeoutMillis)
|
NetworkMapStartParams(
|
||||||
|
LocalSigner(networkMapCa.keyPair, arrayOf(networkMapCa.certificate, rootCaCert)),
|
||||||
|
networkParameters,
|
||||||
|
NetworkMapConfig(cacheTimeout = timeoutMillis, signInterval = timeoutMillis)
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
package com.r3.corda.networkmanage.hsm
|
|
||||||
|
|
||||||
import com.r3.corda.networkmanage.hsm.SigningServiceIntegrationTest.Companion.DB_NAME
|
|
||||||
import com.r3.corda.networkmanage.hsm.SigningServiceIntegrationTest.Companion.H2_TCP_PORT
|
|
||||||
import com.r3.corda.networkmanage.hsm.SigningServiceIntegrationTest.Companion.HOST
|
|
||||||
import com.r3.corda.networkmanage.hsm.configuration.Parameters
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The main method for an interactive HSM signing service test/demo. It is supposed to be executed with the
|
|
||||||
* `DEMO - Create CSR and poll` method located in the [SigningServiceIntegrationTest], which is responsible for simulating
|
|
||||||
* CSR creation on the Doorman side.
|
|
||||||
* Execution instructions:
|
|
||||||
* 1) It is assumed that the HSM simulator is installed locally (or via means of the VM) and accessible under the address
|
|
||||||
* configured under the 'device' parameter (defaults to 3001@127.0.0.1). If that is not the case please specify
|
|
||||||
* a correct 'device' parameter value. Also, it is assumed that the HSM setup consists of a cryptographic user eligible to
|
|
||||||
* sign the CSRs (and potentially to generate new root and intermediate certificates).
|
|
||||||
* 2) Run the `DEMO - Create CSR and poll` as a regular test from your IntelliJ.
|
|
||||||
* The method starts the doorman, creates 3 CSRs for ALICE, BOB and CHARLIE
|
|
||||||
* and then polls the doorman until all 3 requests are signed.
|
|
||||||
* 3) Once the `DEMO - Create CSR and poll` is started, execute the following main method
|
|
||||||
* and interact with console menu options presented.
|
|
||||||
*/
|
|
||||||
fun main(args: Array<String>) {
|
|
||||||
run(Parameters(
|
|
||||||
dataSourceProperties = makeTestDataSourceProperties(),
|
|
||||||
databaseConfig = makeNotInitialisingTestDatabaseProperties(),
|
|
||||||
csrPrivateKeyPassword = "",
|
|
||||||
networkMapPrivateKeyPassword = "",
|
|
||||||
rootPrivateKeyPassword = "",
|
|
||||||
keyGroup = "DEV.DOORMAN",
|
|
||||||
validDays = 3650
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun makeTestDataSourceProperties(): Properties {
|
|
||||||
val props = Properties()
|
|
||||||
props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource")
|
|
||||||
props.setProperty("dataSource.url", "jdbc:h2:tcp://$HOST:$H2_TCP_PORT/mem:$DB_NAME;DB_CLOSE_DELAY=-1")
|
|
||||||
props.setProperty("dataSource.user", "sa")
|
|
||||||
props.setProperty("dataSource.password", "")
|
|
||||||
return props
|
|
||||||
}
|
|
@ -22,27 +22,25 @@ import net.corda.nodeapi.internal.createDevNodeCa
|
|||||||
import net.corda.nodeapi.internal.crypto.*
|
import net.corda.nodeapi.internal.crypto.*
|
||||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||||
import net.corda.testing.ALICE_NAME
|
import net.corda.testing.ALICE_NAME
|
||||||
import net.corda.testing.BOB_NAME
|
|
||||||
import net.corda.testing.CHARLIE_NAME
|
|
||||||
import net.corda.testing.SerializationEnvironmentRule
|
import net.corda.testing.SerializationEnvironmentRule
|
||||||
import net.corda.testing.internal.createDevIntermediateCaCertPath
|
import net.corda.testing.internal.createDevIntermediateCaCertPath
|
||||||
import net.corda.testing.internal.rigorousMock
|
import net.corda.testing.internal.rigorousMock
|
||||||
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
|
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
|
||||||
import org.h2.tools.Server
|
import org.junit.After
|
||||||
import org.junit.*
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
import org.junit.rules.TemporaryFolder
|
import org.junit.rules.TemporaryFolder
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.persistence.PersistenceException
|
import javax.persistence.PersistenceException
|
||||||
import kotlin.concurrent.scheduleAtFixedRate
|
import kotlin.concurrent.scheduleAtFixedRate
|
||||||
import kotlin.concurrent.thread
|
|
||||||
|
|
||||||
class SigningServiceIntegrationTest {
|
class SigningServiceIntegrationTest {
|
||||||
companion object {
|
companion object {
|
||||||
val H2_TCP_PORT = "8092"
|
private val HOST = "localhost"
|
||||||
val HOST = "localhost"
|
private val DB_NAME = "test_db"
|
||||||
val DB_NAME = "test_db"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
@ -92,7 +90,7 @@ class SigningServiceIntegrationTest {
|
|||||||
val database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(runMigration = true))
|
val database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(runMigration = true))
|
||||||
|
|
||||||
NetworkManagementServer().use { server ->
|
NetworkManagementServer().use { server ->
|
||||||
server.start(NetworkHostAndPort(HOST, 0), database, networkMapServiceParameter = null, doormanServiceParameter = DoormanConfig(approveAll = true, approveInterval = 2.seconds.toMillis(), jiraConfig = null), updateNetworkParameters = null)
|
server.start(NetworkHostAndPort(HOST, 0), database, doormanServiceParameter = DoormanConfig(approveAll = true, approveInterval = 2.seconds.toMillis(), jiraConfig = null), startNetworkMap = null)
|
||||||
val doormanHostAndPort = server.hostAndPort
|
val doormanHostAndPort = server.hostAndPort
|
||||||
// Start Corda network registration.
|
// Start Corda network registration.
|
||||||
val config = createConfig().also {
|
val config = createConfig().also {
|
||||||
@ -131,51 +129,6 @@ class SigningServiceIntegrationTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Piece of code is purely for demo purposes and should not be considered as actual test (therefore it is ignored).
|
|
||||||
* Its purpose is to produce 3 CSRs and wait (polling Doorman) for external signature.
|
|
||||||
* The use of the jUnit testing framework was chosen due to the convenience reasons: mocking, tempFolder storage.
|
|
||||||
* It is meant to be run together with the [DemoMain.main] method, which executes HSM signing service.
|
|
||||||
* The split is done due to the limited console support while executing tests and inability to capture user's input there.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Ignore
|
|
||||||
@Test
|
|
||||||
fun `DEMO - Create CSR and poll`() {
|
|
||||||
//Start doorman server
|
|
||||||
val database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(runMigration = true))
|
|
||||||
|
|
||||||
NetworkManagementServer().use { server ->
|
|
||||||
server.start(NetworkHostAndPort(HOST, 0), database, networkMapServiceParameter = null, doormanServiceParameter = DoormanConfig(approveAll = true, approveInterval = 2.seconds.toMillis(), jiraConfig = null), updateNetworkParameters = null)
|
|
||||||
thread(start = true, isDaemon = true) {
|
|
||||||
val h2ServerArgs = arrayOf("-tcpPort", H2_TCP_PORT, "-tcpAllowOthers")
|
|
||||||
Server.createTcpServer(*h2ServerArgs).start()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start Corda network registration.
|
|
||||||
(1..3).map { num ->
|
|
||||||
thread(start = true) {
|
|
||||||
// Start Corda network registration.
|
|
||||||
val config = createConfig().also {
|
|
||||||
doReturn(when (num) {
|
|
||||||
1 -> ALICE_NAME
|
|
||||||
2 -> BOB_NAME
|
|
||||||
3 -> CHARLIE_NAME
|
|
||||||
else -> throw IllegalArgumentException("Unrecognised option")
|
|
||||||
}).whenever(it).myLegalName
|
|
||||||
doReturn(URL("http://$HOST:${server.hostAndPort.port}")).whenever(it).compatibilityZoneURL
|
|
||||||
}
|
|
||||||
config.certificatesDirectory.createDirectories()
|
|
||||||
loadOrCreateKeyStore(config.trustStoreFile, config.trustStorePassword).also {
|
|
||||||
it.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCaCert)
|
|
||||||
it.save(config.trustStoreFile, config.trustStorePassword)
|
|
||||||
}
|
|
||||||
NetworkRegistrationHelper(config, HTTPNetworkRegistrationService(config.compatibilityZoneURL!!)).buildKeystore()
|
|
||||||
}
|
|
||||||
}.map { it.join() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createConfig(): NodeConfiguration {
|
private fun createConfig(): NodeConfiguration {
|
||||||
return rigorousMock<NodeConfiguration>().also {
|
return rigorousMock<NodeConfiguration>().also {
|
||||||
doReturn(tempFolder.root.toPath()).whenever(it).baseDirectory
|
doReturn(tempFolder.root.toPath()).whenever(it).baseDirectory
|
||||||
@ -198,5 +151,3 @@ class SigningServiceIntegrationTest {
|
|||||||
return props
|
return props
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun makeNotInitialisingTestDatabaseProperties() = DatabaseConfig(runMigration = false)
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
package com.r3.corda.networkmanage.common.persistence
|
package com.r3.corda.networkmanage.common.persistence
|
||||||
|
|
||||||
|
import com.r3.corda.networkmanage.common.utils.SignedNetworkMap
|
||||||
|
import com.r3.corda.networkmanage.common.utils.SignedNetworkParameters
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.SignedData
|
|
||||||
import net.corda.nodeapi.internal.network.NetworkParameters
|
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||||
import net.corda.nodeapi.internal.network.SignedNetworkMap
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data access object interface for NetworkMap persistence layer
|
* Data access object interface for NetworkMap persistence layer
|
||||||
@ -34,7 +34,7 @@ interface NetworkMapStorage {
|
|||||||
* Return the signed network parameters object which matches the given hash. The hash is that of the underlying
|
* Return the signed network parameters object which matches the given hash. The hash is that of the underlying
|
||||||
* [NetworkParameters] object and not the `SignedData<NetworkParameters>` object that's returned.
|
* [NetworkParameters] object and not the `SignedData<NetworkParameters>` object that's returned.
|
||||||
*/
|
*/
|
||||||
fun getSignedNetworkParameters(hash: SecureHash): SignedData<NetworkParameters>?
|
fun getSignedNetworkParameters(hash: SecureHash): SignedNetworkParameters?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve network map parameters.
|
* Retrieve network map parameters.
|
||||||
|
@ -1,17 +1,16 @@
|
|||||||
package com.r3.corda.networkmanage.common.persistence
|
package com.r3.corda.networkmanage.common.persistence
|
||||||
|
|
||||||
import com.r3.corda.networkmanage.common.persistence.entity.*
|
import com.r3.corda.networkmanage.common.persistence.entity.*
|
||||||
|
import com.r3.corda.networkmanage.common.utils.SignedNetworkMap
|
||||||
|
import com.r3.corda.networkmanage.common.utils.SignedNetworkParameters
|
||||||
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
|
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
|
||||||
import net.corda.core.crypto.DigitalSignature
|
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.SignedData
|
|
||||||
import net.corda.core.crypto.sha256
|
import net.corda.core.crypto.sha256
|
||||||
import net.corda.core.serialization.SerializedBytes
|
import net.corda.core.serialization.SerializedBytes
|
||||||
import net.corda.core.serialization.deserialize
|
import net.corda.core.serialization.deserialize
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.nodeapi.internal.network.NetworkMap
|
import net.corda.nodeapi.internal.network.NetworkMap
|
||||||
import net.corda.nodeapi.internal.network.NetworkParameters
|
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||||
import net.corda.nodeapi.internal.network.SignedNetworkMap
|
|
||||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -40,8 +39,8 @@ class PersistentNetworkMapStorage(private val database: CordaPersistence, privat
|
|||||||
database.transaction {
|
database.transaction {
|
||||||
val networkMapEntity = NetworkMapEntity(
|
val networkMapEntity = NetworkMapEntity(
|
||||||
networkMap = signedNetworkMap.raw.bytes,
|
networkMap = signedNetworkMap.raw.bytes,
|
||||||
signature = signedNetworkMap.signature.signatureBytes,
|
signature = signedNetworkMap.sig.bytes,
|
||||||
certificate = signedNetworkMap.signature.by.encoded
|
certificate = signedNetworkMap.sig.by.encoded
|
||||||
)
|
)
|
||||||
session.save(networkMapEntity)
|
session.save(networkMapEntity)
|
||||||
}
|
}
|
||||||
@ -49,10 +48,10 @@ class PersistentNetworkMapStorage(private val database: CordaPersistence, privat
|
|||||||
|
|
||||||
// TODO The signing cannot occur here as it won't work with an HSM. The signed network parameters needs to be persisted
|
// TODO The signing cannot occur here as it won't work with an HSM. The signed network parameters needs to be persisted
|
||||||
// into the database.
|
// into the database.
|
||||||
override fun getSignedNetworkParameters(hash: SecureHash): SignedData<NetworkParameters>? {
|
override fun getSignedNetworkParameters(hash: SecureHash): SignedNetworkParameters? {
|
||||||
val netParamsBytes = getNetworkParametersEntity(hash.toString())?.parametersBytes ?: return null
|
val netParamsBytes = getNetworkParametersEntity(hash.toString())?.parametersBytes ?: return null
|
||||||
val sigWithCert = localSigner!!.sign(netParamsBytes)
|
val sigWithCert = localSigner!!.signBytes(netParamsBytes)
|
||||||
return SignedData(SerializedBytes(netParamsBytes), DigitalSignature.WithKey(sigWithCert.by.publicKey, sigWithCert.signatureBytes))
|
return SignedNetworkParameters(SerializedBytes(netParamsBytes), sigWithCert)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getNodeInfoHashes(certificateStatus: CertificateStatus): List<SecureHash> {
|
override fun getNodeInfoHashes(certificateStatus: CertificateStatus): List<SecureHash> {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package com.r3.corda.networkmanage.common.persistence.entity
|
package com.r3.corda.networkmanage.common.persistence.entity
|
||||||
|
|
||||||
|
import net.corda.core.internal.DigitalSignatureWithCert
|
||||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||||
import net.corda.nodeapi.internal.network.DigitalSignatureWithCert
|
|
||||||
import javax.persistence.*
|
import javax.persistence.*
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@ -24,7 +24,7 @@ class NetworkMapEntity(
|
|||||||
val certificate: ByteArray
|
val certificate: ByteArray
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* Deserializes NetworkMapEntity.signatureBytes into the [SignatureAndCertPath] instance
|
* Deserializes NetworkMapEntity.signatureBytes into the [DigitalSignatureWithCert] instance
|
||||||
*/
|
*/
|
||||||
fun signatureAndCertificate(): DigitalSignatureWithCert {
|
fun signatureAndCertificate(): DigitalSignatureWithCert {
|
||||||
return DigitalSignatureWithCert(X509CertificateFactory().generateCertificate(certificate.inputStream()), signature)
|
return DigitalSignatureWithCert(X509CertificateFactory().generateCertificate(certificate.inputStream()), signature)
|
||||||
|
@ -2,9 +2,9 @@ package com.r3.corda.networkmanage.common.signer
|
|||||||
|
|
||||||
import com.r3.corda.networkmanage.common.persistence.CertificateStatus
|
import com.r3.corda.networkmanage.common.persistence.CertificateStatus
|
||||||
import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
|
import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
|
||||||
|
import net.corda.core.internal.SignedDataWithCert
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.nodeapi.internal.network.NetworkMap
|
import net.corda.nodeapi.internal.network.NetworkMap
|
||||||
import net.corda.nodeapi.internal.network.SignedNetworkMap
|
|
||||||
|
|
||||||
class NetworkMapSigner(private val networkMapStorage: NetworkMapStorage, private val signer: Signer) {
|
class NetworkMapSigner(private val networkMapStorage: NetworkMapStorage, private val signer: Signer) {
|
||||||
/**
|
/**
|
||||||
@ -14,12 +14,10 @@ class NetworkMapSigner(private val networkMapStorage: NetworkMapStorage, private
|
|||||||
val currentSignedNetworkMap = networkMapStorage.getCurrentNetworkMap()
|
val currentSignedNetworkMap = networkMapStorage.getCurrentNetworkMap()
|
||||||
val nodeInfoHashes = networkMapStorage.getNodeInfoHashes(CertificateStatus.VALID)
|
val nodeInfoHashes = networkMapStorage.getNodeInfoHashes(CertificateStatus.VALID)
|
||||||
val networkParameters = networkMapStorage.getLatestNetworkParameters()
|
val networkParameters = networkMapStorage.getLatestNetworkParameters()
|
||||||
val networkMap = NetworkMap(nodeInfoHashes, networkParameters.serialize().hash)
|
val serialisedNetworkMap = NetworkMap(nodeInfoHashes, networkParameters.serialize().hash).serialize()
|
||||||
// We wan only check if the data structure is same.
|
if (serialisedNetworkMap != currentSignedNetworkMap?.raw) {
|
||||||
if (networkMap != currentSignedNetworkMap?.verified(null)) {
|
val newSignedNetworkMap = SignedDataWithCert(serialisedNetworkMap, signer.signBytes(serialisedNetworkMap.bytes))
|
||||||
val digitalSignature = signer.sign(networkMap.serialize().bytes)
|
networkMapStorage.saveNetworkMap(newSignedNetworkMap)
|
||||||
val signedHashedNetworkMap = SignedNetworkMap(networkMap.serialize(), digitalSignature)
|
|
||||||
networkMapStorage.saveNetworkMap(signedHashedNetworkMap)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,17 +1,24 @@
|
|||||||
package com.r3.corda.networkmanage.common.signer
|
package com.r3.corda.networkmanage.common.signer
|
||||||
|
|
||||||
import net.corda.nodeapi.internal.network.DigitalSignatureWithCert
|
import net.corda.core.internal.DigitalSignatureWithCert
|
||||||
|
import net.corda.core.internal.SignedDataWithCert
|
||||||
|
import net.corda.core.serialization.serialize
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An interface for arbitrary data signing functionality.
|
* An interface for arbitrary data signing functionality.
|
||||||
*/
|
*/
|
||||||
interface Signer {
|
interface Signer {
|
||||||
/**
|
/**
|
||||||
* Signs given [data]. The signing key selction strategy is left to the implementing class.
|
* Signs given bytes. The signing key selction strategy is left to the implementing class.
|
||||||
* @return [SignatureAndCertPath] that encapsulates the signature and the certificate path used in the signing process.
|
* @return [DigitalSignatureWithCert] that encapsulates the signature and the certificate path used in the signing process.
|
||||||
* @throws [AuthenticationException] if fails authentication
|
* @throws [AuthenticationException] if fails authentication
|
||||||
*/
|
*/
|
||||||
fun sign(data: ByteArray): DigitalSignatureWithCert
|
fun signBytes(data: ByteArray): DigitalSignatureWithCert
|
||||||
|
|
||||||
|
fun <T : Any> signObject(obj: T): SignedDataWithCert<T> {
|
||||||
|
val serialised = obj.serialize()
|
||||||
|
return SignedDataWithCert(serialised, signBytes(serialised.bytes))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthenticationException : Exception()
|
class AuthenticationException : Exception()
|
||||||
|
@ -5,15 +5,18 @@ import com.typesafe.config.Config
|
|||||||
import com.typesafe.config.ConfigFactory
|
import com.typesafe.config.ConfigFactory
|
||||||
import joptsimple.ArgumentAcceptingOptionSpec
|
import joptsimple.ArgumentAcceptingOptionSpec
|
||||||
import joptsimple.OptionParser
|
import joptsimple.OptionParser
|
||||||
import net.corda.core.crypto.DigitalSignature
|
|
||||||
import net.corda.core.crypto.sha256
|
import net.corda.core.crypto.sha256
|
||||||
|
import net.corda.core.internal.SignedDataWithCert
|
||||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||||
import net.corda.nodeapi.internal.network.DigitalSignatureWithCert
|
import net.corda.nodeapi.internal.network.NetworkMap
|
||||||
import org.bouncycastle.cert.X509CertificateHolder
|
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.security.cert.CertPath
|
import java.security.cert.CertPath
|
||||||
import java.security.cert.Certificate
|
import java.security.cert.Certificate
|
||||||
import java.security.cert.X509Certificate
|
|
||||||
|
// TODO These should be defined in node-api
|
||||||
|
typealias SignedNetworkParameters = SignedDataWithCert<NetworkParameters>
|
||||||
|
typealias SignedNetworkMap = SignedDataWithCert<NetworkMap>
|
||||||
|
|
||||||
// TODO: replace this with Crypto.hash when its available.
|
// TODO: replace this with Crypto.hash when its available.
|
||||||
/**
|
/**
|
||||||
@ -39,15 +42,10 @@ fun Array<out String>.toConfigWithOptions(registerOptions: OptionParser.() -> Un
|
|||||||
|
|
||||||
class ShowHelpException(val parser: OptionParser, val errorMessage: String? = null) : Exception()
|
class ShowHelpException(val parser: OptionParser, val errorMessage: String? = null) : Exception()
|
||||||
|
|
||||||
// TODO Remove this as we already have InternalUtils.cert
|
|
||||||
fun X509CertificateHolder.toX509Certificate(): X509Certificate = X509CertificateFactory().generateCertificate(encoded.inputStream())
|
|
||||||
|
|
||||||
fun buildCertPath(vararg certificates: Certificate): CertPath = X509CertificateFactory().delegate.generateCertPath(certificates.asList())
|
fun buildCertPath(vararg certificates: Certificate): CertPath = X509CertificateFactory().delegate.generateCertPath(certificates.asList())
|
||||||
|
|
||||||
fun buildCertPath(certPathBytes: ByteArray): CertPath = X509CertificateFactory().delegate.generateCertPath(certPathBytes.inputStream())
|
fun buildCertPath(certPathBytes: ByteArray): CertPath = X509CertificateFactory().delegate.generateCertPath(certPathBytes.inputStream())
|
||||||
|
|
||||||
fun DigitalSignature.WithKey.withCert(cert: X509Certificate): DigitalSignatureWithCert = DigitalSignatureWithCert(cert, bytes)
|
|
||||||
|
|
||||||
private fun String.toCamelcase(): String {
|
private fun String.toCamelcase(): String {
|
||||||
return if (contains('_') || contains('-')) {
|
return if (contains('_') || contains('-')) {
|
||||||
CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, this.replace("-", "_"))
|
CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, this.replace("-", "_"))
|
||||||
|
@ -53,10 +53,11 @@ data class DoormanConfig(val approveAll: Boolean = false,
|
|||||||
val approveInterval: Long = NetworkManagementServerParameters.DEFAULT_APPROVE_INTERVAL.toMillis())
|
val approveInterval: Long = NetworkManagementServerParameters.DEFAULT_APPROVE_INTERVAL.toMillis())
|
||||||
|
|
||||||
data class NetworkMapConfig(val cacheTimeout: Long,
|
data class NetworkMapConfig(val cacheTimeout: Long,
|
||||||
// TODO: Move signing to signing server.
|
// TODO: Move signing to signing server.
|
||||||
val signInterval: Long = NetworkManagementServerParameters.DEFAULT_SIGN_INTERVAL.toMillis())
|
val signInterval: Long = NetworkManagementServerParameters.DEFAULT_SIGN_INTERVAL.toMillis())
|
||||||
|
|
||||||
enum class Mode {
|
enum class Mode {
|
||||||
|
// TODO CA_KEYGEN now also generates the nework map cert, so it should be renamed.
|
||||||
DOORMAN, CA_KEYGEN, ROOT_KEYGEN
|
DOORMAN, CA_KEYGEN, ROOT_KEYGEN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,10 @@ import com.r3.corda.networkmanage.doorman.signer.LocalSigner
|
|||||||
import com.r3.corda.networkmanage.doorman.webservice.MonitoringWebService
|
import com.r3.corda.networkmanage.doorman.webservice.MonitoringWebService
|
||||||
import com.r3.corda.networkmanage.doorman.webservice.NodeInfoWebService
|
import com.r3.corda.networkmanage.doorman.webservice.NodeInfoWebService
|
||||||
import com.r3.corda.networkmanage.doorman.webservice.RegistrationWebService
|
import com.r3.corda.networkmanage.doorman.webservice.RegistrationWebService
|
||||||
|
import com.r3.corda.networkmanage.hsm.configuration.Parameters.Companion.DEFAULT_CSR_CERTIFICATE_NAME
|
||||||
|
import com.r3.corda.networkmanage.hsm.configuration.Parameters.Companion.DEFAULT_NETWORK_MAP_CERTIFICATE_NAME
|
||||||
import net.corda.core.crypto.Crypto
|
import net.corda.core.crypto.Crypto
|
||||||
|
import net.corda.core.crypto.SignatureScheme
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.internal.createDirectories
|
import net.corda.core.internal.createDirectories
|
||||||
import net.corda.core.internal.div
|
import net.corda.core.internal.div
|
||||||
@ -34,6 +37,7 @@ import java.time.Instant
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.security.auth.x500.X500Principal
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
@ -130,17 +134,16 @@ class NetworkManagementServer : Closeable {
|
|||||||
|
|
||||||
fun start(hostAndPort: NetworkHostAndPort,
|
fun start(hostAndPort: NetworkHostAndPort,
|
||||||
database: CordaPersistence,
|
database: CordaPersistence,
|
||||||
signer: LocalSigner? = null,
|
doormanSigner: LocalSigner? = null,
|
||||||
updateNetworkParameters: NetworkParameters?,
|
doormanServiceParameter: DoormanConfig?, // TODO Doorman config shouldn't be optional as the doorman is always required to run
|
||||||
networkMapServiceParameter: NetworkMapConfig?,
|
startNetworkMap: NetworkMapStartParams?
|
||||||
doormanServiceParameter: DoormanConfig?) {
|
) {
|
||||||
|
|
||||||
val services = mutableListOf<Any>()
|
val services = mutableListOf<Any>()
|
||||||
val serverStatus = NetworkManagementServerStatus()
|
val serverStatus = NetworkManagementServerStatus()
|
||||||
|
|
||||||
// TODO: move signing to signing server.
|
// TODO: move signing to signing server.
|
||||||
networkMapServiceParameter?.let { services += getNetworkMapService(it, database, signer, updateNetworkParameters) }
|
startNetworkMap?.let { services += getNetworkMapService(it.config, database, it.signer, it.updateNetworkParameters) }
|
||||||
doormanServiceParameter?.let { services += getDoormanService(it, database, signer, serverStatus) }
|
doormanServiceParameter?.let { services += getDoormanService(it, database, doormanSigner, serverStatus) }
|
||||||
|
|
||||||
require(services.isNotEmpty()) { "No service created, please provide at least one service config." }
|
require(services.isNotEmpty()) { "No service created, please provide at least one service config." }
|
||||||
|
|
||||||
@ -150,11 +153,13 @@ class NetworkManagementServer : Closeable {
|
|||||||
val webServer = NetworkManagementWebServer(hostAndPort, *services.toTypedArray())
|
val webServer = NetworkManagementWebServer(hostAndPort, *services.toTypedArray())
|
||||||
webServer.start()
|
webServer.start()
|
||||||
|
|
||||||
doOnClose += { webServer.close() }
|
doOnClose += webServer::close
|
||||||
this.hostAndPort = webServer.hostAndPort
|
this.hostAndPort = webServer.hostAndPort
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class NetworkMapStartParams(val signer: LocalSigner?, val updateNetworkParameters: NetworkParameters?, val config: NetworkMapConfig)
|
||||||
|
|
||||||
data class NetworkManagementServerStatus(var serverStartTime: Instant = Instant.now(), var lastRequestCheckTime: Instant? = null)
|
data class NetworkManagementServerStatus(var serverStartTime: Instant = Instant.now(), var lastRequestCheckTime: Instant? = null)
|
||||||
|
|
||||||
/** Read password from console, do a readLine instead if console is null (e.g. when debugging in IDE). */
|
/** Read password from console, do a readLine instead if console is null (e.g. when debugging in IDE). */
|
||||||
@ -203,58 +208,77 @@ fun generateRootKeyPair(rootStoreFile: Path, rootKeystorePass: String?, rootPriv
|
|||||||
println(loadKeyStore(rootStoreFile, rootKeystorePassword).getCertificate(X509Utilities.CORDA_ROOT_CA).publicKey)
|
println(loadKeyStore(rootStoreFile, rootKeystorePassword).getCertificate(X509Utilities.CORDA_ROOT_CA).publicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun generateCAKeyPair(keystoreFile: Path, rootStoreFile: Path, rootKeystorePass: String?, rootPrivateKeyPass: String?, keystorePass: String?, caPrivateKeyPass: String?) {
|
fun generateSigningKeyPairs(keystoreFile: Path, rootStoreFile: Path, rootKeystorePass: String?, rootPrivateKeyPass: String?, keystorePass: String?, caPrivateKeyPass: String?) {
|
||||||
println("Generating Intermediate CA keypair and certificate using root keystore $rootStoreFile.")
|
println("Generating intermediate and network map key pairs and certificates using root key store $rootStoreFile.")
|
||||||
// Get password from console if not in config.
|
// Get password from console if not in config.
|
||||||
val rootKeystorePassword = rootKeystorePass ?: readPassword("Root Keystore Password: ")
|
val rootKeystorePassword = rootKeystorePass ?: readPassword("Root key store password: ")
|
||||||
val rootPrivateKeyPassword = rootPrivateKeyPass ?: readPassword("Root Private Key Password: ")
|
val rootPrivateKeyPassword = rootPrivateKeyPass ?: readPassword("Root private key password: ")
|
||||||
val rootKeyStore = loadKeyStore(rootStoreFile, rootKeystorePassword)
|
val rootKeyStore = loadKeyStore(rootStoreFile, rootKeystorePassword)
|
||||||
|
|
||||||
val rootKeyAndCert = rootKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_ROOT_CA, rootPrivateKeyPassword)
|
val rootKeyPairAndCert = rootKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_ROOT_CA, rootPrivateKeyPassword)
|
||||||
|
|
||||||
val keystorePassword = keystorePass ?: readPassword("Keystore Password: ")
|
val keyStorePassword = keystorePass ?: readPassword("Key store Password: ")
|
||||||
val caPrivateKeyPassword = caPrivateKeyPass ?: readPassword("CA Private Key Password: ")
|
val privateKeyPassword = caPrivateKeyPass ?: readPassword("Private key Password: ")
|
||||||
// Ensure folder exists.
|
// Ensure folder exists.
|
||||||
keystoreFile.parent.createDirectories()
|
keystoreFile.parent.createDirectories()
|
||||||
val keyStore = loadOrCreateKeyStore(keystoreFile, keystorePassword)
|
val keyStore = loadOrCreateKeyStore(keystoreFile, keyStorePassword)
|
||||||
|
|
||||||
if (keyStore.containsAlias(X509Utilities.CORDA_INTERMEDIATE_CA)) {
|
fun storeCertIfAbsent(alias: String, certificateType: CertificateType, subject: X500Principal, signatureScheme: SignatureScheme) {
|
||||||
val oldKey = loadOrCreateKeyStore(keystoreFile, rootKeystorePassword).getCertificate(X509Utilities.CORDA_INTERMEDIATE_CA).publicKey
|
if (keyStore.containsAlias(alias)) {
|
||||||
println("Key ${X509Utilities.CORDA_INTERMEDIATE_CA} already exists in keystore, process will now terminate.")
|
println("$alias already exists in keystore:")
|
||||||
println(oldKey)
|
println(keyStore.getCertificate(alias))
|
||||||
exitProcess(1)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val keyPair = Crypto.generateKeyPair(signatureScheme)
|
||||||
|
val cert = X509Utilities.createCertificate(
|
||||||
|
certificateType,
|
||||||
|
rootKeyPairAndCert.certificate,
|
||||||
|
rootKeyPairAndCert.keyPair,
|
||||||
|
subject,
|
||||||
|
keyPair.public
|
||||||
|
)
|
||||||
|
keyStore.addOrReplaceKey(
|
||||||
|
alias,
|
||||||
|
keyPair.private,
|
||||||
|
privateKeyPassword.toCharArray(),
|
||||||
|
arrayOf(cert, rootKeyPairAndCert.certificate)
|
||||||
|
)
|
||||||
|
keyStore.save(keystoreFile, keyStorePassword)
|
||||||
|
|
||||||
|
println("$certificateType key pair and certificate stored in $keystoreFile.")
|
||||||
|
println(cert)
|
||||||
}
|
}
|
||||||
|
|
||||||
val intermediateKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
storeCertIfAbsent(
|
||||||
val intermediateCert = X509Utilities.createCertificate(
|
DEFAULT_CSR_CERTIFICATE_NAME,
|
||||||
CertificateType.INTERMEDIATE_CA,
|
CertificateType.INTERMEDIATE_CA,
|
||||||
rootKeyAndCert.certificate,
|
X500Principal("CN=Corda Intermediate CA,OU=Corda,O=R3 Ltd,L=London,C=GB"),
|
||||||
rootKeyAndCert.keyPair,
|
X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||||
CordaX500Name(commonName = "Corda Intermediate CA", organisation = "R3 Ltd", organisationUnit = "Corda", locality = "London", country = "GB", state = null).x500Principal,
|
|
||||||
intermediateKeyPair.public
|
storeCertIfAbsent(
|
||||||
)
|
DEFAULT_NETWORK_MAP_CERTIFICATE_NAME,
|
||||||
keyStore.addOrReplaceKey(
|
CertificateType.NETWORK_MAP,
|
||||||
X509Utilities.CORDA_INTERMEDIATE_CA,
|
X500Principal("CN=Corda Network Map,OU=Corda,O=R3 Ltd,L=London,C=GB"),
|
||||||
intermediateKeyPair.private,
|
Crypto.EDDSA_ED25519_SHA512)
|
||||||
caPrivateKeyPassword.toCharArray(),
|
|
||||||
arrayOf(intermediateCert, rootKeyAndCert.certificate)
|
|
||||||
)
|
|
||||||
keyStore.save(keystoreFile, keystorePassword)
|
|
||||||
println("Intermediate CA keypair and certificate stored in $keystoreFile.")
|
|
||||||
println(loadKeyStore(keystoreFile, keystorePassword).getCertificate(X509Utilities.CORDA_INTERMEDIATE_CA).publicKey)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun buildLocalSigner(parameters: NetworkManagementServerParameters): LocalSigner? {
|
private fun buildLocalSigners(parameters: NetworkManagementServerParameters): Pair<LocalSigner, LocalSigner>? {
|
||||||
return parameters.keystorePath?.let {
|
if (parameters.keystorePath == null) return null
|
||||||
// Get password from console if not in config.
|
|
||||||
val keystorePassword = parameters.keystorePassword ?: readPassword("Keystore Password: ")
|
// Get password from console if not in config.
|
||||||
val caPrivateKeyPassword = parameters.caPrivateKeyPassword ?: readPassword("CA Private Key Password: ")
|
val keyStorePassword = parameters.keystorePassword ?: readPassword("Key store password: ")
|
||||||
val keystore = loadOrCreateKeyStore(parameters.keystorePath, keystorePassword)
|
val privateKeyPassword = parameters.caPrivateKeyPassword ?: readPassword("Private key password: ")
|
||||||
val caKeyPair = keystore.getKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, caPrivateKeyPassword)
|
val keyStore = loadOrCreateKeyStore(parameters.keystorePath, keyStorePassword)
|
||||||
val caCertPath = keystore.getCertificateChain(X509Utilities.CORDA_INTERMEDIATE_CA).map { it as X509Certificate }
|
|
||||||
LocalSigner(caKeyPair, caCertPath.toTypedArray())
|
val (doormanSigner, networkMapSigner) = listOf(DEFAULT_CSR_CERTIFICATE_NAME, DEFAULT_NETWORK_MAP_CERTIFICATE_NAME).map {
|
||||||
|
val keyPair = keyStore.getKeyPair(it, privateKeyPassword)
|
||||||
|
val certPath = keyStore.getCertificateChain(it).map { it as X509Certificate }
|
||||||
|
LocalSigner(keyPair, certPath.toTypedArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Pair(doormanSigner, networkMapSigner)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -278,7 +302,7 @@ fun main(args: Array<String>) {
|
|||||||
rootStorePath ?: throw IllegalArgumentException("The 'rootStorePath' parameter must be specified when generating keys!"),
|
rootStorePath ?: throw IllegalArgumentException("The 'rootStorePath' parameter must be specified when generating keys!"),
|
||||||
rootKeystorePassword,
|
rootKeystorePassword,
|
||||||
rootPrivateKeyPassword)
|
rootPrivateKeyPassword)
|
||||||
Mode.CA_KEYGEN -> generateCAKeyPair(
|
Mode.CA_KEYGEN -> generateSigningKeyPairs(
|
||||||
keystorePath ?: throw IllegalArgumentException("The 'keystorePath' parameter must be specified when generating keys!"),
|
keystorePath ?: throw IllegalArgumentException("The 'keystorePath' parameter must be specified when generating keys!"),
|
||||||
rootStorePath ?: throw IllegalArgumentException("The 'rootStorePath' parameter must be specified when generating keys!"),
|
rootStorePath ?: throw IllegalArgumentException("The 'rootStorePath' parameter must be specified when generating keys!"),
|
||||||
rootKeystorePassword,
|
rootKeystorePassword,
|
||||||
@ -289,18 +313,24 @@ fun main(args: Array<String>) {
|
|||||||
initialiseSerialization()
|
initialiseSerialization()
|
||||||
val database = configureDatabase(dataSourceProperties)
|
val database = configureDatabase(dataSourceProperties)
|
||||||
// TODO: move signing to signing server.
|
// TODO: move signing to signing server.
|
||||||
val signer = buildLocalSigner(this)
|
val localSigners = buildLocalSigners(this)
|
||||||
|
|
||||||
if (signer != null) {
|
if (localSigners != null) {
|
||||||
println("Starting network management services with local signer.")
|
println("Starting network management services with local signing")
|
||||||
}
|
}
|
||||||
|
|
||||||
val networkManagementServer = NetworkManagementServer()
|
val networkManagementServer = NetworkManagementServer()
|
||||||
val networkParameter = updateNetworkParameters?.let {
|
val networkParameters = updateNetworkParameters?.let {
|
||||||
println("Parsing network parameter from '${it.fileName}'...")
|
// TODO This check shouldn't be needed. Fix up the config design.
|
||||||
|
requireNotNull(networkMapConfig) { "'networkMapConfig' config is required for applying network parameters" }
|
||||||
|
println("Parsing network parameters from '${it.toAbsolutePath()}'...")
|
||||||
parseNetworkParametersFrom(it)
|
parseNetworkParametersFrom(it)
|
||||||
}
|
}
|
||||||
networkManagementServer.start(NetworkHostAndPort(host, port), database, signer, networkParameter, networkMapConfig, doormanConfig)
|
val networkMapStartParams = networkMapConfig?.let {
|
||||||
|
NetworkMapStartParams(localSigners?.second, networkParameters, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
networkManagementServer.start(NetworkHostAndPort(host, port), database, localSigners?.first, doormanConfig, networkMapStartParams)
|
||||||
|
|
||||||
Runtime.getRuntime().addShutdownHook(thread(start = false) {
|
Runtime.getRuntime().addShutdownHook(thread(start = false) {
|
||||||
networkManagementServer.close()
|
networkManagementServer.close()
|
||||||
|
@ -2,11 +2,10 @@ package com.r3.corda.networkmanage.doorman.signer
|
|||||||
|
|
||||||
import com.r3.corda.networkmanage.common.signer.Signer
|
import com.r3.corda.networkmanage.common.signer.Signer
|
||||||
import com.r3.corda.networkmanage.common.utils.buildCertPath
|
import com.r3.corda.networkmanage.common.utils.buildCertPath
|
||||||
import com.r3.corda.networkmanage.common.utils.withCert
|
import net.corda.core.crypto.Crypto
|
||||||
import net.corda.core.crypto.sign
|
import net.corda.core.internal.DigitalSignatureWithCert
|
||||||
import net.corda.nodeapi.internal.crypto.CertificateType
|
import net.corda.nodeapi.internal.crypto.CertificateType
|
||||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||||
import net.corda.nodeapi.internal.network.DigitalSignatureWithCert
|
|
||||||
import org.bouncycastle.asn1.x509.GeneralName
|
import org.bouncycastle.asn1.x509.GeneralName
|
||||||
import org.bouncycastle.asn1.x509.GeneralSubtree
|
import org.bouncycastle.asn1.x509.GeneralSubtree
|
||||||
import org.bouncycastle.asn1.x509.NameConstraints
|
import org.bouncycastle.asn1.x509.NameConstraints
|
||||||
@ -21,7 +20,9 @@ import javax.security.auth.x500.X500Principal
|
|||||||
* The [LocalSigner] class signs [PKCS10CertificationRequest] using provided CA key pair and certificate path.
|
* The [LocalSigner] class signs [PKCS10CertificationRequest] using provided CA key pair and certificate path.
|
||||||
* This is intended to be used in testing environment where hardware signing module is not available.
|
* This is intended to be used in testing environment where hardware signing module is not available.
|
||||||
*/
|
*/
|
||||||
class LocalSigner(private val caKeyPair: KeyPair, private val caCertPath: Array<X509Certificate>) : Signer {
|
//TODO Use a list instead of array
|
||||||
|
class LocalSigner(private val signingKeyPair: KeyPair, private val signingCertPath: Array<X509Certificate>) : Signer {
|
||||||
|
// TODO This doesn't belong in this class
|
||||||
fun createSignedClientCertificate(certificationRequest: PKCS10CertificationRequest): CertPath {
|
fun createSignedClientCertificate(certificationRequest: PKCS10CertificationRequest): CertPath {
|
||||||
// The sub certs issued by the client must satisfy this directory name (or legal name in Corda) constraints, sub certs' directory name must be within client CA's name's subtree,
|
// The sub certs issued by the client must satisfy this directory name (or legal name in Corda) constraints, sub certs' directory name must be within client CA's name's subtree,
|
||||||
// please see [sun.security.x509.X500Name.isWithinSubtree()] for more information.
|
// please see [sun.security.x509.X500Name.isWithinSubtree()] for more information.
|
||||||
@ -33,15 +34,15 @@ class LocalSigner(private val caKeyPair: KeyPair, private val caCertPath: Array<
|
|||||||
arrayOf())
|
arrayOf())
|
||||||
val nodeCaCert = X509Utilities.createCertificate(
|
val nodeCaCert = X509Utilities.createCertificate(
|
||||||
CertificateType.NODE_CA,
|
CertificateType.NODE_CA,
|
||||||
caCertPath[0],
|
signingCertPath[0],
|
||||||
caKeyPair,
|
signingKeyPair,
|
||||||
X500Principal(request.subject.encoded),
|
X500Principal(request.subject.encoded),
|
||||||
request.publicKey,
|
request.publicKey,
|
||||||
nameConstraints = nameConstraints)
|
nameConstraints = nameConstraints)
|
||||||
return buildCertPath(nodeCaCert, *caCertPath)
|
return buildCertPath(nodeCaCert, *signingCertPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun sign(data: ByteArray): DigitalSignatureWithCert {
|
override fun signBytes(data: ByteArray): DigitalSignatureWithCert {
|
||||||
return caKeyPair.sign(data).withCert(caCertPath.first())
|
return DigitalSignatureWithCert(signingCertPath[0], Crypto.doSign(signingKeyPair.private, data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import com.google.common.cache.LoadingCache
|
|||||||
import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
|
import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
|
||||||
import com.r3.corda.networkmanage.common.persistence.NodeInfoStorage
|
import com.r3.corda.networkmanage.common.persistence.NodeInfoStorage
|
||||||
import com.r3.corda.networkmanage.common.persistence.NodeInfoWithSigned
|
import com.r3.corda.networkmanage.common.persistence.NodeInfoWithSigned
|
||||||
|
import com.r3.corda.networkmanage.common.utils.SignedNetworkMap
|
||||||
import com.r3.corda.networkmanage.doorman.NetworkMapConfig
|
import com.r3.corda.networkmanage.doorman.NetworkMapConfig
|
||||||
import com.r3.corda.networkmanage.doorman.webservice.NodeInfoWebService.Companion.NETWORK_MAP_PATH
|
import com.r3.corda.networkmanage.doorman.webservice.NodeInfoWebService.Companion.NETWORK_MAP_PATH
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
@ -15,7 +16,6 @@ import net.corda.core.serialization.serialize
|
|||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||||
import net.corda.nodeapi.internal.network.NetworkParameters
|
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||||
import net.corda.nodeapi.internal.network.SignedNetworkMap
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.security.InvalidKeyException
|
import java.security.InvalidKeyException
|
||||||
import java.security.SignatureException
|
import java.security.SignatureException
|
||||||
@ -77,9 +77,9 @@ class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("network-parameter/{netParamsHash}") // TODO Fix path to be /network-parameters
|
@Path("network-parameters/{hash}")
|
||||||
fun getNetworkParameters(@PathParam("netParamsHash") netParamsHash: String): Response {
|
fun getNetworkParameters(@PathParam("hash") hash: String): Response {
|
||||||
val signedNetParams = networkMapStorage.getSignedNetworkParameters(SecureHash.parse(netParamsHash))
|
val signedNetParams = networkMapStorage.getSignedNetworkParameters(SecureHash.parse(hash))
|
||||||
return createResponse(signedNetParams)
|
return createResponse(signedNetParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ data class Parameters(val dataSourceProperties: Properties,
|
|||||||
val DEFAULT_KEY_FILE_PATH: Path? = null //Paths.get("/Users/michalkit/WinDev1706Eval/Shared/TEST4.key")
|
val DEFAULT_KEY_FILE_PATH: Path? = null //Paths.get("/Users/michalkit/WinDev1706Eval/Shared/TEST4.key")
|
||||||
val DEFAULT_KEY_FILE_PASSWORD: String? = null
|
val DEFAULT_KEY_FILE_PASSWORD: String? = null
|
||||||
val DEFAULT_AUTO_USERNAME: String? = null
|
val DEFAULT_AUTO_USERNAME: String? = null
|
||||||
val DEFAULT_NETWORK_MAP_CERTIFICATE_NAME = "cordaintermediateca_nm"
|
val DEFAULT_NETWORK_MAP_CERTIFICATE_NAME = "cordaintermediateca_nm" // TODO Change the value to "cordanetworkmap" since this is not a CA
|
||||||
val DEFAULT_SIGN_INTERVAL = 600L // in seconds (10 minutes)
|
val DEFAULT_SIGN_INTERVAL = 600L // in seconds (10 minutes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,16 +4,15 @@ import com.google.common.util.concurrent.MoreExecutors
|
|||||||
import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
|
import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
|
||||||
import com.r3.corda.networkmanage.common.signer.NetworkMapSigner
|
import com.r3.corda.networkmanage.common.signer.NetworkMapSigner
|
||||||
import com.r3.corda.networkmanage.common.signer.Signer
|
import com.r3.corda.networkmanage.common.signer.Signer
|
||||||
import com.r3.corda.networkmanage.common.utils.withCert
|
|
||||||
import com.r3.corda.networkmanage.hsm.authentication.Authenticator
|
import com.r3.corda.networkmanage.hsm.authentication.Authenticator
|
||||||
|
import com.r3.corda.networkmanage.hsm.utils.X509Utilities
|
||||||
import com.r3.corda.networkmanage.hsm.utils.X509Utilities.getAndInitializeKeyStore
|
import com.r3.corda.networkmanage.hsm.utils.X509Utilities.getAndInitializeKeyStore
|
||||||
import com.r3.corda.networkmanage.hsm.utils.X509Utilities.signData
|
|
||||||
import com.r3.corda.networkmanage.hsm.utils.X509Utilities.verify
|
import com.r3.corda.networkmanage.hsm.utils.X509Utilities.verify
|
||||||
|
import net.corda.core.internal.DigitalSignatureWithCert
|
||||||
import net.corda.core.utilities.loggerFor
|
import net.corda.core.utilities.loggerFor
|
||||||
import net.corda.core.utilities.minutes
|
import net.corda.core.utilities.minutes
|
||||||
import net.corda.nodeapi.internal.network.DigitalSignatureWithCert
|
|
||||||
import java.security.KeyPair
|
|
||||||
import java.security.PrivateKey
|
import java.security.PrivateKey
|
||||||
|
import java.security.Signature
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
@ -24,6 +23,7 @@ import java.util.concurrent.TimeUnit
|
|||||||
* Encapsulates logic for periodic network map signing execution.
|
* Encapsulates logic for periodic network map signing execution.
|
||||||
* It uses HSM as the signing entity with keys and certificates specified at the construction time.
|
* It uses HSM as the signing entity with keys and certificates specified at the construction time.
|
||||||
*/
|
*/
|
||||||
|
// TODO Rename this to HsmSigner
|
||||||
class HsmNetworkMapSigner(networkMapStorage: NetworkMapStorage,
|
class HsmNetworkMapSigner(networkMapStorage: NetworkMapStorage,
|
||||||
private val caCertificateKeyName: String,
|
private val caCertificateKeyName: String,
|
||||||
private val caPrivateKeyPass: String,
|
private val caPrivateKeyPass: String,
|
||||||
@ -40,6 +40,7 @@ class HsmNetworkMapSigner(networkMapStorage: NetworkMapStorage,
|
|||||||
private val networkMapSigner = NetworkMapSigner(networkMapStorage, this)
|
private val networkMapSigner = NetworkMapSigner(networkMapStorage, this)
|
||||||
private lateinit var scheduledExecutor: ScheduledExecutorService
|
private lateinit var scheduledExecutor: ScheduledExecutorService
|
||||||
|
|
||||||
|
// TODO This doesn't belong in this class
|
||||||
fun start(): HsmNetworkMapSigner {
|
fun start(): HsmNetworkMapSigner {
|
||||||
val signingPeriodMillis = signingPeriod.toMillis()
|
val signingPeriodMillis = signingPeriod.toMillis()
|
||||||
scheduledExecutor = Executors.newSingleThreadScheduledExecutor()
|
scheduledExecutor = Executors.newSingleThreadScheduledExecutor()
|
||||||
@ -60,14 +61,18 @@ class HsmNetworkMapSigner(networkMapStorage: NetworkMapStorage,
|
|||||||
/**
|
/**
|
||||||
* Signs given data using [CryptoServerJCE.CryptoServerProvider], which connects to the underlying HSM.
|
* Signs given data using [CryptoServerJCE.CryptoServerProvider], which connects to the underlying HSM.
|
||||||
*/
|
*/
|
||||||
override fun sign(data: ByteArray): DigitalSignatureWithCert {
|
override fun signBytes(data: ByteArray): DigitalSignatureWithCert {
|
||||||
return authenticator.connectAndAuthenticate { provider, _ ->
|
return authenticator.connectAndAuthenticate { provider, _ ->
|
||||||
val keyStore = getAndInitializeKeyStore(provider)
|
val keyStore = getAndInitializeKeyStore(provider)
|
||||||
val caCertificateChain = keyStore.getCertificateChain(caCertificateKeyName)
|
val caCertificateChain = keyStore.getCertificateChain(caCertificateKeyName)
|
||||||
val caKey = keyStore.getKey(caCertificateKeyName, caPrivateKeyPass.toCharArray()) as PrivateKey
|
val caKey = keyStore.getKey(caCertificateKeyName, caPrivateKeyPass.toCharArray()) as PrivateKey
|
||||||
val signature = signData(data, KeyPair(caCertificateChain.first().publicKey, caKey), provider)
|
val signature = Signature.getInstance(X509Utilities.SIGNATURE_ALGORITHM, provider).run {
|
||||||
verify(data, signature, caCertificateChain.first().publicKey)
|
initSign(caKey)
|
||||||
signature.withCert(caCertificateChain[0] as X509Certificate)
|
update(data)
|
||||||
|
sign()
|
||||||
|
}
|
||||||
|
verify(data, signature, caCertificateChain[0].publicKey)
|
||||||
|
DigitalSignatureWithCert(caCertificateChain[0] as X509Certificate, signature)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package com.r3.corda.networkmanage.hsm.utils
|
package com.r3.corda.networkmanage.hsm.utils
|
||||||
|
|
||||||
import CryptoServerJCE.CryptoServerProvider
|
import CryptoServerJCE.CryptoServerProvider
|
||||||
import net.corda.core.crypto.DigitalSignature
|
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.internal.x500Name
|
import net.corda.core.internal.x500Name
|
||||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||||
@ -268,27 +267,14 @@ object X509Utilities {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sign data with the given private key
|
|
||||||
*/
|
|
||||||
fun signData(data: ByteArray,
|
|
||||||
keyPair: KeyPair,
|
|
||||||
provider: Provider,
|
|
||||||
signatureAlgorithm: String = SIGNATURE_ALGORITHM): DigitalSignature.WithKey {
|
|
||||||
val signer = Signature.getInstance(signatureAlgorithm, provider)
|
|
||||||
signer.initSign(keyPair.private)
|
|
||||||
signer.update(data)
|
|
||||||
return DigitalSignature.WithKey(keyPair.public, signer.sign())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun verify(data: ByteArray,
|
fun verify(data: ByteArray,
|
||||||
signature: DigitalSignature,
|
signature: ByteArray,
|
||||||
publicKey: PublicKey,
|
publicKey: PublicKey,
|
||||||
signatureAlgorithm: String = SIGNATURE_ALGORITHM) {
|
signatureAlgorithm: String = SIGNATURE_ALGORITHM) {
|
||||||
val verify = Signature.getInstance(signatureAlgorithm)
|
val verify = Signature.getInstance(signatureAlgorithm)
|
||||||
verify.initVerify(publicKey)
|
verify.initVerify(publicKey)
|
||||||
verify.update(data)
|
verify.update(data)
|
||||||
require(verify.verify(signature.bytes)) { "Signature didn't independently verify" }
|
require(verify.verify(signature)) { "Signature didn't independently verify" }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
package com.r3.corda.networkmanage.common.persistence
|
package com.r3.corda.networkmanage.common.persistence
|
||||||
|
|
||||||
import com.r3.corda.networkmanage.TestBase
|
import com.r3.corda.networkmanage.TestBase
|
||||||
import com.r3.corda.networkmanage.common.utils.withCert
|
|
||||||
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
|
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
|
||||||
import net.corda.core.crypto.sign
|
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.internal.signWithCert
|
||||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
import net.corda.nodeapi.internal.createDevNetworkMapCa
|
||||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||||
import net.corda.nodeapi.internal.network.NetworkMap
|
import net.corda.nodeapi.internal.network.NetworkMap
|
||||||
import net.corda.nodeapi.internal.network.SignedNetworkMap
|
import net.corda.nodeapi.internal.network.verifiedNetworkMapCert
|
||||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||||
import net.corda.testing.common.internal.testNetworkParameters
|
import net.corda.testing.common.internal.testNetworkParameters
|
||||||
@ -31,15 +29,15 @@ class PersistentNetworkMapStorageTest : TestBase() {
|
|||||||
private lateinit var requestStorage: PersistentCertificateRequestStorage
|
private lateinit var requestStorage: PersistentCertificateRequestStorage
|
||||||
|
|
||||||
private lateinit var rootCaCert: X509Certificate
|
private lateinit var rootCaCert: X509Certificate
|
||||||
private lateinit var intermediateCa: CertificateAndKeyPair
|
private lateinit var networkMapCa: CertificateAndKeyPair
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun startDb() {
|
fun startDb() {
|
||||||
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
|
val (rootCa) = createDevIntermediateCaCertPath()
|
||||||
rootCaCert = rootCa.certificate
|
rootCaCert = rootCa.certificate
|
||||||
this.intermediateCa = intermediateCa
|
networkMapCa = createDevNetworkMapCa(rootCa)
|
||||||
persistence = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(runMigration = true))
|
persistence = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(runMigration = true))
|
||||||
networkMapStorage = PersistentNetworkMapStorage(persistence, LocalSigner(intermediateCa.keyPair, arrayOf(intermediateCa.certificate, rootCaCert)))
|
networkMapStorage = PersistentNetworkMapStorage(persistence, LocalSigner(networkMapCa.keyPair, arrayOf(networkMapCa.certificate, rootCaCert)))
|
||||||
nodeInfoStorage = PersistentNodeInfoStorage(persistence)
|
nodeInfoStorage = PersistentNodeInfoStorage(persistence)
|
||||||
requestStorage = PersistentCertificateRequestStorage(persistence)
|
requestStorage = PersistentCertificateRequestStorage(persistence)
|
||||||
}
|
}
|
||||||
@ -60,9 +58,7 @@ class PersistentNetworkMapStorageTest : TestBase() {
|
|||||||
val networkParametersHash = networkMapStorage.saveNetworkParameters(testNetworkParameters(emptyList()))
|
val networkParametersHash = networkMapStorage.saveNetworkParameters(testNetworkParameters(emptyList()))
|
||||||
|
|
||||||
val networkMap = NetworkMap(listOf(nodeInfoHash), networkParametersHash)
|
val networkMap = NetworkMap(listOf(nodeInfoHash), networkParametersHash)
|
||||||
val serializedNetworkMap = networkMap.serialize()
|
val signedNetworkMap = networkMap.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)
|
||||||
val signatureData = intermediateCa.keyPair.sign(serializedNetworkMap).withCert(intermediateCa.certificate)
|
|
||||||
val signedNetworkMap = SignedNetworkMap(serializedNetworkMap, signatureData)
|
|
||||||
|
|
||||||
// when
|
// when
|
||||||
networkMapStorage.saveNetworkMap(signedNetworkMap)
|
networkMapStorage.saveNetworkMap(signedNetworkMap)
|
||||||
@ -70,8 +66,8 @@ class PersistentNetworkMapStorageTest : TestBase() {
|
|||||||
// then
|
// then
|
||||||
val persistedSignedNetworkMap = networkMapStorage.getCurrentNetworkMap()
|
val persistedSignedNetworkMap = networkMapStorage.getCurrentNetworkMap()
|
||||||
|
|
||||||
assertEquals(signedNetworkMap.signature, persistedSignedNetworkMap?.signature)
|
assertEquals(signedNetworkMap.sig, persistedSignedNetworkMap?.sig)
|
||||||
assertEquals(signedNetworkMap.verified(rootCaCert), persistedSignedNetworkMap?.verified(rootCaCert))
|
assertEquals(signedNetworkMap.verifiedNetworkMapCert(rootCaCert), persistedSignedNetworkMap?.verifiedNetworkMapCert(rootCaCert))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -96,9 +92,7 @@ class PersistentNetworkMapStorageTest : TestBase() {
|
|||||||
|
|
||||||
// Sign network map making it current network map
|
// Sign network map making it current network map
|
||||||
val networkMap = NetworkMap(emptyList(), networkParametersHash)
|
val networkMap = NetworkMap(emptyList(), networkParametersHash)
|
||||||
val serializedNetworkMap = networkMap.serialize()
|
val signedNetworkMap = networkMap.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)
|
||||||
val signatureData = intermediateCa.keyPair.sign(serializedNetworkMap).withCert(intermediateCa.certificate)
|
|
||||||
val signedNetworkMap = SignedNetworkMap(serializedNetworkMap, signatureData)
|
|
||||||
networkMapStorage.saveNetworkMap(signedNetworkMap)
|
networkMapStorage.saveNetworkMap(signedNetworkMap)
|
||||||
|
|
||||||
// Create new network parameters
|
// Create new network parameters
|
||||||
@ -114,11 +108,11 @@ class PersistentNetworkMapStorageTest : TestBase() {
|
|||||||
// This test will probably won't be needed when we remove the explicit use of LocalSigner
|
// This test will probably won't be needed when we remove the explicit use of LocalSigner
|
||||||
@Test
|
@Test
|
||||||
fun `getSignedNetworkParameters uses the local signer to return a signed object`() {
|
fun `getSignedNetworkParameters uses the local signer to return a signed object`() {
|
||||||
val netParams = testNetworkParameters(emptyList())
|
val networkParameters = testNetworkParameters(emptyList())
|
||||||
val netParamsHash = networkMapStorage.saveNetworkParameters(netParams)
|
val netParamsHash = networkMapStorage.saveNetworkParameters(networkParameters)
|
||||||
val signedNetParams = networkMapStorage.getSignedNetworkParameters(netParamsHash)
|
val signedNetworkParameters = networkMapStorage.getSignedNetworkParameters(netParamsHash)
|
||||||
assertThat(signedNetParams?.verified()).isEqualTo(netParams)
|
assertThat(signedNetworkParameters?.verifiedNetworkMapCert(rootCaCert)).isEqualTo(networkParameters)
|
||||||
assertThat(signedNetParams?.sig?.by).isEqualTo(intermediateCa.keyPair.public)
|
assertThat(signedNetworkParameters?.sig?.by).isEqualTo(networkMapCa.certificate)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -135,9 +129,7 @@ class PersistentNetworkMapStorageTest : TestBase() {
|
|||||||
// Create network parameters
|
// Create network parameters
|
||||||
val networkParametersHash = networkMapStorage.saveNetworkParameters(testNetworkParameters(emptyList()))
|
val networkParametersHash = networkMapStorage.saveNetworkParameters(testNetworkParameters(emptyList()))
|
||||||
val networkMap = NetworkMap(listOf(nodeInfoHashA), networkParametersHash)
|
val networkMap = NetworkMap(listOf(nodeInfoHashA), networkParametersHash)
|
||||||
val serializedNetworkMap = networkMap.serialize()
|
val signedNetworkMap = networkMap.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)
|
||||||
val signatureData = intermediateCa.keyPair.sign(serializedNetworkMap).withCert(intermediateCa.certificate)
|
|
||||||
val signedNetworkMap = SignedNetworkMap(serializedNetworkMap, signatureData)
|
|
||||||
|
|
||||||
// Sign network map
|
// Sign network map
|
||||||
networkMapStorage.saveNetworkMap(signedNetworkMap)
|
networkMapStorage.saveNetworkMap(signedNetworkMap)
|
||||||
|
@ -3,21 +3,24 @@ package com.r3.corda.networkmanage.common.signer
|
|||||||
import com.nhaarman.mockito_kotlin.*
|
import com.nhaarman.mockito_kotlin.*
|
||||||
import com.r3.corda.networkmanage.TestBase
|
import com.r3.corda.networkmanage.TestBase
|
||||||
import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
|
import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
|
||||||
import com.r3.corda.networkmanage.common.utils.withCert
|
import com.r3.corda.networkmanage.common.utils.SignedNetworkMap
|
||||||
|
import net.corda.core.crypto.Crypto
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.sha256
|
import net.corda.core.crypto.sha256
|
||||||
import net.corda.core.crypto.sign
|
import net.corda.core.internal.DigitalSignatureWithCert
|
||||||
|
import net.corda.core.internal.signWithCert
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
|
import net.corda.nodeapi.internal.createDevNetworkMapCa
|
||||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||||
import net.corda.nodeapi.internal.network.NetworkMap
|
import net.corda.nodeapi.internal.network.NetworkMap
|
||||||
import net.corda.nodeapi.internal.network.SignedNetworkMap
|
import net.corda.nodeapi.internal.network.verifiedNetworkMapCert
|
||||||
import net.corda.testing.common.internal.testNetworkParameters
|
import net.corda.testing.common.internal.testNetworkParameters
|
||||||
import net.corda.testing.internal.createDevIntermediateCaCertPath
|
import net.corda.testing.internal.createDevIntermediateCaCertPath
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
class NetworkMapSignerTest : TestBase() {
|
class NetworkMapSignerTest : TestBase() {
|
||||||
private lateinit var signer: Signer
|
private lateinit var signer: Signer
|
||||||
@ -25,13 +28,13 @@ class NetworkMapSignerTest : TestBase() {
|
|||||||
private lateinit var networkMapSigner: NetworkMapSigner
|
private lateinit var networkMapSigner: NetworkMapSigner
|
||||||
|
|
||||||
private lateinit var rootCaCert: X509Certificate
|
private lateinit var rootCaCert: X509Certificate
|
||||||
private lateinit var intermediateCa: CertificateAndKeyPair
|
private lateinit var networkMapCa: CertificateAndKeyPair
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
|
val (rootCa) = createDevIntermediateCaCertPath()
|
||||||
rootCaCert = rootCa.certificate
|
rootCaCert = rootCa.certificate
|
||||||
this.intermediateCa = intermediateCa
|
networkMapCa = createDevNetworkMapCa(rootCa)
|
||||||
signer = mock()
|
signer = mock()
|
||||||
networkMapStorage = mock()
|
networkMapStorage = mock()
|
||||||
networkMapSigner = NetworkMapSigner(networkMapStorage, signer)
|
networkMapSigner = NetworkMapSigner(networkMapStorage, signer)
|
||||||
@ -42,13 +45,13 @@ class NetworkMapSignerTest : TestBase() {
|
|||||||
// given
|
// given
|
||||||
val signedNodeInfoHashes = listOf(SecureHash.randomSHA256(), SecureHash.randomSHA256())
|
val signedNodeInfoHashes = listOf(SecureHash.randomSHA256(), SecureHash.randomSHA256())
|
||||||
val networkParameters = testNetworkParameters(emptyList())
|
val networkParameters = testNetworkParameters(emptyList())
|
||||||
val serializedNetworkMap = NetworkMap(signedNodeInfoHashes, SecureHash.randomSHA256()).serialize()
|
val networkMap = NetworkMap(signedNodeInfoHashes, SecureHash.randomSHA256())
|
||||||
whenever(networkMapStorage.getCurrentNetworkMap())
|
val signedNetworkMap = networkMap.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)
|
||||||
.thenReturn(SignedNetworkMap(serializedNetworkMap, intermediateCa.keyPair.sign(serializedNetworkMap).withCert(intermediateCa.certificate)))
|
whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(signedNetworkMap)
|
||||||
whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(signedNodeInfoHashes)
|
whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(signedNodeInfoHashes)
|
||||||
whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(networkParameters)
|
whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(networkParameters)
|
||||||
whenever(signer.sign(any())).then {
|
whenever(signer.signBytes(any())).then {
|
||||||
intermediateCa.keyPair.sign(it.arguments[0] as ByteArray).withCert(intermediateCa.certificate)
|
DigitalSignatureWithCert(networkMapCa.certificate, Crypto.doSign(networkMapCa.keyPair.private, it.arguments[0] as ByteArray))
|
||||||
}
|
}
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@ -60,10 +63,10 @@ class NetworkMapSignerTest : TestBase() {
|
|||||||
verify(networkMapStorage).getLatestNetworkParameters()
|
verify(networkMapStorage).getLatestNetworkParameters()
|
||||||
argumentCaptor<SignedNetworkMap>().apply {
|
argumentCaptor<SignedNetworkMap>().apply {
|
||||||
verify(networkMapStorage).saveNetworkMap(capture())
|
verify(networkMapStorage).saveNetworkMap(capture())
|
||||||
val networkMap = firstValue.verified(rootCaCert)
|
val capturedNetworkMap = firstValue.verifiedNetworkMapCert(rootCaCert)
|
||||||
assertEquals(networkParameters.serialize().hash, networkMap.networkParameterHash)
|
assertEquals(networkParameters.serialize().hash, capturedNetworkMap.networkParameterHash)
|
||||||
assertEquals(signedNodeInfoHashes.size, networkMap.nodeInfoHashes.size)
|
assertEquals(signedNodeInfoHashes.size, capturedNetworkMap.nodeInfoHashes.size)
|
||||||
assertTrue(networkMap.nodeInfoHashes.containsAll(signedNodeInfoHashes))
|
assertThat(capturedNetworkMap.nodeInfoHashes).containsAll(signedNodeInfoHashes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,8 +76,7 @@ class NetworkMapSignerTest : TestBase() {
|
|||||||
val networkParameters = testNetworkParameters(emptyList())
|
val networkParameters = testNetworkParameters(emptyList())
|
||||||
val networkMapParametersHash = networkParameters.serialize().bytes.sha256()
|
val networkMapParametersHash = networkParameters.serialize().bytes.sha256()
|
||||||
val networkMap = NetworkMap(emptyList(), networkMapParametersHash)
|
val networkMap = NetworkMap(emptyList(), networkMapParametersHash)
|
||||||
val serializedNetworkMap = networkMap.serialize()
|
val signedNetworkMap = networkMap.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)
|
||||||
val signedNetworkMap = SignedNetworkMap(serializedNetworkMap, intermediateCa.keyPair.sign(serializedNetworkMap).withCert(intermediateCa.certificate))
|
|
||||||
whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(signedNetworkMap)
|
whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(signedNetworkMap)
|
||||||
whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(emptyList())
|
whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(emptyList())
|
||||||
whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(networkParameters)
|
whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(networkParameters)
|
||||||
@ -94,8 +96,8 @@ class NetworkMapSignerTest : TestBase() {
|
|||||||
whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(null)
|
whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(null)
|
||||||
whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(emptyList())
|
whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(emptyList())
|
||||||
whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(networkParameters)
|
whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(networkParameters)
|
||||||
whenever(signer.sign(any())).then {
|
whenever(signer.signBytes(any())).then {
|
||||||
intermediateCa.keyPair.sign(it.arguments[0] as ByteArray).withCert(intermediateCa.certificate)
|
DigitalSignatureWithCert(networkMapCa.certificate, Crypto.doSign(networkMapCa.keyPair.private, it.arguments[0] as ByteArray))
|
||||||
}
|
}
|
||||||
// when
|
// when
|
||||||
networkMapSigner.signNetworkMap()
|
networkMapSigner.signNetworkMap()
|
||||||
@ -106,7 +108,7 @@ class NetworkMapSignerTest : TestBase() {
|
|||||||
verify(networkMapStorage).getLatestNetworkParameters()
|
verify(networkMapStorage).getLatestNetworkParameters()
|
||||||
argumentCaptor<SignedNetworkMap>().apply {
|
argumentCaptor<SignedNetworkMap>().apply {
|
||||||
verify(networkMapStorage).saveNetworkMap(capture())
|
verify(networkMapStorage).saveNetworkMap(capture())
|
||||||
val networkMap = firstValue.verified(rootCaCert)
|
val networkMap = firstValue.verifiedNetworkMapCert(rootCaCert)
|
||||||
assertEquals(networkParameters.serialize().hash, networkMap.networkParameterHash)
|
assertEquals(networkParameters.serialize().hash, networkMap.networkParameterHash)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,35 +5,33 @@ import com.nhaarman.mockito_kotlin.times
|
|||||||
import com.nhaarman.mockito_kotlin.verify
|
import com.nhaarman.mockito_kotlin.verify
|
||||||
import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
|
import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
|
||||||
import com.r3.corda.networkmanage.common.persistence.NodeInfoStorage
|
import com.r3.corda.networkmanage.common.persistence.NodeInfoStorage
|
||||||
import com.r3.corda.networkmanage.common.utils.withCert
|
import com.r3.corda.networkmanage.common.utils.SignedNetworkMap
|
||||||
|
import com.r3.corda.networkmanage.common.utils.SignedNetworkParameters
|
||||||
import com.r3.corda.networkmanage.doorman.webservice.NodeInfoWebService
|
import com.r3.corda.networkmanage.doorman.webservice.NodeInfoWebService
|
||||||
import net.corda.core.crypto.SecureHash.Companion.randomSHA256
|
import net.corda.core.crypto.SecureHash.Companion.randomSHA256
|
||||||
import net.corda.core.crypto.SignedData
|
|
||||||
import net.corda.core.crypto.sign
|
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
|
import net.corda.core.internal.checkOkResponse
|
||||||
import net.corda.core.internal.openHttpConnection
|
import net.corda.core.internal.openHttpConnection
|
||||||
import net.corda.core.serialization.deserialize
|
import net.corda.core.internal.responseAs
|
||||||
|
import net.corda.core.internal.signWithCert
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
import net.corda.core.utilities.seconds
|
import net.corda.core.utilities.seconds
|
||||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||||
|
import net.corda.nodeapi.internal.createDevNetworkMapCa
|
||||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||||
import net.corda.nodeapi.internal.network.NetworkMap
|
import net.corda.nodeapi.internal.network.NetworkMap
|
||||||
import net.corda.nodeapi.internal.network.NetworkParameters
|
import net.corda.nodeapi.internal.network.verifiedNetworkMapCert
|
||||||
import net.corda.nodeapi.internal.network.SignedNetworkMap
|
|
||||||
import net.corda.testing.SerializationEnvironmentRule
|
import net.corda.testing.SerializationEnvironmentRule
|
||||||
import net.corda.testing.common.internal.testNetworkParameters
|
import net.corda.testing.common.internal.testNetworkParameters
|
||||||
import net.corda.testing.internal.createDevIntermediateCaCertPath
|
import net.corda.testing.internal.createDevIntermediateCaCertPath
|
||||||
import net.corda.testing.internal.createNodeInfoAndSigned
|
import net.corda.testing.internal.createNodeInfoAndSigned
|
||||||
import org.apache.commons.io.IOUtils
|
|
||||||
import org.assertj.core.api.Assertions.*
|
import org.assertj.core.api.Assertions.*
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.io.FileNotFoundException
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.nio.charset.Charset
|
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import javax.ws.rs.core.MediaType
|
import javax.ws.rs.core.MediaType
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
@ -44,15 +42,15 @@ class NodeInfoWebServiceTest {
|
|||||||
val testSerialization = SerializationEnvironmentRule(true)
|
val testSerialization = SerializationEnvironmentRule(true)
|
||||||
|
|
||||||
private lateinit var rootCaCert: X509Certificate
|
private lateinit var rootCaCert: X509Certificate
|
||||||
private lateinit var intermediateCa: CertificateAndKeyPair
|
private lateinit var networkMapCa: CertificateAndKeyPair
|
||||||
|
|
||||||
private val testNetworkMapConfig = NetworkMapConfig(10.seconds.toMillis(), 10.seconds.toMillis())
|
private val testNetworkMapConfig = NetworkMapConfig(10.seconds.toMillis(), 10.seconds.toMillis())
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun init() {
|
fun init() {
|
||||||
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
|
val (rootCa) = createDevIntermediateCaCertPath()
|
||||||
rootCaCert = rootCa.certificate
|
rootCaCert = rootCa.certificate
|
||||||
this.intermediateCa = intermediateCa
|
networkMapCa = createDevNetworkMapCa(rootCa)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -106,8 +104,7 @@ class NodeInfoWebServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `get network map`() {
|
fun `get network map`() {
|
||||||
val networkMap = NetworkMap(listOf(randomSHA256(), randomSHA256()), randomSHA256())
|
val networkMap = NetworkMap(listOf(randomSHA256(), randomSHA256()), randomSHA256())
|
||||||
val serializedNetworkMap = networkMap.serialize()
|
val signedNetworkMap = networkMap.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)
|
||||||
val signedNetworkMap = SignedNetworkMap(serializedNetworkMap, intermediateCa.keyPair.sign(serializedNetworkMap).withCert(intermediateCa.certificate))
|
|
||||||
|
|
||||||
val networkMapStorage: NetworkMapStorage = mock {
|
val networkMapStorage: NetworkMapStorage = mock {
|
||||||
on { getCurrentNetworkMap() }.thenReturn(signedNetworkMap)
|
on { getCurrentNetworkMap() }.thenReturn(signedNetworkMap)
|
||||||
@ -117,7 +114,7 @@ class NodeInfoWebServiceTest {
|
|||||||
it.start()
|
it.start()
|
||||||
val signedNetworkMapResponse = it.doGet<SignedNetworkMap>("")
|
val signedNetworkMapResponse = it.doGet<SignedNetworkMap>("")
|
||||||
verify(networkMapStorage, times(1)).getCurrentNetworkMap()
|
verify(networkMapStorage, times(1)).getCurrentNetworkMap()
|
||||||
assertEquals(signedNetworkMapResponse.verified(rootCaCert), networkMap)
|
assertEquals(signedNetworkMapResponse.verifiedNetworkMapCert(rootCaCert), networkMap)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,33 +133,32 @@ class NodeInfoWebServiceTest {
|
|||||||
verify(nodeInfoStorage, times(1)).getNodeInfo(nodeInfoHash)
|
verify(nodeInfoStorage, times(1)).getNodeInfo(nodeInfoHash)
|
||||||
assertEquals(nodeInfo, nodeInfoResponse.verified())
|
assertEquals(nodeInfo, nodeInfoResponse.verified())
|
||||||
|
|
||||||
assertThatExceptionOfType(FileNotFoundException::class.java).isThrownBy {
|
assertThatExceptionOfType(IOException::class.java)
|
||||||
it.doGet<SignedNodeInfo>("node-info/${randomSHA256()}")
|
.isThrownBy { it.doGet<SignedNodeInfo>("node-info/${randomSHA256()}") }
|
||||||
}
|
.withMessageContaining("404")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `get network parameters`() {
|
fun `get network parameters`() {
|
||||||
val netParams = testNetworkParameters(emptyList())
|
val networkParameters = testNetworkParameters(emptyList())
|
||||||
val serializedNetParams = netParams.serialize()
|
val signedNetworkParameters = networkParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)
|
||||||
val signedNetParams = SignedData(serializedNetParams, intermediateCa.keyPair.sign(serializedNetParams))
|
val networkParametersHash = signedNetworkParameters.raw.hash
|
||||||
val netParamsHash = serializedNetParams.hash
|
|
||||||
|
|
||||||
val networkMapStorage: NetworkMapStorage = mock {
|
val networkMapStorage: NetworkMapStorage = mock {
|
||||||
on { getSignedNetworkParameters(netParamsHash) }.thenReturn(signedNetParams)
|
on { getSignedNetworkParameters(networkParametersHash) }.thenReturn(signedNetworkParameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), networkMapStorage, testNetworkMapConfig)).use {
|
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), networkMapStorage, testNetworkMapConfig)).use {
|
||||||
it.start()
|
it.start()
|
||||||
val netParamsResponse = it.doGet<SignedData<NetworkParameters>>("network-parameter/$netParamsHash")
|
val netParamsResponse = it.doGet<SignedNetworkParameters>("network-parameters/$networkParametersHash")
|
||||||
verify(networkMapStorage, times(1)).getSignedNetworkParameters(netParamsHash)
|
verify(networkMapStorage, times(1)).getSignedNetworkParameters(networkParametersHash)
|
||||||
assertThat(netParamsResponse.verified()).isEqualTo(netParams)
|
assertThat(netParamsResponse.verified()).isEqualTo(networkParameters)
|
||||||
assertThat(netParamsResponse.sig.by).isEqualTo(intermediateCa.keyPair.public)
|
assertThat(netParamsResponse.sig.by).isEqualTo(networkMapCa.certificate)
|
||||||
|
|
||||||
assertThatExceptionOfType(FileNotFoundException::class.java).isThrownBy {
|
assertThatExceptionOfType(IOException::class.java)
|
||||||
it.doGet<SignedData<NetworkParameters>>("network-parameter/${randomSHA256()}")
|
.isThrownBy { it.doGet<SignedNetworkParameters>("network-parameters/${randomSHA256()}") }
|
||||||
}
|
.withMessageContaining("404")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,15 +169,11 @@ class NodeInfoWebServiceTest {
|
|||||||
requestMethod = "POST"
|
requestMethod = "POST"
|
||||||
setRequestProperty("Content-Type", MediaType.APPLICATION_OCTET_STREAM)
|
setRequestProperty("Content-Type", MediaType.APPLICATION_OCTET_STREAM)
|
||||||
outputStream.write(payload)
|
outputStream.write(payload)
|
||||||
if (responseCode != 200) {
|
checkOkResponse()
|
||||||
throw IOException("Response Code $responseCode: ${IOUtils.toString(errorStream, Charset.defaultCharset())}")
|
|
||||||
}
|
|
||||||
inputStream.close()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inline fun <reified T : Any> NetworkManagementWebServer.doGet(path: String): T {
|
private inline fun <reified T : Any> NetworkManagementWebServer.doGet(path: String): T {
|
||||||
val url = URL("http://$hostAndPort/network-map/$path")
|
return URL("http://$hostAndPort/network-map/$path").openHttpConnection().responseAs()
|
||||||
return url.openHttpConnection().inputStream.use { it.readBytes().deserialize() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -34,13 +34,8 @@ object DevIdentityGenerator {
|
|||||||
override val trustStorePassword get() = throw NotImplementedError("Not expected to be called")
|
override val trustStorePassword get() = throw NotImplementedError("Not expected to be called")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO The passwords for the dev key stores are spread everywhere and should be constants in a single location
|
|
||||||
val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass")
|
|
||||||
val intermediateCa = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass")
|
|
||||||
val rootCert = caKeyStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA)
|
|
||||||
|
|
||||||
nodeSslConfig.certificatesDirectory.createDirectories()
|
nodeSslConfig.certificatesDirectory.createDirectories()
|
||||||
nodeSslConfig.createDevKeyStores(rootCert, intermediateCa, legalName)
|
nodeSslConfig.createDevKeyStores(legalName)
|
||||||
|
|
||||||
val keyStoreWrapper = KeyStoreWrapper(nodeSslConfig.nodeKeystore, nodeSslConfig.keyStorePassword)
|
val keyStoreWrapper = KeyStoreWrapper(nodeSslConfig.nodeKeystore, nodeSslConfig.keyStorePassword)
|
||||||
val identity = keyStoreWrapper.storeLegalIdentity(legalName, "$NODE_IDENTITY_ALIAS_PREFIX-private-key", Crypto.generateKeyPair())
|
val identity = keyStoreWrapper.storeLegalIdentity(legalName, "$NODE_IDENTITY_ALIAS_PREFIX-private-key", Crypto.generateKeyPair())
|
||||||
@ -54,16 +49,12 @@ object DevIdentityGenerator {
|
|||||||
val keyPairs = (1..dirs.size).map { generateKeyPair() }
|
val keyPairs = (1..dirs.size).map { generateKeyPair() }
|
||||||
val compositeKey = CompositeKey.Builder().addKeys(keyPairs.map { it.public }).build(threshold)
|
val compositeKey = CompositeKey.Builder().addKeys(keyPairs.map { it.public }).build(threshold)
|
||||||
|
|
||||||
val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass")
|
|
||||||
val intermediateCa = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass")
|
|
||||||
val rootCert = caKeyStore.getCertificate(X509Utilities.CORDA_ROOT_CA)
|
|
||||||
|
|
||||||
keyPairs.zip(dirs) { keyPair, nodeDir ->
|
keyPairs.zip(dirs) { keyPair, nodeDir ->
|
||||||
val (serviceKeyCert, compositeKeyCert) = listOf(keyPair.public, compositeKey).map { publicKey ->
|
val (serviceKeyCert, compositeKeyCert) = listOf(keyPair.public, compositeKey).map { publicKey ->
|
||||||
X509Utilities.createCertificate(
|
X509Utilities.createCertificate(
|
||||||
CertificateType.SERVICE_IDENTITY,
|
CertificateType.SERVICE_IDENTITY,
|
||||||
intermediateCa.certificate,
|
DEV_INTERMEDIATE_CA.certificate,
|
||||||
intermediateCa.keyPair,
|
DEV_INTERMEDIATE_CA.keyPair,
|
||||||
notaryName.x500Principal,
|
notaryName.x500Principal,
|
||||||
publicKey)
|
publicKey)
|
||||||
}
|
}
|
||||||
@ -74,7 +65,7 @@ object DevIdentityGenerator {
|
|||||||
"$DISTRIBUTED_NOTARY_ALIAS_PREFIX-private-key",
|
"$DISTRIBUTED_NOTARY_ALIAS_PREFIX-private-key",
|
||||||
keyPair.private,
|
keyPair.private,
|
||||||
"cordacadevkeypass".toCharArray(),
|
"cordacadevkeypass".toCharArray(),
|
||||||
arrayOf(serviceKeyCert, intermediateCa.certificate, rootCert))
|
arrayOf(serviceKeyCert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate))
|
||||||
keystore.save(distServKeyStoreFile, "cordacadevpass")
|
keystore.save(distServKeyStoreFile, "cordacadevpass")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,12 +9,17 @@ import org.bouncycastle.asn1.x509.GeneralName
|
|||||||
import org.bouncycastle.asn1.x509.GeneralSubtree
|
import org.bouncycastle.asn1.x509.GeneralSubtree
|
||||||
import org.bouncycastle.asn1.x509.NameConstraints
|
import org.bouncycastle.asn1.x509.NameConstraints
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
|
import javax.security.auth.x500.X500Principal
|
||||||
|
|
||||||
|
// TODO Merge this file and DevIdentityGenerator
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the node and SSL key stores needed by a node. The node key store will be populated with a node CA cert (using
|
* Create the node and SSL key stores needed by a node. The node key store will be populated with a node CA cert (using
|
||||||
* the given legal name), and the SSL key store will store the TLS cert which is a sub-cert of the node CA.
|
* the given legal name), and the SSL key store will store the TLS cert which is a sub-cert of the node CA.
|
||||||
*/
|
*/
|
||||||
fun SSLConfiguration.createDevKeyStores(rootCert: X509Certificate, intermediateCa: CertificateAndKeyPair, legalName: CordaX500Name) {
|
fun SSLConfiguration.createDevKeyStores(legalName: CordaX500Name,
|
||||||
|
rootCert: X509Certificate = DEV_ROOT_CA.certificate,
|
||||||
|
intermediateCa: CertificateAndKeyPair = DEV_INTERMEDIATE_CA) {
|
||||||
val (nodeCaCert, nodeCaKeyPair) = createDevNodeCa(intermediateCa, legalName)
|
val (nodeCaCert, nodeCaKeyPair) = createDevNodeCa(intermediateCa, legalName)
|
||||||
|
|
||||||
loadOrCreateKeyStore(nodeKeystore, keyStorePassword).apply {
|
loadOrCreateKeyStore(nodeKeystore, keyStorePassword).apply {
|
||||||
@ -39,6 +44,17 @@ fun SSLConfiguration.createDevKeyStores(rootCert: X509Certificate, intermediateC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createDevNetworkMapCa(rootCa: CertificateAndKeyPair = DEV_ROOT_CA): CertificateAndKeyPair {
|
||||||
|
val keyPair = Crypto.generateKeyPair()
|
||||||
|
val cert = X509Utilities.createCertificate(
|
||||||
|
CertificateType.NETWORK_MAP,
|
||||||
|
rootCa.certificate,
|
||||||
|
rootCa.keyPair,
|
||||||
|
X500Principal("CN=Network Map,O=R3 Ltd,L=London,C=GB"),
|
||||||
|
keyPair.public)
|
||||||
|
return CertificateAndKeyPair(cert, keyPair)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a dev node CA cert, as a sub-cert of the given [intermediateCa], and matching key pair using the given
|
* Create a dev node CA cert, as a sub-cert of the given [intermediateCa], and matching key pair using the given
|
||||||
* [CordaX500Name] as the cert subject.
|
* [CordaX500Name] as the cert subject.
|
||||||
@ -55,3 +71,16 @@ fun createDevNodeCa(intermediateCa: CertificateAndKeyPair, legalName: CordaX500N
|
|||||||
nameConstraints = nameConstraints)
|
nameConstraints = nameConstraints)
|
||||||
return CertificateAndKeyPair(cert, keyPair)
|
return CertificateAndKeyPair(cert, keyPair)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val DEV_INTERMEDIATE_CA: CertificateAndKeyPair get() = DevCaHelper.loadDevCa(X509Utilities.CORDA_INTERMEDIATE_CA)
|
||||||
|
|
||||||
|
val DEV_ROOT_CA: CertificateAndKeyPair get() = DevCaHelper.loadDevCa(X509Utilities.CORDA_ROOT_CA)
|
||||||
|
|
||||||
|
// We need a class so that we can get hold of the class loader
|
||||||
|
internal object DevCaHelper {
|
||||||
|
fun loadDevCa(alias: String): CertificateAndKeyPair {
|
||||||
|
// TODO: Should be identity scheme
|
||||||
|
val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass")
|
||||||
|
return caKeyStore.getCertificateAndKeyPair(alias, "cordacadevkeypass")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -13,12 +13,14 @@ import java.security.SignatureException
|
|||||||
* A signed [NodeInfo] object containing a signature for each identity. The list of signatures is expected
|
* A signed [NodeInfo] object containing a signature for each identity. The list of signatures is expected
|
||||||
* to be in the same order as the identities.
|
* to be in the same order as the identities.
|
||||||
*/
|
*/
|
||||||
|
// TODO Move this to net.corda.nodeapi.internal.network
|
||||||
// TODO Add signatures for composite keys. The current thinking is to make sure there is a signature for each leaf key
|
// TODO Add signatures for composite keys. The current thinking is to make sure there is a signature for each leaf key
|
||||||
// that the node owns. This check can only be done by the network map server as it can check with the doorman if a node
|
// that the node owns. This check can only be done by the network map server as it can check with the doorman if a node
|
||||||
// is part of a composite identity. This of course further requires the doorman being able to issue CSRs for composite
|
// is part of a composite identity. This of course further requires the doorman being able to issue CSRs for composite
|
||||||
// public keys.
|
// public keys.
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
class SignedNodeInfo(val raw: SerializedBytes<NodeInfo>, val signatures: List<DigitalSignature>) {
|
class SignedNodeInfo(val raw: SerializedBytes<NodeInfo>, val signatures: List<DigitalSignature>) {
|
||||||
|
// TODO Add root cert param (or TrustAnchor) to make sure all the identities belong to the same root
|
||||||
fun verified(): NodeInfo {
|
fun verified(): NodeInfo {
|
||||||
val nodeInfo = raw.deserialize()
|
val nodeInfo = raw.deserialize()
|
||||||
val identities = nodeInfo.legalIdentities.filterNot { it.owningKey is CompositeKey }
|
val identities = nodeInfo.legalIdentities.filterNot { it.owningKey is CompositeKey }
|
||||||
|
@ -1,16 +1,12 @@
|
|||||||
package net.corda.nodeapi.internal.network
|
package net.corda.nodeapi.internal.network
|
||||||
|
|
||||||
import net.corda.core.crypto.DigitalSignature
|
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.verify
|
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.internal.CertRole
|
||||||
|
import net.corda.core.internal.SignedDataWithCert
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.serialization.SerializedBytes
|
|
||||||
import net.corda.core.serialization.deserialize
|
|
||||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||||
import java.security.SignatureException
|
|
||||||
import java.security.cert.CertPathValidatorException
|
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
@ -55,30 +51,8 @@ data class NetworkParameters(
|
|||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
data class NotaryInfo(val identity: Party, val validating: Boolean)
|
data class NotaryInfo(val identity: Party, val validating: Boolean)
|
||||||
|
|
||||||
/**
|
fun <T : Any> SignedDataWithCert<T>.verifiedNetworkMapCert(rootCert: X509Certificate): T {
|
||||||
* A serialized [NetworkMap] and its signature and certificate. Enforces signature validity in order to deserialize the data
|
require(CertRole.extract(sig.by) == CertRole.NETWORK_MAP) { "Incorrect cert role: ${CertRole.extract(sig.by)}" }
|
||||||
* contained within.
|
X509Utilities.validateCertificateChain(rootCert, sig.by, rootCert)
|
||||||
*/
|
return verified()
|
||||||
@CordaSerializable
|
|
||||||
class SignedNetworkMap(val raw: SerializedBytes<NetworkMap>, val signature: DigitalSignatureWithCert) {
|
|
||||||
/**
|
|
||||||
* Return the deserialized NetworkMap if the signature and certificate can be verified.
|
|
||||||
*
|
|
||||||
* @throws CertPathValidatorException if the certificate path is invalid.
|
|
||||||
* @throws SignatureException if the signature is invalid.
|
|
||||||
*/
|
|
||||||
@Throws(SignatureException::class, CertPathValidatorException::class)
|
|
||||||
fun verified(trustedRoot: X509Certificate?): NetworkMap {
|
|
||||||
signature.by.publicKey.verify(raw.bytes, signature)
|
|
||||||
if (trustedRoot != null) {
|
|
||||||
// Assume network map cert is under the default trust root.
|
|
||||||
X509Utilities.validateCertificateChain(trustedRoot, signature.by, trustedRoot)
|
|
||||||
}
|
|
||||||
return raw.deserialize()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This class should reside in the [DigitalSignature] class.
|
|
||||||
// TODO: Removing the val from signatureBytes causes serialisation issues
|
|
||||||
/** A digital signature that identifies who the public key is owned by, and the certificate which provides prove of the identity */
|
|
||||||
class DigitalSignatureWithCert(val by: X509Certificate, val signatureBytes: ByteArray) : DigitalSignature(signatureBytes)
|
|
||||||
|
@ -1,32 +1,29 @@
|
|||||||
package net.corda.nodeapi.internal.network
|
package net.corda.nodeapi.internal.network
|
||||||
|
|
||||||
import net.corda.core.crypto.Crypto
|
|
||||||
import net.corda.core.crypto.SignedData
|
|
||||||
import net.corda.core.crypto.sign
|
|
||||||
import net.corda.core.internal.copyTo
|
import net.corda.core.internal.copyTo
|
||||||
import net.corda.core.internal.div
|
import net.corda.core.internal.div
|
||||||
|
import net.corda.core.internal.signWithCert
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
import net.corda.nodeapi.internal.createDevNetworkMapCa
|
||||||
|
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||||
import java.nio.file.FileAlreadyExistsException
|
import java.nio.file.FileAlreadyExistsException
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.nio.file.StandardCopyOption
|
import java.nio.file.StandardCopyOption
|
||||||
import java.security.KeyPair
|
|
||||||
|
|
||||||
class NetworkParametersCopier(
|
class NetworkParametersCopier(
|
||||||
networkParameters: NetworkParameters,
|
networkParameters: NetworkParameters,
|
||||||
signingKeyPair: KeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME),
|
networkMapCa: CertificateAndKeyPair = createDevNetworkMapCa(),
|
||||||
overwriteFile: Boolean = false
|
overwriteFile: Boolean = false
|
||||||
) {
|
) {
|
||||||
private val copyOptions = if (overwriteFile) arrayOf(StandardCopyOption.REPLACE_EXISTING) else emptyArray()
|
private val copyOptions = if (overwriteFile) arrayOf(StandardCopyOption.REPLACE_EXISTING) else emptyArray()
|
||||||
private val serializedNetworkParameters = networkParameters.let {
|
private val serialisedSignedNetParams = networkParameters.signWithCert(
|
||||||
val serialize = it.serialize()
|
networkMapCa.keyPair.private,
|
||||||
val signature = signingKeyPair.sign(serialize)
|
networkMapCa.certificate
|
||||||
SignedData(serialize, signature).serialize()
|
).serialize()
|
||||||
}
|
|
||||||
|
|
||||||
fun install(nodeDir: Path) {
|
fun install(nodeDir: Path) {
|
||||||
try {
|
try {
|
||||||
serializedNetworkParameters.open().copyTo(nodeDir / NETWORK_PARAMS_FILE_NAME, *copyOptions)
|
serialisedSignedNetParams.open().copyTo(nodeDir / NETWORK_PARAMS_FILE_NAME, *copyOptions)
|
||||||
} catch (e: FileAlreadyExistsException) {
|
} catch (e: FileAlreadyExistsException) {
|
||||||
// This is only thrown if the file already exists and we didn't specify to overwrite it. In that case we
|
// This is only thrown if the file already exists and we didn't specify to overwrite it. In that case we
|
||||||
// ignore this exception as we're happy with the existing file.
|
// ignore this exception as we're happy with the existing file.
|
||||||
|
@ -54,7 +54,6 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) {
|
|||||||
inline fun <reified T : Any> deserialize(bytes: SerializedBytes<T>): T =
|
inline fun <reified T : Any> deserialize(bytes: SerializedBytes<T>): T =
|
||||||
deserialize(bytes, T::class.java)
|
deserialize(bytes, T::class.java)
|
||||||
|
|
||||||
|
|
||||||
@Throws(NotSerializableException::class)
|
@Throws(NotSerializableException::class)
|
||||||
inline internal fun <reified T : Any> deserializeAndReturnEnvelope(bytes: SerializedBytes<T>): ObjectAndEnvelope<T> =
|
inline internal fun <reified T : Any> deserializeAndReturnEnvelope(bytes: SerializedBytes<T>): ObjectAndEnvelope<T> =
|
||||||
deserializeAndReturnEnvelope(bytes, T::class.java)
|
deserializeAndReturnEnvelope(bytes, T::class.java)
|
||||||
|
@ -106,7 +106,7 @@ class EnumEvolutionSerializer(
|
|||||||
// to the name as it exists. We want to test any new constants have been added to the end
|
// to the name as it exists. We want to test any new constants have been added to the end
|
||||||
// of the enum class
|
// of the enum class
|
||||||
val serialisedOrds = ((schemas.schema.types.find { it.name == old.name } as RestrictedType).choices
|
val serialisedOrds = ((schemas.schema.types.find { it.name == old.name } as RestrictedType).choices
|
||||||
.associateBy ({ it.value.toInt() }, { conversions[it.name] }))
|
.associateBy({ it.value.toInt() }, { conversions[it.name] }))
|
||||||
|
|
||||||
if (ordinals.filterNot { serialisedOrds[it.value] == it.key }.isNotEmpty()) {
|
if (ordinals.filterNot { serialisedOrds[it.value] == it.key }.isNotEmpty()) {
|
||||||
throw NotSerializableException("Constants have been reordered, additions must be appended to the end")
|
throw NotSerializableException("Constants have been reordered, additions must be appended to the end")
|
||||||
|
@ -130,3 +130,34 @@ class EvolutionSerializer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instances of this type are injected into a [SerializerFactory] at creation time to dictate the
|
||||||
|
* behaviour of evolution within that factory. Under normal circumstances this will simply
|
||||||
|
* be an object that returns an [EvolutionSerializer]. Of course, any implementation that
|
||||||
|
* extends this class can be written to invoke whatever behaviour is desired.
|
||||||
|
*/
|
||||||
|
abstract class EvolutionSerializerGetterBase {
|
||||||
|
abstract fun getEvolutionSerializer(
|
||||||
|
factory: SerializerFactory,
|
||||||
|
typeNotation: TypeNotation,
|
||||||
|
newSerializer: AMQPSerializer<Any>,
|
||||||
|
schemas: SerializationSchemas): AMQPSerializer<Any>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The normal use case for generating an [EvolutionSerializer]'s based on the differences
|
||||||
|
* between the received schema and the class as it exists now on the class path,
|
||||||
|
*/
|
||||||
|
class EvolutionSerializerGetter : EvolutionSerializerGetterBase() {
|
||||||
|
override fun getEvolutionSerializer(factory: SerializerFactory,
|
||||||
|
typeNotation: TypeNotation,
|
||||||
|
newSerializer: AMQPSerializer<Any>,
|
||||||
|
schemas: SerializationSchemas): AMQPSerializer<Any> =
|
||||||
|
factory.getSerializersByDescriptor().computeIfAbsent(typeNotation.descriptor.name!!) {
|
||||||
|
when (typeNotation) {
|
||||||
|
is CompositeType -> EvolutionSerializer.make(typeNotation, newSerializer as ObjectSerializer, factory)
|
||||||
|
is RestrictedType -> EnumEvolutionSerializer.make(typeNotation, newSerializer, factory, schemas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -346,19 +346,54 @@ private fun Hasher.fingerprintWithCustomSerializerOrElse(factory: SerializerFact
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method concatentates various elements of the types recursively as unencoded strings into the hasher, effectively
|
// This method concatenates various elements of the types recursively as unencoded strings into the hasher, effectively
|
||||||
// creating a unique string for a type which we then hash in the calling function above.
|
// creating a unique string for a type which we then hash in the calling function above.
|
||||||
private fun fingerprintForType(type: Type, contextType: Type?, alreadySeen: MutableSet<Type>, hasher: Hasher, factory: SerializerFactory): Hasher {
|
private fun fingerprintForType(type: Type, contextType: Type?, alreadySeen: MutableSet<Type>,
|
||||||
return if (type in alreadySeen) {
|
hasher: Hasher, factory: SerializerFactory, offset: Int = 4): Hasher {
|
||||||
|
|
||||||
|
// We don't include Example<?> and Example<T> where type is ? or T in this otherwise we
|
||||||
|
// generate different fingerprints for class Outer<T>(val a: Inner<T>) when serialising
|
||||||
|
// and deserializing (assuming deserialization is occurring in a factory that didn't
|
||||||
|
// serialise the object in the first place (and thus the cache lookup fails). This is also
|
||||||
|
// true of Any, where we need Example<A, B> and Example<?, ?> to have the same fingerprint
|
||||||
|
return if (type in alreadySeen && (type !is SerializerFactory.AnyType) && (type !is TypeVariable<*>)) {
|
||||||
hasher.putUnencodedChars(ALREADY_SEEN_HASH)
|
hasher.putUnencodedChars(ALREADY_SEEN_HASH)
|
||||||
} else {
|
} else {
|
||||||
alreadySeen += type
|
alreadySeen += type
|
||||||
try {
|
try {
|
||||||
when (type) {
|
when (type) {
|
||||||
is SerializerFactory.AnyType -> hasher.putUnencodedChars(ANY_TYPE_HASH)
|
is ParameterizedType -> {
|
||||||
|
// Hash the rawType + params
|
||||||
|
val clazz = type.rawType as Class<*>
|
||||||
|
|
||||||
|
val startingHash = if (isCollectionOrMap(clazz)) {
|
||||||
|
hasher.putUnencodedChars(clazz.name)
|
||||||
|
} else {
|
||||||
|
hasher.fingerprintWithCustomSerializerOrElse(factory, clazz, type) {
|
||||||
|
fingerprintForObject(type, type, alreadySeen, hasher, factory, offset+4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... and concatenate the type data for each parameter type.
|
||||||
|
type.actualTypeArguments.fold(startingHash) { orig, paramType ->
|
||||||
|
fingerprintForType(paramType, type, alreadySeen, orig, factory, offset+4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Treat generic types as "any type" to prevent fingerprint mismatch. This case we fall into when
|
||||||
|
// looking at A and B from Example<A, B> (remember we call this function recursively). When
|
||||||
|
// serialising a concrete example of the type we have A and B which are TypeVariables<*>'s but
|
||||||
|
// when deserializing we only have the wildcard placeholder ?, or AnyType
|
||||||
|
//
|
||||||
|
// Note, TypeVariable<*> used to be encoded as TYPE_VARIABLE_HASH but that again produces a
|
||||||
|
// differing fingerprint on serialisation and deserialization
|
||||||
|
is SerializerFactory.AnyType,
|
||||||
|
is TypeVariable<*> -> {
|
||||||
|
hasher.putUnencodedChars("?").putUnencodedChars(ANY_TYPE_HASH)
|
||||||
|
}
|
||||||
is Class<*> -> {
|
is Class<*> -> {
|
||||||
if (type.isArray) {
|
if (type.isArray) {
|
||||||
fingerprintForType(type.componentType, contextType, alreadySeen, hasher, factory).putUnencodedChars(ARRAY_HASH)
|
fingerprintForType(type.componentType, contextType, alreadySeen, hasher, factory, offset+4)
|
||||||
|
.putUnencodedChars(ARRAY_HASH)
|
||||||
} else if (SerializerFactory.isPrimitive(type)) {
|
} else if (SerializerFactory.isPrimitive(type)) {
|
||||||
hasher.putUnencodedChars(type.name)
|
hasher.putUnencodedChars(type.name)
|
||||||
} else if (isCollectionOrMap(type)) {
|
} else if (isCollectionOrMap(type)) {
|
||||||
@ -377,32 +412,18 @@ private fun fingerprintForType(type: Type, contextType: Type?, alreadySeen: Muta
|
|||||||
// to the CorDapp but maybe reference to the JAR in the short term.
|
// to the CorDapp but maybe reference to the JAR in the short term.
|
||||||
hasher.putUnencodedChars(type.name)
|
hasher.putUnencodedChars(type.name)
|
||||||
} else {
|
} else {
|
||||||
fingerprintForObject(type, type, alreadySeen, hasher, factory)
|
fingerprintForObject(type, type, alreadySeen, hasher, factory, offset+4)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is ParameterizedType -> {
|
|
||||||
// Hash the rawType + params
|
|
||||||
val clazz = type.rawType as Class<*>
|
|
||||||
val startingHash = if (isCollectionOrMap(clazz)) {
|
|
||||||
hasher.putUnencodedChars(clazz.name)
|
|
||||||
} else {
|
|
||||||
hasher.fingerprintWithCustomSerializerOrElse(factory, clazz, type) {
|
|
||||||
fingerprintForObject(type, type, alreadySeen, hasher, factory)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ... and concatentate the type data for each parameter type.
|
|
||||||
type.actualTypeArguments.fold(startingHash) { orig, paramType ->
|
|
||||||
fingerprintForType(paramType, type, alreadySeen, orig, factory)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Hash the element type + some array hash
|
// Hash the element type + some array hash
|
||||||
is GenericArrayType -> fingerprintForType(type.genericComponentType, contextType, alreadySeen,
|
is GenericArrayType -> fingerprintForType(type.genericComponentType, contextType, alreadySeen,
|
||||||
hasher, factory).putUnencodedChars(ARRAY_HASH)
|
hasher, factory, offset+4).putUnencodedChars(ARRAY_HASH)
|
||||||
// TODO: include bounds
|
// TODO: include bounds
|
||||||
is TypeVariable<*> -> hasher.putUnencodedChars(type.name).putUnencodedChars(TYPE_VARIABLE_HASH)
|
is WildcardType -> {
|
||||||
is WildcardType -> hasher.putUnencodedChars(type.typeName).putUnencodedChars(WILDCARD_TYPE_HASH)
|
hasher.putUnencodedChars(type.typeName).putUnencodedChars(WILDCARD_TYPE_HASH)
|
||||||
|
}
|
||||||
else -> throw NotSerializableException("Don't know how to hash")
|
else -> throw NotSerializableException("Don't know how to hash")
|
||||||
}
|
}
|
||||||
} catch (e: NotSerializableException) {
|
} catch (e: NotSerializableException) {
|
||||||
@ -416,15 +437,21 @@ private fun fingerprintForType(type: Type, contextType: Type?, alreadySeen: Muta
|
|||||||
private fun isCollectionOrMap(type: Class<*>) = (Collection::class.java.isAssignableFrom(type) || Map::class.java.isAssignableFrom(type)) &&
|
private fun isCollectionOrMap(type: Class<*>) = (Collection::class.java.isAssignableFrom(type) || Map::class.java.isAssignableFrom(type)) &&
|
||||||
!EnumSet::class.java.isAssignableFrom(type)
|
!EnumSet::class.java.isAssignableFrom(type)
|
||||||
|
|
||||||
private fun fingerprintForObject(type: Type, contextType: Type?, alreadySeen: MutableSet<Type>, hasher: Hasher, factory: SerializerFactory): Hasher {
|
private fun fingerprintForObject(
|
||||||
|
type: Type,
|
||||||
|
contextType: Type?,
|
||||||
|
alreadySeen: MutableSet<Type>,
|
||||||
|
hasher: Hasher,
|
||||||
|
factory: SerializerFactory,
|
||||||
|
offset: Int = 0): Hasher {
|
||||||
// Hash the class + properties + interfaces
|
// Hash the class + properties + interfaces
|
||||||
val name = type.asClass()?.name ?: throw NotSerializableException("Expected only Class or ParameterizedType but found $type")
|
val name = type.asClass()?.name ?: throw NotSerializableException("Expected only Class or ParameterizedType but found $type")
|
||||||
propertiesForSerialization(constructorForDeserialization(type), contextType ?: type, factory).getters
|
propertiesForSerialization(constructorForDeserialization(type), contextType ?: type, factory).getters
|
||||||
.fold(hasher.putUnencodedChars(name)) { orig, prop ->
|
.fold(hasher.putUnencodedChars(name)) { orig, prop ->
|
||||||
fingerprintForType(prop.resolvedType, type, alreadySeen, orig, factory)
|
fingerprintForType(prop.resolvedType, type, alreadySeen, orig, factory, offset+4)
|
||||||
.putUnencodedChars(prop.name)
|
.putUnencodedChars(prop.name)
|
||||||
.putUnencodedChars(if (prop.mandatory) NOT_NULLABLE_HASH else NULLABLE_HASH)
|
.putUnencodedChars(if (prop.mandatory) NOT_NULLABLE_HASH else NULLABLE_HASH)
|
||||||
}
|
}
|
||||||
interfacesForSerialization(type, factory).map { fingerprintForType(it, type, alreadySeen, hasher, factory) }
|
interfacesForSerialization(type, factory).map { fingerprintForType(it, type, alreadySeen, hasher, factory, offset+4) }
|
||||||
return hasher
|
return hasher
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,10 @@ data class FactorySchemaAndDescriptor(val schemas: SerializationSchemas, val typ
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory of serializers designed to be shared across threads and invocations.
|
* Factory of serializers designed to be shared across threads and invocations.
|
||||||
|
*
|
||||||
|
* @property evolutionSerializerGetter controls how evolution serializers are generated by the factory. The normal
|
||||||
|
* use case is an [EvolutionSerializer] type is returned. However, in some scenarios, primarily testing, this
|
||||||
|
* can be altered to fit the requirements of the test.
|
||||||
*/
|
*/
|
||||||
// TODO: support for intern-ing of deserialized objects for some core types (e.g. PublicKey) for memory efficiency
|
// TODO: support for intern-ing of deserialized objects for some core types (e.g. PublicKey) for memory efficiency
|
||||||
// TODO: maybe support for caching of serialized form of some core types for performance
|
// TODO: maybe support for caching of serialized form of some core types for performance
|
||||||
@ -33,28 +37,27 @@ data class FactorySchemaAndDescriptor(val schemas: SerializationSchemas, val typ
|
|||||||
// TODO: need to rethink matching of constructor to properties in relation to implementing interfaces and needing those properties etc.
|
// TODO: need to rethink matching of constructor to properties in relation to implementing interfaces and needing those properties etc.
|
||||||
// TODO: need to support super classes as well as interfaces with our current code base... what's involved? If we continue to ban, what is the impact?
|
// TODO: need to support super classes as well as interfaces with our current code base... what's involved? If we continue to ban, what is the impact?
|
||||||
@ThreadSafe
|
@ThreadSafe
|
||||||
open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
|
open class SerializerFactory(
|
||||||
|
val whitelist: ClassWhitelist,
|
||||||
|
cl: ClassLoader,
|
||||||
|
private val evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter()) {
|
||||||
private val serializersByType = ConcurrentHashMap<Type, AMQPSerializer<Any>>()
|
private val serializersByType = ConcurrentHashMap<Type, AMQPSerializer<Any>>()
|
||||||
private val serializersByDescriptor = ConcurrentHashMap<Any, AMQPSerializer<Any>>()
|
private val serializersByDescriptor = ConcurrentHashMap<Any, AMQPSerializer<Any>>()
|
||||||
private val customSerializers = CopyOnWriteArrayList<SerializerFor>()
|
private val customSerializers = CopyOnWriteArrayList<SerializerFor>()
|
||||||
val transformsCache = ConcurrentHashMap<String, EnumMap<TransformTypes, MutableList<Transform>>>()
|
private val transformsCache = ConcurrentHashMap<String, EnumMap<TransformTypes, MutableList<Transform>>>()
|
||||||
|
|
||||||
open val classCarpenter = ClassCarpenter(cl, whitelist)
|
open val classCarpenter = ClassCarpenter(cl, whitelist)
|
||||||
|
|
||||||
val classloader: ClassLoader
|
val classloader: ClassLoader
|
||||||
get() = classCarpenter.classloader
|
get() = classCarpenter.classloader
|
||||||
|
|
||||||
private fun getEvolutionSerializer(
|
private fun getEvolutionSerializer(typeNotation: TypeNotation, newSerializer: AMQPSerializer<Any>,
|
||||||
typeNotation: TypeNotation,
|
schemas: SerializationSchemas)
|
||||||
newSerializer: AMQPSerializer<Any>,
|
= evolutionSerializerGetter.getEvolutionSerializer(this, typeNotation, newSerializer, schemas)
|
||||||
schemas: SerializationSchemas): AMQPSerializer<Any> {
|
|
||||||
return serializersByDescriptor.computeIfAbsent(typeNotation.descriptor.name!!) {
|
fun getSerializersByDescriptor() = serializersByDescriptor
|
||||||
when (typeNotation) {
|
|
||||||
is CompositeType -> EvolutionSerializer.make(typeNotation, newSerializer as ObjectSerializer, this)
|
fun getTransformsCache() = transformsCache
|
||||||
is RestrictedType -> EnumEvolutionSerializer.make(typeNotation, newSerializer, this, schemas)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Look up, and manufacture if necessary, a serializer for the given type.
|
* Look up, and manufacture if necessary, a serializer for the given type.
|
||||||
@ -93,7 +96,9 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
|
|||||||
whitelist.requireWhitelisted(actualType)
|
whitelist.requireWhitelisted(actualType)
|
||||||
EnumSerializer(actualType, actualClass ?: declaredClass, this)
|
EnumSerializer(actualType, actualClass ?: declaredClass, this)
|
||||||
}
|
}
|
||||||
else -> makeClassSerializer(actualClass ?: declaredClass, actualType, declaredType)
|
else -> {
|
||||||
|
makeClassSerializer(actualClass ?: declaredClass, actualType, declaredType)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
serializersByDescriptor.putIfAbsent(serializer.typeDescriptor, serializer)
|
serializersByDescriptor.putIfAbsent(serializer.typeDescriptor, serializer)
|
||||||
@ -102,23 +107,23 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Try and infer concrete types for any generics type variables for the actual class encountered, based on the declared
|
* Try and infer concrete types for any generics type variables for the actual class encountered,
|
||||||
* type.
|
* based on the declared type.
|
||||||
*/
|
*/
|
||||||
// TODO: test GenericArrayType
|
// TODO: test GenericArrayType
|
||||||
private fun inferTypeVariables(actualClass: Class<*>?, declaredClass: Class<*>, declaredType: Type): Type? =
|
private fun inferTypeVariables(actualClass: Class<*>?, declaredClass: Class<*>,
|
||||||
when (declaredType) {
|
declaredType: Type) : Type? = when (declaredType) {
|
||||||
is ParameterizedType -> inferTypeVariables(actualClass, declaredClass, declaredType)
|
is ParameterizedType -> inferTypeVariables(actualClass, declaredClass, declaredType)
|
||||||
// Nothing to infer, otherwise we'd have ParameterizedType
|
// Nothing to infer, otherwise we'd have ParameterizedType
|
||||||
is Class<*> -> actualClass
|
is Class<*> -> actualClass
|
||||||
is GenericArrayType -> {
|
is GenericArrayType -> {
|
||||||
val declaredComponent = declaredType.genericComponentType
|
val declaredComponent = declaredType.genericComponentType
|
||||||
inferTypeVariables(actualClass?.componentType, declaredComponent.asClass()!!, declaredComponent)?.asArray()
|
inferTypeVariables(actualClass?.componentType, declaredComponent.asClass()!!, declaredComponent)?.asArray()
|
||||||
}
|
}
|
||||||
is TypeVariable<*> -> actualClass
|
is TypeVariable<*> -> actualClass
|
||||||
is WildcardType -> actualClass
|
is WildcardType -> actualClass
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Try and infer concrete types for any generics type variables for the actual class encountered, based on the declared
|
* Try and infer concrete types for any generics type variables for the actual class encountered, based on the declared
|
||||||
@ -214,9 +219,9 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
|
|||||||
try {
|
try {
|
||||||
val serialiser = processSchemaEntry(typeNotation)
|
val serialiser = processSchemaEntry(typeNotation)
|
||||||
|
|
||||||
// if we just successfully built a serialiser for the type but the type fingerprint
|
// if we just successfully built a serializer for the type but the type fingerprint
|
||||||
// doesn't match that of the serialised object then we are dealing with different
|
// doesn't match that of the serialised object then we are dealing with different
|
||||||
// instance of the class, as such we need to build an EvolutionSerialiser
|
// instance of the class, as such we need to build an EvolutionSerializer
|
||||||
if (serialiser.typeDescriptor != typeNotation.descriptor.name) {
|
if (serialiser.typeDescriptor != typeNotation.descriptor.name) {
|
||||||
getEvolutionSerializer(typeNotation, serialiser, schemaAndDescriptor.schemas)
|
getEvolutionSerializer(typeNotation, serialiser, schemaAndDescriptor.schemas)
|
||||||
}
|
}
|
||||||
@ -337,7 +342,9 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
|
|||||||
"${nameForType(type.componentType)}${if (type.componentType.isPrimitive) "[p]" else "[]"}"
|
"${nameForType(type.componentType)}${if (type.componentType.isPrimitive) "[p]" else "[]"}"
|
||||||
} else type.name
|
} else type.name
|
||||||
}
|
}
|
||||||
is ParameterizedType -> "${nameForType(type.rawType)}<${type.actualTypeArguments.joinToString { nameForType(it) }}>"
|
is ParameterizedType -> {
|
||||||
|
"${nameForType(type.rawType)}<${type.actualTypeArguments.joinToString { nameForType(it) }}>"
|
||||||
|
}
|
||||||
is GenericArrayType -> "${nameForType(type.genericComponentType)}[]"
|
is GenericArrayType -> "${nameForType(type.genericComponentType)}[]"
|
||||||
is WildcardType -> "?"
|
is WildcardType -> "?"
|
||||||
is TypeVariable<*> -> "?"
|
is TypeVariable<*> -> "?"
|
||||||
|
@ -200,7 +200,7 @@ data class TransformsSchema(val types: Map<String, EnumMap<TransformTypes, Mutab
|
|||||||
* @param sf the [SerializerFactory] building this transform set. Needed as each can define it's own
|
* @param sf the [SerializerFactory] building this transform set. Needed as each can define it's own
|
||||||
* class loader and this dictates which classes we can and cannot see
|
* class loader and this dictates which classes we can and cannot see
|
||||||
*/
|
*/
|
||||||
fun get(name: String, sf: SerializerFactory) = sf.transformsCache.computeIfAbsent(name) {
|
fun get(name: String, sf: SerializerFactory) = sf.getTransformsCache().computeIfAbsent(name) {
|
||||||
val transforms = EnumMap<TransformTypes, MutableList<Transform>>(TransformTypes::class.java)
|
val transforms = EnumMap<TransformTypes, MutableList<Transform>>(TransformTypes::class.java)
|
||||||
try {
|
try {
|
||||||
val clazz = sf.classloader.loadClass(name)
|
val clazz = sf.classloader.loadClass(name)
|
||||||
|
@ -31,7 +31,12 @@ public class ErrorMessageTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testJavaConstructorAnnotations() {
|
public void testJavaConstructorAnnotations() {
|
||||||
SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader());
|
EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
|
||||||
|
SerializerFactory factory1 = new SerializerFactory(
|
||||||
|
AllWhitelist.INSTANCE,
|
||||||
|
ClassLoader.getSystemClassLoader(),
|
||||||
|
evolutionSerialiserGetter);
|
||||||
|
|
||||||
SerializationOutput ser = new SerializationOutput(factory1);
|
SerializationOutput ser = new SerializationOutput(factory1);
|
||||||
|
|
||||||
Assertions.assertThatThrownBy(() -> ser.serialize(new C(1)))
|
Assertions.assertThatThrownBy(() -> ser.serialize(new C(1)))
|
||||||
|
@ -0,0 +1,93 @@
|
|||||||
|
package net.corda.nodeapi.internal.serialization.amqp;
|
||||||
|
|
||||||
|
import net.corda.core.serialization.SerializedBytes;
|
||||||
|
import net.corda.nodeapi.internal.serialization.AllWhitelist;
|
||||||
|
import org.junit.Test;
|
||||||
|
import java.io.NotSerializableException;
|
||||||
|
|
||||||
|
import static org.jgroups.util.Util.assertEquals;
|
||||||
|
|
||||||
|
public class JavaGenericsTest {
|
||||||
|
private static class Inner {
|
||||||
|
private final Integer v;
|
||||||
|
|
||||||
|
private Inner(Integer v) { this.v = v; }
|
||||||
|
public Integer getV() { return v; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class A<T> {
|
||||||
|
private final T t;
|
||||||
|
|
||||||
|
private A(T t) { this.t = t; }
|
||||||
|
public T getT() { return t; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void basicGeneric() throws NotSerializableException {
|
||||||
|
A a1 = new A(1);
|
||||||
|
|
||||||
|
SerializerFactory factory = new SerializerFactory(
|
||||||
|
AllWhitelist.INSTANCE,
|
||||||
|
ClassLoader.getSystemClassLoader(),
|
||||||
|
new EvolutionSerializerGetter());
|
||||||
|
|
||||||
|
SerializationOutput ser = new SerializationOutput(factory);
|
||||||
|
SerializedBytes<?> bytes = ser.serialize(a1);
|
||||||
|
|
||||||
|
DeserializationInput des = new DeserializationInput(factory);
|
||||||
|
A a2 = des.deserialize(bytes, A.class);
|
||||||
|
|
||||||
|
assertEquals(1, a2.getT());
|
||||||
|
}
|
||||||
|
|
||||||
|
private SerializedBytes<?> forceWildcardSerialize(A<?> a) throws NotSerializableException {
|
||||||
|
SerializerFactory factory = new SerializerFactory(
|
||||||
|
AllWhitelist.INSTANCE,
|
||||||
|
ClassLoader.getSystemClassLoader(),
|
||||||
|
new EvolutionSerializerGetter());
|
||||||
|
|
||||||
|
return (new SerializationOutput(factory)).serialize(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SerializedBytes<?> forceWildcardSerializeFactory(
|
||||||
|
A<?> a,
|
||||||
|
SerializerFactory factory) throws NotSerializableException {
|
||||||
|
return (new SerializationOutput(factory)).serialize(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
private A<?> forceWildcardDeserialize(SerializedBytes<?> bytes) throws NotSerializableException {
|
||||||
|
SerializerFactory factory = new SerializerFactory(
|
||||||
|
AllWhitelist.INSTANCE,
|
||||||
|
ClassLoader.getSystemClassLoader(),
|
||||||
|
new EvolutionSerializerGetter());
|
||||||
|
|
||||||
|
DeserializationInput des = new DeserializationInput(factory);
|
||||||
|
return des.deserialize(bytes, A.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private A<?> forceWildcardDeserializeFactory(
|
||||||
|
SerializedBytes<?> bytes,
|
||||||
|
SerializerFactory factory) throws NotSerializableException {
|
||||||
|
return (new DeserializationInput(factory)).deserialize(bytes, A.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void forceWildcard() throws NotSerializableException {
|
||||||
|
SerializedBytes<?> bytes = forceWildcardSerialize(new A(new Inner(29)));
|
||||||
|
Inner i = (Inner)forceWildcardDeserialize(bytes).getT();
|
||||||
|
assertEquals(29, i.getV());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void forceWildcardSharedFactory() throws NotSerializableException {
|
||||||
|
SerializerFactory factory = new SerializerFactory(
|
||||||
|
AllWhitelist.INSTANCE,
|
||||||
|
ClassLoader.getSystemClassLoader(),
|
||||||
|
new EvolutionSerializerGetter());
|
||||||
|
|
||||||
|
SerializedBytes<?> bytes = forceWildcardSerializeFactory(new A(new Inner(29)), factory);
|
||||||
|
Inner i = (Inner)forceWildcardDeserializeFactory(bytes, factory).getT();
|
||||||
|
|
||||||
|
assertEquals(29, i.getV());
|
||||||
|
}
|
||||||
|
}
|
@ -25,7 +25,9 @@ public class JavaPrivatePropertyTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void singlePrivateWithConstructor() throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
|
public void singlePrivateWithConstructor() throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
|
||||||
SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader());
|
EvolutionSerializerGetterBase evolutionSerializerGetter = new EvolutionSerializerGetter();
|
||||||
|
SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
|
||||||
|
evolutionSerializerGetter);
|
||||||
SerializationOutput ser = new SerializationOutput(factory);
|
SerializationOutput ser = new SerializationOutput(factory);
|
||||||
DeserializationInput des = new DeserializationInput(factory);
|
DeserializationInput des = new DeserializationInput(factory);
|
||||||
|
|
||||||
@ -53,7 +55,9 @@ public class JavaPrivatePropertyTests {
|
|||||||
@Test
|
@Test
|
||||||
public void singlePrivateWithConstructorAndGetter()
|
public void singlePrivateWithConstructorAndGetter()
|
||||||
throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
|
throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
|
||||||
SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader());
|
EvolutionSerializerGetterBase evolutionSerializerGetter = new EvolutionSerializerGetter();
|
||||||
|
SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE,
|
||||||
|
ClassLoader.getSystemClassLoader(), evolutionSerializerGetter);
|
||||||
SerializationOutput ser = new SerializationOutput(factory);
|
SerializationOutput ser = new SerializationOutput(factory);
|
||||||
DeserializationInput des = new DeserializationInput(factory);
|
DeserializationInput des = new DeserializationInput(factory);
|
||||||
|
|
||||||
|
@ -29,7 +29,9 @@ public class JavaSerialiseEnumTests {
|
|||||||
public void testJavaConstructorAnnotations() throws NotSerializableException {
|
public void testJavaConstructorAnnotations() throws NotSerializableException {
|
||||||
Bra bra = new Bra(Bras.UNDERWIRE);
|
Bra bra = new Bra(Bras.UNDERWIRE);
|
||||||
|
|
||||||
SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader());
|
EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
|
||||||
|
SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
|
||||||
|
evolutionSerialiserGetter);
|
||||||
SerializationOutput ser = new SerializationOutput(factory1);
|
SerializationOutput ser = new SerializationOutput(factory1);
|
||||||
SerializedBytes<Object> bytes = ser.serialize(bra);
|
SerializedBytes<Object> bytes = ser.serialize(bra);
|
||||||
}
|
}
|
||||||
|
@ -172,8 +172,11 @@ public class JavaSerializationOutputTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Object serdes(Object obj) throws NotSerializableException {
|
private Object serdes(Object obj) throws NotSerializableException {
|
||||||
SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader());
|
EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
|
||||||
SerializerFactory factory2 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader());
|
SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
|
||||||
|
evolutionSerialiserGetter);
|
||||||
|
SerializerFactory factory2 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
|
||||||
|
evolutionSerialiserGetter);
|
||||||
SerializationOutput ser = new SerializationOutput(factory1);
|
SerializationOutput ser = new SerializationOutput(factory1);
|
||||||
SerializedBytes<Object> bytes = ser.serialize(obj);
|
SerializedBytes<Object> bytes = ser.serialize(obj);
|
||||||
|
|
||||||
|
@ -125,7 +125,9 @@ public class ListsSerializationJavaTest {
|
|||||||
|
|
||||||
// Have to have own version as Kotlin inline functions cannot be easily called from Java
|
// Have to have own version as Kotlin inline functions cannot be easily called from Java
|
||||||
private static <T> void assertEqualAfterRoundTripSerialization(T container, Class<T> clazz) throws Exception {
|
private static <T> void assertEqualAfterRoundTripSerialization(T container, Class<T> clazz) throws Exception {
|
||||||
SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader());
|
EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
|
||||||
|
SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
|
||||||
|
evolutionSerialiserGetter);
|
||||||
SerializationOutput ser = new SerializationOutput(factory1);
|
SerializationOutput ser = new SerializationOutput(factory1);
|
||||||
SerializedBytes<Object> bytes = ser.serialize(container);
|
SerializedBytes<Object> bytes = ser.serialize(container);
|
||||||
DeserializationInput des = new DeserializationInput(factory1);
|
DeserializationInput des = new DeserializationInput(factory1);
|
||||||
|
@ -109,7 +109,12 @@ public class SetterConstructorTests {
|
|||||||
// despite having no constructor we should still be able to serialise an instance of C
|
// despite having no constructor we should still be able to serialise an instance of C
|
||||||
@Test
|
@Test
|
||||||
public void serialiseC() throws NotSerializableException {
|
public void serialiseC() throws NotSerializableException {
|
||||||
SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader());
|
EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
|
||||||
|
SerializerFactory factory1 = new SerializerFactory(
|
||||||
|
AllWhitelist.INSTANCE,
|
||||||
|
ClassLoader.getSystemClassLoader(),
|
||||||
|
evolutionSerialiserGetter);
|
||||||
|
|
||||||
SerializationOutput ser = new SerializationOutput(factory1);
|
SerializationOutput ser = new SerializationOutput(factory1);
|
||||||
|
|
||||||
C c1 = new C();
|
C c1 = new C();
|
||||||
@ -178,7 +183,11 @@ public class SetterConstructorTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void deserialiseC() throws NotSerializableException {
|
public void deserialiseC() throws NotSerializableException {
|
||||||
SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader());
|
EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
|
||||||
|
SerializerFactory factory1 = new SerializerFactory(
|
||||||
|
AllWhitelist.INSTANCE,
|
||||||
|
ClassLoader.getSystemClassLoader(),
|
||||||
|
evolutionSerialiserGetter);
|
||||||
|
|
||||||
C cPre1 = new C();
|
C cPre1 = new C();
|
||||||
|
|
||||||
@ -241,7 +250,11 @@ public class SetterConstructorTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void serialiseOuterAndInner() throws NotSerializableException {
|
public void serialiseOuterAndInner() throws NotSerializableException {
|
||||||
SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader());
|
EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
|
||||||
|
SerializerFactory factory1 = new SerializerFactory(
|
||||||
|
AllWhitelist.INSTANCE,
|
||||||
|
ClassLoader.getSystemClassLoader(),
|
||||||
|
evolutionSerialiserGetter);
|
||||||
|
|
||||||
Inner1 i1 = new Inner1("Hello");
|
Inner1 i1 = new Inner1("Hello");
|
||||||
Inner2 i2 = new Inner2();
|
Inner2 i2 = new Inner2();
|
||||||
@ -263,7 +276,11 @@ public class SetterConstructorTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void typeMistmatch() throws NotSerializableException {
|
public void typeMistmatch() throws NotSerializableException {
|
||||||
SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader());
|
EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
|
||||||
|
SerializerFactory factory1 = new SerializerFactory(
|
||||||
|
AllWhitelist.INSTANCE,
|
||||||
|
ClassLoader.getSystemClassLoader(),
|
||||||
|
evolutionSerialiserGetter);
|
||||||
|
|
||||||
TypeMismatch tm = new TypeMismatch();
|
TypeMismatch tm = new TypeMismatch();
|
||||||
tm.setA(10);
|
tm.setA(10);
|
||||||
@ -279,7 +296,11 @@ public class SetterConstructorTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void typeMistmatch2() throws NotSerializableException {
|
public void typeMistmatch2() throws NotSerializableException {
|
||||||
SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader());
|
EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
|
||||||
|
SerializerFactory factory1 = new SerializerFactory(
|
||||||
|
AllWhitelist.INSTANCE,
|
||||||
|
ClassLoader.getSystemClassLoader(),
|
||||||
|
evolutionSerialiserGetter);
|
||||||
|
|
||||||
TypeMismatch2 tm = new TypeMismatch2();
|
TypeMismatch2 tm = new TypeMismatch2();
|
||||||
tm.setA("10");
|
tm.setA("10");
|
||||||
|
@ -168,7 +168,7 @@ class X509UtilitiesTest {
|
|||||||
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
|
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
|
||||||
|
|
||||||
// Generate server cert and private key and populate another keystore suitable for SSL
|
// Generate server cert and private key and populate another keystore suitable for SSL
|
||||||
sslConfig.createDevKeyStores(rootCa.certificate, intermediateCa, MEGA_CORP.name)
|
sslConfig.createDevKeyStores(MEGA_CORP.name, rootCa.certificate, intermediateCa)
|
||||||
|
|
||||||
// Load back server certificate
|
// Load back server certificate
|
||||||
val serverKeyStore = loadKeyStore(sslConfig.nodeKeystore, sslConfig.keyStorePassword)
|
val serverKeyStore = loadKeyStore(sslConfig.nodeKeystore, sslConfig.keyStorePassword)
|
||||||
@ -203,7 +203,7 @@ class X509UtilitiesTest {
|
|||||||
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
|
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
|
||||||
|
|
||||||
// Generate server cert and private key and populate another keystore suitable for SSL
|
// Generate server cert and private key and populate another keystore suitable for SSL
|
||||||
sslConfig.createDevKeyStores(rootCa.certificate, intermediateCa, MEGA_CORP.name)
|
sslConfig.createDevKeyStores(MEGA_CORP.name, rootCa.certificate, intermediateCa)
|
||||||
sslConfig.createTrustStore(rootCa.certificate)
|
sslConfig.createTrustStore(rootCa.certificate)
|
||||||
|
|
||||||
val keyStore = loadKeyStore(sslConfig.sslKeystore, sslConfig.keyStorePassword)
|
val keyStore = loadKeyStore(sslConfig.sslKeystore, sslConfig.keyStorePassword)
|
||||||
|
@ -5,6 +5,8 @@ import net.corda.nodeapi.internal.serialization.AllWhitelist
|
|||||||
import net.corda.nodeapi.internal.serialization.EmptyWhitelist
|
import net.corda.nodeapi.internal.serialization.EmptyWhitelist
|
||||||
|
|
||||||
fun testDefaultFactory() = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
|
fun testDefaultFactory() = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
|
||||||
|
fun testDefaultFactoryNoEvolution() = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader(),
|
||||||
|
EvolutionSerializerGetterTesting())
|
||||||
fun testDefaultFactoryWithWhitelist() = SerializerFactory(EmptyWhitelist, ClassLoader.getSystemClassLoader())
|
fun testDefaultFactoryWithWhitelist() = SerializerFactory(EmptyWhitelist, ClassLoader.getSystemClassLoader())
|
||||||
|
|
||||||
class TestSerializationOutput(
|
class TestSerializationOutput(
|
||||||
|
@ -11,13 +11,14 @@ class DeserializeAndReturnEnvelopeTests {
|
|||||||
@Suppress("NOTHING_TO_INLINE")
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
inline private fun classTestName(clazz: String) = "${this.javaClass.name}\$${testName()}\$$clazz"
|
inline private fun classTestName(clazz: String) = "${this.javaClass.name}\$${testName()}\$$clazz"
|
||||||
|
|
||||||
|
val factory = testDefaultFactoryNoEvolution()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun oneType() {
|
fun oneType() {
|
||||||
data class A(val a: Int, val b: String)
|
data class A(val a: Int, val b: String)
|
||||||
|
|
||||||
val a = A(10, "20")
|
val a = A(10, "20")
|
||||||
|
|
||||||
val factory = testDefaultFactory()
|
|
||||||
fun serialise(clazz: Any) = SerializationOutput(factory).serialize(clazz)
|
fun serialise(clazz: Any) = SerializationOutput(factory).serialize(clazz)
|
||||||
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a))
|
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a))
|
||||||
|
|
||||||
@ -33,7 +34,6 @@ class DeserializeAndReturnEnvelopeTests {
|
|||||||
|
|
||||||
val b = B(A(10, "20"), 30.0F)
|
val b = B(A(10, "20"), 30.0F)
|
||||||
|
|
||||||
val factory = testDefaultFactory()
|
|
||||||
fun serialise(clazz: Any) = SerializationOutput(factory).serialize(clazz)
|
fun serialise(clazz: Any) = SerializationOutput(factory).serialize(clazz)
|
||||||
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b))
|
val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b))
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ class DeserializeMapTests {
|
|||||||
private const val VERBOSE = false
|
private const val VERBOSE = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private val sf = testDefaultFactory()
|
private val sf = testDefaultFactoryNoEvolution()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun mapTest() {
|
fun mapTest() {
|
||||||
|
@ -18,7 +18,7 @@ class DeserializeNeedingCarpentryOfEnumsTest : AmqpCarpenterBase(AllWhitelist) {
|
|||||||
//
|
//
|
||||||
// Setup the test
|
// Setup the test
|
||||||
//
|
//
|
||||||
val setupFactory = testDefaultFactory()
|
val setupFactory = testDefaultFactoryNoEvolution()
|
||||||
|
|
||||||
val enumConstants = listOf("AAA", "BBB", "CCC", "DDD", "EEE", "FFF",
|
val enumConstants = listOf("AAA", "BBB", "CCC", "DDD", "EEE", "FFF",
|
||||||
"GGG", "HHH", "III", "JJJ").associateBy({ it }, { EnumField() })
|
"GGG", "HHH", "III", "JJJ").associateBy({ it }, { EnumField() })
|
||||||
@ -57,7 +57,7 @@ class DeserializeNeedingCarpentryOfEnumsTest : AmqpCarpenterBase(AllWhitelist) {
|
|||||||
//
|
//
|
||||||
// Setup the test
|
// Setup the test
|
||||||
//
|
//
|
||||||
val setupFactory = testDefaultFactory()
|
val setupFactory = testDefaultFactoryNoEvolution()
|
||||||
|
|
||||||
val enumConstants = listOf("AAA", "BBB", "CCC", "DDD", "EEE", "FFF",
|
val enumConstants = listOf("AAA", "BBB", "CCC", "DDD", "EEE", "FFF",
|
||||||
"GGG", "HHH", "III", "JJJ").associateBy({ it }, { EnumField() })
|
"GGG", "HHH", "III", "JJJ").associateBy({ it }, { EnumField() })
|
||||||
|
@ -17,8 +17,8 @@ class DeserializeNeedingCarpentrySimpleTypesTest : AmqpCarpenterBase(AllWhitelis
|
|||||||
private const val VERBOSE = false
|
private const val VERBOSE = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private val sf = testDefaultFactory()
|
private val sf = testDefaultFactoryNoEvolution()
|
||||||
private val sf2 = testDefaultFactory()
|
private val sf2 = testDefaultFactoryNoEvolution()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun singleInt() {
|
fun singleInt() {
|
||||||
|
@ -27,7 +27,7 @@ class DeserializeNeedingCarpentryTests : AmqpCarpenterBase(AllWhitelist) {
|
|||||||
private const val VERBOSE = false
|
private const val VERBOSE = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private val sf1 = testDefaultFactory()
|
private val sf1 = testDefaultFactoryNoEvolution()
|
||||||
|
|
||||||
// Deserialize with whitelisting on to check that `CordaSerializable` annotation present.
|
// Deserialize with whitelisting on to check that `CordaSerializable` annotation present.
|
||||||
private val sf2 = testDefaultFactoryWithWhitelist()
|
private val sf2 = testDefaultFactoryWithWhitelist()
|
||||||
|
@ -16,8 +16,8 @@ class DeserializeSimpleTypesTests {
|
|||||||
private const val VERBOSE = false
|
private const val VERBOSE = false
|
||||||
}
|
}
|
||||||
|
|
||||||
val sf1 = testDefaultFactory()
|
val sf1 = testDefaultFactoryNoEvolution()
|
||||||
val sf2 = testDefaultFactory()
|
val sf2 = testDefaultFactoryNoEvolution()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testChar() {
|
fun testChar() {
|
||||||
|
@ -6,6 +6,8 @@ import org.assertj.core.api.Assertions
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.NotSerializableException
|
import java.io.NotSerializableException
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
@ -387,21 +389,25 @@ class EnumEvolvabilityTests {
|
|||||||
data class C1(val annotatedEnum: AnnotatedEnumOnce)
|
data class C1(val annotatedEnum: AnnotatedEnumOnce)
|
||||||
|
|
||||||
val sf = testDefaultFactory()
|
val sf = testDefaultFactory()
|
||||||
|
val f = sf.javaClass.getDeclaredField("transformsCache")
|
||||||
|
f.isAccessible = true
|
||||||
|
|
||||||
assertEquals(0, sf.transformsCache.size)
|
val transformsCache = f.get(sf) as ConcurrentHashMap<String, EnumMap<TransformTypes, MutableList<Transform>>>
|
||||||
|
|
||||||
|
assertEquals(0, transformsCache.size)
|
||||||
|
|
||||||
val sb1 = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C1(AnnotatedEnumOnce.D))
|
val sb1 = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C1(AnnotatedEnumOnce.D))
|
||||||
|
|
||||||
assertEquals(2, sf.transformsCache.size)
|
assertEquals(2, transformsCache.size)
|
||||||
assertTrue(sf.transformsCache.containsKey(C1::class.java.name))
|
assertTrue(transformsCache.containsKey(C1::class.java.name))
|
||||||
assertTrue(sf.transformsCache.containsKey(AnnotatedEnumOnce::class.java.name))
|
assertTrue(transformsCache.containsKey(AnnotatedEnumOnce::class.java.name))
|
||||||
|
|
||||||
val sb2 = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C2(AnnotatedEnumOnce.D))
|
val sb2 = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C2(AnnotatedEnumOnce.D))
|
||||||
|
|
||||||
assertEquals(3, sf.transformsCache.size)
|
assertEquals(3, transformsCache.size)
|
||||||
assertTrue(sf.transformsCache.containsKey(C1::class.java.name))
|
assertTrue(transformsCache.containsKey(C1::class.java.name))
|
||||||
assertTrue(sf.transformsCache.containsKey(C2::class.java.name))
|
assertTrue(transformsCache.containsKey(C2::class.java.name))
|
||||||
assertTrue(sf.transformsCache.containsKey(AnnotatedEnumOnce::class.java.name))
|
assertTrue(transformsCache.containsKey(AnnotatedEnumOnce::class.java.name))
|
||||||
|
|
||||||
assertEquals(sb1.transformsSchema.types[AnnotatedEnumOnce::class.java.name],
|
assertEquals(sb1.transformsSchema.types[AnnotatedEnumOnce::class.java.name],
|
||||||
sb2.transformsSchema.types[AnnotatedEnumOnce::class.java.name])
|
sb2.transformsSchema.types[AnnotatedEnumOnce::class.java.name])
|
||||||
|
@ -65,7 +65,7 @@ class EnumTests {
|
|||||||
@Suppress("NOTHING_TO_INLINE")
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
inline private fun classTestName(clazz: String) = "${this.javaClass.name}\$${testName()}\$$clazz"
|
inline private fun classTestName(clazz: String) = "${this.javaClass.name}\$${testName()}\$$clazz"
|
||||||
|
|
||||||
private val sf1 = testDefaultFactory()
|
private val sf1 = testDefaultFactoryNoEvolution()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun serialiseSimpleTest() {
|
fun serialiseSimpleTest() {
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
package net.corda.nodeapi.internal.serialization.amqp
|
||||||
|
|
||||||
|
import java.io.NotSerializableException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of [EvolutionSerializerGetterBase] that disables all evolution within a
|
||||||
|
* [SerializerFactory]. This is most useful in testing where it is known that evolution should not be
|
||||||
|
* occurring and where bugs may be hidden by transparent invocation of an [EvolutionSerializer]. This
|
||||||
|
* prevents that by simply throwing an exception whenever such a serializer is requested.
|
||||||
|
*/
|
||||||
|
class EvolutionSerializerGetterTesting : EvolutionSerializerGetterBase() {
|
||||||
|
override fun getEvolutionSerializer(factory: SerializerFactory,
|
||||||
|
typeNotation: TypeNotation,
|
||||||
|
newSerializer: AMQPSerializer<Any>,
|
||||||
|
schemas: SerializationSchemas): AMQPSerializer<Any> {
|
||||||
|
throw NotSerializableException("No evolution should be occurring\n" +
|
||||||
|
" ${typeNotation.name}\n" +
|
||||||
|
" ${typeNotation.descriptor.name}\n" +
|
||||||
|
" ${newSerializer.type.typeName}\n" +
|
||||||
|
" ${newSerializer.typeDescriptor}\n\n${schemas.schema}")
|
||||||
|
}
|
||||||
|
}
|
@ -2,26 +2,90 @@ package net.corda.nodeapi.internal.serialization.amqp
|
|||||||
|
|
||||||
import net.corda.core.serialization.SerializedBytes
|
import net.corda.core.serialization.SerializedBytes
|
||||||
import net.corda.nodeapi.internal.serialization.AllWhitelist
|
import net.corda.nodeapi.internal.serialization.AllWhitelist
|
||||||
|
import net.corda.testing.common.internal.ProjectStructure.projectRootDir
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import java.io.File
|
||||||
|
import java.net.URI
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class GenericsTests {
|
class GenericsTests {
|
||||||
|
companion object {
|
||||||
|
val VERBOSE = false
|
||||||
|
|
||||||
|
@Suppress("UNUSED")
|
||||||
|
var localPath = projectRootDir.toUri().resolve(
|
||||||
|
"node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun printSeparator() = if (VERBOSE) println("\n\n-------------------------------------------\n\n") else Unit
|
||||||
|
|
||||||
|
private fun <T : Any> BytesAndSchemas<T>.printSchema() = if (VERBOSE) println("${this.schema}\n") else Unit
|
||||||
|
|
||||||
|
private fun ConcurrentHashMap<Any, AMQPSerializer<Any>>.printKeyToType() {
|
||||||
|
if (!VERBOSE) return
|
||||||
|
|
||||||
|
forEach {
|
||||||
|
println("Key = ${it.key} - ${it.value.type.typeName}")
|
||||||
|
}
|
||||||
|
println()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun twoDifferentTypesSameParameterizedOuter() {
|
||||||
|
data class G<A>(val a: A)
|
||||||
|
|
||||||
|
val factory = testDefaultFactoryNoEvolution()
|
||||||
|
|
||||||
|
val bytes1 = SerializationOutput(factory).serializeAndReturnSchema(G("hi")).apply { printSchema() }
|
||||||
|
|
||||||
|
factory.getSerializersByDescriptor().printKeyToType()
|
||||||
|
|
||||||
|
val bytes2 = SerializationOutput(factory).serializeAndReturnSchema(G(121)).apply { printSchema() }
|
||||||
|
|
||||||
|
factory.getSerializersByDescriptor().printKeyToType()
|
||||||
|
|
||||||
|
listOf(factory, testDefaultFactory()).forEach { f ->
|
||||||
|
DeserializationInput(f).deserialize(bytes1.obj).apply { assertEquals("hi", this.a) }
|
||||||
|
DeserializationInput(f).deserialize(bytes2.obj).apply { assertEquals(121, this.a) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun doWeIgnoreMultipleParams() {
|
||||||
|
data class G1<out T>(val a: T)
|
||||||
|
data class G2<out T>(val a: T)
|
||||||
|
data class Wrapper<out T>(val a: Int, val b: G1<T>, val c: G2<T>)
|
||||||
|
|
||||||
|
val factory = testDefaultFactoryNoEvolution()
|
||||||
|
val factory2 = testDefaultFactoryNoEvolution()
|
||||||
|
|
||||||
|
val bytes = SerializationOutput(factory).serializeAndReturnSchema(Wrapper(1, G1("hi"), G2("poop"))).apply { printSchema() }
|
||||||
|
printSeparator()
|
||||||
|
DeserializationInput(factory2).deserialize(bytes.obj)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun nestedSerializationOfGenerics() {
|
fun nestedSerializationOfGenerics() {
|
||||||
data class G<T>(val a: T)
|
data class G<out T>(val a: T)
|
||||||
data class Wrapper<T>(val a: Int, val b: G<T>)
|
data class Wrapper<out T>(val a: Int, val b: G<T>)
|
||||||
|
|
||||||
val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
|
val factory = testDefaultFactoryNoEvolution()
|
||||||
val altContextFactory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
|
val altContextFactory = testDefaultFactoryNoEvolution()
|
||||||
val ser = SerializationOutput(factory)
|
val ser = SerializationOutput(factory)
|
||||||
|
|
||||||
val bytes = ser.serializeAndReturnSchema(G("hi"))
|
val bytes = ser.serializeAndReturnSchema(G("hi")).apply { printSchema() }
|
||||||
|
|
||||||
|
factory.getSerializersByDescriptor().printKeyToType()
|
||||||
|
|
||||||
assertEquals("hi", DeserializationInput(factory).deserialize(bytes.obj).a)
|
assertEquals("hi", DeserializationInput(factory).deserialize(bytes.obj).a)
|
||||||
assertEquals("hi", DeserializationInput(altContextFactory).deserialize(bytes.obj).a)
|
assertEquals("hi", DeserializationInput(altContextFactory).deserialize(bytes.obj).a)
|
||||||
|
|
||||||
val bytes2 = ser.serializeAndReturnSchema(Wrapper(1, G("hi")))
|
val bytes2 = ser.serializeAndReturnSchema(Wrapper(1, G("hi"))).apply { printSchema() }
|
||||||
|
|
||||||
|
factory.getSerializersByDescriptor().printKeyToType()
|
||||||
|
|
||||||
|
printSeparator()
|
||||||
|
|
||||||
DeserializationInput(factory).deserialize(bytes2.obj).apply {
|
DeserializationInput(factory).deserialize(bytes2.obj).apply {
|
||||||
assertEquals(1, a)
|
assertEquals(1, a)
|
||||||
@ -36,7 +100,7 @@ class GenericsTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun nestedGenericsReferencesByteArrayViaSerializedBytes() {
|
fun nestedGenericsReferencesByteArrayViaSerializedBytes() {
|
||||||
data class G(val a : Int)
|
data class G(val a: Int)
|
||||||
data class Wrapper<T : Any>(val a: Int, val b: SerializedBytes<T>)
|
data class Wrapper<T : Any>(val a: Int, val b: SerializedBytes<T>)
|
||||||
|
|
||||||
val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
|
val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
|
||||||
@ -71,27 +135,30 @@ class GenericsTests {
|
|||||||
ser.serialize(Wrapper(Container(InnerA(1)))).apply {
|
ser.serialize(Wrapper(Container(InnerA(1)))).apply {
|
||||||
factories.forEach {
|
factories.forEach {
|
||||||
DeserializationInput(it).deserialize(this).apply { assertEquals(1, c.b.a_a) }
|
DeserializationInput(it).deserialize(this).apply { assertEquals(1, c.b.a_a) }
|
||||||
|
it.getSerializersByDescriptor().printKeyToType(); printSeparator()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ser.serialize(Wrapper(Container(InnerB(1)))).apply {
|
ser.serialize(Wrapper(Container(InnerB(1)))).apply {
|
||||||
factories.forEach {
|
factories.forEach {
|
||||||
DeserializationInput(it).deserialize(this).apply { assertEquals(1, c.b.a_b) }
|
DeserializationInput(it).deserialize(this).apply { assertEquals(1, c.b.a_b) }
|
||||||
|
it.getSerializersByDescriptor().printKeyToType(); printSeparator()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ser.serialize(Wrapper(Container(InnerC("Ho ho ho")))).apply {
|
ser.serialize(Wrapper(Container(InnerC("Ho ho ho")))).apply {
|
||||||
factories.forEach {
|
factories.forEach {
|
||||||
DeserializationInput(it).deserialize(this).apply { assertEquals("Ho ho ho", c.b.a_c) }
|
DeserializationInput(it).deserialize(this).apply { assertEquals("Ho ho ho", c.b.a_c) }
|
||||||
|
it.getSerializersByDescriptor().printKeyToType(); printSeparator()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun nestedSerializationWhereGenericDoesntImpactFingerprint() {
|
fun nestedSerializationWhereGenericDoesntImpactFingerprint() {
|
||||||
data class Inner(val a : Int)
|
data class Inner(val a: Int)
|
||||||
data class Container<T : Any>(val b: Inner)
|
data class Container<T : Any>(val b: Inner)
|
||||||
data class Wrapper<T: Any>(val c: Container<T>)
|
data class Wrapper<T : Any>(val c: Container<T>)
|
||||||
|
|
||||||
val factorys = listOf(
|
val factorys = listOf(
|
||||||
SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()),
|
SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()),
|
||||||
@ -111,4 +178,103 @@ class GenericsTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class ForceWildcard<out T>(val t: T)
|
||||||
|
|
||||||
|
private fun forceWildcardSerialize(
|
||||||
|
a: ForceWildcard<*>,
|
||||||
|
factory: SerializerFactory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())): SerializedBytes<*> {
|
||||||
|
val bytes = SerializationOutput(factory).serializeAndReturnSchema(a)
|
||||||
|
factory.getSerializersByDescriptor().printKeyToType()
|
||||||
|
bytes.printSchema()
|
||||||
|
return bytes.obj
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
private fun forceWildcardDeserializeString(
|
||||||
|
bytes: SerializedBytes<*>,
|
||||||
|
factory: SerializerFactory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())) {
|
||||||
|
DeserializationInput(factory).deserialize(bytes as SerializedBytes<ForceWildcard<String>>)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
private fun forceWildcardDeserializeDouble(
|
||||||
|
bytes: SerializedBytes<*>,
|
||||||
|
factory: SerializerFactory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())) {
|
||||||
|
DeserializationInput(factory).deserialize(bytes as SerializedBytes<ForceWildcard<Double>>)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
private fun forceWildcardDeserialize(
|
||||||
|
bytes: SerializedBytes<*>,
|
||||||
|
factory: SerializerFactory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())) {
|
||||||
|
DeserializationInput(factory).deserialize(bytes as SerializedBytes<ForceWildcard<*>>)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun forceWildcard() {
|
||||||
|
forceWildcardDeserializeString(forceWildcardSerialize(ForceWildcard("hello")))
|
||||||
|
forceWildcardDeserializeDouble(forceWildcardSerialize(ForceWildcard(3.0)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun forceWildcardSharedFactory() {
|
||||||
|
val f = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
|
||||||
|
forceWildcardDeserializeString(forceWildcardSerialize(ForceWildcard("hello"), f), f)
|
||||||
|
forceWildcardDeserializeDouble(forceWildcardSerialize(ForceWildcard(3.0), f), f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun forceWildcardDeserialize() {
|
||||||
|
forceWildcardDeserialize(forceWildcardSerialize(ForceWildcard("hello")))
|
||||||
|
forceWildcardDeserialize(forceWildcardSerialize(ForceWildcard(10)))
|
||||||
|
forceWildcardDeserialize(forceWildcardSerialize(ForceWildcard(20.0)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun forceWildcardDeserializeSharedFactory() {
|
||||||
|
val f = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
|
||||||
|
forceWildcardDeserialize(forceWildcardSerialize(ForceWildcard("hello"), f), f)
|
||||||
|
forceWildcardDeserialize(forceWildcardSerialize(ForceWildcard(10), f), f)
|
||||||
|
forceWildcardDeserialize(forceWildcardSerialize(ForceWildcard(20.0), f), f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun loadGenericFromFile() {
|
||||||
|
val resource = "${javaClass.simpleName}.${testName()}"
|
||||||
|
val sf = testDefaultFactory()
|
||||||
|
|
||||||
|
// Uncomment to re-generate test files, needs to be done in three stages
|
||||||
|
// File(URI("$localPath/$resource")).writeBytes(forceWildcardSerialize(ForceWildcard("wibble")).bytes)
|
||||||
|
|
||||||
|
assertEquals("wibble",
|
||||||
|
DeserializationInput(sf).deserialize(SerializedBytes<ForceWildcard<*>>(
|
||||||
|
File(GenericsTests::class.java.getResource(resource).toURI()).readBytes())).t)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DifferentBounds {
|
||||||
|
fun go()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun differentBounds() {
|
||||||
|
data class A (val a: Int): DifferentBounds {
|
||||||
|
override fun go() {
|
||||||
|
println(a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class G<out T : DifferentBounds>(val b: T)
|
||||||
|
|
||||||
|
val factorys = listOf(
|
||||||
|
SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()),
|
||||||
|
SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()))
|
||||||
|
|
||||||
|
val ser = SerializationOutput(factorys[0])
|
||||||
|
|
||||||
|
ser.serialize(G(A(10))).apply {
|
||||||
|
factorys.forEach {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -169,9 +169,11 @@ class SerializationOutputTests {
|
|||||||
|
|
||||||
private inline fun <reified T : Any> serdes(obj: T,
|
private inline fun <reified T : Any> serdes(obj: T,
|
||||||
factory: SerializerFactory = SerializerFactory(
|
factory: SerializerFactory = SerializerFactory(
|
||||||
AllWhitelist, ClassLoader.getSystemClassLoader()),
|
AllWhitelist, ClassLoader.getSystemClassLoader(),
|
||||||
|
EvolutionSerializerGetterTesting()),
|
||||||
freshDeserializationFactory: SerializerFactory = SerializerFactory(
|
freshDeserializationFactory: SerializerFactory = SerializerFactory(
|
||||||
AllWhitelist, ClassLoader.getSystemClassLoader()),
|
AllWhitelist, ClassLoader.getSystemClassLoader(),
|
||||||
|
EvolutionSerializerGetterTesting()),
|
||||||
expectedEqual: Boolean = true,
|
expectedEqual: Boolean = true,
|
||||||
expectDeserializedEqual: Boolean = true): T {
|
expectDeserializedEqual: Boolean = true): T {
|
||||||
val ser = SerializationOutput(factory)
|
val ser = SerializationOutput(factory)
|
||||||
|
@ -10,7 +10,7 @@ class SerializeAndReturnSchemaTest {
|
|||||||
@Suppress("NOTHING_TO_INLINE")
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
inline private fun classTestName(clazz: String) = "${this.javaClass.name}\$${testName()}\$$clazz"
|
inline private fun classTestName(clazz: String) = "${this.javaClass.name}\$${testName()}\$$clazz"
|
||||||
|
|
||||||
val factory = testDefaultFactory()
|
val factory = testDefaultFactoryNoEvolution()
|
||||||
|
|
||||||
// just a simple test to verify the internal test extension for serialize does
|
// just a simple test to verify the internal test extension for serialize does
|
||||||
// indeed give us the correct schema back. This is more useful in support of other
|
// indeed give us the correct schema back. This is more useful in support of other
|
||||||
|
@ -45,7 +45,7 @@ class StaticInitialisationOfSerializedObjectTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun KotlinObjectWithCompanionObject() {
|
fun kotlinObjectWithCompanionObject() {
|
||||||
data class D(val c: C)
|
data class D(val c: C)
|
||||||
|
|
||||||
val sf = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
|
val sf = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
|
||||||
@ -104,7 +104,7 @@ class StaticInitialisationOfSerializedObjectTest {
|
|||||||
override val classCarpenter = ClassCarpenter(ClassLoader.getSystemClassLoader(), wl2)
|
override val classCarpenter = ClassCarpenter(ClassLoader.getSystemClassLoader(), wl2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// This time have the serilization factory and the carpenter use different whitelists
|
// This time have the serialization factory and the carpenter use different whitelists
|
||||||
@Test
|
@Test
|
||||||
fun deserializeTest2() {
|
fun deserializeTest2() {
|
||||||
data class D(val c: C2)
|
data class D(val c: C2)
|
||||||
|
Binary file not shown.
@ -102,21 +102,21 @@ class AuthDBTests : NodeBasedTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `login with correct credentials`() {
|
fun `login with correct credentials`() {
|
||||||
client.start("user", "foo")
|
client.start("user", "foo").close()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `login with wrong credentials`() {
|
fun `login with wrong credentials`() {
|
||||||
client.start("user", "foo")
|
client.start("user", "foo").close()
|
||||||
assertFailsWith(
|
assertFailsWith(
|
||||||
ActiveMQSecurityException::class,
|
ActiveMQSecurityException::class,
|
||||||
"Login with incorrect password should fail") {
|
"Login with incorrect password should fail") {
|
||||||
client.start("user", "bar")
|
client.start("user", "bar").close()
|
||||||
}
|
}
|
||||||
assertFailsWith(
|
assertFailsWith(
|
||||||
ActiveMQSecurityException::class,
|
ActiveMQSecurityException::class,
|
||||||
"Login with unknown username should fail") {
|
"Login with unknown username should fail") {
|
||||||
client.start("X", "foo")
|
client.start("X", "foo").close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,7 +158,7 @@ class AuthDBTests : NodeBasedTest() {
|
|||||||
assertFailsWith(
|
assertFailsWith(
|
||||||
ActiveMQSecurityException::class,
|
ActiveMQSecurityException::class,
|
||||||
"Login with incorrect password should fail") {
|
"Login with incorrect password should fail") {
|
||||||
client.start("user2", "bar")
|
client.start("user2", "bar").close()
|
||||||
}
|
}
|
||||||
|
|
||||||
db.insert(UserAndRoles(
|
db.insert(UserAndRoles(
|
||||||
@ -166,7 +166,7 @@ class AuthDBTests : NodeBasedTest() {
|
|||||||
password = encodePassword("bar"),
|
password = encodePassword("bar"),
|
||||||
roles = listOf("default")))
|
roles = listOf("default")))
|
||||||
|
|
||||||
client.start("user2", "bar")
|
client.start("user2", "bar").close()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -22,6 +22,7 @@ import net.corda.testing.node.ClusterSpec
|
|||||||
import net.corda.testing.node.NotarySpec
|
import net.corda.testing.node.NotarySpec
|
||||||
import net.corda.testing.node.startFlow
|
import net.corda.testing.node.startFlow
|
||||||
import org.junit.ClassRule
|
import org.junit.ClassRule
|
||||||
|
import org.junit.Ignore
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
@ -35,6 +36,7 @@ class RaftNotaryServiceTests : IntegrationTest() {
|
|||||||
}
|
}
|
||||||
private val notaryName = CordaX500Name("RAFT Notary Service", "London", "GB")
|
private val notaryName = CordaX500Name("RAFT Notary Service", "London", "GB")
|
||||||
|
|
||||||
|
@Ignore("Test has undeterministic capacity to hang, ignore till fixed")
|
||||||
@Test
|
@Test
|
||||||
fun `detect double spend`() {
|
fun `detect double spend`() {
|
||||||
driver(
|
driver(
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
package net.corda.node.services.network
|
package net.corda.node.services.network
|
||||||
|
|
||||||
import net.corda.cordform.CordformNode
|
import net.corda.cordform.CordformNode
|
||||||
import net.corda.core.crypto.SignedData
|
|
||||||
import net.corda.core.crypto.random63BitValue
|
import net.corda.core.crypto.random63BitValue
|
||||||
|
import net.corda.core.internal.*
|
||||||
import net.corda.core.internal.concurrent.transpose
|
import net.corda.core.internal.concurrent.transpose
|
||||||
import net.corda.core.internal.div
|
|
||||||
import net.corda.core.internal.exists
|
|
||||||
import net.corda.core.internal.list
|
|
||||||
import net.corda.core.internal.readAll
|
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.serialization.deserialize
|
import net.corda.core.serialization.deserialize
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
@ -75,7 +71,7 @@ class NetworkMapTest : IntegrationTest() {
|
|||||||
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
|
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
|
||||||
val networkParameters = (alice.configuration.baseDirectory / NETWORK_PARAMS_FILE_NAME)
|
val networkParameters = (alice.configuration.baseDirectory / NETWORK_PARAMS_FILE_NAME)
|
||||||
.readAll()
|
.readAll()
|
||||||
.deserialize<SignedData<NetworkParameters>>()
|
.deserialize<SignedDataWithCert<NetworkParameters>>()
|
||||||
.verified()
|
.verified()
|
||||||
// We use a random modified time above to make the network parameters unqiue so that we're sure they came
|
// We use a random modified time above to make the network parameters unqiue so that we're sure they came
|
||||||
// from the server
|
// from the server
|
||||||
|
@ -67,7 +67,11 @@ class NodeRegistrationTest : IntegrationTest() {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun startServer() {
|
fun startServer() {
|
||||||
server = NetworkMapServer(1.minutes, portAllocation.nextHostAndPort(), DEV_ROOT_CA, "localhost", registrationHandler)
|
server = NetworkMapServer(
|
||||||
|
cacheTimeout = 1.minutes,
|
||||||
|
hostAndPort = portAllocation.nextHostAndPort(),
|
||||||
|
myHostNameValue = "localhost",
|
||||||
|
additionalServices = registrationHandler)
|
||||||
serverHostAndPort = server.start()
|
serverHostAndPort = server.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,10 +2,15 @@ package net.corda.services.messaging
|
|||||||
|
|
||||||
import net.corda.core.crypto.Crypto
|
import net.corda.core.crypto.Crypto
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.internal.*
|
import net.corda.core.internal.copyTo
|
||||||
|
import net.corda.core.internal.createDirectories
|
||||||
|
import net.corda.core.internal.exists
|
||||||
|
import net.corda.core.internal.x500Name
|
||||||
|
import net.corda.nodeapi.RPCApi
|
||||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER
|
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER
|
||||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
|
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
|
||||||
import net.corda.nodeapi.RPCApi
|
import net.corda.nodeapi.internal.DEV_INTERMEDIATE_CA
|
||||||
|
import net.corda.nodeapi.internal.DEV_ROOT_CA
|
||||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||||
import net.corda.nodeapi.internal.crypto.*
|
import net.corda.nodeapi.internal.crypto.*
|
||||||
import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration
|
import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration
|
||||||
@ -90,20 +95,13 @@ class MQSecurityAsNodeTest : MQSecurityTest() {
|
|||||||
javaClass.classLoader.getResourceAsStream("certificates/cordatruststore.jks").copyTo(trustStoreFile)
|
javaClass.classLoader.getResourceAsStream("certificates/cordatruststore.jks").copyTo(trustStoreFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
val caKeyStore = loadKeyStore(
|
|
||||||
javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"),
|
|
||||||
"cordacadevpass")
|
|
||||||
|
|
||||||
val rootCACert = caKeyStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA)
|
|
||||||
val intermediateCA = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass")
|
|
||||||
|
|
||||||
val clientKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
val clientKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||||
// Set name constrain to the legal name.
|
// Set name constrain to the legal name.
|
||||||
val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName.x500Name))), arrayOf())
|
val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName.x500Name))), arrayOf())
|
||||||
val clientCACert = X509Utilities.createCertificate(
|
val clientCACert = X509Utilities.createCertificate(
|
||||||
CertificateType.INTERMEDIATE_CA,
|
CertificateType.INTERMEDIATE_CA,
|
||||||
intermediateCA.certificate,
|
DEV_INTERMEDIATE_CA.certificate,
|
||||||
intermediateCA.keyPair,
|
DEV_INTERMEDIATE_CA.keyPair,
|
||||||
legalName.x500Principal,
|
legalName.x500Principal,
|
||||||
clientKeyPair.public,
|
clientKeyPair.public,
|
||||||
nameConstraints = nameConstraints)
|
nameConstraints = nameConstraints)
|
||||||
@ -123,7 +121,7 @@ class MQSecurityAsNodeTest : MQSecurityTest() {
|
|||||||
X509Utilities.CORDA_CLIENT_CA,
|
X509Utilities.CORDA_CLIENT_CA,
|
||||||
clientKeyPair.private,
|
clientKeyPair.private,
|
||||||
keyPass,
|
keyPass,
|
||||||
arrayOf(clientCACert, intermediateCA.certificate, rootCACert))
|
arrayOf(clientCACert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate))
|
||||||
clientCAKeystore.save(nodeKeystore, keyStorePassword)
|
clientCAKeystore.save(nodeKeystore, keyStorePassword)
|
||||||
|
|
||||||
val tlsKeystore = loadOrCreateKeyStore(sslKeystore, keyStorePassword)
|
val tlsKeystore = loadOrCreateKeyStore(sslKeystore, keyStorePassword)
|
||||||
@ -131,7 +129,7 @@ class MQSecurityAsNodeTest : MQSecurityTest() {
|
|||||||
X509Utilities.CORDA_CLIENT_TLS,
|
X509Utilities.CORDA_CLIENT_TLS,
|
||||||
tlsKeyPair.private,
|
tlsKeyPair.private,
|
||||||
keyPass,
|
keyPass,
|
||||||
arrayOf(clientTLSCert, clientCACert, intermediateCA.certificate, rootCACert))
|
arrayOf(clientTLSCert, clientCACert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate))
|
||||||
tlsKeystore.save(sslKeystore, keyStorePassword)
|
tlsKeystore.save(sslKeystore, keyStorePassword)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,6 +68,7 @@ import net.corda.nodeapi.internal.crypto.X509Utilities
|
|||||||
import net.corda.nodeapi.internal.crypto.loadKeyStore
|
import net.corda.nodeapi.internal.crypto.loadKeyStore
|
||||||
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME
|
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME
|
||||||
import net.corda.nodeapi.internal.network.NetworkParameters
|
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||||
|
import net.corda.nodeapi.internal.network.verifiedNetworkMapCert
|
||||||
import net.corda.nodeapi.internal.persistence.*
|
import net.corda.nodeapi.internal.persistence.*
|
||||||
import org.apache.activemq.artemis.utils.ReusableLatch
|
import org.apache.activemq.artemis.utils.ReusableLatch
|
||||||
import org.hibernate.type.descriptor.java.JavaTypeDescriptorRegistry
|
import org.hibernate.type.descriptor.java.JavaTypeDescriptorRegistry
|
||||||
@ -223,7 +224,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
|||||||
val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null)
|
val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null)
|
||||||
val identityService = makeIdentityService(identity.certificate)
|
val identityService = makeIdentityService(identity.certificate)
|
||||||
networkMapClient = configuration.compatibilityZoneURL?.let { NetworkMapClient(it, identityService.trustRoot) }
|
networkMapClient = configuration.compatibilityZoneURL?.let { NetworkMapClient(it, identityService.trustRoot) }
|
||||||
retrieveNetworkParameters()
|
retrieveNetworkParameters(identityService.trustRoot)
|
||||||
// Do all of this in a database transaction so anything that might need a connection has one.
|
// Do all of this in a database transaction so anything that might need a connection has one.
|
||||||
val (startedImpl, schedulerService) = initialiseDatabasePersistence(schemaService, identityService) { database ->
|
val (startedImpl, schedulerService) = initialiseDatabasePersistence(schemaService, identityService) { database ->
|
||||||
val networkMapCache = NetworkMapCacheImpl(PersistentNetworkMapCache(database, networkParameters.notaries), identityService)
|
val networkMapCache = NetworkMapCacheImpl(PersistentNetworkMapCache(database, networkParameters.notaries), identityService)
|
||||||
@ -665,23 +666,19 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
|||||||
return PersistentKeyManagementService(identityService, keyPairs)
|
return PersistentKeyManagementService(identityService, keyPairs)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun retrieveNetworkParameters() {
|
private fun retrieveNetworkParameters(trustRoot: X509Certificate) {
|
||||||
val networkParamsFile = configuration.baseDirectory.list { paths ->
|
val networkParamsFile = configuration.baseDirectory / NETWORK_PARAMS_FILE_NAME
|
||||||
paths.filter { it.fileName.toString() == NETWORK_PARAMS_FILE_NAME }.findFirst().orElse(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
networkParameters = if (networkParamsFile != null) {
|
networkParameters = if (networkParamsFile.exists()) {
|
||||||
networkParamsFile.readAll().deserialize<SignedData<NetworkParameters>>().verified()
|
networkParamsFile.readAll().deserialize<SignedDataWithCert<NetworkParameters>>().verifiedNetworkMapCert(trustRoot)
|
||||||
} else {
|
} else {
|
||||||
log.info("No network-parameters file found. Expecting network parameters to be available from the network map.")
|
log.info("No network-parameters file found. Expecting network parameters to be available from the network map.")
|
||||||
val networkMapClient = checkNotNull(networkMapClient) {
|
val networkMapClient = checkNotNull(networkMapClient) {
|
||||||
"Node hasn't been configured to connect to a network map from which to get the network parameters"
|
"Node hasn't been configured to connect to a network map from which to get the network parameters"
|
||||||
}
|
}
|
||||||
val (networkMap, _) = networkMapClient.getNetworkMap()
|
val (networkMap, _) = networkMapClient.getNetworkMap()
|
||||||
val signedParams = checkNotNull(networkMapClient.getNetworkParameter(networkMap.networkParameterHash)) {
|
val signedParams = networkMapClient.getNetworkParameters(networkMap.networkParameterHash)
|
||||||
"Failed loading network parameters from network map server"
|
val verifiedParams = signedParams.verifiedNetworkMapCert(trustRoot)
|
||||||
}
|
|
||||||
val verifiedParams = signedParams.verified()
|
|
||||||
signedParams.serialize().open().copyTo(configuration.baseDirectory / NETWORK_PARAMS_FILE_NAME)
|
signedParams.serialize().open().copyTo(configuration.baseDirectory / NETWORK_PARAMS_FILE_NAME)
|
||||||
verifiedParams
|
verifiedParams
|
||||||
}
|
}
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
package net.corda.node.internal
|
|
||||||
|
|
||||||
import net.corda.core.flows.FlowLogic
|
|
||||||
import net.corda.core.messaging.CordaRPCOps
|
|
||||||
import net.corda.node.services.Permissions.Companion.all
|
|
||||||
import net.corda.node.services.Permissions.Companion.startFlow
|
|
||||||
import net.corda.node.services.Permissions.Companion.invokeRpc
|
|
||||||
import kotlin.reflect.KVisibility
|
|
||||||
import kotlin.reflect.full.declaredMemberFunctions
|
|
||||||
|
|
||||||
object DefaultCordaRpcPermissions {
|
|
||||||
|
|
||||||
private val invokePermissions = CordaRPCOps::class.declaredMemberFunctions.filter { it.visibility == KVisibility.PUBLIC }.associate { it.name to setOf(invokeRpc(it), all()) }
|
|
||||||
private val startFlowPermissions = setOf("startFlow", "startFlowDynamic", "startTrackedFlow", "startTrackedFlowDynamic").associate { it to this::startFlowPermission }
|
|
||||||
|
|
||||||
fun permissionsAllowing(methodName: String, args: List<Any?>): Set<String> {
|
|
||||||
|
|
||||||
val invoke = invokePermissions[methodName] ?: emptySet()
|
|
||||||
val start = startFlowPermissions[methodName]?.invoke(args)
|
|
||||||
return if (start != null) invoke + start else invoke
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
private fun startFlowPermission(args: List<Any?>): String = if (args[0] is Class<*>) startFlow(args[0] as Class<FlowLogic<*>>) else startFlow(args[0] as String)
|
|
||||||
}
|
|
@ -20,7 +20,7 @@ import java.io.InputStream
|
|||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
|
||||||
// TODO change to KFunction reference after Kotlin fixes https://youtrack.jetbrains.com/issue/KT-12140
|
// TODO change to KFunction reference after Kotlin fixes https://youtrack.jetbrains.com/issue/KT-12140
|
||||||
class RpcAuthorisationProxy(private val implementation: CordaRPCOps, private val context: () -> RpcAuthContext, private val permissionsAllowing: (methodName: String, args: List<Any?>) -> Set<String>) : CordaRPCOps {
|
class RpcAuthorisationProxy(private val implementation: CordaRPCOps, private val context: () -> RpcAuthContext) : CordaRPCOps {
|
||||||
|
|
||||||
override fun uploadAttachmentWithMetadata(jar: InputStream, uploader: String, filename: String): SecureHash = guard("uploadAttachmentWithMetadata") {
|
override fun uploadAttachmentWithMetadata(jar: InputStream, uploader: String, filename: String): SecureHash = guard("uploadAttachmentWithMetadata") {
|
||||||
implementation.uploadAttachmentWithMetadata(jar, uploader, filename)
|
implementation.uploadAttachmentWithMetadata(jar, uploader, filename)
|
||||||
|
@ -14,7 +14,7 @@ class SecureCordaRPCOps(services: ServiceHubInternal,
|
|||||||
smm: StateMachineManager,
|
smm: StateMachineManager,
|
||||||
database: CordaPersistence,
|
database: CordaPersistence,
|
||||||
flowStarter: FlowStarter,
|
flowStarter: FlowStarter,
|
||||||
val unsafe: CordaRPCOps = CordaRPCOpsImpl(services, smm, database, flowStarter)) : CordaRPCOps by RpcAuthorisationProxy(unsafe, ::rpcContext, DefaultCordaRpcPermissions::permissionsAllowing) {
|
val unsafe: CordaRPCOps = CordaRPCOpsImpl(services, smm, database, flowStarter)) : CordaRPCOps by RpcAuthorisationProxy(unsafe, ::rpcContext) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the RPC protocol version, which is the same the node's Platform Version. Exists since version 1 so guaranteed
|
* Returns the RPC protocol version, which is the same the node's Platform Version. Exists since version 1 so guaranteed
|
||||||
|
@ -103,7 +103,7 @@ class RPCSecurityManagerImpl(config: AuthServiceConfig) : RPCSecurityManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* Provide a representation of RPC permissions based on Apache Shiro permissions framework.
|
* Provide a representation of RPC permissions based on Apache Shiro permissions framework.
|
||||||
* A permission represents a set of actions: for example, the set of all RPC invocations, or the set
|
* A permission represents a set of actions: for example, the set of all RPC invocations, or the set
|
||||||
* of RPC invocations acting on a given class of Flows in input. A permission `implies` another one if
|
* of RPC invocations acting on a given class of Flows in input. A permission `implies` another one if
|
||||||
@ -128,7 +128,7 @@ private class RPCPermission : DomainPermission {
|
|||||||
constructor() : super()
|
constructor() : super()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* A [org.apache.shiro.authz.permission.PermissionResolver] implementation for RPC permissions.
|
* A [org.apache.shiro.authz.permission.PermissionResolver] implementation for RPC permissions.
|
||||||
* Provides a method to construct an [RPCPermission] instance from its string representation
|
* Provides a method to construct an [RPCPermission] instance from its string representation
|
||||||
* in the form used by a Node admin.
|
* in the form used by a Node admin.
|
||||||
@ -141,7 +141,6 @@ private class RPCPermission : DomainPermission {
|
|||||||
*
|
*
|
||||||
* - `StartFlow.$FlowClassName`: allowing to call a `startFlow*` RPC method targeting a Flow instance
|
* - `StartFlow.$FlowClassName`: allowing to call a `startFlow*` RPC method targeting a Flow instance
|
||||||
* of a given class
|
* of a given class
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
private object RPCPermissionResolver : PermissionResolver {
|
private object RPCPermissionResolver : PermissionResolver {
|
||||||
|
|
||||||
@ -253,7 +252,7 @@ private class NodeJdbcRealm(config: SecurityConfiguration.AuthService.DataSource
|
|||||||
|
|
||||||
private typealias ShiroCache<K, V> = org.apache.shiro.cache.Cache<K, V>
|
private typealias ShiroCache<K, V> = org.apache.shiro.cache.Cache<K, V>
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* Adapts a [com.google.common.cache.Cache] to a [org.apache.shiro.cache.Cache] implementation.
|
* Adapts a [com.google.common.cache.Cache] to a [org.apache.shiro.cache.Cache] implementation.
|
||||||
*/
|
*/
|
||||||
private fun <K, V> Cache<K, V>.toShiroCache(name: String) = object : ShiroCache<K, V> {
|
private fun <K, V> Cache<K, V>.toShiroCache(name: String) = object : ShiroCache<K, V> {
|
||||||
@ -285,7 +284,7 @@ private fun <K, V> Cache<K, V>.toShiroCache(name: String) = object : ShiroCache<
|
|||||||
override fun toString() = "Guava cache adapter [$impl]"
|
override fun toString() = "Guava cache adapter [$impl]"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* Implementation of [org.apache.shiro.cache.CacheManager] based on
|
* Implementation of [org.apache.shiro.cache.CacheManager] based on
|
||||||
* cache implementation in [com.google.common.cache]
|
* cache implementation in [com.google.common.cache]
|
||||||
*/
|
*/
|
||||||
|
@ -68,10 +68,7 @@ fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) {
|
|||||||
loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordatruststore.jks"), "trustpass").save(trustStoreFile, trustStorePassword)
|
loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordatruststore.jks"), "trustpass").save(trustStoreFile, trustStorePassword)
|
||||||
}
|
}
|
||||||
if (!sslKeystore.exists() || !nodeKeystore.exists()) {
|
if (!sslKeystore.exists() || !nodeKeystore.exists()) {
|
||||||
val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass")
|
createDevKeyStores(myLegalName)
|
||||||
val rootCert = caKeyStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA)
|
|
||||||
val intermediateCa = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass")
|
|
||||||
createDevKeyStores(rootCert, intermediateCa, myLegalName)
|
|
||||||
|
|
||||||
// Move distributed service composite key (generated by IdentityGenerator.generateToDisk) to keystore if exists.
|
// Move distributed service composite key (generated by IdentityGenerator.generateToDisk) to keystore if exists.
|
||||||
val distributedServiceKeystore = certificatesDirectory / "distributedService.jks"
|
val distributedServiceKeystore = certificatesDirectory / "distributedService.jks"
|
||||||
|
@ -2,26 +2,26 @@ package net.corda.node.services.network
|
|||||||
|
|
||||||
import com.google.common.util.concurrent.MoreExecutors
|
import com.google.common.util.concurrent.MoreExecutors
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.SignedData
|
import net.corda.core.internal.SignedDataWithCert
|
||||||
|
import net.corda.core.internal.checkOkResponse
|
||||||
import net.corda.core.internal.openHttpConnection
|
import net.corda.core.internal.openHttpConnection
|
||||||
|
import net.corda.core.internal.responseAs
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.serialization.deserialize
|
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import net.corda.core.utilities.minutes
|
import net.corda.core.utilities.minutes
|
||||||
import net.corda.core.utilities.seconds
|
import net.corda.core.utilities.seconds
|
||||||
import net.corda.node.services.api.NetworkMapCacheInternal
|
import net.corda.node.services.api.NetworkMapCacheInternal
|
||||||
import net.corda.node.utilities.NamedThreadFactory
|
import net.corda.node.utilities.NamedThreadFactory
|
||||||
|
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||||
import net.corda.nodeapi.internal.network.NetworkMap
|
import net.corda.nodeapi.internal.network.NetworkMap
|
||||||
import net.corda.nodeapi.internal.network.NetworkParameters
|
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||||
import net.corda.nodeapi.internal.network.SignedNetworkMap
|
import net.corda.nodeapi.internal.network.verifiedNetworkMapCert
|
||||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
|
||||||
import okhttp3.CacheControl
|
import okhttp3.CacheControl
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
import java.net.HttpURLConnection
|
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
@ -33,47 +33,34 @@ class NetworkMapClient(compatibilityZoneURL: URL, private val trustedRoot: X509C
|
|||||||
|
|
||||||
fun publish(signedNodeInfo: SignedNodeInfo) {
|
fun publish(signedNodeInfo: SignedNodeInfo) {
|
||||||
val publishURL = URL("$networkMapUrl/publish")
|
val publishURL = URL("$networkMapUrl/publish")
|
||||||
val conn = publishURL.openHttpConnection()
|
publishURL.openHttpConnection().apply {
|
||||||
conn.doOutput = true
|
doOutput = true
|
||||||
conn.requestMethod = "POST"
|
requestMethod = "POST"
|
||||||
conn.setRequestProperty("Content-Type", "application/octet-stream")
|
setRequestProperty("Content-Type", "application/octet-stream")
|
||||||
conn.outputStream.use { signedNodeInfo.serialize().open().copyTo(it) }
|
outputStream.use { signedNodeInfo.serialize().open().copyTo(it) }
|
||||||
|
checkOkResponse()
|
||||||
// This will throw IOException if the response code is not HTTP 200.
|
}
|
||||||
// This gives a much better exception then reading the error stream.
|
|
||||||
conn.inputStream.close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getNetworkMap(): NetworkMapResponse {
|
fun getNetworkMap(): NetworkMapResponse {
|
||||||
val conn = networkMapUrl.openHttpConnection()
|
val connection = networkMapUrl.openHttpConnection()
|
||||||
val signedNetworkMap = conn.inputStream.use { it.readBytes() }.deserialize<SignedNetworkMap>()
|
val signedNetworkMap = connection.responseAs<SignedDataWithCert<NetworkMap>>()
|
||||||
val networkMap = signedNetworkMap.verified(trustedRoot)
|
val networkMap = signedNetworkMap.verifiedNetworkMapCert(trustedRoot)
|
||||||
val timeout = CacheControl.parse(Headers.of(conn.headerFields.filterKeys { it != null }.mapValues { it.value.first() })).maxAgeSeconds().seconds
|
val timeout = CacheControl.parse(Headers.of(connection.headerFields.filterKeys { it != null }.mapValues { it.value[0] })).maxAgeSeconds().seconds
|
||||||
return NetworkMapResponse(networkMap, timeout)
|
return NetworkMapResponse(networkMap, timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getNodeInfo(nodeInfoHash: SecureHash): NodeInfo? {
|
fun getNodeInfo(nodeInfoHash: SecureHash): NodeInfo {
|
||||||
val conn = URL("$networkMapUrl/node-info/$nodeInfoHash").openHttpConnection()
|
return URL("$networkMapUrl/node-info/$nodeInfoHash").openHttpConnection().responseAs<SignedNodeInfo>().verified()
|
||||||
return if (conn.responseCode == HttpURLConnection.HTTP_NOT_FOUND) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
val signedNodeInfo = conn.inputStream.use { it.readBytes() }.deserialize<SignedNodeInfo>()
|
|
||||||
signedNodeInfo.verified()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getNetworkParameter(networkParameterHash: SecureHash): SignedData<NetworkParameters>? {
|
fun getNetworkParameters(networkParameterHash: SecureHash): SignedDataWithCert<NetworkParameters> {
|
||||||
val conn = URL("$networkMapUrl/network-parameter/$networkParameterHash").openHttpConnection()
|
return URL("$networkMapUrl/network-parameters/$networkParameterHash").openHttpConnection().responseAs()
|
||||||
return if (conn.responseCode == HttpURLConnection.HTTP_NOT_FOUND) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
conn.inputStream.use { it.readBytes() }.deserialize()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun myPublicHostname(): String {
|
fun myPublicHostname(): String {
|
||||||
val conn = URL("$networkMapUrl/my-hostname").openHttpConnection()
|
val connection = URL("$networkMapUrl/my-hostname").openHttpConnection()
|
||||||
return conn.inputStream.bufferedReader().use(BufferedReader::readLine)
|
return connection.inputStream.bufferedReader().use(BufferedReader::readLine)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,8 +24,7 @@ class HTTPNetworkRegistrationService(compatibilityZoneURL: URL) : NetworkRegistr
|
|||||||
@Throws(CertificateRequestException::class)
|
@Throws(CertificateRequestException::class)
|
||||||
override fun retrieveCertificates(requestId: String): Array<Certificate>? {
|
override fun retrieveCertificates(requestId: String): Array<Certificate>? {
|
||||||
// Poll server to download the signed certificate once request has been approved.
|
// Poll server to download the signed certificate once request has been approved.
|
||||||
val url = URL("$registrationURL/$requestId")
|
val conn = URL("$registrationURL/$requestId").openHttpConnection()
|
||||||
val conn = url.openConnection() as HttpURLConnection
|
|
||||||
conn.requestMethod = "GET"
|
conn.requestMethod = "GET"
|
||||||
|
|
||||||
return when (conn.responseCode) {
|
return when (conn.responseCode) {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package net.corda.node.services.network
|
package net.corda.node.services.network
|
||||||
|
|
||||||
import net.corda.core.crypto.SecureHash
|
|
||||||
import net.corda.core.crypto.sha256
|
import net.corda.core.crypto.sha256
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.core.utilities.seconds
|
import net.corda.core.utilities.seconds
|
||||||
@ -9,16 +8,19 @@ import net.corda.testing.BOB_NAME
|
|||||||
import net.corda.testing.DEV_ROOT_CA
|
import net.corda.testing.DEV_ROOT_CA
|
||||||
import net.corda.testing.SerializationEnvironmentRule
|
import net.corda.testing.SerializationEnvironmentRule
|
||||||
import net.corda.testing.driver.PortAllocation
|
import net.corda.testing.driver.PortAllocation
|
||||||
|
import net.corda.testing.internal.TestNodeInfoBuilder
|
||||||
import net.corda.testing.internal.createNodeInfoAndSigned
|
import net.corda.testing.internal.createNodeInfoAndSigned
|
||||||
|
import net.corda.testing.internal.signWith
|
||||||
import net.corda.testing.node.internal.network.NetworkMapServer
|
import net.corda.testing.node.internal.network.NetworkMapServer
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import java.io.IOException
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertNotNull
|
|
||||||
|
|
||||||
class NetworkMapClientTest {
|
class NetworkMapClientTest {
|
||||||
@Rule
|
@Rule
|
||||||
@ -63,12 +65,24 @@ class NetworkMapClientTest {
|
|||||||
assertEquals(nodeInfo2, networkMapClient.getNodeInfo(nodeInfoHash2))
|
assertEquals(nodeInfo2, networkMapClient.getNodeInfo(nodeInfoHash2))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `errors return a meaningful error message`() {
|
||||||
|
val nodeInfoBuilder = TestNodeInfoBuilder()
|
||||||
|
val (_, aliceKey) = nodeInfoBuilder.addIdentity(ALICE_NAME)
|
||||||
|
nodeInfoBuilder.addIdentity(BOB_NAME)
|
||||||
|
val nodeInfo3 = nodeInfoBuilder.build()
|
||||||
|
val signedNodeInfo3 = nodeInfo3.signWith(listOf(aliceKey))
|
||||||
|
|
||||||
|
assertThatThrownBy { networkMapClient.publish(signedNodeInfo3) }
|
||||||
|
.isInstanceOf(IOException::class.java)
|
||||||
|
.hasMessage("Response Code 403: Missing signatures. Found 1 expected 2")
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `download NetworkParameter correctly`() {
|
fun `download NetworkParameter correctly`() {
|
||||||
// The test server returns same network parameter for any hash.
|
// The test server returns same network parameter for any hash.
|
||||||
val networkParameter = networkMapClient.getNetworkParameter(SecureHash.randomSHA256())?.verified()
|
val parametersHash = server.networkParameters.serialize().hash
|
||||||
assertNotNull(networkParameter)
|
val networkParameter = networkMapClient.getNetworkParameters(parametersHash).verified()
|
||||||
assertEquals(server.networkParameters, networkParameter)
|
assertEquals(server.networkParameters, networkParameter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,6 +161,8 @@ data class JmxPolicy(val startJmxHttpServer: Boolean = false,
|
|||||||
* @param useTestClock If true the test clock will be used in Node.
|
* @param useTestClock If true the test clock will be used in Node.
|
||||||
* @param startNodesInProcess Provides the default behaviour of whether new nodes should start inside this process or
|
* @param startNodesInProcess Provides the default behaviour of whether new nodes should start inside this process or
|
||||||
* not. Note that this may be overridden in [DriverDSL.startNode].
|
* not. Note that this may be overridden in [DriverDSL.startNode].
|
||||||
|
* @param waitForAllNodesToFinish If true, the nodes will not shut down automatically after executing the code in the driver DSL block.
|
||||||
|
* It will wait for them to be shut down externally instead.
|
||||||
* @param notarySpecs The notaries advertised for this network. These nodes will be started automatically and will be
|
* @param notarySpecs The notaries advertised for this network. These nodes will be started automatically and will be
|
||||||
* available from [DriverDSL.notaryHandles]. Defaults to a simple validating notary.
|
* available from [DriverDSL.notaryHandles]. Defaults to a simple validating notary.
|
||||||
* @param jmxPolicy Used to specify whether to expose JMX metrics via Jolokia HHTP/JSON. Defines two attributes:
|
* @param jmxPolicy Used to specify whether to expose JMX metrics via Jolokia HHTP/JSON. Defines two attributes:
|
||||||
|
@ -1,20 +1,16 @@
|
|||||||
package net.corda.testing.node.internal.network
|
package net.corda.testing.node.internal.network
|
||||||
|
|
||||||
import net.corda.core.crypto.*
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.internal.signWithCert
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.serialization.deserialize
|
import net.corda.core.serialization.deserialize
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||||
|
import net.corda.nodeapi.internal.createDevNetworkMapCa
|
||||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||||
import net.corda.nodeapi.internal.crypto.CertificateType
|
|
||||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
|
||||||
import net.corda.nodeapi.internal.network.DigitalSignatureWithCert
|
|
||||||
import net.corda.nodeapi.internal.network.NetworkMap
|
import net.corda.nodeapi.internal.network.NetworkMap
|
||||||
import net.corda.nodeapi.internal.network.NetworkParameters
|
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||||
import net.corda.nodeapi.internal.network.SignedNetworkMap
|
|
||||||
import net.corda.testing.DEV_ROOT_CA
|
|
||||||
import org.eclipse.jetty.server.Server
|
import org.eclipse.jetty.server.Server
|
||||||
import org.eclipse.jetty.server.ServerConnector
|
import org.eclipse.jetty.server.ServerConnector
|
||||||
import org.eclipse.jetty.server.handler.HandlerCollection
|
import org.eclipse.jetty.server.handler.HandlerCollection
|
||||||
@ -25,46 +21,31 @@ import org.glassfish.jersey.servlet.ServletContainer
|
|||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
|
import java.security.SignatureException
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.security.auth.x500.X500Principal
|
|
||||||
import javax.ws.rs.*
|
import javax.ws.rs.*
|
||||||
import javax.ws.rs.core.MediaType
|
import javax.ws.rs.core.MediaType
|
||||||
import javax.ws.rs.core.Response
|
import javax.ws.rs.core.Response
|
||||||
import javax.ws.rs.core.Response.ok
|
import javax.ws.rs.core.Response.ok
|
||||||
|
import javax.ws.rs.core.Response.status
|
||||||
|
|
||||||
class NetworkMapServer(cacheTimeout: Duration,
|
class NetworkMapServer(private val cacheTimeout: Duration,
|
||||||
hostAndPort: NetworkHostAndPort,
|
hostAndPort: NetworkHostAndPort,
|
||||||
rootCa: CertificateAndKeyPair = DEV_ROOT_CA,
|
private val networkMapCa: CertificateAndKeyPair = createDevNetworkMapCa(),
|
||||||
private val myHostNameValue: String = "test.host.name",
|
private val myHostNameValue: String = "test.host.name",
|
||||||
vararg additionalServices: Any) : Closeable {
|
vararg additionalServices: Any) : Closeable {
|
||||||
companion object {
|
companion object {
|
||||||
private val stubNetworkParameters = NetworkParameters(1, emptyList(), 10485760, 40000, Instant.now(), 10)
|
private val stubNetworkParameters = NetworkParameters(1, emptyList(), 10485760, 40000, Instant.now(), 10)
|
||||||
|
|
||||||
private fun networkMapKeyAndCert(rootCAKeyAndCert: CertificateAndKeyPair): CertificateAndKeyPair {
|
|
||||||
val networkMapKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
|
||||||
val networkMapCert = X509Utilities.createCertificate(
|
|
||||||
CertificateType.NETWORK_MAP,
|
|
||||||
rootCAKeyAndCert.certificate,
|
|
||||||
rootCAKeyAndCert.keyPair,
|
|
||||||
X500Principal("CN=Corda Network Map,O=R3 Ltd,L=London,C=GB"),
|
|
||||||
networkMapKey.public)
|
|
||||||
// Check that the certificate validates. Nodes will perform this check upon receiving a network map,
|
|
||||||
// it's better to fail here than there.
|
|
||||||
X509Utilities.validateCertificateChain(rootCAKeyAndCert.certificate, networkMapCert)
|
|
||||||
return CertificateAndKeyPair(networkMapCert, networkMapKey)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val server: Server
|
private val server: Server
|
||||||
var networkParameters: NetworkParameters = stubNetworkParameters
|
var networkParameters: NetworkParameters = stubNetworkParameters
|
||||||
set(networkParameters) {
|
set(networkParameters) {
|
||||||
check(field == stubNetworkParameters) { "Network parameters can be set only once" }
|
check(field == stubNetworkParameters) { "Network parameters can be set only once" }
|
||||||
field = networkParameters
|
field = networkParameters
|
||||||
}
|
}
|
||||||
private val serializedParameters get() = networkParameters.serialize()
|
private val service = InMemoryNetworkMapService()
|
||||||
private val service = InMemoryNetworkMapService(cacheTimeout, networkMapKeyAndCert(rootCa))
|
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply {
|
server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply {
|
||||||
@ -104,32 +85,34 @@ class NetworkMapServer(cacheTimeout: Duration,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Path("network-map")
|
@Path("network-map")
|
||||||
inner class InMemoryNetworkMapService(private val cacheTimeout: Duration,
|
inner class InMemoryNetworkMapService {
|
||||||
private val networkMapKeyAndCert: CertificateAndKeyPair) {
|
|
||||||
private val nodeInfoMap = mutableMapOf<SecureHash, SignedNodeInfo>()
|
private val nodeInfoMap = mutableMapOf<SecureHash, SignedNodeInfo>()
|
||||||
private val parametersHash by lazy { serializedParameters.hash }
|
private val signedNetParams by lazy {
|
||||||
private val signedParameters by lazy { SignedData(
|
networkParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)
|
||||||
serializedParameters,
|
}
|
||||||
DigitalSignature.WithKey(networkMapKeyAndCert.keyPair.public, Crypto.doSign(networkMapKeyAndCert.keyPair.private, serializedParameters.bytes))) }
|
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("publish")
|
@Path("publish")
|
||||||
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
|
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
fun publishNodeInfo(input: InputStream): Response {
|
fun publishNodeInfo(input: InputStream): Response {
|
||||||
val registrationData = input.readBytes().deserialize<SignedNodeInfo>()
|
return try {
|
||||||
val nodeInfo = registrationData.verified()
|
val signedNodeInfo = input.readBytes().deserialize<SignedNodeInfo>()
|
||||||
val nodeInfoHash = nodeInfo.serialize().sha256()
|
signedNodeInfo.verified()
|
||||||
nodeInfoMap.put(nodeInfoHash, registrationData)
|
nodeInfoMap[signedNodeInfo.raw.hash] = signedNodeInfo
|
||||||
return ok().build()
|
ok()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
when (e) {
|
||||||
|
is SignatureException -> status(Response.Status.FORBIDDEN).entity(e.message)
|
||||||
|
else -> status(Response.Status.INTERNAL_SERVER_ERROR).entity(e.message)
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.APPLICATION_OCTET_STREAM)
|
@Produces(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
fun getNetworkMap(): Response {
|
fun getNetworkMap(): Response {
|
||||||
val networkMap = NetworkMap(nodeInfoMap.keys.toList(), parametersHash)
|
val networkMap = NetworkMap(nodeInfoMap.keys.toList(), signedNetParams.raw.hash)
|
||||||
val serializedNetworkMap = networkMap.serialize()
|
val signedNetworkMap = networkMap.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)
|
||||||
val signature = Crypto.doSign(networkMapKeyAndCert.keyPair.private, serializedNetworkMap.bytes)
|
|
||||||
val signedNetworkMap = SignedNetworkMap(networkMap.serialize(), DigitalSignatureWithCert(networkMapKeyAndCert.certificate, signature))
|
|
||||||
return Response.ok(signedNetworkMap.serialize().bytes).header("Cache-Control", "max-age=${cacheTimeout.seconds}").build()
|
return Response.ok(signedNetworkMap.serialize().bytes).header("Cache-Control", "max-age=${cacheTimeout.seconds}").build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,16 +134,15 @@ class NetworkMapServer(cacheTimeout: Duration,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("network-parameter/{var}")
|
@Path("network-parameters/{var}")
|
||||||
@Produces(MediaType.APPLICATION_OCTET_STREAM)
|
@Produces(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
fun getNetworkParameter(@PathParam("var") networkParameterHash: String): Response {
|
fun getNetworkParameter(@PathParam("var") hash: String): Response {
|
||||||
return Response.ok(signedParameters.serialize().bytes).build()
|
require(signedNetParams.raw.hash == SecureHash.parse(hash))
|
||||||
|
return Response.ok(signedNetParams.serialize().bytes).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("my-hostname")
|
@Path("my-hostname")
|
||||||
fun getHostName(): Response {
|
fun getHostName(): Response = Response.ok(myHostNameValue).build()
|
||||||
return Response.ok(myHostNameValue).build()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,6 @@ import net.corda.core.contracts.TypeOnlyCommandData
|
|||||||
import net.corda.core.crypto.generateKeyPair
|
import net.corda.core.crypto.generateKeyPair
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
|
||||||
import net.corda.nodeapi.internal.crypto.getCertificateAndKeyPair
|
|
||||||
import net.corda.nodeapi.internal.crypto.loadKeyStore
|
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
@ -32,17 +29,10 @@ val ALICE_NAME = CordaX500Name("Alice Corp", "Madrid", "ES")
|
|||||||
val BOB_NAME = CordaX500Name("Bob Plc", "Rome", "IT")
|
val BOB_NAME = CordaX500Name("Bob Plc", "Rome", "IT")
|
||||||
@JvmField
|
@JvmField
|
||||||
val CHARLIE_NAME = CordaX500Name("Charlie Ltd", "Athens", "GR")
|
val CHARLIE_NAME = CordaX500Name("Charlie Ltd", "Athens", "GR")
|
||||||
val DEV_INTERMEDIATE_CA: CertificateAndKeyPair by lazy {
|
|
||||||
// TODO: Should be identity scheme
|
|
||||||
val caKeyStore = loadKeyStore(ClassLoader.getSystemResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass")
|
|
||||||
caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass")
|
|
||||||
}
|
|
||||||
|
|
||||||
val DEV_ROOT_CA: CertificateAndKeyPair by lazy {
|
val DEV_INTERMEDIATE_CA: CertificateAndKeyPair by lazy { net.corda.nodeapi.internal.DEV_INTERMEDIATE_CA }
|
||||||
// TODO: Should be identity scheme
|
|
||||||
val caKeyStore = loadKeyStore(ClassLoader.getSystemResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass")
|
val DEV_ROOT_CA: CertificateAndKeyPair by lazy { net.corda.nodeapi.internal.DEV_ROOT_CA }
|
||||||
caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_ROOT_CA, "cordacadevkeypass")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dummyCommand(vararg signers: PublicKey = arrayOf(generateKeyPair().public)) = Command<TypeOnlyCommandData>(DummyCommandData, signers.toList())
|
fun dummyCommand(vararg signers: PublicKey = arrayOf(generateKeyPair().public)) = Command<TypeOnlyCommandData>(DummyCommandData, signers.toList())
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ object DummyDealStateSchemaV1 : MappedSchema(schemaFamily = DummyDealStateSchema
|
|||||||
@Entity
|
@Entity
|
||||||
@Table(name = "dummy_deal_states")
|
@Table(name = "dummy_deal_states")
|
||||||
class PersistentDummyDealState(
|
class PersistentDummyDealState(
|
||||||
|
/** parent attributes */
|
||||||
@ElementCollection
|
@ElementCollection
|
||||||
@Column(name = "participants")
|
@Column(name = "participants")
|
||||||
@CollectionTable(name = "dummy_deal_states_participants", joinColumns = arrayOf(
|
@CollectionTable(name = "dummy_deal_states_participants", joinColumns = arrayOf(
|
||||||
|
Loading…
Reference in New Issue
Block a user