CORDA-881: Signed network parameters has the network map cert attached to it instead of just the public key. (#2346)

Introduced DigitalSignatureWithCert and SignedDataWithCert as internal APIs, with the expectation that they will become public; renamed the network parameters end-point to network-parameters; updated the network-map.rst doc; and did some refactoring.
This commit is contained in:
Shams Asari 2018-01-12 07:59:08 +00:00 committed by GitHub
parent 0a56c75543
commit bbfbb08c43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 236 additions and 262 deletions

1
.gitignore vendored
View File

@ -37,6 +37,7 @@ lib/quasar.jar
.idea/markdown-navigator
.idea/runConfigurations
.idea/dictionaries
.idea/codeStyles/
/gradle-plugins/.idea/
# Include the -parameters compiler option by default in IntelliJ required for serialization.

View File

@ -407,7 +407,7 @@ object Crypto {
*/
@JvmStatic
@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].

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
import net.corda.core.cordapp.CordappProvider
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256
import net.corda.core.identity.CordaX500Name
import net.corda.core.node.ServicesForResolution
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.WireTransaction
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.file.*
import java.nio.file.attribute.FileAttribute
import java.security.PrivateKey
import java.security.cert.X509Certificate
import java.time.Duration
import java.time.temporal.Temporal
import java.util.*
@ -308,6 +313,19 @@ fun TransactionBuilder.toLedgerTransaction(services: ServicesForResolution, seri
val KClass<*>.packageName: String get() = java.`package`.name
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]. */
fun ExecutorService.join() {
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
val CordaX500Name.Companion.unspecifiedCountry
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

@ -1,84 +1,78 @@
Network Map
===========
The network map stores a collection of ``NodeInfo`` objects, each representing another node with which the node can interact.
There are two sources from which a Corda node can retrieve ``NodeInfo`` objects:
The network map is a collection of signed ``NodeInfo`` objects (signed by the node it represents and thus tamper-proof)
forming the set of reachable nodes in a compatbility zone. A node can receive these objects from two sources:
1. the REST protocol with the network map service, which also provides a publishing API,
1. The HTTP network map service if the ``compatibilityZoneURL`` config key is specified.
2. The ``additional-node-infos`` directory within the node's directory.
2. the ``additional-node-infos`` directory.
HTTP network map service
------------------------
If the node is configured with the ``compatibilityZoneURL`` config then it first uploads its own signed ``NodeInfo``
to the server (and each time it changes on startup) and then proceeds to download the entire network map. The network map
consists of a list of ``NodeInfo`` hashes. The node periodically polls for the network map (based on the HTTP cache expiry
header) and any new hash entries are downloaded and cached. Entries which no longer exist are deleted from the node's cache.
Protocol Design
---------------
The node info publishing protocol:
The set of REST end-points for the network map service are as follows.
* Create a ``NodeInfo`` object, and sign it to create a ``SignedNodeInfo`` object.
* Serialise the signed data and POST the data to the network map server.
* The network map server validates the signature and acknowledges the registration with a HTTP 200 response, it will return HTTP 400 "Bad Request" if the data failed validation or if the public key wasn't registered with the network.
* The network map server will sign and distribute the new network map periodically.
Node side network map update protocol:
* The Corda node will query the network map service periodically according to the ``Expires`` attribute in the HTTP header.
* The network map service returns a signed ``NetworkMap`` object which looks as follows:
.. container:: codeset
.. sourcecode:: kotlin
data class NetworkMap {
val nodeInfoHashes: List<SecureHash>,
val networkParametersHash: SecureHash
}
The object contains list of node info hashes and hash of the network parameters data structure (without the signatures).
* The node updates its local copy of ``NodeInfos`` if it is different from the newly downloaded ``NetworkMap``.
Network Map service REST API:
+----------------+-----------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+
| Request method | Path | Description |
+================+===================================+========================================================================================================================================================+
| POST | /network-map/publish | Publish new ``NodeInfo`` to the network map service, the legal identity in ``NodeInfo`` must match with the identity registered with the doorman. |
+----------------+-----------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+
| GET | /network-map | Retrieve ``NetworkMap`` from the server, the ``NetworkMap`` object contains list of node info hashes and ``NetworkParameters`` hash. |
+----------------+-----------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+
| GET | /network-map/node-info/{hash} | Retrieve ``NodeInfo`` object with the same hash. |
+----------------+-----------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+
| GET | /network-map/parameters/{hash} | Retrieve ``NetworkParameters`` object with the same hash. |
+----------------+-----------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+
TODO: Access control of the network map will be added in the future.
+----------------+-----------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------+
| Request method | Path | Description |
+================+=========================================+==============================================================================================================================================+
| POST | /network-map/publish | For the node to upload its signed ``NodeInfo`` object to the network map. |
+----------------+-----------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------+
| GET | /network-map | Retrieve the current signed network map object. The entire object is signed with the network map certificate which is also attached. |
+----------------+-----------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------+
| GET | /network-map/node-info/{hash} | Retrieve a signed ``NodeInfo`` as specified in the network map object. |
+----------------+-----------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------+
| GET | /network-map/network-parameters/{hash} | Retrieve the signed network parameters (see below). The entire object is signed with the network map certificate which is also attached. |
+----------------+-----------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------+
The ``additional-node-infos`` directory
---------------------------------------
Each Corda node reads, and continuously polls, the files contained in a directory named ``additional-node-infos`` inside the node base directory.
Nodes expect to find a serialized ``SignedNodeInfo`` object, the same object which is sent to network map server.
Whenever a node starts it writes on disk a file containing its own ``NodeInfo``, this file is called ``nodeInfo-XXX`` where ``XXX`` is a long string.
Hence if an operator wants node A to see node B they can pick B's ``NodeInfo`` file from B base directory and drop it into A's ``additional-node-infos`` directory.
Alongside the HTTP network map service, or as a replacement if the node isn't connected to one, the node polls the
contents of the ``additional-node-infos`` directory located in its base directory. Each file is expected to be the same
signed ``NodeInfo`` object that the network map service vends. These are automtically added to the node's cache and can
be used to supplement or replace the HTTP network map. If the same node is advertised through both mechanisms then the
latest one is taken.
On startup the node generates its own signed node info file, filename of the format ``nodeInfo-${hash}``. To create a simple
network without the HTTP network map service then simply place this file in the ``additional-node-infos`` directory
of every node that's part of this network.
Network parameters
------------------
Network parameters are constants that every node participating in the network needs to agree on and use for interop purposes.
The structure is distributed as a file containing serialized ``SignedData<NetworkParameters>`` with a signature from
a sub-key of the compatibility zone root cert. Network map advertises the hash of currently used network parameters.
The ``NetworkParameters`` structure contains:
* ``minimumPlatformVersion`` - minimum version of Corda platform that is required for nodes in the network.
* ``notaries`` - list of well known and trusted notary identities with information on validation type.
* ``maxMessageSize`` - maximum P2P message size sent over the wire in bytes.
* ``maxTransactionSize`` - maximum permitted transaction size in bytes.
* ``modifiedTime`` - the time the network parameters were created by the CZ operator.
* ``epoch`` - version number of the network parameters. Starting from 1, this will always increment on each new set of parameters.
The set of parameters is still under development and we may find the need to add additional fields.
Network parameters are a set of values that every node participating in the network needs to agree on and use to
correctly interoperate with each other. If the node is using the HTTP network map service then on first startup it will
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
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.
.. note:: A future release will support the notion of network parameters changes.
If the node isn't using a HTTP network map service then it's expected the signed file is provided by some other means.
For such a scenario there is the network bootstrapper tool which in addition to generating the network parameters file
also distributes the node info files to the node directories. More information can be found in :doc:`setting-up-a-corda-network`.
The current set of network parameters:
:minimumPlatformVersion: The minimum platform version that the nodes must be running. Any node which is below this will
not start.
:notaries: List of identity and validation type (either validating or non-validating) of the notaries which are permitted
in the compatibility zone.
:maxMessageSize: Maximum allowed P2P message size sent over the wire in bytes. Any message larger than this will be
split up.
:maxTransactionSize: Maximum permitted transaction size in bytes.
: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
parameters change.
.. note:: ``maxTransactionSize`` is currently not enforced in the node, but will be in a later release.
More parameters may be added in future releases.

View File

@ -34,13 +34,8 @@ object DevIdentityGenerator {
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.createDevKeyStores(rootCert, intermediateCa, legalName)
nodeSslConfig.createDevKeyStores(legalName)
val keyStoreWrapper = KeyStoreWrapper(nodeSslConfig.nodeKeystore, nodeSslConfig.keyStorePassword)
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 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 ->
val (serviceKeyCert, compositeKeyCert) = listOf(keyPair.public, compositeKey).map { publicKey ->
X509Utilities.createCertificate(
CertificateType.SERVICE_IDENTITY,
intermediateCa.certificate,
intermediateCa.keyPair,
DEV_INTERMEDIATE_CA.certificate,
DEV_INTERMEDIATE_CA.keyPair,
notaryName.x500Principal,
publicKey)
}
@ -74,7 +65,7 @@ object DevIdentityGenerator {
"$DISTRIBUTED_NOTARY_ALIAS_PREFIX-private-key",
keyPair.private,
"cordacadevkeypass".toCharArray(),
arrayOf(serviceKeyCert, intermediateCa.certificate, rootCert))
arrayOf(serviceKeyCert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate))
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.NameConstraints
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
* 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)
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
* [CordaX500Name] as the cert subject.
@ -55,3 +71,16 @@ fun createDevNodeCa(intermediateCa: CertificateAndKeyPair, legalName: CordaX500N
nameConstraints = nameConstraints)
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
* 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
// 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
// public keys.
@CordaSerializable
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 {
val nodeInfo = raw.deserialize()
val identities = nodeInfo.legalIdentities.filterNot { it.owningKey is CompositeKey }

View File

@ -1,16 +1,12 @@
package net.corda.nodeapi.internal.network
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.verify
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.serialization.CordaSerializable
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize
import net.corda.nodeapi.internal.crypto.X509Utilities
import java.security.SignatureException
import java.security.cert.CertPathValidatorException
import java.security.cert.X509Certificate
import java.time.Instant
@ -55,28 +51,8 @@ data class NetworkParameters(
@CordaSerializable
data class NotaryInfo(val identity: Party, val validating: Boolean)
/**
* A serialized [NetworkMap] and its signature and certificate. Enforces signature validity in order to deserialize the data
* contained within.
*/
@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)
// 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)
fun <T : Any> SignedDataWithCert<T>.verifiedNetworkMapCert(rootCert: X509Certificate): T {
require(CertRole.extract(sig.by) == CertRole.NETWORK_MAP) { "Incorrect cert role: ${CertRole.extract(sig.by)}" }
X509Utilities.validateCertificateChain(rootCert, sig.by, rootCert)
return verified()
}

View File

@ -1,35 +1,32 @@
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.div
import net.corda.core.internal.signWithCert
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.Path
import java.nio.file.StandardCopyOption
import java.security.KeyPair
class NetworkParametersCopier(
networkParameters: NetworkParameters,
signingKeyPair: KeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME),
networkMapCa: CertificateAndKeyPair = createDevNetworkMapCa(),
overwriteFile: Boolean = false
) {
private val copyOptions = if (overwriteFile) arrayOf(StandardCopyOption.REPLACE_EXISTING) else emptyArray()
private val serializedNetworkParameters = networkParameters.let {
val serialize = it.serialize()
val signature = signingKeyPair.sign(serialize)
SignedData(serialize, signature).serialize()
}
private val serialisedSignedNetParams = networkParameters.signWithCert(
networkMapCa.keyPair.private,
networkMapCa.certificate
).serialize()
fun install(nodeDir: Path) {
try {
serializedNetworkParameters.open().copyTo(nodeDir / NETWORK_PARAMS_FILE_NAME, *copyOptions)
serialisedSignedNetParams.open().copyTo(nodeDir / NETWORK_PARAMS_FILE_NAME, *copyOptions)
} catch (e: FileAlreadyExistsException) {
// 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.
}
}
}
}

