Merge branch 'master' into colljos-merge-release3-dp-master

This commit is contained in:
josecoll 2018-01-16 09:58:51 +00:00
commit 24bf6df868
98 changed files with 1679 additions and 678 deletions

2
.gitignore vendored
View File

@ -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

View File

@ -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 {

View File

@ -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].

View File

@ -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>())
}
}

View File

@ -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))
}

View File

@ -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

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

View 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. Wont 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. Doesnt mesh with the user access controls of the organisation.
4. Doesnt interface to the existing monitoring andcontrol systems i.e. Nagios, Geneos ITRS, Docker Swarm, etc.
### 2. Delegate to external tools
#### Advantages
1. Doesnt require change from our customers
2. Will work even if node is completely stuck
3. Allows scripted node restart schedules
4. Doesnt 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 peoples control software.
4. Doesnt coordinate with the node for graceful shutdown.
5. Doesnt 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)

View 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. Doesnt 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)

View 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)

View 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).

View 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 doesnt like the health probes. So far though testing of the Azure Load balancer doesnt need this.
3. Wont work over very large data centre separations, but that doesnt 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 cant 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)

View 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)

View 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)

View 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.

View File

@ -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"

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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
^^^^^^^ ^^^^^^^

View File

@ -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
``` ```

View File

@ -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)
)
}
) )
} }
} }

View File

@ -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
}

View File

@ -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)

View File

@ -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.

View File

@ -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> {

View File

@ -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)

View File

@ -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)
} }
} }
} }

View File

@ -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()

View File

@ -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("-", "_"))

View File

@ -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
} }

View File

@ -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()

View File

@ -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))
} }
} }

View File

@ -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)
} }

View File

@ -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)
} }
} }

View File

@ -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)
} }
} }
} }

View File

@ -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" }
} }
/** /**

View File

@ -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)

View File

@ -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)
} }
} }

View File

@ -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() }
} }
} }

View File

@ -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")
} }

View File

@ -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")
}
}

View File

@ -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 }

View File

@ -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)

View File

@ -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.

View 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)

View File

@ -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")

View File

@ -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)
}
}
}

View File

@ -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
} }

View File

@ -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<*> -> "?"

View File

@ -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)

View File

@ -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)))

View File

@ -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());
}
}

View File

@ -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);

View File

@ -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);
} }

View File

@ -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);

View File

@ -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);

View File

@ -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");

View File

@ -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)

View File

@ -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(

View File

@ -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))

View File

@ -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() {

View File

@ -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() })

View File

@ -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() {

View File

@ -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()

View File

@ -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() {

View File

@ -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])

View File

@ -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() {

View File

@ -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}")
}
}

View File

@ -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 {
}
}
}
} }

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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()
} }

View File

@ -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)
} }
} }

View File

@ -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
} }

View File

@ -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)
}

View File

@ -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)

View File

@ -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

View File

@ -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]
*/ */

View File

@ -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"

View File

@ -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)
} }
} }

View File

@ -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) {

View File

@ -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)
} }

View File

@ -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:

View File

@ -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()
}
} }
} }

View File

@ -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())

View File

@ -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(