View File

@ -168,7 +168,7 @@ class X509UtilitiesTest {
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
// 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
val serverKeyStore = loadKeyStore(sslConfig.nodeKeystore, sslConfig.keyStorePassword)
@ -203,7 +203,7 @@ class X509UtilitiesTest {
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
// 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)
val keyStore = loadKeyStore(sslConfig.sslKeystore, sslConfig.keyStorePassword)

View File

@ -1,13 +1,9 @@
package net.corda.node.services.network
import net.corda.cordform.CordformNode
import net.corda.core.crypto.SignedData
import net.corda.core.crypto.random63BitValue
import net.corda.core.internal.*
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.serialization.deserialize
import net.corda.core.utilities.getOrThrow
@ -69,7 +65,7 @@ class NetworkMapTest {
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
val networkParameters = (alice.configuration.baseDirectory / NETWORK_PARAMS_FILE_NAME)
.readAll()
.deserialize<SignedData<NetworkParameters>>()
.deserialize<SignedDataWithCert<NetworkParameters>>()
.verified()
// We use a random modified time above to make the network parameters unqiue so that we're sure they came
// from the server

View File

@ -64,7 +64,11 @@ class NodeRegistrationTest {
@Before
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()
}

View File

@ -2,10 +2,15 @@ package net.corda.services.messaging
import net.corda.core.crypto.Crypto
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.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.crypto.*
import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration
@ -90,20 +95,13 @@ class MQSecurityAsNodeTest : MQSecurityTest() {
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)
// Set name constrain to the legal name.
val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName.x500Name))), arrayOf())
val clientCACert = X509Utilities.createCertificate(
CertificateType.INTERMEDIATE_CA,
intermediateCA.certificate,
intermediateCA.keyPair,
DEV_INTERMEDIATE_CA.certificate,
DEV_INTERMEDIATE_CA.keyPair,
legalName.x500Principal,
clientKeyPair.public,
nameConstraints = nameConstraints)
@ -123,7 +121,7 @@ class MQSecurityAsNodeTest : MQSecurityTest() {
X509Utilities.CORDA_CLIENT_CA,
clientKeyPair.private,
keyPass,
arrayOf(clientCACert, intermediateCA.certificate, rootCACert))
arrayOf(clientCACert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate))
clientCAKeystore.save(nodeKeystore, keyStorePassword)
val tlsKeystore = loadOrCreateKeyStore(sslKeystore, keyStorePassword)
@ -131,7 +129,7 @@ class MQSecurityAsNodeTest : MQSecurityTest() {
X509Utilities.CORDA_CLIENT_TLS,
tlsKeyPair.private,
keyPass,
arrayOf(clientTLSCert, clientCACert, intermediateCA.certificate, rootCACert))
arrayOf(clientTLSCert, clientCACert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate))
tlsKeystore.save(sslKeystore, keyStorePassword)
}
}

View File

@ -12,7 +12,6 @@ import net.corda.core.concurrent.CordaFuture
import net.corda.core.context.InvocationContext
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SignedData
import net.corda.core.crypto.sign
import net.corda.core.flows.*
import net.corda.core.identity.CordaX500Name
@ -67,6 +66,7 @@ import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.crypto.loadKeyStore
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME
import net.corda.nodeapi.internal.network.NetworkParameters
import net.corda.nodeapi.internal.network.verifiedNetworkMapCert
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.nodeapi.internal.persistence.HibernateConfiguration
@ -208,7 +208,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null)
val identityService = makeIdentityService(identity.certificate)
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.
val (startedImpl, schedulerService) = initialiseDatabasePersistence(schemaService, identityService) { database ->
val networkMapCache = NetworkMapCacheImpl(PersistentNetworkMapCache(database, networkParameters.notaries), identityService)
@ -643,23 +643,19 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
return PersistentKeyManagementService(identityService, keyPairs)
}
private fun retrieveNetworkParameters() {
val networkParamsFile = configuration.baseDirectory.list { paths ->
paths.filter { it.fileName.toString() == NETWORK_PARAMS_FILE_NAME }.findFirst().orElse(null)
}
private fun retrieveNetworkParameters(trustRoot: X509Certificate) {
val networkParamsFile = configuration.baseDirectory / NETWORK_PARAMS_FILE_NAME
networkParameters = if (networkParamsFile != null) {
networkParamsFile.readAll().deserialize<SignedData<NetworkParameters>>().verified()
networkParameters = if (networkParamsFile.exists()) {
networkParamsFile.readAll().deserialize<SignedDataWithCert<NetworkParameters>>().verifiedNetworkMapCert(trustRoot)
} else {
log.info("No network-parameters file found. Expecting network parameters to be available from the network map.")
val networkMapClient = checkNotNull(networkMapClient) {
"Node hasn't been configured to connect to a network map from which to get the network parameters"
}
val (networkMap, _) = networkMapClient.getNetworkMap()
val signedParams = checkNotNull(networkMapClient.getNetworkParameter(networkMap.networkParameterHash)) {
"Failed loading network parameters from network map server"
}
val verifiedParams = signedParams.verified()
val signedParams = networkMapClient.getNetworkParameters(networkMap.networkParameterHash)
val verifiedParams = signedParams.verifiedNetworkMapCert(trustRoot)
signedParams.serialize().open().copyTo(configuration.baseDirectory / NETWORK_PARAMS_FILE_NAME)
verifiedParams
}

View File

@ -52,10 +52,7 @@ fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) {
loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordatruststore.jks"), "trustpass").save(trustStoreFile, trustStorePassword)
}
if (!sslKeystore.exists() || !nodeKeystore.exists()) {
val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass")
val rootCert = caKeyStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA)
val intermediateCa = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass")
createDevKeyStores(rootCert, intermediateCa, myLegalName)
createDevKeyStores(myLegalName)
// Move distributed service composite key (generated by IdentityGenerator.generateToDisk) to keystore if exists.
val distributedServiceKeystore = certificatesDirectory / "distributedService.jks"

View File

@ -2,28 +2,26 @@ package net.corda.node.services.network
import com.google.common.util.concurrent.MoreExecutors
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.responseAs
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.minutes
import net.corda.core.utilities.seconds
import net.corda.node.services.api.NetworkMapCacheInternal
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.NetworkParameters
import net.corda.nodeapi.internal.network.SignedNetworkMap
import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.network.verifiedNetworkMapCert
import okhttp3.CacheControl
import okhttp3.Headers
import org.apache.commons.io.IOUtils
import rx.Subscription
import java.io.BufferedReader
import java.io.Closeable
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL
import java.security.cert.X509Certificate
import java.time.Duration
@ -40,42 +38,29 @@ class NetworkMapClient(compatibilityZoneURL: URL, private val trustedRoot: X509C
requestMethod = "POST"
setRequestProperty("Content-Type", "application/octet-stream")
outputStream.use { signedNodeInfo.serialize().open().copyTo(it) }
if (responseCode != 200) {
throw IOException("Response Code $responseCode: ${IOUtils.toString(errorStream)}")
}
checkOkResponse()
}
}
fun getNetworkMap(): NetworkMapResponse {
val conn = networkMapUrl.openHttpConnection()
val signedNetworkMap = conn.inputStream.use { it.readBytes() }.deserialize<SignedNetworkMap>()
val networkMap = signedNetworkMap.verified(trustedRoot)
val timeout = CacheControl.parse(Headers.of(conn.headerFields.filterKeys { it != null }.mapValues { it.value.first() })).maxAgeSeconds().seconds
val connection = networkMapUrl.openHttpConnection()
val signedNetworkMap = connection.responseAs<SignedDataWithCert<NetworkMap>>()
val networkMap = signedNetworkMap.verifiedNetworkMapCert(trustedRoot)
val timeout = CacheControl.parse(Headers.of(connection.headerFields.filterKeys { it != null }.mapValues { it.value[0] })).maxAgeSeconds().seconds
return NetworkMapResponse(networkMap, timeout)
}
fun getNodeInfo(nodeInfoHash: SecureHash): NodeInfo? {
val conn = URL("$networkMapUrl/node-info/$nodeInfoHash").openHttpConnection()
return if (conn.responseCode == HttpURLConnection.HTTP_NOT_FOUND) {
null
} else {
val signedNodeInfo = conn.inputStream.use { it.readBytes() }.deserialize<SignedNodeInfo>()
signedNodeInfo.verified()
}
fun getNodeInfo(nodeInfoHash: SecureHash): NodeInfo {
return URL("$networkMapUrl/node-info/$nodeInfoHash").openHttpConnection().responseAs<SignedNodeInfo>().verified()
}
fun getNetworkParameter(networkParameterHash: SecureHash): SignedData<NetworkParameters>? {
val conn = URL("$networkMapUrl/network-parameter/$networkParameterHash").openHttpConnection()
return if (conn.responseCode == HttpURLConnection.HTTP_NOT_FOUND) {
null
} else {
conn.inputStream.use { it.readBytes() }.deserialize()
}
fun getNetworkParameters(networkParameterHash: SecureHash): SignedDataWithCert<NetworkParameters> {
return URL("$networkMapUrl/network-parameters/$networkParameterHash").openHttpConnection().responseAs()
}
fun myPublicHostname(): String {
val conn = URL("$networkMapUrl/my-hostname").openHttpConnection()
return conn.inputStream.bufferedReader().use(BufferedReader::readLine)
val connection = URL("$networkMapUrl/my-hostname").openHttpConnection()
return connection.inputStream.bufferedReader().use(BufferedReader::readLine)
}
}

View File

@ -24,9 +24,7 @@ class HTTPNetworkRegistrationService(compatibilityZoneURL: URL) : NetworkRegistr
@Throws(CertificateRequestException::class)
override fun retrieveCertificates(requestId: String): Array<Certificate>? {
// Poll server to download the signed certificate once request has been approved.
val url = URL("$registrationURL/$requestId")
val conn = url.openConnection() as HttpURLConnection
val conn = URL("$registrationURL/$requestId").openHttpConnection()
conn.requestMethod = "GET"
return when (conn.responseCode) {

View File

@ -1,6 +1,5 @@
package net.corda.node.services.network
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256
import net.corda.core.serialization.serialize
import net.corda.core.utilities.seconds
@ -22,7 +21,6 @@ import org.junit.Test
import java.io.IOException
import java.net.URL
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
class NetworkMapClientTest {
@Rule
@ -83,8 +81,8 @@ class NetworkMapClientTest {
@Test
fun `download NetworkParameter correctly`() {
// The test server returns same network parameter for any hash.
val networkParameter = networkMapClient.getNetworkParameter(SecureHash.randomSHA256())?.verified()
assertNotNull(networkParameter)
val parametersHash = server.networkParameters.serialize().hash
val networkParameter = networkMapClient.getNetworkParameters(parametersHash).verified()
assertEquals(server.networkParameters, networkParameter)
}

View File

@ -1,20 +1,16 @@
package net.corda.testing.node.internal.network
import net.corda.core.crypto.*
import net.corda.core.identity.CordaX500Name
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.signWithCert
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.utilities.NetworkHostAndPort
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.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.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.ServerConnector
import org.eclipse.jetty.server.handler.HandlerCollection
@ -28,34 +24,19 @@ import java.net.InetSocketAddress
import java.security.SignatureException
import java.time.Duration
import java.time.Instant
import javax.security.auth.x500.X500Principal
import javax.ws.rs.*
import javax.ws.rs.core.MediaType
import javax.ws.rs.core.Response
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,
rootCa: CertificateAndKeyPair = DEV_ROOT_CA,
private val networkMapCa: CertificateAndKeyPair = createDevNetworkMapCa(),
private val myHostNameValue: String = "test.host.name",
vararg additionalServices: Any) : Closeable {
companion object {
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
@ -64,9 +45,7 @@ class NetworkMapServer(cacheTimeout: Duration,
check(field == stubNetworkParameters) { "Network parameters can be set only once" }
field = networkParameters
}
private val serializedParameters get() = networkParameters.serialize()
private val service = InMemoryNetworkMapService(cacheTimeout, networkMapKeyAndCert(rootCa))
private val service = InMemoryNetworkMapService()
init {
server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply {
@ -106,14 +85,10 @@ class NetworkMapServer(cacheTimeout: Duration,
}
@Path("network-map")
inner class InMemoryNetworkMapService(private val cacheTimeout: Duration,
private val networkMapKeyAndCert: CertificateAndKeyPair) {
inner class InMemoryNetworkMapService {
private val nodeInfoMap = mutableMapOf<SecureHash, SignedNodeInfo>()
private val parametersHash by lazy { serializedParameters.hash }
private val signedParameters by lazy {
SignedData(
serializedParameters,
DigitalSignature.WithKey(networkMapKeyAndCert.keyPair.public, Crypto.doSign(networkMapKeyAndCert.keyPair.private, serializedParameters.bytes)))
private val signedNetParams by lazy {
networkParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)
}
@POST
@ -121,10 +96,9 @@ class NetworkMapServer(cacheTimeout: Duration,
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
fun publishNodeInfo(input: InputStream): Response {
return try {
val registrationData = input.readBytes().deserialize<SignedNodeInfo>()
val nodeInfo = registrationData.verified()
val nodeInfoHash = nodeInfo.serialize().sha256()
nodeInfoMap.put(nodeInfoHash, registrationData)
val signedNodeInfo = input.readBytes().deserialize<SignedNodeInfo>()
signedNodeInfo.verified()
nodeInfoMap[signedNodeInfo.raw.hash] = signedNodeInfo
ok()
} catch (e: Exception) {
when (e) {
@ -137,10 +111,8 @@ class NetworkMapServer(cacheTimeout: Duration,
@GET
@Produces(MediaType.APPLICATION_OCTET_STREAM)
fun getNetworkMap(): Response {
val networkMap = NetworkMap(nodeInfoMap.keys.toList(), parametersHash)
val serializedNetworkMap = networkMap.serialize()
val signature = Crypto.doSign(networkMapKeyAndCert.keyPair.private, serializedNetworkMap.bytes)
val signedNetworkMap = SignedNetworkMap(networkMap.serialize(), DigitalSignatureWithCert(networkMapKeyAndCert.certificate, signature))
val networkMap = NetworkMap(nodeInfoMap.keys.toList(), signedNetParams.raw.hash)
val signedNetworkMap = networkMap.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)
return Response.ok(signedNetworkMap.serialize().bytes).header("Cache-Control", "max-age=${cacheTimeout.seconds}").build()
}
@ -162,16 +134,15 @@ class NetworkMapServer(cacheTimeout: Duration,
}
@GET
@Path("network-parameter/{var}")
@Path("network-parameters/{var}")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
fun getNetworkParameter(@PathParam("var") networkParameterHash: String): Response {
return Response.ok(signedParameters.serialize().bytes).build()
fun getNetworkParameter(@PathParam("var") hash: String): Response {
require(signedNetParams.raw.hash == SecureHash.parse(hash))
return Response.ok(signedNetParams.serialize().bytes).build()
}
@GET
@Path("my-hostname")
fun getHostName(): Response {
return Response.ok(myHostNameValue).build()
}
fun getHostName(): Response = 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.identity.CordaX500Name
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.time.Instant
@ -32,17 +29,10 @@ val ALICE_NAME = CordaX500Name("Alice Corp", "Madrid", "ES")
val BOB_NAME = CordaX500Name("Bob Plc", "Rome", "IT")
@JvmField
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 {
// TODO: Should be identity scheme
val caKeyStore = loadKeyStore(ClassLoader.getSystemResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass")
caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_ROOT_CA, "cordacadevkeypass")
}
val DEV_INTERMEDIATE_CA: CertificateAndKeyPair by lazy { net.corda.nodeapi.internal.DEV_INTERMEDIATE_CA }
val DEV_ROOT_CA: CertificateAndKeyPair by lazy { net.corda.nodeapi.internal.DEV_ROOT_CA }
fun dummyCommand(vararg signers: PublicKey = arrayOf(generateKeyPair().public)) = Command<TypeOnlyCommandData>(DummyCommandData, signers.toList())