Merge branch 'master' into colljos-schema-split-for-notaries

This commit is contained in:
josecoll 2018-01-22 14:36:27 +00:00
commit 4bff002b41
109 changed files with 1862 additions and 1176 deletions

3
.idea/compiler.xml generated
View File

@ -179,4 +179,7 @@
<module name="webserver_test" target="1.8" /> <module name="webserver_test" target="1.8" />
</bytecodeTargetLevel> </bytecodeTargetLevel>
</component> </component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_STRING" value="-parameters" />
</component>
</project> </project>

View File

@ -29,7 +29,7 @@ import net.corda.core.utilities.getOrThrow
import net.corda.nodeapi.ArtemisConsumer import net.corda.nodeapi.ArtemisConsumer
import net.corda.nodeapi.ArtemisProducer import net.corda.nodeapi.ArtemisProducer
import net.corda.nodeapi.RPCApi import net.corda.nodeapi.RPCApi
import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration import org.apache.activemq.artemis.api.core.RoutingType
import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE
import org.apache.activemq.artemis.api.core.client.ClientMessage import org.apache.activemq.artemis.api.core.client.ClientMessage
@ -194,7 +194,7 @@ class RPCClientProxyHandler(
TimeUnit.MILLISECONDS TimeUnit.MILLISECONDS
) )
sessionAndProducerPool.run { sessionAndProducerPool.run {
it.session.createTemporaryQueue(clientAddress, ActiveMQDefaultConfiguration.getDefaultRoutingType(), clientAddress) it.session.createTemporaryQueue(clientAddress, RoutingType.ANYCAST, clientAddress)
} }
val sessionFactory = serverLocator.createSessionFactory() val sessionFactory = serverLocator.createSessionFactory()
val session = sessionFactory.createSession(rpcUsername, rpcPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) val session = sessionFactory.createSession(rpcUsername, rpcPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE)

View File

@ -4,4 +4,3 @@ trustStorePassword : "trustpass"
p2pAddress : "localhost:10002" p2pAddress : "localhost:10002"
rpcAddress : "localhost:10003" rpcAddress : "localhost:10003"
webAddress : "localhost:10004" webAddress : "localhost:10004"
useHTTPS : false

View File

@ -4,4 +4,3 @@ trustStorePassword : "trustpass"
p2pAddress : "localhost:10005" p2pAddress : "localhost:10005"
rpcAddress : "localhost:10006" rpcAddress : "localhost:10006"
webAddress : "localhost:10007" webAddress : "localhost:10007"
useHTTPS : false

View File

@ -6,4 +6,3 @@ webAddress : "localhost:10001"
notary : { notary : {
validating : true validating : true
} }
useHTTPS : false

View File

@ -66,26 +66,24 @@ object NodeInfoSchemaV1 : MappedSchema(
} }
} }
@Embeddable
data class PKHostAndPort(
val host: String? = null,
val port: Int? = null
) : Serializable
@Entity @Entity
@Table(name = "node_info_hosts") @Table(name = "node_info_hosts")
data class DBHostAndPort( data class DBHostAndPort(
@EmbeddedId @Id
private val pk: PKHostAndPort @GeneratedValue
@Column(name = "hosts_id")
var id: Int,
val host: String? = null,
val port: Int? = null
) { ) {
companion object { companion object {
fun fromHostAndPort(hostAndPort: NetworkHostAndPort) = DBHostAndPort( fun fromHostAndPort(hostAndPort: NetworkHostAndPort) = DBHostAndPort(
PKHostAndPort(hostAndPort.host, hostAndPort.port) 0, hostAndPort.host, hostAndPort.port
) )
} }
fun toHostAndPort(): NetworkHostAndPort { fun toHostAndPort(): NetworkHostAndPort {
return NetworkHostAndPort(this.pk.host!!, this.pk.port!!) return NetworkHostAndPort(host!!, port!!)
} }
} }

View File

@ -158,6 +158,13 @@ R3 Corda 3.0 Developer Preview
* Peer-to-peer communications is now via AMQP 1.0 as default. * Peer-to-peer communications is now via AMQP 1.0 as default.
Although the legacy Artemis CORE bridging can still be used by setting the ``useAMQPBridges`` configuration property to false. Although the legacy Artemis CORE bridging can still be used by setting the ``useAMQPBridges`` configuration property to false.
* The Artemis topics used for peer-to-peer communication have been changed to be more consistent with future cryptographic
agility and to open up the future possibility of sharing brokers between nodes. This is a breaking wire level change
as it means that nodes after this change will not be able to communicate correctly with nodes running the previous version.
Also, any pending enqueued messages in the Artemis message store will not be delivered correctly to their original target.
However, assuming a clean reset of the artemis data and that the nodes are consistent versions,
data persisted via the AMQP serializer will be forward compatible.
* Enterprise Corda only: Compatibility with SQL Server 2017 and SQL Azure databases. * Enterprise Corda only: Compatibility with SQL Server 2017 and SQL Azure databases.
* Enterprise Corda only: node configuration property ``database.schema`` and documented existing database properties. * Enterprise Corda only: node configuration property ``database.schema`` and documented existing database properties.

View File

@ -43,7 +43,6 @@ Simple Notary configuration file.
notary : { notary : {
validating : false validating : false
} }
useHTTPS : false
devMode : true devMode : true
compatibilityZoneURL : "https://cz.corda.net" compatibilityZoneURL : "https://cz.corda.net"
@ -136,10 +135,6 @@ path to the node's base directory.
Only one of ``raft``, ``bftSMaRt`` or ``custom`` configuration values may be specified. Only one of ``raft``, ``bftSMaRt`` or ``custom`` configuration values may be specified.
:useHTTPS: If false the node's web server will be plain HTTP. If true the node will use the same certificate and private
key from the ``<workspace>/certificates/sslkeystore.jks`` file as the ArtemisMQ port for HTTPS. If HTTPS is enabled
then unencrypted HTTP traffic to the node's **webAddress** port is not supported.
:rpcUsers: A list of users who are authorised to access the RPC system. Each user in the list is a config object with the :rpcUsers: A list of users who are authorised to access the RPC system. Each user in the list is a config object with the
following fields: following fields:

View File

@ -46,7 +46,6 @@ handling, and ensures the Corda service is run at boot.
myLegalName : "O=Bank of Breakfast Tea, L=London, C=GB" myLegalName : "O=Bank of Breakfast Tea, L=London, C=GB"
keyStorePassword : "cordacadevpass" keyStorePassword : "cordacadevpass"
trustStorePassword : "trustpass" trustStorePassword : "trustpass"
useHTTPS : false
devMode : false devMode : false
rpcUsers=[ rpcUsers=[
{ {
@ -217,7 +216,6 @@ at boot, and means the Corda service stays running with no users connected to th
keyStorePassword : "cordacadevpass" keyStorePassword : "cordacadevpass"
trustStorePassword : "trustpass" trustStorePassword : "trustpass"
extraAdvertisedServiceIds: [ "" ] extraAdvertisedServiceIds: [ "" ]
useHTTPS : false
devMode : false devMode : false
rpcUsers=[ rpcUsers=[
{ {

View File

@ -4,4 +4,3 @@ trustStorePassword : "trustpass"
p2pAddress : "my-network-map:10000" p2pAddress : "my-network-map:10000"
webAddress : "localhost:10001" webAddress : "localhost:10001"
sshdAddress : "localhost:10002" sshdAddress : "localhost:10002"
useHTTPS : false

View File

@ -10,7 +10,6 @@ dataSourceProperties : {
p2pAddress : "my-corda-node:10002" p2pAddress : "my-corda-node:10002"
rpcAddress : "my-corda-node:10003" rpcAddress : "my-corda-node:10003"
webAddress : "localhost:10004" webAddress : "localhost:10004"
useHTTPS : false
rpcUsers : [ rpcUsers : [
{ username=user1, password=letmein, permissions=[ StartFlow.net.corda.protocols.CashProtocol ] } { username=user1, password=letmein, permissions=[ StartFlow.net.corda.protocols.CashProtocol ] }
] ]

View File

@ -1,6 +1,8 @@
Glossary Glossary
======== ========
AMQP
The serialisation mechanism used within Corda for everything except flow checkpoints and RPC.
Artemis Artemis
The message queuing middleware used within Corda The message queuing middleware used within Corda
Attachment Attachment
@ -30,7 +32,7 @@ Gradle
Kotlin Kotlin
The language used to code Corda. Fully compatible with any JVM language, including (obviously) Java. The language used to code Corda. Fully compatible with any JVM language, including (obviously) Java.
Kryo Kryo
The serialisation mechanism used within Corda - which is subject to change in a future release. The serialisation mechanism used within Corda for flow checkpoints and RPC.
Input Input
In Corda terms, an input state is one that is used and consumed within a transaction. Once consumed, it cannot be re-used. In Corda terms, an input state is one that is used and consumed within a transaction. Once consumed, it cannot be re-used.
JVM JVM

View File

@ -46,7 +46,7 @@ Message queues
The node makes use of various queues for its operation. The more important ones are described below. Others are used The node makes use of various queues for its operation. The more important ones are described below. Others are used
for maintenance and other minor purposes. for maintenance and other minor purposes.
:``p2p.inbound``: :``p2p.inbound.$identity``:
The node listens for messages sent from other peer nodes on this queue. Only clients who are authenticated to be The node listens for messages sent from other peer nodes on this queue. Only clients who are authenticated to be
nodes on the same network are given permission to send. Messages which are routed internally are also sent to this nodes on the same network are given permission to send. Messages which are routed internally are also sent to this
queue (e.g. two flows on the same node communicating with each other). queue (e.g. two flows on the same node communicating with each other).
@ -54,7 +54,7 @@ for maintenance and other minor purposes.
:``internal.peers.$identity``: :``internal.peers.$identity``:
These are a set of private queues only available to the node which it uses to route messages destined to other peers. These are a set of private queues only available to the node which it uses to route messages destined to other peers.
The queue name ends in the base 58 encoding of the peer's identity key. There is at most one queue per peer. The broker The queue name ends in the base 58 encoding of the peer's identity key. There is at most one queue per peer. The broker
creates a bridge from this queue to the peer's ``p2p.inbound`` queue, using the network map service to lookup the creates a bridge from this queue to the peer's ``p2p.inbound.$identity`` queue, using the network map service to lookup the
peer's network address. peer's network address.
:``internal.services.$identity``: :``internal.services.$identity``:
@ -86,7 +86,7 @@ Clients attempting to connect to the node's broker fall in one of four groups:
#. Anyone connecting with the username ``SystemUsers/Peer`` is treated as a peer on the same Corda network as the node. Their #. Anyone connecting with the username ``SystemUsers/Peer`` is treated as a peer on the same Corda network as the node. Their
TLS root CA must be the same as the node's root CA - the root CA is the doorman of the network and having the same root CA TLS root CA must be the same as the node's root CA - the root CA is the doorman of the network and having the same root CA
implies we've been let in by the same doorman. If they are part of the same network then they are only given permission implies we've been let in by the same doorman. If they are part of the same network then they are only given permission
to send to our ``p2p.inbound`` queue, otherwise they are rejected. to send to our ``p2p.inbound.$identity`` queue, otherwise they are rejected.
#. Every other username is treated as a RPC user and authenticated against the node's list of valid RPC users. If that #. Every other username is treated as a RPC user and authenticated against the node's list of valid RPC users. If that
is successful then they are only given sufficient permission to perform RPC, otherwise they are rejected. is successful then they are only given sufficient permission to perform RPC, otherwise they are rejected.

View File

@ -37,7 +37,7 @@ It's reproduced here as an example of both ways you can do this for a couple of
:end-before: END 7 :end-before: END 7
.. note:: Several of the core interfaces at the heart of Corda are already annotated and so any classes that implement .. note:: Several of the core interfaces at the heart of Corda are already annotated and so any classes that implement
them will automatically be whitelisted. This includes `Contract`, `ContractState` and `CommandData`. them will automatically be whitelisted. This includes ``Contract``, ``ContractState`` and ``CommandData``.
.. warning:: Java 8 Lambda expressions are not serializable except in flow checkpoints, and then not by default. The syntax to declare a serializable Lambda .. warning:: Java 8 Lambda expressions are not serializable except in flow checkpoints, and then not by default. The syntax to declare a serializable Lambda
expression that will work with Corda is ``Runnable r = (Runnable & Serializable) () -> System.out.println("Hello World");``, or expression that will work with Corda is ``Runnable r = (Runnable & Serializable) () -> System.out.println("Hello World");``, or
@ -52,62 +52,62 @@ AMQP
==== ====
Originally Corda used a ``Kryo``-based serialization scheme throughout for all serialization contexts. However, it was realised there Originally Corda used a ``Kryo``-based serialization scheme throughout for all serialization contexts. However, it was realised there
was a compelling use case for the definition and development of a custom format based upon AMQP 1.0. The primary drivers for this were was a compelling use case for the definition and development of a custom format based upon AMQP 1.0. The primary drivers for this were:
#. 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 alongside 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 data archived long ago (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
#. A desire to use a documented and static wire format that is platform independent, and is not subject to change with #. A desire to use a documented and static wire format that is platform independent, and is not subject to change with
3rd party library upgrades etc. 3rd party library upgrades, etc.
#. A desire to support open-ended polymorphism, where the number of subclasses of a superclass can expand over time #. A desire to support open-ended polymorphism, where the number of subclasses of a superclass can expand over time
and do not need to be defined in the schema *upfront*, which is key to many Corda concepts, such as contract states. and the subclasses do not need to be defined in the schema *upfront*. This is key to many Corda concepts, such as states.
#. Increased security from deserialized objects being constructed through supported constructors rather than having #. Increased security by constructing deserialized objects through supported constructors, rather than having
data poked directly into their fields without an opportunity to validate consistency or intercept attempts to manipulate data inserted directly into their fields without an opportunity to validate consistency or intercept attempts to manipulate
supposed invariants. supposed invariants
Delivering this is an ongoing effort by the Corda development team. At present, the ``Kryo``-based format is still used by the RPC framework on Delivering this is an ongoing effort by the Corda development team. At present, the ``Kryo``-based format is still used by the RPC framework on
both the client and server side. However, it is planned that this will move to the AMQP framework when ready. both the client and server side. However, it is planned that the RPC framework will move to the AMQP framework when ready.
The AMQP framework is currently used for: The AMQP framework is currently used for:
#. The peer to peer context, representing inter-node communication. #. The peer-to-peer context, representing inter-node communication
#. The persistence layer, representing contract states persisted into the vault. #. The persistence layer, representing contract states persisted into the vault
Finally, for the checkpointing of flows Corda will continue to use the existing ``Kryo`` scheme. Finally, for the checkpointing of flows, Corda will continue to use the existing ``Kryo`` scheme.
This separation of serialization schemes into different contexts allows us to use the most suitable framework for that context rather than This separation of serialization schemes into different contexts allows us to use the most suitable framework for that context rather than
attempting to force a one size fits all approach. Where ``Kryo`` is more suited to the serialization of a programs stack frames, being more flexible attempting to force a one-size-fits-all approach. ``Kryo`` is more suited to the serialization of a program's stack frames, as it is more flexible
than our AMQP framework in what it can construct and serialize, that flexibility makes it exceptionally difficult to make secure. Conversely than our AMQP framework in what it can construct and serialize. However, that flexibility makes it exceptionally difficult to make secure. Conversely,
our AMQP framework allows us to concentrate on a robust a secure framework that can be reasoned about thus made safer with far fewer unforeseen our AMQP framework allows us to concentrate on a secure framework that can be reasoned about and thus made safer, with far fewer
security holes. security holes.
.. note:: Selection of serialization context should, for the most part, be opaque to CorDapp developers, the Corda framework selecting .. note:: Selection of serialization context should, for the most part, be opaque to CorDapp developers, the Corda framework selecting
the correct context as confugred. the correct context as configured.
.. For information on our choice of AMQP 1.0, see :doc:`amqp-choice`. For detail on how we utilise AMQP 1.0 and represent .. note:: For information on our choice of AMQP 1.0, see :doc:`amqp-choice`. For detail on how we utilise AMQP 1.0 and represent
objects in AMQP types, see :doc:`amqp-format`. objects in AMQP types, see :doc:`amqp-format`.
We describe here what is and will be supported in the Corda AMQP format from the perspective This document describes what is currently and what will be supported in the Corda AMQP format from the perspective
of CorDapp developers, to allow for CorDapps to take into consideration the future state. The AMQP serialization format will of of CorDapp developers, to allow CorDapps to take into consideration the future state. The AMQP serialization format will
course continue to apply the whitelisting functionality that is already in place and described in :doc:`serialization`. continue to apply the whitelisting functionality that is already in place and described in :doc:`serialization`.
Core Types Core Types
---------- ----------
Here we describe the classes and interfaces that the AMQP serialization format will support. This section describes the classes and interfaces that the AMQP serialization format supports.
Collection Types Collection Types
```````````````` ````````````````
The following collection types are supported. Any implementation of the following will be mapped to *an* implementation of the interface or class on the other end. The following collection types are supported. Any implementation of the following will be mapped to *an* implementation of the interface or class on the other end.
e.g. If you, for example, use a Guava implementation of a collection it will deserialize as a different implementation, For example, if you use a Guava implementation of a collection, it will deserialize as the primitive collection type.
but will continue to adhere to the most specific of any of the following interfaces. You should use only these types
as the declared types of fields and properties, and not the concrete implementation types. Collections must be used The declared types of properties should only use these types, and not any concrete implementation types (e.g.
in their generic form, the generic type parameters will be included in the schema, and the elements type checked against the Guava implementations). Collections must specify their generic type, the generic type parameters will be included in
generic parameters when deserialized. the schema, and the element's type will be checked against the generic parameters when deserialized.
:: ::
@ -121,8 +121,8 @@ generic parameters when deserialized.
java.util.SortedMap java.util.SortedMap
java.util.NavigableMap java.util.NavigableMap
However, we will support the concrete implementation types below explicitly and also as the declared type of a field, as However, as a convenience, we explicitly support the concrete implementation types below, and they can be used as the
a convenience. declared types of properties.
:: ::
@ -151,12 +151,12 @@ All the primitive types are supported.
Arrays Arrays
`````` ``````
We also support arrays of any supported type, primitive or otherwise. Arrays of any type are supported, primitive or otherwise.
JDK Types JDK Types
````````` `````````
The following types are supported from the JDK libraries. The following JDK library types are supported:
:: ::
@ -200,10 +200,10 @@ The following types are supported from the JDK libraries.
java.util.Currency java.util.Currency
java.util.UUID java.util.UUID
Third Party Types Third-Party Types
````````````````` `````````````````
The following 3rd party types are supported. The following 3rd-party types are supported:
:: ::
@ -215,11 +215,11 @@ The following 3rd party types are supported.
Corda Types Corda Types
``````````` ```````````
Classes and interfaces in the Corda codebase annotated with ``@CordaSerializable`` are of course supported. Any classes and interfaces in the Corda codebase annotated with ``@CordaSerializable`` are supported.
All Corda exceptions that are expected to be serialized inherit from ``CordaThrowable`` via either ``CordaException``, for All Corda exceptions that are expected to be serialized inherit from ``CordaThrowable`` via either ``CordaException`` (for
checked exceptions, or ``CordaRuntimeException``, for unchecked exceptions. Any ``Throwable`` that is serialized but does checked exceptions) or ``CordaRuntimeException`` (for unchecked exceptions). Any ``Throwable`` that is serialized but does
not conform to ``CordaThrowable`` will be converted to a ``CordaRuntimeException`` with the original exception type not conform to ``CordaThrowable`` will be converted to a ``CordaRuntimeException``, with the original exception type
and other properties retained within it. and other properties retained within it.
.. _amqp_custom_types_ref: .. _amqp_custom_types_ref:
@ -227,7 +227,7 @@ and other properties retained within it.
Custom Types Custom Types
------------ ------------
Here are the rules to adhere to for support of your own types: You own types must adhere to the following rules to be supported:
Classes Classes
``````` ```````
@ -236,34 +236,40 @@ General Rules
''''''''''''' '''''''''''''
#. The class must be compiled with parameter names included in the ``.class`` file. This is the default in Kotlin #. The class must be compiled with parameter names included in the ``.class`` file. This is the default in Kotlin
but must be turned on in Java (``-parameters`` command line option to ``javac``). but must be turned on in Java using the ``-parameters`` command line option to ``javac``
#. The class is annotated with ``@CordaSerializable``.
#. The declared types of constructor arguments, getters, and setters must be supported, and where generics are used the .. note:: In circumstances where classes cannot be recompiled, such as when using a third-party library, a
proxy serializer can be used to avoid this problem. Details on creating such an object can be found on the
:doc:`cordapp-custom-serializers` page.
#. The class must be annotated with ``@CordaSerializable``
#. The declared types of constructor arguments, getters, and setters must be supported, and where generics are used, the
generic parameter must be a supported type, an open wildcard (``*``), or a bounded wildcard which is currently generic parameter must be a supported type, an open wildcard (``*``), or a bounded wildcard which is currently
widened to an open wildcard. widened to an open wildcard
#. Any superclass must adhere to the same rules, but can be abstract. #. Any superclass must adhere to the same rules, but can be abstract
#. Object graph cycles are not supported, so an object cannot refer to itself, directly or indirectly. #. Object graph cycles are not supported, so an object cannot refer to itself, directly or indirectly
Constructor Instantiation Constructor Instantiation
''''''''''''''''''''''''' '''''''''''''''''''''''''
The primary way the AMQP serialization framework for Corda instantiates objects is via a defined constructor. This is The primary way Corda's AMQP serialization framework instantiates objects is via a specified constructor. This is
used to first determine which properties of an object are to be serialised then, on deserialization, it is used to used to first determine which properties of an object are to be serialised, then, on deserialization, it is used to
instantiate the object with the serialized values. instantiate the object with the serialized values.
This is the recommended design idiom for serializable objects in Corda as it allows for immutable state objects to It is recommended that serializable objects in Corda adhere to the following rules, as they allow immutable state
be created objects to be deserialised:
#. A Java Bean getter for each of the properties in the constructor, with the names matching up. For example, for a constructor #. A Java Bean getter for each of the properties in the constructor, with a name of the form ``getX``. For example, for a constructor
parameter ``foo``, there must be a getter called ``getFoo()``. If the type of ``foo`` is boolean, the getter may parameter ``foo``, there must be a getter called ``getFoo()``. If ``foo`` is a boolean, the getter may
optionally be called ``isFoo()``. This is why the class must be compiled with parameter names turned on. optionally be called ``isFoo()`` (this is why the class must be compiled with parameter names turned on)
#. A constructor which takes all of the properties that you wish to record in the serialized form. This is required in #. A constructor which takes all of the properties that you wish to record in the serialized form. This is required in
order for the serialization framework to reconstruct an instance of your class. order for the serialization framework to reconstruct an instance of your class
#. If more than one constructor is provided, the serialization framework needs to know which one to use. The ``@ConstructorForDeserialization`` #. If more than one constructor is provided, the serialization framework needs to know which one to use. The ``@ConstructorForDeserialization``
annotation can be used to indicate which one. For a Kotlin class, without the ``@ConstructorForDeserialization`` annotation, the annotation can be used to indicate which one. For a Kotlin class, without the ``@ConstructorForDeserialization`` annotation, the
*primary constructor* will be selected. *primary constructor* will be selected
In Kotlin, this maps cleanly to a data class where there getters are synthesized automatically. For example, In Kotlin, this maps cleanly to a data class where there getters are synthesized automatically. For example, suppose we
have the following data class:
.. container:: codeset .. container:: codeset
@ -271,9 +277,10 @@ In Kotlin, this maps cleanly to a data class where there getters are synthesized
data class Example (val a: Int, val b: String) data class Example (val a: Int, val b: String)
Both properties a and b will be included in the serialised form. However, as stated above, properties not mentioned in Properties ``a`` and ``b`` will be included in the serialised form.
the constructor will not be serialised. For example, in the following code property c will not be considered part of the
serialised form However, properties not mentioned in the constructor will not be serialised. For example, in the following code,
property ``c`` will not be considered part of the serialised form:
.. container:: codeset .. container:: codeset
@ -291,14 +298,14 @@ serialised form
Setter Instantiation Setter Instantiation
'''''''''''''''''''' ''''''''''''''''''''
As an alternative to constructor based initialisation Corda can also determine the important elements of an As an alternative to constructor-based initialisation, Corda can also determine the important elements of an
object by inspecting the getter and setter methods present on a class. If a class has **only** a default object by inspecting the getter and setter methods present on the class. If a class has **only** a default
constructor **and** properties then the serializable properties will be determined by the presence of constructor **and** properties then the serializable properties will be determined by the presence of
both a getter and setter for that property that are both publicly visible. I.e. the class adheres to both a getter and setter for that property that are both publicly visible (i.e. the class adheres to
the classic *idiom* of mutable JavaBeans. the classic *idiom* of mutable JavaBeans).
On deserialization, a default instance will first be created and then, in turn, the setters invoked On deserialization, a default instance will first be created, and then the setters will be invoked on that object to
on that object to populate the correct values. populate it with the correct values.
For example: For example:
@ -324,7 +331,7 @@ Inaccessible Private Properties
``````````````````````````````` ```````````````````````````````
Whilst the Corda AMQP serialization framework supports private object properties without publicly Whilst the Corda AMQP serialization framework supports private object properties without publicly
accessible getter methods this development idiom is strongly discouraged. accessible getter methods, this development idiom is strongly discouraged.
For example. For example.
@ -350,15 +357,14 @@ For example.
} }
} }
When designing stateful objects is should be remembered that they are not, despite appearances, traditional When designing stateful objects, is should be remembered that they are not, despite appearances, traditional
programmatic constructs. They are signed over, transformed, serialised, and relationally mapped. As such, programmatic constructs. They are signed over, transformed, serialised, and relationally mapped. As such,
all elements should be publicly accessible by design all elements should be publicly accessible by design.
.. warning:: IDEs will indiciate erroneously that properties can be given something other than public .. warning:: IDEs will indicate erroneously that properties can be given something other than public visibility. Ignore
visibility. Ignore this as whilst it will work, as discussed above there are many reasons why this isn't this, as whilst it will work, as discussed above there are many reasons why this isn't a good idea.
a good idea and those are beyond the scope of the IDEs inference rules
Providing a public getter, as per the following example, is acceptable Providing a public getter, as per the following example, is acceptable:
.. container:: codeset .. container:: codeset
@ -392,7 +398,7 @@ Providing a public getter, as per the following example, is acceptable
Enums Enums
````` `````
#. All enums are supported, provided they are annotated with ``@CordaSerializable``. #. All enums are supported, provided they are annotated with ``@CordaSerializable``
Exceptions Exceptions
@ -401,24 +407,25 @@ Exceptions
The following rules apply to supported ``Throwable`` implementations. The following rules apply to supported ``Throwable`` implementations.
#. If you wish for your exception to be serializable and transported type safely it should inherit from either #. If you wish for your exception to be serializable and transported type safely it should inherit from either
``CordaException`` or ``CordaRuntimeException``. ``CordaException`` or ``CordaRuntimeException``
#. If not, the ``Throwable`` will deserialize to a ``CordaRuntimeException`` with the details of the original #. If not, the ``Throwable`` will deserialize to a ``CordaRuntimeException`` with the details of the original
``Throwable`` contained within it, including the class name of the original ``Throwable``. ``Throwable`` contained within it, including the class name of the original ``Throwable``
Kotlin Objects Kotlin Objects
`````````````` ``````````````
#. Kotlin ``object`` s are singletons and treated differently. They are recorded into the stream with no properties #. Kotlin's non-anonymous ``object`` s (i.e. constructs like ``object foo : Contract {...}``) are singletons and
and deserialize back to the singleton instance. Currently, the same is not true of Java singletons, treated differently. They are recorded into the stream with no properties, and deserialize back to the
and they will deserialize to new instances of the class. singleton instance. Currently, the same is not true of Java singletons, which will deserialize to new instances
#. Kotlin's anonymous ``object`` s are not currently supported. I.e. constructs like: of the class
``object : Contract {...}`` will not serialize correctly and need to be re-written as an explicit class declaration. #. Kotlin's anonymous ``object`` s (i.e. constructs like ``object : Contract {...}``) are not currently supported
and will not serialize correctly. They need to be re-written as an explicit class declaration
The Carpenter The Carpenter
````````````` `````````````
We will support a class carpenter that can dynamically manufacture classes from the supplied schema when deserializing We support a class carpenter that can dynamically manufacture classes from the supplied schema when deserializing,
in the JVM without the supporting classes on the classpath. This can be useful where other components might expect to without the supporting classes being present on the classpath. This can be useful where other components might expect to
be able to use reflection over the deserialized data, and also for ensuring classes not on the classpath can be be able to use reflection over the deserialized data, and also for ensuring classes not on the classpath can be
deserialized without loading potentially malicious code dynamically without security review outside of a fully sandboxed deserialized without loading potentially malicious code dynamically without security review outside of a fully sandboxed
environment. A more detailed discussion of the carpenter will be provided in a future update to the documentation. environment. A more detailed discussion of the carpenter will be provided in a future update to the documentation.
@ -427,25 +434,25 @@ Future Enhancements
``````````````````` ```````````````````
#. Java singleton support. We will add support for identifying classes which are singletons and identifying the #. Java singleton support. We will add support for identifying classes which are singletons and identifying the
static method responsible for returning the singleton instance. static method responsible for returning the singleton instance
#. Instance internalizing support. We will add support for identifying classes that should be resolved against an instances map to avoid #. Instance internalizing support. We will add support for identifying classes that should be resolved against an instances map to avoid
creating many duplicate instances that are equal. Similar to ``String.intern()``. creating many duplicate instances that are equal (similar to ``String.intern()``)
.. Type Evolution: .. Type Evolution:
Type Evolution Type Evolution
-------------- --------------
Type evolution is the mechanisms by which classes can be altered over time yet still remain serializable and deserializable across Type evolution is the mechanism by which classes can be altered over time yet still remain serializable and deserializable across
all versions of the class. This ensures an object serialized with an older idea of what the class "looked like" can be deserialized all versions of the class. This ensures an object serialized with an older idea of what the class "looked like" can be deserialized
and a version of the current state of the class instantiated. and a version of the current state of the class instantiated.
More detail can be found in :doc:`serialization-default-evolution` More detail can be found in :doc:`serialization-default-evolution`.
Enum Evolution Enum Evolution
`````````````` ``````````````
Corda supports interoperability of enumerated type versions. This allows such types to be changed over time without breaking Corda supports interoperability of enumerated type versions. This allows such types to be changed over time without breaking
backward (or forward) compatibility. The rules and mechanisms for doing this are discussed in :doc:`serialization-enum-evolution`` backward (or forward) compatibility. The rules and mechanisms for doing this are discussed in :doc:`serialization-enum-evolution``.

View File

@ -1,3 +1,9 @@
.. highlight:: kotlin
.. raw:: html
<script type="text/javascript" src="_static/jquery.js"></script>
<script type="text/javascript" src="_static/codesets.js"></script>
Upgrading a CorDapp (outside of platform version upgrades) Upgrading a CorDapp (outside of platform version upgrades)
========================================================== ==========================================================
@ -127,17 +133,39 @@ The ``InitiatingFlow`` version number is included in the flow session handshake
the flow running on the other side. In particular, it has a ``flowVersion`` property which can be used to the flow running on the other side. In particular, it has a ``flowVersion`` property which can be used to
programmatically evolve flows across versions. For example: programmatically evolve flows across versions. For example:
.. sourcecode:: kotlin .. container:: codeset
@Suspendable .. sourcecode:: kotlin
override fun call() {
val otherFlowVersion = otherSession.getCounterpartyFlowInfo().flowVersion @Suspendable
val receivedString = if (otherFlowVersion == 1) { override fun call() {
receive<Int>(otherParty).unwrap { it.toString() } val otherFlowVersion = otherSession.getCounterpartyFlowInfo().flowVersion
} else { val receivedString = if (otherFlowVersion == 1) {
receive<String>(otherParty).unwrap { it } otherSession.receive<Int>().unwrap { it.toString() }
} else {
otherSession.receive<String>().unwrap { it }
}
}
.. sourcecode:: java
@Suspendable
@Override public Void call() throws FlowException {
int otherFlowVersion = otherSession.getCounterpartyFlowInfo().getFlowVersion();
String receivedString;
if (otherFlowVersion == 1) {
receivedString = otherSession.receive(Integer.class).unwrap(integer -> {
return integer.toString();
});
} else {
receivedString = otherSession.receive(String.class).unwrap(string -> {
return string;
});
}
return null;
} }
}
This code shows a flow that in its first version expected to receive an Int, but in subsequent versions was modified to This code shows a flow that in its first version expected to receive an Int, but in subsequent versions was modified to
expect a String. This flow is still able to communicate with parties that are running the older CorDapp containing expect a String. This flow is still able to communicate with parties that are running the older CorDapp containing
@ -147,30 +175,73 @@ How do I deal with interface changes to inlined subflows?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Here is an example of an in-lined subflow: Here is an example of an in-lined subflow:
.. sourcecode:: kotlin .. container:: codeset
@StartableByRPC .. sourcecode:: kotlin
@InitiatingFlow
class FlowA(val recipient: Party) : FlowLogic<Unit>() { @StartableByRPC
@Suspendable @InitiatingFlow
override fun call() { class FlowA(val recipient: Party) : FlowLogic<Unit>() {
subFlow(FlowB(recipient)) @Suspendable
override fun call() {
subFlow(FlowB(recipient))
}
} }
}
@InitiatedBy(FlowA::class) @InitiatedBy(FlowA::class)
class FlowC(val otherSession: FlowSession) : FlowLogic() { class FlowC(val otherSession: FlowSession) : FlowLogic() {
// Omitted. // Omitted.
} }
// Note: No annotations. This is used as an inlined subflow. // Note: No annotations. This is used as an inlined subflow.
class FlowB(val recipient: Party) : FlowLogic<Unit>() { class FlowB(val recipient: Party) : FlowLogic<Unit>() {
@Suspendable @Suspendable
override fun call() { override fun call() {
val message = "I'm an inlined subflow, so I inherit the @InitiatingFlow's session ID and type." val message = "I'm an inlined subflow, so I inherit the @InitiatingFlow's session ID and type."
initiateFlow(recipient).send(message) initiateFlow(recipient).send(message)
}
}
.. sourcecode:: java
@StartableByRPC
@InitiatingFlow
class FlowA extends FlowLogic<Void> {
private final Party recipient;
public FlowA(Party recipient) {
this.recipient = recipient;
}
@Suspendable
@Override public Void call() throws FlowException {
subFlow(new FlowB(recipient));
return null;
}
}
@InitiatedBy(FlowA.class)
class FlowC extends FlowLogic<Void> {
// Omitted.
}
// Note: No annotations. This is used as an inlined subflow.
class FlowB extends FlowLogic<Void> {
private final Party recipient;
public FlowB(Party recipient) {
this.recipient = recipient;
}
@Suspendable
@Override public Void call() {
String message = "I'm an inlined subflow, so I inherit the @InitiatingFlow's session ID and type.";
initiateFlow(recipient).send(message);
return null;
}
} }
}
Inlined subflows are treated as being the flow that invoked them when initiating a new flow session with a counterparty. Inlined subflows are treated as being the flow that invoked them when initiating a new flow session with a counterparty.
Suppose flow ``A`` calls inlined subflow B, which, in turn, initiates a session with a counterparty. The ``FlowLogic`` Suppose flow ``A`` calls inlined subflow B, which, in turn, initiates a session with a counterparty. The ``FlowLogic``
@ -361,34 +432,138 @@ for details.
For backwards compatible changes such as adding columns, the procedure for upgrading a state schema is to extend the For backwards compatible changes such as adding columns, the procedure for upgrading a state schema is to extend the
existing object relational mapper. For example, we can update: existing object relational mapper. For example, we can update:
.. sourcecode:: kotlin .. container:: codeset
object ObligationSchemaV1 : MappedSchema(Obligation::class.java, 1, listOf(ObligationEntity::class.java)) { .. sourcecode:: kotlin
@Entity @Table(name = "obligations")
class ObligationEntity(obligation: Obligation) : PersistentState() { object ObligationSchemaV1 : MappedSchema(Obligation::class.java, 1, listOf(ObligationEntity::class.java)) {
@Column var currency: String = obligation.amount.token.toString() @Entity @Table(name = "obligations")
@Column var amount: Long = obligation.amount.quantity class ObligationEntity(obligation: Obligation) : PersistentState() {
@Column @Lob var lender: ByteArray = obligation.lender.owningKey.encoded @Column var currency: String = obligation.amount.token.toString()
@Column @Lob var borrower: ByteArray = obligation.borrower.owningKey.encoded @Column var amount: Long = obligation.amount.quantity
@Column var linear_id: String = obligation.linearId.id.toString() @Column @Lob var lender: ByteArray = obligation.lender.owningKey.encoded
@Column @Lob var borrower: ByteArray = obligation.borrower.owningKey.encoded
@Column var linear_id: String = obligation.linearId.id.toString()
}
}
.. sourcecode:: java
public class ObligationSchemaV1 extends MappedSchema {
public IOUSchemaV1() {
super(Obligation.class, 1, ImmutableList.of(ObligationEntity.class));
}
@Entity
@Table(name = "obligations")
public static class ObligationEntity extends PersistentState {
@Column(name = "currency") private final String currency;
@Column(name = "amount") private final Long amount;
@Column(name = "lender") @Lob private final Byte[] lender;
@Column(name = "borrower") @Lob private final Byte[] borrower;
@Column(name = "linear_id") private final UUID linearId;
public ObligationEntity(String currency, Long amount, Byte[] lender, Byte[] borrower, UUID linearId) {
this.currency = currency;
this.amount = amount;
this.lender = lender;
this.borrower = borrower;
this.linearId = linearId;
}
public String getCurrency() {
return currency;
}
public Long getAmount() {
return amount;
}
public ByteArray getLender() {
return lender;
}
public ByteArray getBorrower() {
return borrower;
}
public UUID getId() {
return linearId;
}
}
} }
}
To: To:
.. sourcecode:: kotlin .. container:: codeset
object ObligationSchemaV1 : MappedSchema(Obligation::class.java, 1, listOf(ObligationEntity::class.java)) { .. sourcecode:: kotlin
@Entity @Table(name = "obligations")
class ObligationEntity(obligation: Obligation) : PersistentState() { object ObligationSchemaV1 : MappedSchema(Obligation::class.java, 1, listOf(ObligationEntity::class.java)) {
@Column var currency: String = obligation.amount.token.toString() @Entity @Table(name = "obligations")
@Column var amount: Long = obligation.amount.quantity class ObligationEntity(obligation: Obligation) : PersistentState() {
@Column @Lob var lender: ByteArray = obligation.lender.owningKey.encoded @Column var currency: String = obligation.amount.token.toString()
@Column @Lob var borrower: ByteArray = obligation.borrower.owningKey.encoded @Column var amount: Long = obligation.amount.quantity
@Column var linear_id: String = obligation.linearId.id.toString() @Column @Lob var lender: ByteArray = obligation.lender.owningKey.encoded
@Column var defaulted: Bool = obligation.amount.inDefault // NEW COLUNM! @Column @Lob var borrower: ByteArray = obligation.borrower.owningKey.encoded
@Column var linear_id: String = obligation.linearId.id.toString()
@Column var defaulted: Bool = obligation.amount.inDefault // NEW COLUMN!
}
}
.. sourcecode:: java
public class ObligationSchemaV1 extends MappedSchema {
public IOUSchemaV1() {
super(Obligation.class, 1, ImmutableList.of(ObligationEntity.class));
}
@Entity
@Table(name = "obligations")
public static class ObligationEntity extends PersistentState {
@Column(name = "currency") private final String currency;
@Column(name = "amount") private final Long amount;
@Column(name = "lender") @Lob private final Byte[] lender;
@Column(name = "borrower") @Lob private final Byte[] borrower;
@Column(name = "linear_id") private final UUID linearId;
@Column(name = "defaulted") private final Boolean defaulted; // NEW COLUMN!
public ObligationEntity(String currency, Long amount, Byte[] lender, Byte[] borrower, UUID linearId, Boolean defaulted) {
this.currency = currency;
this.amount = amount;
this.lender = lender;
this.borrower = borrower;
this.linearId = linearId;
this.defaulted = defaulted;
}
public String getCurrency() {
return currency;
}
public Long getAmount() {
return amount;
}
public ByteArray getLender() {
return lender;
}
public ByteArray getBorrower() {
return borrower;
}
public UUID getId() {
return linearId;
}
public Boolean isDefaulted() {
return defaulted;
}
}
} }
}
Thus adding a new column with a default value. Thus adding a new column with a default value.
@ -397,21 +572,43 @@ used, as changes to the state are required. To make a backwards-incompatible cha
because a property was removed from a state object), the procedure is to define another object relational mapper, then because a property was removed from a state object), the procedure is to define another object relational mapper, then
add it to the ``supportedSchemas`` property of your ``QueryableState``, like so: add it to the ``supportedSchemas`` property of your ``QueryableState``, like so:
.. sourcecode:: kotlin .. container:: codeset
override fun supportedSchemas(): Iterable<MappedSchema> = listOf(ExampleSchemaV1, ExampleSchemaV2) .. sourcecode:: kotlin
override fun supportedSchemas(): Iterable<MappedSchema> = listOf(ExampleSchemaV1, ExampleSchemaV2)
.. sourcecode:: java
@Override public Iterable<MappedSchema> supportedSchemas() {
return ImmutableList.of(new ExampleSchemaV1(), new ExampleSchemaV2());
}
Then, in ``generateMappedObject``, add support for the new schema: Then, in ``generateMappedObject``, add support for the new schema:
.. sourcecode:: kotlin .. container:: codeset
override fun generateMappedObject(schema: MappedSchema): PersistentState { .. sourcecode:: kotlin
return when (schema) {
is DummyLinearStateSchemaV1 -> // Omitted. override fun generateMappedObject(schema: MappedSchema): PersistentState {
is DummyLinearStateSchemaV2 -> // Omitted. return when (schema) {
else -> throw IllegalArgumentException("Unrecognised schema $schema") is DummyLinearStateSchemaV1 -> // Omitted.
is DummyLinearStateSchemaV2 -> // Omitted.
else -> throw IllegalArgumentException("Unrecognised schema $schema")
}
}
.. sourcecode:: java
@Override public PersistentState generateMappedObject(MappedSchema schema) {
if (schema instanceof DummyLinearStateSchemaV1) {
// Omitted.
} else if (schema instanceof DummyLinearStateSchemaV2) {
// Omitted.
} else {
throw new IllegalArgumentException("Unrecognised schema $schema");
}
} }
}
With this approach, whenever the state object is stored in the vault, a representation of it will be stored in two With this approach, whenever the state object is stored in the vault, a representation of it will be stored in two
separate database tables where possible - one for each supported schema. separate database tables where possible - one for each supported schema.

View File

@ -2,7 +2,7 @@ package net.corda.flowhook
import co.paralleluniverse.fibers.Fiber import co.paralleluniverse.fibers.Fiber
import net.corda.node.services.statemachine.Event import net.corda.node.services.statemachine.Event
import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager import net.corda.nodeapi.internal.persistence.contextTransactionOrNull
import java.sql.Connection import java.sql.Connection
@Suppress("UNUSED") @Suppress("UNUSED")
@ -156,7 +156,7 @@ object FlowHookContainer {
private fun currentTransactionOrThread(): Any { private fun currentTransactionOrThread(): Any {
return try { return try {
DatabaseTransactionManager.currentOrNull() contextTransactionOrNull
} catch (exception: IllegalStateException) { } catch (exception: IllegalStateException) {
null null
} ?: Thread.currentThread() } ?: Thread.currentThread()

View File

@ -65,13 +65,15 @@ The doorman service can use JIRA to manage the certificate signing request appro
projectCode = "TD" projectCode = "TD"
username = "username" username = "username"
password = "password" password = "password"
doneTransitionCode = 41
} }
. .
. .
. .
} }
``` ```
#### JIRA project configuration
* The JIRA project should setup as "Business Project" with "Task" workflow.
* Custom text field input "Request ID", and "Reject Reason" should be created in JIRA, doorman will exit with error without these custom fields.
### Auto approval ### Auto approval
When `approveAll` is set to `true`, the doorman will approve all requests on receive. (*This should only be enabled in a test environment) When `approveAll` is set to `true`, the doorman will approve all requests on receive. (*This should only be enabled in a test environment)
@ -118,7 +120,6 @@ doormanConfig {
projectCode = "TD" projectCode = "TD"
username = "username" username = "username"
password = "password" password = "password"
doneTransitionCode = 41
} }
} }

View File

@ -88,7 +88,7 @@ dependencies {
testCompile "com.nhaarman:mockito-kotlin:0.6.1" testCompile "com.nhaarman:mockito-kotlin:0.6.1"
testCompile "com.spotify:docker-client:8.9.1" testCompile "com.spotify:docker-client:8.9.1"
compile('com.atlassian.jira:jira-rest-java-client-core:4.0.0') { compile('com.atlassian.jira:jira-rest-java-client-core:5.0.4') {
// The jira client includes jersey-core 1.5 which breaks everything. // The jira client includes jersey-core 1.5 which breaks everything.
exclude module: 'jersey-core' exclude module: 'jersey-core'
} }

View File

@ -23,7 +23,6 @@ doormanConfig{
projectCode = "TD" projectCode = "TD"
username = "username" username = "username"
password = "password" password = "password"
doneTransitionCode = 41
} }
} }

View File

@ -1,6 +1,7 @@
package com.r3.corda.networkmanage.doorman package com.r3.corda.networkmanage.doorman
import com.r3.corda.networkmanage.common.persistence.configureDatabase import com.r3.corda.networkmanage.common.persistence.configureDatabase
import com.r3.corda.networkmanage.common.utils.CertPathAndKey
import com.r3.corda.networkmanage.doorman.signer.LocalSigner import com.r3.corda.networkmanage.doorman.signer.LocalSigner
import net.corda.cordform.CordformNode import net.corda.cordform.CordformNode
import net.corda.core.crypto.random63BitValue import net.corda.core.crypto.random63BitValue
@ -141,11 +142,11 @@ class NodeRegistrationTest : IntegrationTest() {
start( start(
serverAddress, serverAddress,
configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(runMigration = true)), configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(runMigration = true)),
LocalSigner(csrCa.keyPair, arrayOf(csrCa.certificate, rootCaCert)), CertPathAndKey(listOf(csrCa.certificate, rootCaCert), csrCa.keyPair.private),
DoormanConfig(approveAll = true, jiraConfig = null, approveInterval = timeoutMillis), DoormanConfig(approveAll = true, jiraConfig = null, approveInterval = timeoutMillis),
networkParameters?.let { networkParameters?.let {
NetworkMapStartParams( NetworkMapStartParams(
LocalSigner(networkMapCa.keyPair, arrayOf(networkMapCa.certificate, rootCaCert)), LocalSigner(networkMapCa),
networkParameters, networkParameters,
NetworkMapConfig(cacheTimeout = timeoutMillis, signInterval = timeoutMillis) NetworkMapConfig(cacheTimeout = timeoutMillis, signInterval = timeoutMillis)
) )

View File

@ -12,6 +12,7 @@ import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder import org.junit.rules.TemporaryFolder
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.test.assertTrue import kotlin.test.assertTrue
class HsmTest { class HsmTest {
@ -19,7 +20,12 @@ class HsmTest {
@Rule @Rule
@JvmField @JvmField
val hsmSimulator: HsmSimulator = HsmSimulator() val hsmSimulator: HsmSimulator = HsmSimulator()
val testParameters = Parameters(
@Rule
@JvmField
val tempFolder = TemporaryFolder()
private val testParameters = Parameters(
dataSourceProperties = mock(), dataSourceProperties = mock(),
device = "${hsmSimulator.port}@${hsmSimulator.host}", device = "${hsmSimulator.port}@${hsmSimulator.host}",
keySpecifier = 1, keySpecifier = 1,
@ -30,10 +36,6 @@ class HsmTest {
validDays = 3650 validDays = 3650
) )
@Rule
@JvmField
val tempFolder = TemporaryFolder()
private lateinit var inputReader: InputReader private lateinit var inputReader: InputReader
@Before @Before
@ -47,14 +49,12 @@ class HsmTest {
fun `Authenticator executes the block once user is successfully authenticated`() { fun `Authenticator executes the block once user is successfully authenticated`() {
// given // given
val authenticator = Authenticator(testParameters.createProvider(), inputReader = inputReader) val authenticator = Authenticator(testParameters.createProvider(), inputReader = inputReader)
var executed = false val executed = AtomicBoolean(false)
// when // when
authenticator.connectAndAuthenticate({ provider, signers -> authenticator.connectAndAuthenticate { _, _ -> executed.set(true) }
executed = true
})
// then // then
assertTrue(executed) assertTrue(executed.get())
} }
} }

View File

@ -90,7 +90,12 @@ 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, doormanServiceParameter = DoormanConfig(approveAll = true, approveInterval = 2.seconds.toMillis(), jiraConfig = null), startNetworkMap = null) server.start(
hostAndPort = NetworkHostAndPort(HOST, 0),
database = database,
csrCertPathAndKey = null,
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 {

View File

@ -1,12 +1,14 @@
package com.r3.corda.networkmanage.common.persistence package com.r3.corda.networkmanage.common.persistence
import net.corda.core.crypto.SecureHash
import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.PKCS10CertificationRequest
import java.security.cert.CertPath import java.security.cert.CertPath
data class CertificateData(val publicKeyHash: String, val certStatus: CertificateStatus, val certPath: CertPath) data class CertificateData(val certStatus: CertificateStatus, val certPath: CertPath)
data class CertificateSigningRequest(val requestId: String, data class CertificateSigningRequest(val requestId: String,
val legalName: String, val legalName: String,
val publicKeyHash: SecureHash,
val status: RequestStatus, val status: RequestStatus,
val request: PKCS10CertificationRequest, val request: PKCS10CertificationRequest,
val remark: String?, val remark: String?,
@ -59,7 +61,7 @@ interface CertificationRequestStorage {
* @param rejectedBy authority (its identifier) rejecting this request. * @param rejectedBy authority (its identifier) rejecting this request.
* @param rejectReason brief description of the rejection reason * @param rejectReason brief description of the rejection reason
*/ */
fun rejectRequest(requestId: String, rejectedBy: String, rejectReason: String) fun rejectRequest(requestId: String, rejectedBy: String, rejectReason: String?)
/** /**
* Store certificate path with [requestId], this will store the encoded [CertPath] and transit request status to [RequestStatus.SIGNED]. * Store certificate path with [requestId], this will store the encoded [CertPath] and transit request status to [RequestStatus.SIGNED].

View File

@ -39,10 +39,9 @@ interface NetworkMapStorage {
fun getSignedNetworkParameters(hash: SecureHash): SignedNetworkParameters? fun getSignedNetworkParameters(hash: SecureHash): SignedNetworkParameters?
/** /**
* Retrieve network map parameters. * Retrieve the network parameters of the current network map, or null if there's no network map.
* @return signed current network map parameters or null if they don't exist
*/ */
fun getCurrentSignedNetworkParameters(): SignedNetworkParameters? fun getNetworkParametersOfNetworkMap(): SignedNetworkParameters?
/** /**
* Persists given network parameters with signature if provided. * Persists given network parameters with signature if provided.
@ -55,5 +54,5 @@ interface NetworkMapStorage {
* Note that they may not have been signed up yet. * Note that they may not have been signed up yet.
* @return latest network parameters * @return latest network parameters
*/ */
fun getLatestUnsignedNetworkParameters(): NetworkParameters fun getLatestNetworkParameters(): NetworkParameters?
} }

View File

@ -3,6 +3,7 @@ package com.r3.corda.networkmanage.common.persistence
import com.r3.corda.networkmanage.common.persistence.entity.CertificateDataEntity import com.r3.corda.networkmanage.common.persistence.entity.CertificateDataEntity
import com.r3.corda.networkmanage.common.persistence.entity.CertificateSigningRequestEntity import com.r3.corda.networkmanage.common.persistence.entity.CertificateSigningRequestEntity
import com.r3.corda.networkmanage.common.utils.hashString import com.r3.corda.networkmanage.common.utils.hashString
import net.corda.core.crypto.Crypto.toSupportedPublicKey
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
@ -26,14 +27,12 @@ class PersistentCertificateRequestStorage(private val database: CordaPersistence
builder.and(requestIdEq, statusEq) builder.and(requestIdEq, statusEq)
} }
request ?: throw IllegalArgumentException("Cannot retrieve 'APPROVED' certificate signing request for request id: $requestId") request ?: throw IllegalArgumentException("Cannot retrieve 'APPROVED' certificate signing request for request id: $requestId")
val publicKeyHash = certificates.certificates.first().publicKey.hashString()
val certificateSigningRequest = request.copy( val certificateSigningRequest = request.copy(
modifiedBy = signedBy, modifiedBy = signedBy,
modifiedAt = Instant.now(), modifiedAt = Instant.now(),
status = RequestStatus.SIGNED) status = RequestStatus.SIGNED)
session.merge(certificateSigningRequest) session.merge(certificateSigningRequest)
val certificateDataEntity = CertificateDataEntity( val certificateDataEntity = CertificateDataEntity(
publicKeyHash = publicKeyHash,
certificateStatus = CertificateStatus.VALID, certificateStatus = CertificateStatus.VALID,
certificatePathBytes = certificates.encoded, certificatePathBytes = certificates.encoded,
certificateSigningRequest = certificateSigningRequest) certificateSigningRequest = certificateSigningRequest)
@ -48,6 +47,7 @@ class PersistentCertificateRequestStorage(private val database: CordaPersistence
session.save(CertificateSigningRequestEntity( session.save(CertificateSigningRequestEntity(
requestId = requestId, requestId = requestId,
legalName = legalName, legalName = legalName,
publicKeyHash = toSupportedPublicKey(request.subjectPublicKeyInfo).hashString(),
requestBytes = request.encoded, requestBytes = request.encoded,
remark = rejectReason, remark = rejectReason,
modifiedBy = emptyList(), modifiedBy = emptyList(),
@ -93,7 +93,7 @@ class PersistentCertificateRequestStorage(private val database: CordaPersistence
} }
} }
override fun rejectRequest(requestId: String, rejectedBy: String, rejectReason: String) { override fun rejectRequest(requestId: String, rejectedBy: String, rejectReason: String?) {
database.transaction(TransactionIsolationLevel.SERIALIZABLE) { database.transaction(TransactionIsolationLevel.SERIALIZABLE) {
val request = findRequest(requestId) val request = findRequest(requestId)
request ?: throw IllegalArgumentException("Error when rejecting request with id: $requestId. Request does not exist.") request ?: throw IllegalArgumentException("Error when rejecting request with id: $requestId. Request does not exist.")
@ -134,7 +134,7 @@ class PersistentCertificateRequestStorage(private val database: CordaPersistence
return Pair(request.subject.toString(), "Name validation failed: ${e.message}") return Pair(request.subject.toString(), "Name validation failed: ${e.message}")
} }
val query = session.criteriaBuilder.run { val duplicateNameQuery = session.criteriaBuilder.run {
val criteriaQuery = createQuery(CertificateSigningRequestEntity::class.java) val criteriaQuery = createQuery(CertificateSigningRequestEntity::class.java)
criteriaQuery.from(CertificateSigningRequestEntity::class.java).run { criteriaQuery.from(CertificateSigningRequestEntity::class.java).run {
criteriaQuery.where(equal(get<String>(CertificateSigningRequestEntity::legalName.name), legalName)) criteriaQuery.where(equal(get<String>(CertificateSigningRequestEntity::legalName.name), legalName))
@ -144,10 +144,27 @@ class PersistentCertificateRequestStorage(private val database: CordaPersistence
// TODO consider scenario: There is a CSR that is signed but the certificate itself has expired or was revoked // TODO consider scenario: There is a CSR that is signed but the certificate itself has expired or was revoked
// Also, at the moment we assume that once the CSR is approved it cannot be rejected. // Also, at the moment we assume that once the CSR is approved it cannot be rejected.
// What if we approved something by mistake. // What if we approved something by mistake.
val duplicates = session.createQuery(query).resultList.filter { val nameDuplicates = session.createQuery(duplicateNameQuery).resultList.filter {
it.status != RequestStatus.REJECTED it.status != RequestStatus.REJECTED
} }
return Pair(legalName, if (duplicates.isEmpty()) null else "Duplicate legal name") if (nameDuplicates.isNotEmpty()) {
return Pair(legalName, "Duplicate legal name")
}
val publicKey = toSupportedPublicKey(request.subjectPublicKeyInfo).hashString()
val duplicatePkQuery = session.criteriaBuilder.run {
val criteriaQuery = createQuery(CertificateSigningRequestEntity::class.java)
criteriaQuery.from(CertificateSigningRequestEntity::class.java).run {
criteriaQuery.where(equal(get<String>(CertificateSigningRequestEntity::publicKeyHash.name), publicKey))
}
}
//TODO Consider following scenario: There is a CSR that is signed but the certificate itself has expired or was revoked
val pkDuplicates = session.createQuery(duplicatePkQuery).resultList.filter {
it.status != RequestStatus.REJECTED
}
return Pair(legalName, if (pkDuplicates.isEmpty()) null else "Duplicate public key")
} }
} }

View File

@ -6,10 +6,7 @@ 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.sha256 import net.corda.core.crypto.sha256
import net.corda.core.internal.DigitalSignatureWithCert import net.corda.core.internal.DigitalSignatureWithCert
import net.corda.core.serialization.SerializedBytes
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.NetworkParameters import net.corda.nodeapi.internal.network.NetworkParameters
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
@ -19,18 +16,16 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence
class PersistentNetworkMapStorage(private val database: CordaPersistence) : NetworkMapStorage { class PersistentNetworkMapStorage(private val database: CordaPersistence) : NetworkMapStorage {
override fun getCurrentNetworkMap(): SignedNetworkMap? { override fun getCurrentNetworkMap(): SignedNetworkMap? {
return database.transaction { return database.transaction {
getCurrentNetworkMapEntity()?.let { getCurrentNetworkMapEntity()?.toSignedNetworkMap()
val signatureAndCertPath = it.signatureAndCertificate()
SignedNetworkMap(SerializedBytes(it.networkMap), signatureAndCertPath)
}
} }
} }
override fun getCurrentSignedNetworkParameters(): SignedNetworkParameters? { override fun getNetworkParametersOfNetworkMap(): SignedNetworkParameters? {
return database.transaction { return database.transaction {
getCurrentNetworkMapEntity()?.let { getCurrentNetworkMapEntity()?.let {
val netParamsHash = it.networkMap.deserialize<NetworkMap>().networkParameterHash val netParamsHash = it.toNetworkMap().networkParameterHash
getSignedNetworkParameters(netParamsHash) getSignedNetworkParameters(netParamsHash) ?:
throw IllegalStateException("Current network map is pointing to network parameters that do not exist: $netParamsHash")
} }
} }
} }
@ -47,7 +42,9 @@ class PersistentNetworkMapStorage(private val database: CordaPersistence) : Netw
} }
override fun getSignedNetworkParameters(hash: SecureHash): SignedNetworkParameters? { override fun getSignedNetworkParameters(hash: SecureHash): SignedNetworkParameters? {
return getNetworkParametersEntity(hash.toString())?.signedParameters() return getNetworkParametersEntity(hash.toString())?.let {
if (it.isSigned) it.toSignedNetworkParameters() else null
}
} }
override fun getNodeInfoHashes(certificateStatus: CertificateStatus): List<SecureHash> { override fun getNodeInfoHashes(certificateStatus: CertificateStatus): List<SecureHash> {
@ -79,18 +76,17 @@ class PersistentNetworkMapStorage(private val database: CordaPersistence) : Netw
} }
} }
override fun getLatestUnsignedNetworkParameters(): NetworkParameters = getLatestNetworkParametersEntity().networkParameters() override fun getLatestNetworkParameters(): NetworkParameters? {
private fun getLatestNetworkParametersEntity(): NetworkParametersEntity {
return database.transaction { return database.transaction {
val builder = session.criteriaBuilder val query = session.criteriaBuilder.run {
val query = builder.createQuery(NetworkParametersEntity::class.java).run { createQuery(NetworkParametersEntity::class.java).run {
from(NetworkParametersEntity::class.java).run { from(NetworkParametersEntity::class.java).run {
orderBy(builder.desc(get<String>(NetworkParametersEntity::created.name))) orderBy(desc(get<String>(NetworkParametersEntity::created.name)))
}
} }
} }
// We just want the last entry // We just want the last entry
session.createQuery(query).setMaxResults(1).resultList.singleOrNull() ?: throw IllegalArgumentException("No network parameters found in network map storage") session.createQuery(query).setMaxResults(1).uniqueResult()?.toNetworkParameters()
} }
} }

View File

@ -1,6 +1,5 @@
package com.r3.corda.networkmanage.common.persistence package com.r3.corda.networkmanage.common.persistence
import com.r3.corda.networkmanage.common.persistence.entity.CertificateDataEntity
import com.r3.corda.networkmanage.common.persistence.entity.CertificateSigningRequestEntity import com.r3.corda.networkmanage.common.persistence.entity.CertificateSigningRequestEntity
import com.r3.corda.networkmanage.common.persistence.entity.NodeInfoEntity import com.r3.corda.networkmanage.common.persistence.entity.NodeInfoEntity
import com.r3.corda.networkmanage.common.utils.buildCertPath import com.r3.corda.networkmanage.common.utils.buildCertPath
@ -10,7 +9,7 @@ import net.corda.core.internal.CertRole
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel import net.corda.nodeapi.internal.persistence.DatabaseTransaction
import java.security.cert.CertPath import java.security.cert.CertPath
/** /**
@ -21,16 +20,13 @@ class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeIn
val nodeInfo = nodeInfoWithSigned.nodeInfo val nodeInfo = nodeInfoWithSigned.nodeInfo
val signedNodeInfo = nodeInfoWithSigned.signedNodeInfo val signedNodeInfo = nodeInfoWithSigned.signedNodeInfo
val nodeCaCert = nodeInfo.legalIdentitiesAndCerts[0].certPath.certificates.find { CertRole.extract(it) == CertRole.NODE_CA } val nodeCaCert = nodeInfo.legalIdentitiesAndCerts[0].certPath.certificates.find { CertRole.extract(it) == CertRole.NODE_CA }
return database.transaction(TransactionIsolationLevel.SERIALIZABLE) { return database.transaction {
// TODO Move these checks out of data access layer // TODO Move these checks out of data access layer
val request = nodeCaCert?.let { val request = nodeCaCert?.let {
singleRequestWhere(CertificateDataEntity::class.java) { builder, path -> getSignedRequestByPublicHash(it.publicKey.encoded.sha256(), this)
val certPublicKeyHashEq = builder.equal(path.get<String>(CertificateDataEntity::publicKeyHash.name), it.publicKey.encoded.sha256().toString())
val certStatusValid = builder.equal(path.get<CertificateStatus>(CertificateDataEntity::certificateStatus.name), CertificateStatus.VALID)
builder.and(certPublicKeyHashEq, certStatusValid)
}
} }
request ?: throw IllegalArgumentException("Unknown node info, this public key is not registered with the network management service.") request ?: throw IllegalArgumentException("Unknown node info, this public key is not registered with the network management service.")
require(request.certificateData!!.certificateStatus == CertificateStatus.VALID) { "Certificate is no longer valid" }
/* /*
* Delete any previous [NodeInfoEntity] instance for this CSR * Delete any previous [NodeInfoEntity] instance for this CSR
@ -40,13 +36,13 @@ class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeIn
* but it has been confirmed that this fact has been acknowledged at the design time and we are fine with it. * but it has been confirmed that this fact has been acknowledged at the design time and we are fine with it.
*/ */
deleteRequest(NodeInfoEntity::class.java) { builder, path -> deleteRequest(NodeInfoEntity::class.java) { builder, path ->
builder.equal(path.get<CertificateSigningRequestEntity>(NodeInfoEntity::certificateSigningRequest.name), request.certificateSigningRequest) builder.equal(path.get<CertificateSigningRequestEntity>(NodeInfoEntity::certificateSigningRequest.name), request)
} }
val hash = signedNodeInfo.raw.hash val hash = signedNodeInfo.raw.hash
val hashedNodeInfo = NodeInfoEntity( val hashedNodeInfo = NodeInfoEntity(
nodeInfoHash = hash.toString(), nodeInfoHash = hash.toString(),
certificateSigningRequest = request.certificateSigningRequest, certificateSigningRequest = request,
signedNodeInfoBytes = signedNodeInfo.serialize().bytes) signedNodeInfoBytes = signedNodeInfo.serialize().bytes)
session.save(hashedNodeInfo) session.save(hashedNodeInfo)
hash hash
@ -61,16 +57,16 @@ class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeIn
override fun getCertificatePath(publicKeyHash: SecureHash): CertPath? { override fun getCertificatePath(publicKeyHash: SecureHash): CertPath? {
return database.transaction { return database.transaction {
val builder = session.criteriaBuilder val request = getSignedRequestByPublicHash(publicKeyHash, this)
val query = builder.createQuery(ByteArray::class.java).run { request?.let { buildCertPath(it.certificateData!!.certificatePathBytes) }
from(CertificateSigningRequestEntity::class.java).run { }
select(get<CertificateDataEntity>(CertificateSigningRequestEntity::certificateData.name) }
.get<ByteArray>(CertificateDataEntity::certificatePathBytes.name))
where(builder.equal(get<CertificateDataEntity>(CertificateSigningRequestEntity::certificateData.name) private fun getSignedRequestByPublicHash(publicKeyHash: SecureHash, transaction: DatabaseTransaction): CertificateSigningRequestEntity? {
.get<String>(CertificateDataEntity::publicKeyHash.name), publicKeyHash.toString())) return transaction.singleRequestWhere(CertificateSigningRequestEntity::class.java) { builder, path ->
} val publicKeyEq = builder.equal(path.get<String>(CertificateSigningRequestEntity::publicKeyHash.name), publicKeyHash.toString())
} val statusEq = builder.equal(path.get<RequestStatus>(CertificateSigningRequestEntity::status.name), RequestStatus.SIGNED)
session.createQuery(query).uniqueResultOptional().orElseGet { null }?.let { buildCertPath(it) } builder.and(publicKeyEq, statusEq)
} }
} }
} }

View File

@ -4,6 +4,7 @@ import com.r3.corda.networkmanage.common.persistence.CertificateData
import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequest import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequest
import com.r3.corda.networkmanage.common.persistence.CertificateStatus import com.r3.corda.networkmanage.common.persistence.CertificateStatus
import com.r3.corda.networkmanage.common.persistence.RequestStatus import com.r3.corda.networkmanage.common.persistence.RequestStatus
import net.corda.core.crypto.SecureHash
import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.PKCS10CertificationRequest
import org.hibernate.envers.Audited import org.hibernate.envers.Audited
import java.security.cert.CertPath import java.security.cert.CertPath
@ -12,7 +13,7 @@ import java.time.Instant
import javax.persistence.* import javax.persistence.*
@Entity @Entity
@Table(name = "certificate_signing_request") @Table(name = "certificate_signing_request", indexes = arrayOf(Index(name = "IDX_PUB_KEY_HASH", columnList = "public_key_hash")))
class CertificateSigningRequestEntity( class CertificateSigningRequestEntity(
@Id @Id
@Column(name = "request_id", length = 64) @Column(name = "request_id", length = 64)
@ -22,6 +23,9 @@ class CertificateSigningRequestEntity(
@Column(name = "legal_name", length = 256, nullable = false) @Column(name = "legal_name", length = 256, nullable = false)
val legalName: String, val legalName: String,
@Column(name = "public_key_hash", length = 64)
val publicKeyHash: String,
@Audited @Audited
@Column(name = "status", nullable = false) @Column(name = "status", nullable = false)
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
@ -50,6 +54,7 @@ class CertificateSigningRequestEntity(
fun toCertificateSigningRequest() = CertificateSigningRequest( fun toCertificateSigningRequest() = CertificateSigningRequest(
requestId = requestId, requestId = requestId,
legalName = legalName, legalName = legalName,
publicKeyHash = SecureHash.parse(publicKeyHash),
status = status, status = status,
request = request(), request = request(),
remark = remark, remark = remark,
@ -59,6 +64,7 @@ class CertificateSigningRequestEntity(
fun copy(requestId: String = this.requestId, fun copy(requestId: String = this.requestId,
legalName: String = this.legalName, legalName: String = this.legalName,
publicKeyHash: String = this.publicKeyHash,
status: RequestStatus = this.status, status: RequestStatus = this.status,
modifiedBy: List<String> = this.modifiedBy, modifiedBy: List<String> = this.modifiedBy,
modifiedAt: Instant = this.modifiedAt, modifiedAt: Instant = this.modifiedAt,
@ -69,6 +75,7 @@ class CertificateSigningRequestEntity(
return CertificateSigningRequestEntity( return CertificateSigningRequestEntity(
requestId = requestId, requestId = requestId,
legalName = legalName, legalName = legalName,
publicKeyHash = publicKeyHash,
status = status, status = status,
modifiedAt = modifiedAt, modifiedAt = modifiedAt,
modifiedBy = modifiedBy, modifiedBy = modifiedBy,
@ -82,16 +89,13 @@ class CertificateSigningRequestEntity(
} }
@Entity @Entity
@Table(name = "certificate_data", indexes = arrayOf(Index(name = "IDX_PUB_KEY_HASH", columnList = "public_key_hash"))) @Table(name = "certificate_data")
class CertificateDataEntity( class CertificateDataEntity(
@Id @Id
@GeneratedValue(strategy = GenerationType.SEQUENCE) @GeneratedValue(strategy = GenerationType.SEQUENCE)
val id: Long? = null, val id: Long? = null,
@Column(name = "public_key_hash", length = 64)
val publicKeyHash: String,
@Column(name = "certificate_status") @Column(name = "certificate_status")
val certificateStatus: CertificateStatus, val certificateStatus: CertificateStatus,
@ -105,7 +109,6 @@ class CertificateDataEntity(
) { ) {
fun toCertificateData(): CertificateData { fun toCertificateData(): CertificateData {
return CertificateData( return CertificateData(
publicKeyHash = publicKeyHash,
certStatus = certificateStatus, certStatus = certificateStatus,
certPath = toCertificatePath() certPath = toCertificatePath()
) )

View File

@ -1,7 +1,11 @@
package com.r3.corda.networkmanage.common.persistence.entity package com.r3.corda.networkmanage.common.persistence.entity
import com.r3.corda.networkmanage.common.utils.SignedNetworkMap
import net.corda.core.internal.DigitalSignatureWithCert import net.corda.core.internal.DigitalSignatureWithCert
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize
import net.corda.nodeapi.internal.crypto.X509CertificateFactory import net.corda.nodeapi.internal.crypto.X509CertificateFactory
import net.corda.nodeapi.internal.network.NetworkMap
import javax.persistence.* import javax.persistence.*
@Entity @Entity
@ -23,11 +27,12 @@ class NetworkMapEntity(
@Column(name = "certificate") @Column(name = "certificate")
val certificate: ByteArray val certificate: ByteArray
) { ) {
/** fun toNetworkMap(): NetworkMap = networkMap.deserialize()
* Deserializes NetworkMapEntity.signatureBytes into the [DigitalSignatureWithCert] instance
*/
fun signatureAndCertificate(): DigitalSignatureWithCert {
return DigitalSignatureWithCert(X509CertificateFactory().generateCertificate(certificate.inputStream()), signature)
}
} fun toSignedNetworkMap(): SignedNetworkMap {
return SignedNetworkMap(
SerializedBytes(networkMap),
DigitalSignatureWithCert(X509CertificateFactory().generateCertificate(certificate.inputStream()), signature)
)
}
}

View File

@ -35,13 +35,15 @@ class NetworkParametersEntity(
@Column(name = "certificate") @Column(name = "certificate")
val certificate: ByteArray? val certificate: ByteArray?
) { ) {
fun networkParameters(): NetworkParameters = parametersBytes.deserialize() val isSigned: Boolean get() = certificate != null && signature != null
// Return signed network parameters or null if they haven't been signed yet. fun toNetworkParameters(): NetworkParameters = parametersBytes.deserialize()
fun signedParameters(): SignedNetworkParameters? {
return if (certificate != null && signature != null) { fun toSignedNetworkParameters(): SignedNetworkParameters {
val sigWithCert = DigitalSignatureWithCert(X509CertificateFactory().generateCertificate(certificate.inputStream()), signature) if (certificate == null || signature == null) throw IllegalStateException("Network parameters entity is not signed: $parametersHash")
SignedDataWithCert(SerializedBytes(parametersBytes), sigWithCert) return SignedDataWithCert(
} else null SerializedBytes(parametersBytes),
DigitalSignatureWithCert(X509CertificateFactory().generateCertificate(certificate.inputStream()), signature)
)
} }
} }

View File

@ -4,20 +4,30 @@ 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.internal.SignedDataWithCert
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.utilities.contextLogger
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
class NetworkMapSigner(private val networkMapStorage: NetworkMapStorage, private val signer: Signer) { class NetworkMapSigner(private val networkMapStorage: NetworkMapStorage, private val signer: Signer) {
private companion object {
val logger = contextLogger()
}
/** /**
* Signs the network map and latest network parameters if they haven't been signed yet. * Signs the network map and latest network parameters if they haven't been signed yet.
*/ */
fun signNetworkMap() { fun signNetworkMap() {
// TODO There is no network parameters update process in place yet. We assume that latest parameters are to be used // TODO There is no network parameters update process in place yet. We assume that latest parameters are to be used
// in current network map. // in current network map.
val latestNetworkParameters = networkMapStorage.getLatestUnsignedNetworkParameters() val latestNetworkParameters = networkMapStorage.getLatestNetworkParameters()
val currentNetworkParameters = networkMapStorage.getCurrentSignedNetworkParameters() if (latestNetworkParameters == null) {
if (currentNetworkParameters?.verified() != latestNetworkParameters) logger.debug("No network parameters present")
signNetworkParameters(latestNetworkParameters) return
}
val currentNetworkParameters = networkMapStorage.getNetworkParametersOfNetworkMap()
if (currentNetworkParameters?.verified() != latestNetworkParameters) {
persistSignedNetworkParameters(latestNetworkParameters)
}
val currentSignedNetworkMap = networkMapStorage.getCurrentNetworkMap() val currentSignedNetworkMap = networkMapStorage.getCurrentNetworkMap()
val nodeInfoHashes = networkMapStorage.getNodeInfoHashes(CertificateStatus.VALID) val nodeInfoHashes = networkMapStorage.getNodeInfoHashes(CertificateStatus.VALID)
val serialisedNetworkMap = NetworkMap(nodeInfoHashes, latestNetworkParameters.serialize().hash).serialize() val serialisedNetworkMap = NetworkMap(nodeInfoHashes, latestNetworkParameters.serialize().hash).serialize()
@ -27,10 +37,8 @@ class NetworkMapSigner(private val networkMapStorage: NetworkMapStorage, private
} }
} }
/** fun persistSignedNetworkParameters(networkParameters: NetworkParameters) {
* Signs latest inserted network parameters. logger.info("Signing and persisting network parameters: $networkParameters")
*/
fun signNetworkParameters(networkParameters: NetworkParameters) {
val digitalSignature = signer.signObject(networkParameters).sig val digitalSignature = signer.signObject(networkParameters).sig
networkMapStorage.saveNetworkParameters(networkParameters, digitalSignature) networkMapStorage.saveNetworkParameters(networkParameters, digitalSignature)
} }

View File

@ -10,14 +10,20 @@ 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.NetworkMap import net.corda.nodeapi.internal.network.NetworkMap
import net.corda.nodeapi.internal.network.NetworkParameters import net.corda.nodeapi.internal.network.NetworkParameters
import java.security.KeyPair
import java.security.PrivateKey
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.X509Certificate
// TODO These should be defined in node-api // TODO These should be defined in node-api
typealias SignedNetworkParameters = SignedDataWithCert<NetworkParameters> typealias SignedNetworkParameters = SignedDataWithCert<NetworkParameters>
typealias SignedNetworkMap = SignedDataWithCert<NetworkMap> typealias SignedNetworkMap = SignedDataWithCert<NetworkMap>
data class CertPathAndKey(val certPath: List<X509Certificate>, val key: PrivateKey) {
fun toKeyPair(): KeyPair = KeyPair(certPath[0].publicKey, key)
}
// TODO: replace this with Crypto.hash when its available. // TODO: replace this with Crypto.hash when its available.
/** /**
* Returns SHA256 hash of this public key * Returns SHA256 hash of this public key
@ -42,7 +48,7 @@ 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()
fun buildCertPath(vararg certificates: Certificate): CertPath = X509CertificateFactory().delegate.generateCertPath(certificates.asList()) fun buildCertPath(vararg certificates: X509Certificate): CertPath = X509CertificateFactory().generateCertPath(certificates.asList())
fun buildCertPath(certPathBytes: ByteArray): CertPath = X509CertificateFactory().delegate.generateCertPath(certPathBytes.inputStream()) fun buildCertPath(certPathBytes: ByteArray): CertPath = X509CertificateFactory().delegate.generateCertPath(certPathBytes.inputStream())

View File

@ -65,8 +65,7 @@ data class JiraConfig(
val address: String, val address: String,
val projectCode: String, val projectCode: String,
val username: String, val username: String,
val password: String, val password: String
val doneTransitionCode: Int
) )
/** /**

View File

@ -1,13 +1,15 @@
package com.r3.corda.networkmanage.doorman package com.r3.corda.networkmanage.doorman
import com.atlassian.jira.rest.client.api.IssueRestClient
import com.atlassian.jira.rest.client.api.JiraRestClient import com.atlassian.jira.rest.client.api.JiraRestClient
import com.atlassian.jira.rest.client.api.domain.Comment
import com.atlassian.jira.rest.client.api.domain.Field import com.atlassian.jira.rest.client.api.domain.Field
import com.atlassian.jira.rest.client.api.domain.Issue import com.atlassian.jira.rest.client.api.domain.Issue
import com.atlassian.jira.rest.client.api.domain.IssueType import com.atlassian.jira.rest.client.api.domain.IssueType
import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder
import com.atlassian.jira.rest.client.api.domain.input.TransitionInput import com.atlassian.jira.rest.client.api.domain.input.TransitionInput
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.crypto.X509Utilities
import org.bouncycastle.asn1.x500.style.BCStyle import org.bouncycastle.asn1.x500.style.BCStyle
import org.bouncycastle.openssl.jcajce.JcaPEMWriter import org.bouncycastle.openssl.jcajce.JcaPEMWriter
@ -17,15 +19,20 @@ import java.io.StringWriter
import java.security.cert.CertPath import java.security.cert.CertPath
import javax.security.auth.x500.X500Principal import javax.security.auth.x500.X500Principal
class JiraClient(private val restClient: JiraRestClient, private val projectCode: String, private val doneTransitionCode: Int) { class JiraClient(private val restClient: JiraRestClient, private val projectCode: String) {
companion object { companion object {
val logger = loggerFor<JiraClient>() val logger = contextLogger()
} }
// The JIRA project must have a Request ID field and the Task issue type. // The JIRA project must have a Request ID and reject reason field, and the Task issue type.
private val requestIdField: Field = restClient.metadataClient.fields.claim().find { it.name == "Request ID" } ?: throw IllegalArgumentException("Request ID field not found in JIRA '$projectCode'") private val requestIdField: Field = restClient.metadataClient.fields.claim().find { it.name == "Request ID" } ?: throw IllegalArgumentException("Request ID field not found in JIRA '$projectCode'")
private val rejectReasonField: Field = restClient.metadataClient.fields.claim().find { it.name == "Reject Reason" } ?: throw IllegalArgumentException("Reject Reason field not found in JIRA '$projectCode'")
private val taskIssueType: IssueType = restClient.metadataClient.issueTypes.claim().find { it.name == "Task" } ?: throw IllegalArgumentException("Task issue type field not found in JIRA '$projectCode'") private val taskIssueType: IssueType = restClient.metadataClient.issueTypes.claim().find { it.name == "Task" } ?: throw IllegalArgumentException("Task issue type field not found in JIRA '$projectCode'")
private var doneTransitionId: Int = -1
private var canceledTransitionId: Int = -1
private var startProgressTransitionId: Int = -1
fun createRequestTicket(requestId: String, signingRequest: PKCS10CertificationRequest) { fun createRequestTicket(requestId: String, signingRequest: PKCS10CertificationRequest) {
// Check there isn't already a ticket for this request. // Check there isn't already a ticket for this request.
if (getIssueById(requestId) != null) { if (getIssueById(requestId) != null) {
@ -54,14 +61,26 @@ class JiraClient(private val restClient: JiraRestClient, private val projectCode
restClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim() restClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim()
} }
fun getApprovedRequests(): List<Pair<String, String>> { fun getApprovedRequests(): List<ApprovedRequest> {
val issues = restClient.searchClient.searchJql("project = $projectCode AND status = Approved").claim().issues val issues = restClient.searchClient.searchJql("project = $projectCode AND status = Approved").claim().issues
return issues.map { issue -> return issues.mapNotNull { issue ->
issue.getField(requestIdField.id)?.value?.toString().let { val requestId = issue.getField(requestIdField.id)?.value?.toString() ?: throw IllegalArgumentException("Error processing request '${issue.key}' : RequestId cannot be null.")
val requestId = it ?: throw IllegalArgumentException("RequestId cannot be null.") // Issue retrieved via search doesn't contain change logs.
val approvedBy = issue.assignee?.displayName ?: "Unknown" val fullIssue = restClient.issueClient.getIssue(issue.key, listOf(IssueRestClient.Expandos.CHANGELOG)).claim()
Pair(requestId, approvedBy) val approvedBy = fullIssue.changelog?.last { it.items.any { it.field == "status" && it.toString == "Approved" } }
} ApprovedRequest(requestId, approvedBy?.author?.displayName ?: "Unknown")
}
}
fun getRejectedRequests(): List<RejectedRequest> {
val issues = restClient.searchClient.searchJql("project = $projectCode AND status = Rejected").claim().issues
return issues.mapNotNull { issue ->
val requestId = issue.getField(requestIdField.id)?.value?.toString() ?: throw IllegalArgumentException("Error processing request '${issue.key}' : RequestId cannot be null.")
val rejectedReason = issue.getField(rejectReasonField.id)?.value?.toString()
// Issue retrieved via search doesn't contain comments.
val fullIssue = restClient.issueClient.getIssue(issue.key, listOf(IssueRestClient.Expandos.CHANGELOG)).claim()
val rejectedBy = fullIssue.changelog?.last { it.items.any { it.field == "status" && it.toString == "Rejected" } }
RejectedRequest(requestId, rejectedBy?.author?.displayName ?: "Unknown", rejectedReason)
} }
} }
@ -69,16 +88,41 @@ class JiraClient(private val restClient: JiraRestClient, private val projectCode
// Retrieving certificates for signed CSRs to attach to the jira tasks. // Retrieving certificates for signed CSRs to attach to the jira tasks.
signedRequests.forEach { (id, certPath) -> signedRequests.forEach { (id, certPath) ->
val certificate = certPath.certificates.first() val certificate = certPath.certificates.first()
// Jira only support ~ (contains) search for custom textfield.
val issue = getIssueById(id) val issue = getIssueById(id)
if (issue != null) { if (issue != null) {
restClient.issueClient.transition(issue, TransitionInput(doneTransitionCode)).fail { logger.error("Exception when transiting JIRA status.", it) }.claim() if (doneTransitionId == -1) {
restClient.issueClient.addAttachment(issue.attachmentsUri, certificate?.encoded?.inputStream(), "${X509Utilities.CORDA_CLIENT_CA}.cer") doneTransitionId = restClient.issueClient.getTransitions(issue.transitionsUri).claim().single { it.name == "Done" }.id
.fail { logger.error("Exception when uploading attachment to JIRA.", it) }.claim() }
restClient.issueClient.transition(issue, TransitionInput(doneTransitionId)).fail { logger.error("Exception when transiting JIRA status.", it) }.claim()
restClient.issueClient.addAttachment(issue.attachmentsUri, certificate.encoded.inputStream(), "${X509Utilities.CORDA_CLIENT_CA}.cer")
.fail { logger.error("Error processing request '${issue.key}' : Exception when uploading attachment to JIRA.", it) }.claim()
} }
} }
} }
private fun getIssueById(requestId: String): Issue? = fun updateRejectedRequests(rejectedRequests: List<String>) {
restClient.searchClient.searchJql("'Request ID' ~ $requestId").claim().issues.firstOrNull() rejectedRequests.mapNotNull { getIssueById(it) }
.forEach { issue ->
// Move status to in progress.
if (startProgressTransitionId == -1) {
startProgressTransitionId = restClient.issueClient.getTransitions(issue.transitionsUri).claim().single { it.name == "Start Progress" }.id
}
restClient.issueClient.transition(issue, TransitionInput(startProgressTransitionId)).fail { logger.error("Error processing request '${issue.key}' : Exception when transiting JIRA status.", it) }.claim()
// Move status to cancelled.
if (canceledTransitionId == -1) {
canceledTransitionId = restClient.issueClient.getTransitions(issue.transitionsUri).claim().single { it.name == "Stop Progress" }.id
}
restClient.issueClient.transition(issue, TransitionInput(canceledTransitionId)).fail { logger.error("Error processing request '${issue.key}' : Exception when transiting JIRA status.", it) }.claim()
restClient.issueClient.addComment(issue.commentsUri, Comment.valueOf("Request cancelled by doorman.")).claim()
}
}
private fun getIssueById(requestId: String): Issue? {
// Jira only support ~ (contains) search for custom textfield.
return restClient.searchClient.searchJql("'Request ID' ~ $requestId").claim().issues.firstOrNull()
}
} }
data class ApprovedRequest(val requestId: String, val approvedBy: String)
data class RejectedRequest(val requestId: String, val rejectedBy: String, val reason: String?)

View File

@ -1,16 +1,11 @@
package com.r3.corda.networkmanage.doorman package com.r3.corda.networkmanage.doorman
import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage
import com.r3.corda.networkmanage.common.persistence.*
import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage.Companion.DOORMAN_SIGNATURE import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage.Companion.DOORMAN_SIGNATURE
import com.r3.corda.networkmanage.common.signer.NetworkMapSigner import com.r3.corda.networkmanage.common.persistence.configureDatabase
import com.r3.corda.networkmanage.common.utils.CertPathAndKey
import com.r3.corda.networkmanage.common.utils.ShowHelpException import com.r3.corda.networkmanage.common.utils.ShowHelpException
import com.r3.corda.networkmanage.doorman.signer.DefaultCsrHandler
import com.r3.corda.networkmanage.doorman.signer.JiraCsrHandler
import com.r3.corda.networkmanage.doorman.signer.LocalSigner import com.r3.corda.networkmanage.doorman.signer.LocalSigner
import com.r3.corda.networkmanage.doorman.webservice.MonitoringWebService
import com.r3.corda.networkmanage.doorman.webservice.NodeInfoWebService
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_CSR_CERTIFICATE_NAME
import com.r3.corda.networkmanage.hsm.configuration.Parameters.Companion.DEFAULT_NETWORK_MAP_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
@ -21,141 +16,19 @@ import net.corda.core.internal.div
import net.corda.core.serialization.internal.SerializationEnvironmentImpl import net.corda.core.serialization.internal.SerializationEnvironmentImpl
import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.serialization.internal.nodeSerializationEnv
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.loggerFor
import net.corda.nodeapi.internal.crypto.* import net.corda.nodeapi.internal.crypto.*
import net.corda.nodeapi.internal.network.NetworkParameters import net.corda.nodeapi.internal.network.NetworkParameters
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
import net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme import net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme
import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.PKCS10CertificationRequest
import java.io.Closeable
import java.net.URI
import java.nio.file.Path import java.nio.file.Path
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.time.Instant import java.time.Instant
import java.util.*
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import javax.security.auth.x500.X500Principal import javax.security.auth.x500.X500Principal
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.system.exitProcess import kotlin.system.exitProcess
class NetworkManagementServer : Closeable {
private val doOnClose = mutableListOf<() -> Unit>()
lateinit var hostAndPort: NetworkHostAndPort
override fun close() = doOnClose.forEach { it() }
companion object {
private val logger = loggerFor<NetworkManagementServer>()
}
private fun getNetworkMapService(config: NetworkMapConfig, database: CordaPersistence, signer: LocalSigner?, updateNetworkParameters: NetworkParameters?): NodeInfoWebService {
val networkMapStorage = PersistentNetworkMapStorage(database)
val nodeInfoStorage = PersistentNodeInfoStorage(database)
val localNetworkMapSigner = if (signer != null) NetworkMapSigner(networkMapStorage, signer) else null
updateNetworkParameters?.let {
// Persisting new network parameters
val currentNetworkParameters = networkMapStorage.getCurrentSignedNetworkParameters()
if (currentNetworkParameters == null) {
localNetworkMapSigner?.signNetworkParameters(it) ?: networkMapStorage.saveNetworkParameters(it, null)
} else {
throw UnsupportedOperationException("Network parameters already exist. Updating them via the file config is not supported yet.")
}
}
// This call will fail if parameter is null in DB.
try {
val latestParameter = networkMapStorage.getLatestUnsignedNetworkParameters()
logger.info("Starting network map service with network parameters : $latestParameter")
} catch (e: NoSuchElementException) {
logger.error("No network parameter found, please upload new network parameter before starting network map service. The server will now exit.")
exitProcess(-1)
}
// Thread sign network map in case of change (i.e. a new node info has been added or a node info has been removed).
if (localNetworkMapSigner != null) {
val scheduledExecutor = Executors.newScheduledThreadPool(1)
val signingThread = Runnable {
try {
localNetworkMapSigner.signNetworkMap()
} catch (e: Exception) {
// Log the error and carry on.
logger.error("Error encountered when processing node info changes.", e)
}
}
scheduledExecutor.scheduleAtFixedRate(signingThread, config.signInterval, config.signInterval, TimeUnit.MILLISECONDS)
doOnClose += { scheduledExecutor.shutdown() }
}
return NodeInfoWebService(nodeInfoStorage, networkMapStorage, config)
}
private fun getDoormanService(config: DoormanConfig, database: CordaPersistence, signer: LocalSigner?, serverStatus: NetworkManagementServerStatus): RegistrationWebService {
logger.info("Starting Doorman server.")
val requestService = if (config.approveAll) {
logger.warn("Doorman server is in 'Approve All' mode, this will approve all incoming certificate signing requests.")
ApproveAllCertificateRequestStorage(PersistentCertificateRequestStorage(database))
} else {
PersistentCertificateRequestStorage(database)
}
val jiraConfig = config.jiraConfig
val requestProcessor = if (jiraConfig != null) {
val jiraWebAPI = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig.address), jiraConfig.username, jiraConfig.password)
val jiraClient = JiraClient(jiraWebAPI, jiraConfig.projectCode, jiraConfig.doneTransitionCode)
JiraCsrHandler(jiraClient, requestService, DefaultCsrHandler(requestService, signer))
} else {
DefaultCsrHandler(requestService, signer)
}
val scheduledExecutor = Executors.newScheduledThreadPool(1)
val approvalThread = Runnable {
try {
serverStatus.lastRequestCheckTime = Instant.now()
// Create tickets for requests which don't have one yet.
requestProcessor.createTickets()
// Process Jira approved tickets.
requestProcessor.processApprovedRequests()
} catch (e: Exception) {
// Log the error and carry on.
logger.error("Error encountered when approving request.", e)
}
}
scheduledExecutor.scheduleAtFixedRate(approvalThread, config.approveInterval, config.approveInterval, TimeUnit.MILLISECONDS)
doOnClose += { scheduledExecutor.shutdown() }
return RegistrationWebService(requestProcessor)
}
fun start(hostAndPort: NetworkHostAndPort,
database: CordaPersistence,
doormanSigner: LocalSigner? = null,
doormanServiceParameter: DoormanConfig?, // TODO Doorman config shouldn't be optional as the doorman is always required to run
startNetworkMap: NetworkMapStartParams?
) {
val services = mutableListOf<Any>()
val serverStatus = NetworkManagementServerStatus()
startNetworkMap?.let { services += getNetworkMapService(it.config, database, it.signer, it.updateNetworkParameters) }
doormanServiceParameter?.let { services += getDoormanService(it, database, doormanSigner, serverStatus) }
require(services.isNotEmpty()) { "No service created, please provide at least one service config." }
// TODO: use mbean to expose audit data?
services += MonitoringWebService(serverStatus)
val webServer = NetworkManagementWebServer(hostAndPort, *services.toTypedArray())
webServer.start()
doOnClose += webServer::close
this.hostAndPort = webServer.hostAndPort
}
}
data class NetworkMapStartParams(val signer: LocalSigner?, val updateNetworkParameters: NetworkParameters?, val config: NetworkMapConfig) 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)
@ -181,9 +54,8 @@ fun generateRootKeyPair(rootStoreFile: Path, rootKeystorePass: String?, rootPriv
val rootPrivateKeyPassword = rootPrivateKeyPass ?: readPassword("Root Private Key Password: ") val rootPrivateKeyPassword = rootPrivateKeyPass ?: readPassword("Root Private Key Password: ")
if (rootStore.containsAlias(X509Utilities.CORDA_ROOT_CA)) { if (rootStore.containsAlias(X509Utilities.CORDA_ROOT_CA)) {
val oldKey = loadOrCreateKeyStore(rootStoreFile, rootKeystorePassword).getCertificate(X509Utilities.CORDA_ROOT_CA).publicKey println("${X509Utilities.CORDA_ROOT_CA} already exists in keystore, process will now terminate.")
println("Key ${X509Utilities.CORDA_ROOT_CA} already exists in keystore, process will now terminate.") println(rootStore.getCertificate(X509Utilities.CORDA_ROOT_CA))
println(oldKey)
exitProcess(1) exitProcess(1)
} }
@ -203,7 +75,7 @@ fun generateRootKeyPair(rootStoreFile: Path, rootKeystorePass: String?, rootPriv
println("Trust store for distribution to nodes created in $nodeTrustStore") println("Trust store for distribution to nodes created in $nodeTrustStore")
println("Root CA keypair and certificate stored in ${rootStoreFile.toAbsolutePath()}.") println("Root CA keypair and certificate stored in ${rootStoreFile.toAbsolutePath()}.")
println(loadKeyStore(rootStoreFile, rootKeystorePassword).getCertificate(X509Utilities.CORDA_ROOT_CA).publicKey) println(selfSignCert)
} }
fun generateSigningKeyPairs(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?) {
@ -262,7 +134,7 @@ fun generateSigningKeyPairs(keystoreFile: Path, rootStoreFile: Path, rootKeystor
} }
private fun buildLocalSigners(parameters: NetworkManagementServerParameters): Pair<LocalSigner, LocalSigner>? { private fun processKeyStore(parameters: NetworkManagementServerParameters): Pair<CertPathAndKey, LocalSigner>? {
if (parameters.keystorePath == null) return null if (parameters.keystorePath == null) return null
// Get password from console if not in config. // Get password from console if not in config.
@ -270,19 +142,22 @@ private fun buildLocalSigners(parameters: NetworkManagementServerParameters): Pa
val privateKeyPassword = parameters.caPrivateKeyPassword ?: readPassword("Private key password: ") val privateKeyPassword = parameters.caPrivateKeyPassword ?: readPassword("Private key password: ")
val keyStore = loadOrCreateKeyStore(parameters.keystorePath, keyStorePassword) val keyStore = loadOrCreateKeyStore(parameters.keystorePath, keyStorePassword)
val (doormanSigner, networkMapSigner) = listOf(DEFAULT_CSR_CERTIFICATE_NAME, DEFAULT_NETWORK_MAP_CERTIFICATE_NAME).map { val csrCertPathAndKey = keyStore.run {
val keyPair = keyStore.getKeyPair(it, privateKeyPassword) CertPathAndKey(
val certPath = keyStore.getCertificateChain(it).map { it as X509Certificate } keyStore.getCertificateChain(DEFAULT_CSR_CERTIFICATE_NAME).map { it as X509Certificate },
LocalSigner(keyPair, certPath.toTypedArray()) keyStore.getSupportedKey(DEFAULT_CSR_CERTIFICATE_NAME, privateKeyPassword)
)
} }
return Pair(doormanSigner, networkMapSigner) val networkMapSigner = LocalSigner(keyStore.getCertificateAndKeyPair(DEFAULT_NETWORK_MAP_CERTIFICATE_NAME, privateKeyPassword))
return Pair(csrCertPathAndKey, networkMapSigner)
} }
/** /**
* This storage automatically approves all created requests. * This storage automatically approves all created requests.
*/ */
private class ApproveAllCertificateRequestStorage(private val delegate: CertificationRequestStorage) : CertificationRequestStorage by delegate { class ApproveAllCertificateRequestStorage(private val delegate: CertificationRequestStorage) : CertificationRequestStorage by delegate {
override fun saveRequest(request: PKCS10CertificationRequest): String { override fun saveRequest(request: PKCS10CertificationRequest): String {
val requestId = delegate.saveRequest(request) val requestId = delegate.saveRequest(request)
delegate.markRequestTicketCreated(requestId) delegate.markRequestTicketCreated(requestId)
@ -311,9 +186,9 @@ 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 localSigners = buildLocalSigners(this) val csrAndNetworkMap = processKeyStore(this)
if (localSigners != null) { if (csrAndNetworkMap != null) {
println("Starting network management services with local signing") println("Starting network management services with local signing")
} }
@ -325,10 +200,10 @@ fun main(args: Array<String>) {
parseNetworkParametersFrom(it) parseNetworkParametersFrom(it)
} }
val networkMapStartParams = networkMapConfig?.let { val networkMapStartParams = networkMapConfig?.let {
NetworkMapStartParams(localSigners?.second, networkParameters, it) NetworkMapStartParams(csrAndNetworkMap?.second, networkParameters, it)
} }
networkManagementServer.start(NetworkHostAndPort(host, port), database, localSigners?.first, doormanConfig, networkMapStartParams) networkManagementServer.start(NetworkHostAndPort(host, port), database, csrAndNetworkMap?.first, doormanConfig, networkMapStartParams)
Runtime.getRuntime().addShutdownHook(thread(start = false) { Runtime.getRuntime().addShutdownHook(thread(start = false) {
networkManagementServer.close() networkManagementServer.close()

View File

@ -1,58 +1,140 @@
package com.r3.corda.networkmanage.doorman package com.r3.corda.networkmanage.doorman
import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory
import com.r3.corda.networkmanage.common.persistence.PersistentCertificateRequestStorage
import com.r3.corda.networkmanage.common.persistence.PersistentNetworkMapStorage
import com.r3.corda.networkmanage.common.persistence.PersistentNodeInfoStorage
import com.r3.corda.networkmanage.common.signer.NetworkMapSigner
import com.r3.corda.networkmanage.common.utils.CertPathAndKey
import com.r3.corda.networkmanage.doorman.signer.DefaultCsrHandler
import com.r3.corda.networkmanage.doorman.signer.JiraCsrHandler
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
import com.r3.corda.networkmanage.doorman.webservice.MonitoringWebService
import com.r3.corda.networkmanage.doorman.webservice.NetworkMapWebService
import com.r3.corda.networkmanage.doorman.webservice.RegistrationWebService
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import org.eclipse.jetty.server.Server import net.corda.nodeapi.internal.network.NetworkParameters
import org.eclipse.jetty.server.ServerConnector import net.corda.nodeapi.internal.persistence.CordaPersistence
import org.eclipse.jetty.server.handler.HandlerCollection
import org.eclipse.jetty.servlet.ServletContextHandler
import org.eclipse.jetty.servlet.ServletHolder
import org.glassfish.jersey.server.ResourceConfig
import org.glassfish.jersey.servlet.ServletContainer
import java.io.Closeable import java.io.Closeable
import java.net.InetSocketAddress import java.net.URI
import java.time.Instant
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
/** class NetworkManagementServer : Closeable {
* NetworkManagementWebServer runs on Jetty server and provides service via http.
*/
class NetworkManagementWebServer(hostAndPort: NetworkHostAndPort, private vararg val webServices: Any) : Closeable {
companion object { companion object {
val logger = loggerFor<NetworkManagementServer>() private val logger = loggerFor<NetworkManagementServer>()
} }
private val server: Server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply { private val closeActions = mutableListOf<() -> Unit>()
handler = HandlerCollection().apply { lateinit var hostAndPort: NetworkHostAndPort
addHandler(buildServletContextHandler())
}
}
val hostAndPort: NetworkHostAndPort
get() = server.connectors.mapNotNull { it as? ServerConnector }
.map { NetworkHostAndPort(it.host, it.localPort) }
.first()
override fun close() { override fun close() {
logger.info("Shutting down network management web services...") for (closeAction in closeActions) {
server.stop() try {
server.join() closeAction()
} } catch (e: Exception) {
logger.warn("Discregarding exception thrown during close", e)
fun start() {
logger.info("Starting network management web services...")
server.start()
logger.info("Network management web services started on $hostAndPort with ${webServices.map { it.javaClass.simpleName }}")
println("Network management web services started on $hostAndPort with ${webServices.map { it.javaClass.simpleName }}")
}
private fun buildServletContextHandler(): ServletContextHandler {
return ServletContextHandler().apply {
contextPath = "/"
val resourceConfig = ResourceConfig().apply {
// Add your API provider classes (annotated for JAX-RS) here
webServices.forEach { register(it) }
} }
val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply { initOrder = 0 }// Initialise at server start
addServlet(jerseyServlet, "/*")
} }
} }
private fun getNetworkMapService(config: NetworkMapConfig, database: CordaPersistence, signer: LocalSigner?, newNetworkParameters: NetworkParameters?): NetworkMapWebService {
val networkMapStorage = PersistentNetworkMapStorage(database)
val nodeInfoStorage = PersistentNodeInfoStorage(database)
val localNetworkMapSigner = signer?.let { NetworkMapSigner(networkMapStorage, it) }
newNetworkParameters?.let {
val netParamsOfNetworkMap = networkMapStorage.getNetworkParametersOfNetworkMap()
if (netParamsOfNetworkMap == null) {
localNetworkMapSigner?.persistSignedNetworkParameters(it) ?: networkMapStorage.saveNetworkParameters(it, null)
} else {
throw UnsupportedOperationException("Network parameters already exist. Updating them is not supported yet.")
}
}
val latestParameters = networkMapStorage.getLatestNetworkParameters() ?:
throw IllegalStateException("No network parameters were found. Please upload new network parameters before starting network map service")
logger.info("Starting network map service with network parameters: $latestParameters")
if (localNetworkMapSigner != null) {
logger.info("Starting background worker for signing the network map using the local key store")
val scheduledExecutor = Executors.newScheduledThreadPool(1)
scheduledExecutor.scheduleAtFixedRate({
try {
localNetworkMapSigner.signNetworkMap()
} catch (e: Exception) {
// Log the error and carry on.
logger.error("Unable to sign network map", e)
}
}, config.signInterval, config.signInterval, TimeUnit.MILLISECONDS)
closeActions += scheduledExecutor::shutdown
}
return NetworkMapWebService(nodeInfoStorage, networkMapStorage, config)
}
private fun getDoormanService(config: DoormanConfig,
database: CordaPersistence,
csrCertPathAndKey: CertPathAndKey?,
serverStatus: NetworkManagementServerStatus): RegistrationWebService {
logger.info("Starting Doorman server.")
val requestService = if (config.approveAll) {
logger.warn("Doorman server is in 'Approve All' mode, this will approve all incoming certificate signing requests.")
ApproveAllCertificateRequestStorage(PersistentCertificateRequestStorage(database))
} else {
PersistentCertificateRequestStorage(database)
}
val jiraConfig = config.jiraConfig
val requestProcessor = if (jiraConfig != null) {
val jiraWebAPI = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig.address), jiraConfig.username, jiraConfig.password)
val jiraClient = JiraClient(jiraWebAPI, jiraConfig.projectCode)
JiraCsrHandler(jiraClient, requestService, DefaultCsrHandler(requestService, csrCertPathAndKey))
} else {
DefaultCsrHandler(requestService, csrCertPathAndKey)
}
val scheduledExecutor = Executors.newScheduledThreadPool(1)
val approvalThread = Runnable {
try {
serverStatus.lastRequestCheckTime = Instant.now()
// Process Jira approved tickets.
requestProcessor.processRequests()
} catch (e: Exception) {
// Log the error and carry on.
logger.error("Error encountered when approving request.", e)
}
}
scheduledExecutor.scheduleAtFixedRate(approvalThread, config.approveInterval, config.approveInterval, TimeUnit.MILLISECONDS)
closeActions += scheduledExecutor::shutdown
return RegistrationWebService(requestProcessor)
}
fun start(hostAndPort: NetworkHostAndPort,
database: CordaPersistence,
csrCertPathAndKey: CertPathAndKey?,
doormanServiceParameter: DoormanConfig?, // TODO Doorman config shouldn't be optional as the doorman is always required to run
startNetworkMap: NetworkMapStartParams?
) {
val services = mutableListOf<Any>()
val serverStatus = NetworkManagementServerStatus()
startNetworkMap?.let { services += getNetworkMapService(it.config, database, it.signer, it.updateNetworkParameters) }
doormanServiceParameter?.let { services += getDoormanService(it, database, csrCertPathAndKey, serverStatus) }
require(services.isNotEmpty()) { "No service created, please provide at least one service config." }
// TODO: use mbean to expose audit data?
services += MonitoringWebService(serverStatus)
val webServer = NetworkManagementWebServer(hostAndPort, *services.toTypedArray())
webServer.start()
closeActions += webServer::close
this.hostAndPort = webServer.hostAndPort
}
} }

View File

@ -0,0 +1,59 @@
package com.r3.corda.networkmanage.doorman
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.loggerFor
import org.eclipse.jetty.server.Server
import org.eclipse.jetty.server.ServerConnector
import org.eclipse.jetty.server.handler.HandlerCollection
import org.eclipse.jetty.servlet.ServletContextHandler
import org.eclipse.jetty.servlet.ServletHolder
import org.glassfish.jersey.server.ResourceConfig
import org.glassfish.jersey.servlet.ServletContainer
import java.io.Closeable
import java.net.InetSocketAddress
/**
* NetworkManagementWebServer runs on Jetty server and provides service via http.
*/
class NetworkManagementWebServer(hostAndPort: NetworkHostAndPort, private vararg val webServices: Any) : Closeable {
companion object {
val logger = contextLogger()
}
private val server: Server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply {
handler = HandlerCollection().apply {
addHandler(buildServletContextHandler())
}
}
val hostAndPort: NetworkHostAndPort
get() = server.connectors.mapNotNull { it as? ServerConnector }
.map { NetworkHostAndPort(it.host, it.localPort) }
.first()
override fun close() {
logger.info("Shutting down network management web services...")
server.stop()
server.join()
}
fun start() {
logger.info("Starting network management web services...")
server.start()
logger.info("Network management web services started on $hostAndPort with ${webServices.map { it.javaClass.simpleName }}")
println("Network management web services started on $hostAndPort with ${webServices.map { it.javaClass.simpleName }}")
}
private fun buildServletContextHandler(): ServletContextHandler {
return ServletContextHandler().apply {
contextPath = "/"
val resourceConfig = ResourceConfig().apply {
// Add your API provider classes (annotated for JAX-RS) here
webServices.forEach { register(it) }
}
val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply { initOrder = 0 }// Initialise at server start
addServlet(jerseyServlet, "/*")
}
}
}

View File

@ -1,41 +1,41 @@
package com.r3.corda.networkmanage.doorman.signer package com.r3.corda.networkmanage.doorman.signer
import com.r3.corda.networkmanage.common.persistence.CertificateResponse import com.r3.corda.networkmanage.common.persistence.CertificateResponse
import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequest
import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage
import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage.Companion.DOORMAN_SIGNATURE import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage.Companion.DOORMAN_SIGNATURE
import com.r3.corda.networkmanage.common.persistence.RequestStatus import com.r3.corda.networkmanage.common.persistence.RequestStatus
import com.r3.corda.networkmanage.doorman.JiraClient import com.r3.corda.networkmanage.common.utils.CertPathAndKey
import net.corda.core.utilities.loggerFor import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
import net.corda.nodeapi.internal.crypto.X509Utilities
import org.bouncycastle.asn1.x509.GeneralName
import org.bouncycastle.asn1.x509.GeneralSubtree
import org.bouncycastle.asn1.x509.NameConstraints
import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.PKCS10CertificationRequest
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
import java.security.cert.CertPath
import javax.security.auth.x500.X500Principal
interface CsrHandler { interface CsrHandler {
fun saveRequest(rawRequest: PKCS10CertificationRequest): String fun saveRequest(rawRequest: PKCS10CertificationRequest): String
fun createTickets() fun processRequests()
fun processApprovedRequests()
fun getResponse(requestId: String): CertificateResponse fun getResponse(requestId: String): CertificateResponse
} }
class DefaultCsrHandler(private val storage: CertificationRequestStorage, private val signer: LocalSigner?) : CsrHandler { class DefaultCsrHandler(private val storage: CertificationRequestStorage,
override fun processApprovedRequests() { private val csrCertPathAndKey: CertPathAndKey?) : CsrHandler {
storage.getRequests(RequestStatus.APPROVED)
.forEach { processRequest(it.requestId, it.request) }
}
override fun createTickets() {} override fun processRequests() {
if (csrCertPathAndKey == null) return
private fun processRequest(requestId: String, request: PKCS10CertificationRequest) { storage.getRequests(RequestStatus.APPROVED).forEach {
if (signer != null) { val nodeCertPath = createSignedNodeCertificate(it.request, csrCertPathAndKey)
val certs = signer.createSignedClientCertificate(request)
// Since Doorman is deployed in the auto-signing mode (i.e. signer != null), // Since Doorman is deployed in the auto-signing mode (i.e. signer != null),
// we use DOORMAN_SIGNATURE as the signer. // we use DOORMAN_SIGNATURE as the signer.
storage.putCertificatePath(requestId, certs, listOf(DOORMAN_SIGNATURE)) storage.putCertificatePath(it.requestId, nodeCertPath, listOf(DOORMAN_SIGNATURE))
} }
} }
override fun saveRequest(rawRequest: PKCS10CertificationRequest): String { override fun saveRequest(rawRequest: PKCS10CertificationRequest): String = storage.saveRequest(rawRequest)
return storage.saveRequest(rawRequest)
}
override fun getResponse(requestId: String): CertificateResponse { override fun getResponse(requestId: String): CertificateResponse {
val response = storage.getRequest(requestId) val response = storage.getRequest(requestId)
@ -45,63 +45,25 @@ class DefaultCsrHandler(private val storage: CertificationRequestStorage, privat
RequestStatus.SIGNED -> CertificateResponse.Ready(response.certData?.certPath ?: throw IllegalArgumentException("Certificate should not be null.")) RequestStatus.SIGNED -> CertificateResponse.Ready(response.certData?.certPath ?: throw IllegalArgumentException("Certificate should not be null."))
} }
} }
}
class JiraCsrHandler(private val jiraClient: JiraClient, private val storage: CertificationRequestStorage, private val delegate: CsrHandler) : CsrHandler by delegate { private fun createSignedNodeCertificate(certificationRequest: PKCS10CertificationRequest,
private companion object { csrCertPathAndKey: CertPathAndKey): CertPath {
val log = loggerFor<JiraCsrHandler>() // 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.
override fun saveRequest(rawRequest: PKCS10CertificationRequest): String { // We assume all attributes in the subject name has been checked prior approval.
val requestId = delegate.saveRequest(rawRequest) // TODO: add validation to subject name.
// Make sure request has been accepted. val request = JcaPKCS10CertificationRequest(certificationRequest)
try { val nameConstraints = NameConstraints(
if (delegate.getResponse(requestId) !is CertificateResponse.Unauthorised) { arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, request.subject))),
jiraClient.createRequestTicket(requestId, rawRequest) arrayOf())
storage.markRequestTicketCreated(requestId) val nodeCaCert = X509Utilities.createCertificate(
} CertificateType.NODE_CA,
} catch (e: Exception) { csrCertPathAndKey.certPath[0],
log.warn("There was an error while creating Jira tickets", e) csrCertPathAndKey.toKeyPair(),
} finally { X500Principal(request.subject.encoded),
return requestId request.publicKey,
} nameConstraints = nameConstraints)
} return X509CertificateFactory().generateCertPath(listOf(nodeCaCert) + csrCertPathAndKey.certPath)
override fun processApprovedRequests() {
val approvedRequest = jiraClient.getApprovedRequests()
approvedRequest.forEach { (id, approvedBy) -> storage.approveRequest(id, approvedBy) }
delegate.processApprovedRequests()
val signedRequests = approvedRequest.mapNotNull { (id, _) ->
val request = storage.getRequest(id)
if (request != null && request.status == RequestStatus.SIGNED) {
request.certData?.certPath?.let { certs -> id to certs }
} else {
null
}
}.toMap()
jiraClient.updateSignedRequests(signedRequests)
}
/**
* Creates Jira tickets for all request in [RequestStatus.NEW] state.
*
* Usually requests are expected to move to the [RequestStatus.TICKET_CREATED] state immediately,
* they might be left in the [RequestStatus.NEW] state if Jira is down.
*/
override fun createTickets() {
try {
for (signingRequest in storage.getRequests(RequestStatus.NEW)) {
createTicket(signingRequest)
}
} catch (e: Exception) {
log.warn("There were errors while creating Jira tickets", e)
}
}
private fun createTicket(signingRequest: CertificateSigningRequest) {
jiraClient.createRequestTicket(signingRequest.requestId, signingRequest.request)
storage.markRequestTicketCreated(signingRequest.requestId)
} }
} }

View File

@ -0,0 +1,84 @@
package com.r3.corda.networkmanage.doorman.signer
import com.r3.corda.networkmanage.common.persistence.CertificateResponse
import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequest
import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage
import com.r3.corda.networkmanage.common.persistence.RequestStatus
import com.r3.corda.networkmanage.doorman.ApprovedRequest
import com.r3.corda.networkmanage.doorman.JiraClient
import com.r3.corda.networkmanage.doorman.RejectedRequest
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.loggerFor
import org.bouncycastle.pkcs.PKCS10CertificationRequest
class JiraCsrHandler(private val jiraClient: JiraClient, private val storage: CertificationRequestStorage, private val delegate: CsrHandler) : CsrHandler by delegate {
private companion object {
val log = contextLogger()
}
override fun saveRequest(rawRequest: PKCS10CertificationRequest): String {
val requestId = delegate.saveRequest(rawRequest)
// Make sure request has been accepted.
try {
if (delegate.getResponse(requestId) !is CertificateResponse.Unauthorised) {
jiraClient.createRequestTicket(requestId, rawRequest)
storage.markRequestTicketCreated(requestId)
}
} catch (e: Exception) {
log.warn("There was an error while creating Jira tickets", e)
} finally {
return requestId
}
}
override fun processRequests() {
createTickets()
val (approvedRequests, rejectedRequests) = updateRequestStatus()
delegate.processRequests()
updateJiraTickets(approvedRequests, rejectedRequests)
}
private fun updateRequestStatus(): Pair<List<ApprovedRequest>, List<RejectedRequest>> {
// Update local request statuses.
val approvedRequest = jiraClient.getApprovedRequests()
approvedRequest.forEach { (id, approvedBy) -> storage.approveRequest(id, approvedBy) }
val rejectedRequest = jiraClient.getRejectedRequests()
rejectedRequest.forEach { (id, rejectedBy, reason) -> storage.rejectRequest(id, rejectedBy, reason) }
return Pair(approvedRequest, rejectedRequest)
}
private fun updateJiraTickets(approvedRequest: List<ApprovedRequest>, rejectedRequest: List<RejectedRequest>) {
// Reconfirm request status and update jira status
val signedRequests = approvedRequest.mapNotNull { storage.getRequest(it.requestId) }
.filter { it.status == RequestStatus.SIGNED && it.certData != null }
.associateBy { it.requestId }
.mapValues { it.value.certData!!.certPath }
jiraClient.updateSignedRequests(signedRequests)
val rejectedRequestIDs = rejectedRequest.mapNotNull { storage.getRequest(it.requestId) }
.filter { it.status == RequestStatus.REJECTED }
.map { it.requestId }
jiraClient.updateRejectedRequests(rejectedRequestIDs)
}
/**
* Creates Jira tickets for all request in [RequestStatus.NEW] state.
*
* Usually requests are expected to move to the [RequestStatus.TICKET_CREATED] state immediately,
* they might be left in the [RequestStatus.NEW] state if Jira is down.
*/
private fun createTickets() {
storage.getRequests(RequestStatus.NEW).forEach {
try {
createTicket(it)
} catch (e: Exception) {
log.warn("There were errors while creating Jira tickets for request '${it.requestId}'", e)
}
}
}
private fun createTicket(signingRequest: CertificateSigningRequest) {
jiraClient.createRequestTicket(signingRequest.requestId, signingRequest.request)
storage.markRequestTicketCreated(signingRequest.requestId)
}
}

View File

@ -1,48 +1,20 @@
package com.r3.corda.networkmanage.doorman.signer 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 net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto
import net.corda.core.internal.DigitalSignatureWithCert import net.corda.core.internal.DigitalSignatureWithCert
import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
import net.corda.nodeapi.internal.crypto.X509Utilities import java.security.KeyStore
import org.bouncycastle.asn1.x509.GeneralName import java.security.PrivateKey
import org.bouncycastle.asn1.x509.GeneralSubtree
import org.bouncycastle.asn1.x509.NameConstraints
import org.bouncycastle.pkcs.PKCS10CertificationRequest
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
import java.security.KeyPair
import java.security.cert.CertPath
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import javax.security.auth.x500.X500Principal
/** /**
* The [LocalSigner] class signs [PKCS10CertificationRequest] using provided CA key pair and certificate path. * This local signer 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.
*/ */
//TODO Use a list instead of array class LocalSigner(private val signingKey: PrivateKey, private val signingCert: X509Certificate) : Signer {
class LocalSigner(private val signingKeyPair: KeyPair, private val signingCertPath: Array<X509Certificate>) : Signer { constructor(certAndKeyPair: CertificateAndKeyPair) : this(certAndKeyPair.keyPair.private, certAndKeyPair.certificate)
// TODO This doesn't belong in this class
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,
// please see [sun.security.x509.X500Name.isWithinSubtree()] for more information.
// We assume all attributes in the subject name has been checked prior approval.
// TODO: add validation to subject name.
val request = JcaPKCS10CertificationRequest(certificationRequest)
val nameConstraints = NameConstraints(
arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, request.subject))),
arrayOf())
val nodeCaCert = X509Utilities.createCertificate(
CertificateType.NODE_CA,
signingCertPath[0],
signingKeyPair,
X500Principal(request.subject.encoded),
request.publicKey,
nameConstraints = nameConstraints)
return buildCertPath(nodeCaCert, *signingCertPath)
}
override fun signBytes(data: ByteArray): DigitalSignatureWithCert { override fun signBytes(data: ByteArray): DigitalSignatureWithCert {
return DigitalSignatureWithCert(signingCertPath[0], Crypto.doSign(signingKeyPair.private, data)) return DigitalSignatureWithCert(signingCert, Crypto.doSign(signingKey, data))
} }
} }

View File

@ -7,9 +7,8 @@ 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.common.utils.SignedNetworkMap
import com.r3.corda.networkmanage.common.utils.SignedNetworkParameters
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.NetworkMapWebService.Companion.NETWORK_MAP_PATH
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
@ -31,9 +30,9 @@ import javax.ws.rs.core.Response.ok
import javax.ws.rs.core.Response.status import javax.ws.rs.core.Response.status
@Path(NETWORK_MAP_PATH) @Path(NETWORK_MAP_PATH)
class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage, class NetworkMapWebService(private val nodeInfoStorage: NodeInfoStorage,
private val networkMapStorage: NetworkMapStorage, private val networkMapStorage: NetworkMapStorage,
private val config: NetworkMapConfig) { private val config: NetworkMapConfig) {
companion object { companion object {
val log = contextLogger() val log = contextLogger()
@ -42,7 +41,7 @@ class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage,
private val networkMapCache: LoadingCache<Boolean, Pair<SignedNetworkMap?, NetworkParameters?>> = CacheBuilder.newBuilder() private val networkMapCache: LoadingCache<Boolean, Pair<SignedNetworkMap?, NetworkParameters?>> = CacheBuilder.newBuilder()
.expireAfterWrite(config.cacheTimeout, TimeUnit.MILLISECONDS) .expireAfterWrite(config.cacheTimeout, TimeUnit.MILLISECONDS)
.build(CacheLoader.from { _ -> Pair(networkMapStorage.getCurrentNetworkMap(), networkMapStorage.getCurrentSignedNetworkParameters()?.verified()) }) .build(CacheLoader.from { _ -> Pair(networkMapStorage.getCurrentNetworkMap(), networkMapStorage.getNetworkParametersOfNetworkMap()?.verified()) })
@POST @POST
@Path("publish") @Path("publish")
@ -87,7 +86,6 @@ class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage,
@GET @GET
@Path("my-ip") @Path("my-ip")
fun myIp(@Context request: HttpServletRequest): Response { fun myIp(@Context request: HttpServletRequest): Response {
// TODO: Verify this returns IP correctly.
return ok(request.getHeader("X-Forwarded-For")?.split(",")?.first() ?: "${request.remoteHost}:${request.remotePort}").build() return ok(request.getHeader("X-Forwarded-For")?.split(",")?.first() ?: "${request.remoteHost}:${request.remotePort}").build()
} }

View File

@ -1,7 +1,10 @@
package com.r3.corda.networkmanage.hsm package com.r3.corda.networkmanage.hsm
import com.google.common.util.concurrent.MoreExecutors
import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
import com.r3.corda.networkmanage.common.persistence.PersistentNetworkMapStorage import com.r3.corda.networkmanage.common.persistence.PersistentNetworkMapStorage
import com.r3.corda.networkmanage.common.persistence.configureDatabase import com.r3.corda.networkmanage.common.persistence.configureDatabase
import com.r3.corda.networkmanage.common.signer.NetworkMapSigner
import com.r3.corda.networkmanage.common.utils.ShowHelpException import com.r3.corda.networkmanage.common.utils.ShowHelpException
import com.r3.corda.networkmanage.hsm.authentication.AuthMode import com.r3.corda.networkmanage.hsm.authentication.AuthMode
import com.r3.corda.networkmanage.hsm.authentication.Authenticator import com.r3.corda.networkmanage.hsm.authentication.Authenticator
@ -16,10 +19,19 @@ import com.r3.corda.networkmanage.hsm.persistence.DBSignedCertificateRequestStor
import com.r3.corda.networkmanage.hsm.signer.HsmCsrSigner import com.r3.corda.networkmanage.hsm.signer.HsmCsrSigner
import com.r3.corda.networkmanage.hsm.signer.HsmNetworkMapSigner import com.r3.corda.networkmanage.hsm.signer.HsmNetworkMapSigner
import com.r3.corda.networkmanage.hsm.utils.mapCryptoServerException import com.r3.corda.networkmanage.hsm.utils.mapCryptoServerException
import net.corda.core.utilities.minutes
import org.apache.logging.log4j.LogManager
import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.security.Security import java.security.Security
import java.time.Duration
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit.MILLISECONDS
import java.util.concurrent.TimeUnit.SECONDS
import javax.crypto.Cipher import javax.crypto.Cipher
private val log = LogManager.getLogger("com.r3.corda.networkmanage.hsm.Main")
fun main(args: Array<String>) { fun main(args: Array<String>) {
// Grabbed from https://stackoverflow.com/questions/7953567/checking-if-unlimited-cryptography-is-available // Grabbed from https://stackoverflow.com/questions/7953567/checking-if-unlimited-cryptography-is-available
if (Cipher.getMaxAllowedKeyLength("AES") < 256) { if (Cipher.getMaxAllowedKeyLength("AES") < 256) {
@ -46,12 +58,15 @@ fun run(parameters: Parameters) {
checkNotNull(dataSourceProperties) checkNotNull(dataSourceProperties)
val database = configureDatabase(dataSourceProperties, databaseConfig) val database = configureDatabase(dataSourceProperties, databaseConfig)
val csrStorage = DBSignedCertificateRequestStorage(database) val csrStorage = DBSignedCertificateRequestStorage(database)
val networkMapStorage = PersistentNetworkMapStorage(database) val hsmSigner = HsmNetworkMapSigner(
val hsmNetworkMapSigningThread = HsmNetworkMapSigner(
networkMapStorage,
networkMapCertificateName, networkMapCertificateName,
networkMapPrivateKeyPassword, networkMapPrivateKeyPassword,
Authenticator(createProvider(), AuthMode.KEY_FILE, autoUsername, authKeyFilePath, authKeyFilePassword, signAuthThreshold)).start() Authenticator(createProvider(), AuthMode.KEY_FILE, autoUsername, authKeyFilePath, authKeyFilePassword, signAuthThreshold))
val networkMapStorage = PersistentNetworkMapStorage(database)
val scheduler = Executors.newSingleThreadScheduledExecutor()
startNetworkingMapSigningPolling(networkMapStorage, hsmSigner, scheduler, 10.minutes)
val sign: (List<ApprovedCertificateRequestData>) -> Unit = { val sign: (List<ApprovedCertificateRequestData>) -> Unit = {
val signer = HsmCsrSigner( val signer = HsmCsrSigner(
csrStorage, csrStorage,
@ -62,6 +77,7 @@ fun run(parameters: Parameters) {
Authenticator(createProvider(), authMode, autoUsername, authKeyFilePath, authKeyFilePassword, signAuthThreshold)) Authenticator(createProvider(), authMode, autoUsername, authKeyFilePath, authKeyFilePassword, signAuthThreshold))
signer.sign(it) signer.sign(it)
} }
Menu().withExceptionHandler(::processError).addItem("1", "Generate root and intermediate certificates", { Menu().withExceptionHandler(::processError).addItem("1", "Generate root and intermediate certificates", {
if (confirmedKeyGen()) { if (confirmedKeyGen()) {
val generator = KeyCertificateGenerator( val generator = KeyCertificateGenerator(
@ -104,13 +120,29 @@ fun run(parameters: Parameters) {
println("There is no approved and unsigned CSR") println("There is no approved and unsigned CSR")
} }
}).showMenu() }).showMenu()
hsmNetworkMapSigningThread.stop()
MoreExecutors.shutdownAndAwaitTermination(scheduler, 30, SECONDS)
} }
} }
private fun startNetworkingMapSigningPolling(networkMapStorage: NetworkMapStorage,
signer: HsmNetworkMapSigner,
executor: ScheduledExecutorService,
signingPeriod: Duration) {
val networkMapSigner = NetworkMapSigner(networkMapStorage, signer)
executor.scheduleAtFixedRate({
try {
networkMapSigner.signNetworkMap()
} catch (e: Exception) {
log.warn("Exception thrown while signing network map", e)
}
}, signingPeriod.toMillis(), signingPeriod.toMillis(), MILLISECONDS)
}
private fun processError(exception: Exception) { private fun processError(exception: Exception) {
val processed = mapCryptoServerException(exception) val processed = mapCryptoServerException(exception)
System.err.println("An error occurred: ${processed.message}") System.err.println("An error occurred:")
processed.printStackTrace()
} }
private fun confirmedSign(selectedItems: List<ApprovedCertificateRequestData>): Boolean { private fun confirmedSign(selectedItems: List<ApprovedCertificateRequestData>): Boolean {

View File

@ -1,78 +1,39 @@
package com.r3.corda.networkmanage.hsm.signer package com.r3.corda.networkmanage.hsm.signer
import com.google.common.util.concurrent.MoreExecutors
import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
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.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
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.verify import com.r3.corda.networkmanage.hsm.utils.X509Utilities.verify
import net.corda.core.internal.DigitalSignatureWithCert import net.corda.core.internal.DigitalSignatureWithCert
import net.corda.core.utilities.loggerFor import net.corda.nodeapi.internal.crypto.getX509Certificate
import net.corda.core.utilities.minutes
import java.security.PrivateKey import java.security.PrivateKey
import java.security.Signature import java.security.Signature
import java.security.cert.X509Certificate
import java.time.Duration
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
/** /**
* Encapsulates logic for periodic network map signing execution. * Signer which connects to a HSM using the given [authenticator] to sign bytes.
* It uses HSM as the signing entity with keys and certificates specified at the construction time.
*/ */
// TODO Rename this to HsmSigner // TODO Rename this to HsmSigner
class HsmNetworkMapSigner(networkMapStorage: NetworkMapStorage, class HsmNetworkMapSigner(private val certificateKeyName: String,
private val caCertificateKeyName: String, private val privateKeyPassword: String,
private val caPrivateKeyPass: String, private val authenticator: Authenticator) : Signer {
private val authenticator: Authenticator,
private val signingPeriod: Duration = DEFAULT_SIGNING_PERIOD_MS) : Signer {
companion object {
val log = loggerFor<HsmNetworkMapSigner>()
val DEFAULT_SIGNING_PERIOD_MS = 10.minutes
private val TERMINATION_TIMEOUT_SEC = 2L
}
private val networkMapSigner = NetworkMapSigner(networkMapStorage, this)
private lateinit var scheduledExecutor: ScheduledExecutorService
// TODO This doesn't belong in this class
fun start(): HsmNetworkMapSigner {
val signingPeriodMillis = signingPeriod.toMillis()
scheduledExecutor = Executors.newSingleThreadScheduledExecutor()
scheduledExecutor.scheduleAtFixedRate({
try {
networkMapSigner.signNetworkMap()
} catch (exception: Exception) {
log.warn("Exception thrown while signing network map", exception)
}
}, signingPeriodMillis, signingPeriodMillis, TimeUnit.MILLISECONDS)
return this
}
fun stop() {
MoreExecutors.shutdownAndAwaitTermination(scheduledExecutor, TERMINATION_TIMEOUT_SEC, TimeUnit.SECONDS)
}
/** /**
* 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 signBytes(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 certificate = keyStore.getX509Certificate(certificateKeyName)
val caKey = keyStore.getKey(caCertificateKeyName, caPrivateKeyPass.toCharArray()) as PrivateKey // Don't worry this is not a real private key but a pointer to one that resides in the HSM. It only works
// when used with the given provider.
val key = keyStore.getKey(certificateKeyName, privateKeyPassword.toCharArray()) as PrivateKey
val signature = Signature.getInstance(X509Utilities.SIGNATURE_ALGORITHM, provider).run { val signature = Signature.getInstance(X509Utilities.SIGNATURE_ALGORITHM, provider).run {
initSign(caKey) initSign(key)
update(data) update(data)
sign() sign()
} }
verify(data, signature, caCertificateChain[0].publicKey) verify(data, signature, certificate.publicKey)
DigitalSignatureWithCert(caCertificateChain[0] as X509Certificate, signature) DigitalSignatureWithCert(certificate, signature)
} }
} }
} }

View File

@ -33,9 +33,9 @@ object HsmErrors {
fun mapCryptoServerException(exception: Exception): Exception { fun mapCryptoServerException(exception: Exception): Exception {
// Try to decode the error code // Try to decode the error code
val crypto = exception as? CryptoServerException ?: exception.cause as? CryptoServerException val crypto = exception as? CryptoServerException ?: exception.cause as? CryptoServerException
if (crypto != null) { return if (crypto != null) {
return Exception("(CryptoServer) ${HsmErrors.errors[crypto.ErrorCode]}", exception) Exception("(CryptoServer) ${HsmErrors.errors[crypto.ErrorCode]}", exception)
} else { } else {
return exception exception
} }
} }

View File

@ -93,13 +93,11 @@
</changeSet> </changeSet>
<changeSet author="R3.Corda" id="1513267683777-8"> <changeSet author="R3.Corda" id="1513267683777-8">
<createTable tableName="network_parameters"> <createTable tableName="network_parameters">
<column name="hash" type="NVARCHAR(64)"> <column name="version" type="BIGINT">
<constraints nullable="false"/> <constraints nullable="false"/>
</column> </column>
<column name="certificate" type="BLOB"/> <column name="bytes" type="BLOB"/>
<column name="created" type="TIMESTAMP"/> <column name="hash" type="NVARCHAR(64)"/>
<column name="parameters_bytes" type="BLOB"/>
<column name="signature" type="BLOB"/>
</createTable> </createTable>
</changeSet> </changeSet>
<changeSet author="R3.Corda" id="1513267683777-9"> <changeSet author="R3.Corda" id="1513267683777-9">
@ -122,7 +120,7 @@
</createTable> </createTable>
</changeSet> </changeSet>
<changeSet author="R3.Corda" id="1513267683777-11"> <changeSet author="R3.Corda" id="1513267683777-11">
<addPrimaryKey columnNames="hash" constraintName="CONSTRAINT_3" tableName="network_parameters"/> <addPrimaryKey columnNames="version" constraintName="CONSTRAINT_3" tableName="network_parameters"/>
</changeSet> </changeSet>
<changeSet author="R3.Corda" id="1513267683777-12"> <changeSet author="R3.Corda" id="1513267683777-12">
<addPrimaryKey columnNames="id" constraintName="CONSTRAINT_7" tableName="certificate_data"/> <addPrimaryKey columnNames="id" constraintName="CONSTRAINT_7" tableName="certificate_data"/>
@ -169,11 +167,6 @@
<column name="public_key_hash"/> <column name="public_key_hash"/>
</createIndex> </createIndex>
</changeSet> </changeSet>
<changeSet author="R3.Corda" id="network-parameters-idx">
<createIndex indexName="IDX_NET_PARAMS_HASH" tableName="network_parameters">
<column name="hash"/>
</createIndex>
</changeSet>
<changeSet author="R3.Corda" id="1513267683777-24"> <changeSet author="R3.Corda" id="1513267683777-24">
<addForeignKeyConstraint baseColumnNames="rev" baseTableName="certificate_signing_request_AUD" <addForeignKeyConstraint baseColumnNames="rev" baseTableName="certificate_signing_request_AUD"
constraintName="FK5g5cagcrx7siu8lwtavirunxd" constraintName="FK5g5cagcrx7siu8lwtavirunxd"

View File

@ -2,5 +2,7 @@
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd" > <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd" >
<include file="migration/network-manager.changelog-init.xml"/> <include file="migration/network-manager.changelog-init.xml"/>
<include file="migration/network-manager.changelog-signing-network-params.xml"/>
<include file="migration/network-manager.changelog-pub-key-move.xml"/>
</databaseChangeLog> </databaseChangeLog>

View File

@ -0,0 +1,22 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
<changeSet author="R3.Corda" id="Move public key hash">
<delete tableName="certificate_signing_request"/>
<delete tableName="certificate_data"/>
<addColumn tableName="certificate_signing_request">
<column name="public_key_hash" type="NVARCHAR(64)"/>
</addColumn>
<dropIndex indexName="IDX_PUB_KEY_HASH" tableName="certificate_data"/>
<createIndex indexName="IDX_PUB_KEY_HASH" tableName="certificate_signing_request">
<column name="public_key_hash"/>
</createIndex>
<dropColumn columnName="public_key_hash" tableName="certificate_data"/>
<delete tableName="CertificateSigningRequestEntity_modifiedBy"/>
<delete tableName="CertificateSigningRequestEntity_modifiedBy_AUD"/>
<delete tableName="certificate_signing_request_AUD"/>
<delete tableName="network_map"/>
<delete tableName="network_parameters"/>
<delete tableName="node_info"/>
<delete tableName="REVINFO"/>
</changeSet>
</databaseChangeLog>

View File

@ -0,0 +1,27 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
<changeSet author="R3.Corda" id="signing_network_map_parameters">
<dropTable tableName="network_parameters"/>
<createTable tableName="network_parameters">
<column name="hash" type="NVARCHAR(64)">
<constraints nullable="false"/>
</column>
<column name="certificate" type="BLOB"/>
<column name="created" type="TIMESTAMP"/>
<column name="parameters_bytes" type="BLOB"/>
<column name="signature" type="BLOB"/>
</createTable>
<addPrimaryKey columnNames="hash" constraintName="CONSTRAINT_3" tableName="network_parameters"/>
<createIndex indexName="IDX_NET_PARAMS_HASH" tableName="network_parameters">
<column name="hash"/>
</createIndex>
<delete tableName="certificate_data"/>
<delete tableName="CertificateSigningRequestEntity_modifiedBy"/>
<delete tableName="CertificateSigningRequestEntity_modifiedBy_AUD"/>
<delete tableName="certificate_signing_request"/>
<delete tableName="certificate_signing_request_AUD"/>
<delete tableName="network_map"/>
<delete tableName="node_info"/>
<delete tableName="REVINFO"/>
</changeSet>
</databaseChangeLog>

View File

@ -20,6 +20,7 @@ abstract class TestBase {
requestId: String = SecureHash.randomSHA256().toString(), requestId: String = SecureHash.randomSHA256().toString(),
status: RequestStatus = RequestStatus.NEW, status: RequestStatus = RequestStatus.NEW,
legalName: String = "TestLegalName", legalName: String = "TestLegalName",
publicKeyHash: SecureHash = SecureHash.randomSHA256(),
remark: String = "Test remark", remark: String = "Test remark",
request: PKCS10CertificationRequest = mock(), request: PKCS10CertificationRequest = mock(),
certData: CertificateData = mock(), certData: CertificateData = mock(),
@ -29,6 +30,7 @@ abstract class TestBase {
requestId = requestId, requestId = requestId,
status = status, status = status,
legalName = legalName, legalName = legalName,
publicKeyHash = publicKeyHash,
remark = remark, remark = remark,
certData = certData, certData = certData,
request = request, request = request,
@ -36,11 +38,9 @@ abstract class TestBase {
) )
} }
protected fun certificateData(publicKeyHash: String = SecureHash.randomSHA256().toString(), protected fun certificateData(certStatus: CertificateStatus = CertificateStatus.VALID,
certStatus: CertificateStatus = CertificateStatus.VALID,
certPath: CertPath = mock()): CertificateData { certPath: CertPath = mock()): CertificateData {
return CertificateData( return CertificateData(
publicKeyHash = publicKeyHash,
certStatus = certStatus, certStatus = certStatus,
certPath = certPath certPath = certPath
) )

View File

@ -19,6 +19,7 @@ import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import java.security.KeyPair import java.security.KeyPair
import java.security.cert.CertPath
import java.util.* import java.util.*
import javax.security.auth.x500.X500Principal import javax.security.auth.x500.X500Principal
import kotlin.test.* import kotlin.test.*
@ -69,7 +70,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
@Test @Test
fun `sign request`() { fun `sign request`() {
val (csr, _) = createRequest("LegalName") val (csr, nodeKeyPair) = createRequest("LegalName")
// Add request to DB. // Add request to DB.
val requestId = storage.saveRequest(csr) val requestId = storage.saveRequest(csr)
// New request should equals to 1. // New request should equals to 1.
@ -86,11 +87,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
// Sign certificate // Sign certificate
storage.putCertificatePath( storage.putCertificatePath(
requestId, requestId,
JcaPKCS10CertificationRequest(csr).run { generateSignedCertPath(csr, nodeKeyPair),
// TODO We need a utility in InternalUtils for converting X500Name -> CordaX500Name
val (rootCa, intermediateCa, nodeCa) = createDevNodeCaCertPath(CordaX500Name.build(X500Principal(subject.encoded)))
buildCertPath(nodeCa.certificate, intermediateCa.certificate, rootCa.certificate)
},
listOf(DOORMAN_SIGNATURE) listOf(DOORMAN_SIGNATURE)
) )
// Check request is ready // Check request is ready
@ -99,33 +96,51 @@ class PersistentCertificateRequestStorageTest : TestBase() {
@Test @Test
fun `sign request ignores subsequent sign requests`() { fun `sign request ignores subsequent sign requests`() {
val (csr, _) = createRequest("LegalName") val (csr, nodeKeyPair) = createRequest("LegalName")
// Add request to DB. // Add request to DB.
val requestId = storage.saveRequest(csr) val requestId = storage.saveRequest(csr)
// Store certificate to DB. // Store certificate to DB.
storage.markRequestTicketCreated(requestId) storage.markRequestTicketCreated(requestId)
storage.approveRequest(requestId, DOORMAN_SIGNATURE) storage.approveRequest(requestId, DOORMAN_SIGNATURE)
// Sign certificate
storage.putCertificatePath( storage.putCertificatePath(
requestId, requestId,
JcaPKCS10CertificationRequest(csr).run { generateSignedCertPath(csr, nodeKeyPair),
val (rootCa, intermediateCa, nodeCa) = createDevNodeCaCertPath(CordaX500Name.build(X500Principal(subject.encoded)))
buildCertPath(nodeCa.certificate, intermediateCa.certificate, rootCa.certificate)
},
listOf(DOORMAN_SIGNATURE) listOf(DOORMAN_SIGNATURE)
) )
// Sign certificate
// When subsequent signature requested // When subsequent signature requested
assertFailsWith(IllegalArgumentException::class) { assertFailsWith(IllegalArgumentException::class) {
storage.putCertificatePath( storage.putCertificatePath(
requestId, requestId,
JcaPKCS10CertificationRequest(csr).run { generateSignedCertPath(csr, nodeKeyPair),
val (rootCa, intermediateCa, nodeCa) = createDevNodeCaCertPath(CordaX500Name.build(X500Principal(subject.encoded)))
buildCertPath(nodeCa.certificate, intermediateCa.certificate, rootCa.certificate)
},
listOf(DOORMAN_SIGNATURE)) listOf(DOORMAN_SIGNATURE))
} }
} }
@Test
fun `sign request rejects requests with the same public key`() {
val (csr, nodeKeyPair) = createRequest("LegalName")
// Add request to DB.
val requestId = storage.saveRequest(csr)
// Store certificate to DB.
storage.markRequestTicketCreated(requestId)
storage.approveRequest(requestId, DOORMAN_SIGNATURE)
// Sign certificate
storage.putCertificatePath(
requestId,
generateSignedCertPath(csr, nodeKeyPair),
listOf(DOORMAN_SIGNATURE)
)
// Sign certificate
// When request with the same public key is requested
val (newCsr, _) = createRequest("NewLegalName", nodeKeyPair)
val duplicateRequestId = storage.saveRequest(newCsr)
assertThat(storage.getRequests(RequestStatus.NEW)).isEmpty()
val duplicateRequest = storage.getRequest(duplicateRequestId)
assertThat(duplicateRequest!!.status).isEqualTo(RequestStatus.REJECTED)
assertThat(duplicateRequest.remark).isEqualTo("Duplicate public key")
}
@Test @Test
fun `reject request`() { fun `reject request`() {
val requestId = storage.saveRequest(createRequest("BankA").first) val requestId = storage.saveRequest(createRequest("BankA").first)
@ -159,18 +174,14 @@ class PersistentCertificateRequestStorageTest : TestBase() {
@Test @Test
fun `request with the same legal name as a previously signed request`() { fun `request with the same legal name as a previously signed request`() {
val csr = createRequest("BankA").first val (csr, nodeKeyPair) = createRequest("BankA")
val requestId = storage.saveRequest(csr) val requestId = storage.saveRequest(csr)
storage.markRequestTicketCreated(requestId) storage.markRequestTicketCreated(requestId)
storage.approveRequest(requestId, DOORMAN_SIGNATURE) storage.approveRequest(requestId, DOORMAN_SIGNATURE)
// Sign certificate // Sign certificate
storage.putCertificatePath( storage.putCertificatePath(
requestId, requestId,
JcaPKCS10CertificationRequest(csr).run { generateSignedCertPath(csr, nodeKeyPair),
// TODO We need a utility in InternalUtils for converting X500Name -> CordaX500Name
val (rootCa, intermediateCa, nodeCa) = createDevNodeCaCertPath(CordaX500Name.build(X500Principal(subject.encoded)))
buildCertPath(nodeCa.certificate, intermediateCa.certificate, rootCa.certificate)
},
listOf(DOORMAN_SIGNATURE) listOf(DOORMAN_SIGNATURE)
) )
val rejectedRequestId = storage.saveRequest(createRequest("BankA").first) val rejectedRequestId = storage.saveRequest(createRequest("BankA").first)
@ -215,6 +226,14 @@ class PersistentCertificateRequestStorageTest : TestBase() {
} }
} }
private fun generateSignedCertPath(csr: PKCS10CertificationRequest, keyPair: KeyPair): CertPath {
return JcaPKCS10CertificationRequest(csr).run {
// TODO We need a utility in InternalUtils for converting X500Name -> CordaX500Name
val (rootCa, intermediateCa, nodeCa) = createDevNodeCaCertPath(CordaX500Name.build(X500Principal(subject.encoded)), keyPair)
buildCertPath(nodeCa.certificate, intermediateCa.certificate, rootCa.certificate)
}
}
private fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().toString()): Properties { private fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().toString()): Properties {
val props = Properties() val props = Properties()
props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource") props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource")
@ -225,8 +244,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
} }
} }
internal fun createRequest(organisation: String): Pair<PKCS10CertificationRequest, KeyPair> { internal fun createRequest(organisation: String, keyPair: KeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)): Pair<PKCS10CertificationRequest, KeyPair> {
val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val request = X509Utilities.createCertificateSigningRequest(X500Principal("O=$organisation,L=London,C=GB"), "my@mail.com", keyPair) val request = X509Utilities.createCertificateSigningRequest(X500Principal("O=$organisation,L=London,C=GB"), "my@mail.com", keyPair)
return Pair(request, keyPair) return Pair(request, keyPair)
} }

View File

@ -5,13 +5,11 @@ import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.signWithCert import net.corda.core.internal.signWithCert
import net.corda.nodeapi.internal.createDevNetworkMapCa 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.network.NetworkMap import net.corda.nodeapi.internal.network.NetworkMap
import net.corda.nodeapi.internal.network.verifiedNetworkMapCert 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
import net.corda.testing.internal.TestNodeInfoBuilder
import net.corda.testing.internal.createDevIntermediateCaCertPath import net.corda.testing.internal.createDevIntermediateCaCertPath
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
@ -50,7 +48,7 @@ class PersistentNetworkMapStorageTest : TestBase() {
fun `saveNetworkMap and saveNetworkParameters create current network map and parameters`() { fun `saveNetworkMap and saveNetworkParameters create current network map and parameters`() {
// given // given
// Create node info. // Create node info.
val signedNodeInfo = createValidSignedNodeInfo("Test") val (signedNodeInfo) = createValidSignedNodeInfo("Test", requestStorage)
val nodeInfoHash = nodeInfoStorage.putNodeInfo(signedNodeInfo) val nodeInfoHash = nodeInfoStorage.putNodeInfo(signedNodeInfo)
val networkParameters = testNetworkParameters(emptyList()) val networkParameters = testNetworkParameters(emptyList())
@ -65,7 +63,7 @@ class PersistentNetworkMapStorageTest : TestBase() {
// then // then
val persistedSignedNetworkMap = networkMapStorage.getCurrentNetworkMap() val persistedSignedNetworkMap = networkMapStorage.getCurrentNetworkMap()
val persistedSignedParameters = networkMapStorage.getCurrentSignedNetworkParameters() val persistedSignedParameters = networkMapStorage.getNetworkParametersOfNetworkMap()
assertEquals(networkParameters, persistedSignedParameters?.verifiedNetworkMapCert(rootCaCert)) assertEquals(networkParameters, persistedSignedParameters?.verifiedNetworkMapCert(rootCaCert))
assertEquals(parametersSignature, persistedSignedParameters?.sig) assertEquals(parametersSignature, persistedSignedParameters?.sig)
@ -84,13 +82,13 @@ class PersistentNetworkMapStorageTest : TestBase() {
networkMapStorage.saveNetworkParameters(params2, null) networkMapStorage.saveNetworkParameters(params2, null)
// when // when
val latest = networkMapStorage.getLatestUnsignedNetworkParameters() val latest = networkMapStorage.getLatestNetworkParameters()?.minimumPlatformVersion
// then // then
assertEquals(2, latest.minimumPlatformVersion) assertEquals(2, latest)
} }
@Test @Test
fun `getCurrentNetworkParameters returns current network map parameters`() { fun `getNetworkParametersOfNetworkMap returns current network map parameters`() {
// given // given
// Create network parameters // Create network parameters
val testParameters1 = testNetworkParameters(emptyList()) val testParameters1 = testNetworkParameters(emptyList())
@ -107,7 +105,7 @@ class PersistentNetworkMapStorageTest : TestBase() {
networkMapStorage.saveNetworkParameters(testParameters2, testParameters2.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate).sig) networkMapStorage.saveNetworkParameters(testParameters2, testParameters2.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate).sig)
// when // when
val result = networkMapStorage.getCurrentSignedNetworkParameters()?.verifiedNetworkMapCert(rootCaCert) val result = networkMapStorage.getNetworkParametersOfNetworkMap()?.verifiedNetworkMapCert(rootCaCert)
// then // then
assertEquals(1, result?.minimumPlatformVersion) assertEquals(1, result?.minimumPlatformVersion)
@ -117,8 +115,8 @@ class PersistentNetworkMapStorageTest : TestBase() {
fun `getValidNodeInfoHashes returns only valid and signed node info hashes`() { fun `getValidNodeInfoHashes returns only valid and signed node info hashes`() {
// given // given
// Create node infos. // Create node infos.
val signedNodeInfoA = createValidSignedNodeInfo("TestA") val (signedNodeInfoA) = createValidSignedNodeInfo("TestA", requestStorage)
val signedNodeInfoB = createValidSignedNodeInfo("TestB") val (signedNodeInfoB) = createValidSignedNodeInfo("TestB", requestStorage)
// Put signed node info data // Put signed node info data
val nodeInfoHashA = nodeInfoStorage.putNodeInfo(signedNodeInfoA) val nodeInfoHashA = nodeInfoStorage.putNodeInfo(signedNodeInfoA)
@ -139,15 +137,4 @@ class PersistentNetworkMapStorageTest : TestBase() {
// then // then
assertThat(validNodeInfoHash).containsOnly(nodeInfoHashA, nodeInfoHashB) assertThat(validNodeInfoHash).containsOnly(nodeInfoHashA, nodeInfoHashB)
} }
private fun createValidSignedNodeInfo(organisation: String): NodeInfoWithSigned {
val nodeInfoBuilder = TestNodeInfoBuilder()
val requestId = requestStorage.saveRequest(createRequest(organisation).first)
requestStorage.markRequestTicketCreated(requestId)
requestStorage.approveRequest(requestId, "TestUser")
val (identity) = nodeInfoBuilder.addIdentity(CordaX500Name(organisation, "London", "GB"))
val nodeCaCertPath = X509CertificateFactory().generateCertPath(identity.certPath.certificates.drop(1))
requestStorage.putCertificatePath(requestId, nodeCaCertPath, emptyList())
return NodeInfoWithSigned(nodeInfoBuilder.buildWithSigned().second)
}
} }

View File

@ -9,7 +9,6 @@ import net.corda.core.identity.CordaX500Name
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
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.CertificateType
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.crypto.X509Utilities
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
@ -23,6 +22,7 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
import java.security.PrivateKey import java.security.PrivateKey
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import javax.security.auth.x500.X500Principal
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertNotNull import kotlin.test.assertNotNull
import kotlin.test.assertNull import kotlin.test.assertNull
@ -83,8 +83,8 @@ class PersistentNodeInfoStorageTest : TestBase() {
@Test @Test
fun `getNodeInfo returns persisted SignedNodeInfo using the hash of just the NodeInfo`() { fun `getNodeInfo returns persisted SignedNodeInfo using the hash of just the NodeInfo`() {
// given // given
val (nodeA) = createValidSignedNodeInfo("TestA") val (nodeA) = createValidSignedNodeInfo("TestA", requestStorage)
val (nodeB) = createValidSignedNodeInfo("TestB") val (nodeB) = createValidSignedNodeInfo("TestB", requestStorage)
// Put signed node info data // Put signed node info data
nodeInfoStorage.putNodeInfo(nodeA) nodeInfoStorage.putNodeInfo(nodeA)
@ -102,7 +102,7 @@ class PersistentNodeInfoStorageTest : TestBase() {
@Test @Test
fun `same public key with different node info`() { fun `same public key with different node info`() {
// Create node info. // Create node info.
val (node1, key) = createValidSignedNodeInfo("Test", serial = 1) val (node1, key) = createValidSignedNodeInfo("Test", requestStorage)
val nodeInfo2 = node1.nodeInfo.copy(serial = 2) val nodeInfo2 = node1.nodeInfo.copy(serial = 2)
val node2 = NodeInfoWithSigned(nodeInfo2.signWith(listOf(key))) val node2 = NodeInfoWithSigned(nodeInfo2.signWith(listOf(key)))
@ -120,7 +120,7 @@ class PersistentNodeInfoStorageTest : TestBase() {
@Test @Test
fun `putNodeInfo persists SignedNodeInfo with its signature`() { fun `putNodeInfo persists SignedNodeInfo with its signature`() {
// given // given
val (nodeInfoWithSigned) = createValidSignedNodeInfo("Test") val (nodeInfoWithSigned) = createValidSignedNodeInfo("Test", requestStorage)
// when // when
val nodeInfoHash = nodeInfoStorage.putNodeInfo(nodeInfoWithSigned) val nodeInfoHash = nodeInfoStorage.putNodeInfo(nodeInfoWithSigned)
@ -129,16 +129,17 @@ class PersistentNodeInfoStorageTest : TestBase() {
val persistedSignedNodeInfo = nodeInfoStorage.getNodeInfo(nodeInfoHash) val persistedSignedNodeInfo = nodeInfoStorage.getNodeInfo(nodeInfoHash)
assertThat(persistedSignedNodeInfo?.signatures).isEqualTo(nodeInfoWithSigned.signedNodeInfo.signatures) assertThat(persistedSignedNodeInfo?.signatures).isEqualTo(nodeInfoWithSigned.signedNodeInfo.signatures)
} }
}
private fun createValidSignedNodeInfo(organisation: String, serial: Long = 1): Pair<NodeInfoWithSigned, PrivateKey> { internal fun createValidSignedNodeInfo(organisation: String,
val nodeInfoBuilder = TestNodeInfoBuilder() storage: CertificationRequestStorage): Pair<NodeInfoWithSigned, PrivateKey> {
val requestId = requestStorage.saveRequest(createRequest(organisation).first) val (csr, nodeKeyPair) = createRequest(organisation)
requestStorage.markRequestTicketCreated(requestId) val requestId = storage.saveRequest(csr)
requestStorage.approveRequest(requestId, "TestUser") storage.markRequestTicketCreated(requestId)
val (identity, key) = nodeInfoBuilder.addIdentity(CordaX500Name(organisation, "London", "GB")) storage.approveRequest(requestId, "TestUser")
val nodeCaCertPath = X509CertificateFactory().generateCertPath(identity.certPath.certificates.drop(1)) val nodeInfoBuilder = TestNodeInfoBuilder()
requestStorage.putCertificatePath(requestId, nodeCaCertPath, emptyList()) val (identity, key) = nodeInfoBuilder.addIdentity(CordaX500Name.build(X500Principal(csr.subject.encoded)), nodeKeyPair)
val (_, signedNodeInfo) = nodeInfoBuilder.buildWithSigned(serial) storage.putCertificatePath(requestId, identity.certPath, listOf("Test"))
return Pair(NodeInfoWithSigned(signedNodeInfo), key) val (_, signedNodeInfo) = nodeInfoBuilder.buildWithSigned(1)
} return Pair(NodeInfoWithSigned(signedNodeInfo), key)
} }

View File

@ -51,8 +51,8 @@ class NetworkMapSignerTest : TestBase() {
val signedNetworkMap = networkMap.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate) val signedNetworkMap = networkMap.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)
whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(signedNetworkMap) whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(signedNetworkMap)
whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(signedNodeInfoHashes) whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(signedNodeInfoHashes)
whenever(networkMapStorage.getLatestUnsignedNetworkParameters()).thenReturn(latestNetworkParameters) whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(latestNetworkParameters)
whenever(networkMapStorage.getCurrentSignedNetworkParameters()).thenReturn(currentParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)) whenever(networkMapStorage.getNetworkParametersOfNetworkMap()).thenReturn(currentParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate))
whenever(signer.signBytes(any())).then { whenever(signer.signBytes(any())).then {
DigitalSignatureWithCert(networkMapCa.certificate, Crypto.doSign(networkMapCa.keyPair.private, it.arguments[0] as ByteArray)) DigitalSignatureWithCert(networkMapCa.certificate, Crypto.doSign(networkMapCa.keyPair.private, it.arguments[0] as ByteArray))
} }
@ -67,8 +67,8 @@ class NetworkMapSignerTest : TestBase() {
// then // then
// Verify networkMapStorage calls // Verify networkMapStorage calls
verify(networkMapStorage).getNodeInfoHashes(any()) verify(networkMapStorage).getNodeInfoHashes(any())
verify(networkMapStorage).getLatestUnsignedNetworkParameters() verify(networkMapStorage).getLatestNetworkParameters()
verify(networkMapStorage).getCurrentSignedNetworkParameters() verify(networkMapStorage).getNetworkParametersOfNetworkMap()
argumentCaptor<SignedNetworkMap>().apply { argumentCaptor<SignedNetworkMap>().apply {
verify(networkMapStorage).saveNetworkMap(capture()) verify(networkMapStorage).saveNetworkMap(capture())
val capturedNetworkMap = firstValue.verifiedNetworkMapCert(rootCaCert) val capturedNetworkMap = firstValue.verifiedNetworkMapCert(rootCaCert)
@ -87,8 +87,8 @@ class NetworkMapSignerTest : TestBase() {
val signedNetworkMap = networkMap.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate) val signedNetworkMap = networkMap.signWithCert(networkMapCa.keyPair.private, networkMapCa.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.getLatestUnsignedNetworkParameters()).thenReturn(networkParameters) whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(networkParameters)
whenever(networkMapStorage.getCurrentSignedNetworkParameters()).thenReturn(networkParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)) whenever(networkMapStorage.getNetworkParametersOfNetworkMap()).thenReturn(networkParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate))
// when // when
networkMapSigner.signNetworkMap() networkMapSigner.signNetworkMap()
@ -104,7 +104,7 @@ class NetworkMapSignerTest : TestBase() {
val networkParameters = testNetworkParameters(emptyList()) val networkParameters = testNetworkParameters(emptyList())
whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(null) whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(null)
whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(emptyList()) whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(emptyList())
whenever(networkMapStorage.getLatestUnsignedNetworkParameters()).thenReturn(networkParameters) whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(networkParameters)
whenever(signer.signBytes(any())).then { whenever(signer.signBytes(any())).then {
DigitalSignatureWithCert(networkMapCa.certificate, Crypto.doSign(networkMapCa.keyPair.private, it.arguments[0] as ByteArray)) DigitalSignatureWithCert(networkMapCa.certificate, Crypto.doSign(networkMapCa.keyPair.private, it.arguments[0] as ByteArray))
} }
@ -118,7 +118,7 @@ class NetworkMapSignerTest : TestBase() {
// then // then
// Verify networkMapStorage calls // Verify networkMapStorage calls
verify(networkMapStorage).getNodeInfoHashes(any()) verify(networkMapStorage).getNodeInfoHashes(any())
verify(networkMapStorage).getLatestUnsignedNetworkParameters() verify(networkMapStorage).getLatestNetworkParameters()
argumentCaptor<SignedNetworkMap>().apply { argumentCaptor<SignedNetworkMap>().apply {
verify(networkMapStorage).saveNetworkMap(capture()) verify(networkMapStorage).saveNetworkMap(capture())
val networkMap = firstValue.verifiedNetworkMapCert(rootCaCert) val networkMap = firstValue.verifiedNetworkMapCert(rootCaCert)

View File

@ -1,62 +0,0 @@
package com.r3.corda.networkmanage.doorman
import com.nhaarman.mockito_kotlin.any
import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.times
import com.nhaarman.mockito_kotlin.verify
import com.r3.corda.networkmanage.TestBase
import com.r3.corda.networkmanage.common.persistence.CertificateResponse
import com.r3.corda.networkmanage.common.persistence.CertificateStatus
import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage
import com.r3.corda.networkmanage.common.persistence.RequestStatus
import com.r3.corda.networkmanage.common.utils.buildCertPath
import com.r3.corda.networkmanage.doorman.signer.DefaultCsrHandler
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
import net.corda.core.crypto.Crypto
import net.corda.nodeapi.internal.crypto.X509Utilities
import org.junit.Test
import javax.security.auth.x500.X500Principal
import kotlin.test.assertEquals
class DefaultRequestProcessorTest : TestBase() {
@Test
fun `get response`() {
val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val cert = X509Utilities.createSelfSignedCACertificate(X500Principal("O=Test,L=London,C=GB"), keyPair)
val requestStorage: CertificationRequestStorage = mock {
on { getRequest("New") }.thenReturn(certificateSigningRequest())
on { getRequest("Signed") }.thenReturn(certificateSigningRequest(status = RequestStatus.SIGNED, certData = certificateData("", CertificateStatus.VALID, buildCertPath(cert))))
on { getRequest("Rejected") }.thenReturn(certificateSigningRequest(status = RequestStatus.REJECTED, remark = "Random reason"))
}
val signer: LocalSigner = mock()
val requestProcessor = DefaultCsrHandler(requestStorage, signer)
assertEquals(CertificateResponse.NotReady, requestProcessor.getResponse("random"))
assertEquals(CertificateResponse.NotReady, requestProcessor.getResponse("New"))
assertEquals(CertificateResponse.Ready(buildCertPath(cert)), requestProcessor.getResponse("Signed"))
assertEquals(CertificateResponse.Unauthorised("Random reason"), requestProcessor.getResponse("Rejected"))
}
@Test
fun `process request`() {
val (request1, request2, request3) = (1..3).map {
X509Utilities.createCertificateSigningRequest(X500Principal("O=Test1,L=London,C=GB"), "my@email.com", Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME))
}
val requestStorage: CertificationRequestStorage = mock {
on { getRequests(RequestStatus.APPROVED) }.thenReturn(listOf(
certificateSigningRequest(requestId = "1", request = request1, status = RequestStatus.APPROVED),
certificateSigningRequest(requestId = "2", request = request2, status = RequestStatus.APPROVED),
certificateSigningRequest(requestId = "3", request = request3, status = RequestStatus.APPROVED)
))
}
val signer: LocalSigner = mock()
val requestProcessor = DefaultCsrHandler(requestStorage, signer)
requestProcessor.processApprovedRequests()
verify(signer, times(3)).createSignedClientCertificate(any())
verify(requestStorage, times(1)).getRequests(any())
}
}

View File

@ -52,6 +52,5 @@ class DoormanParametersTest {
assertEquals("TD", parameter.jiraConfig?.projectCode) assertEquals("TD", parameter.jiraConfig?.projectCode)
assertEquals("username", parameter.jiraConfig?.username) assertEquals("username", parameter.jiraConfig?.username)
assertEquals("password", parameter.jiraConfig?.password) assertEquals("password", parameter.jiraConfig?.password)
assertEquals(41, parameter.jiraConfig?.doneTransitionCode)
} }
} }

View File

@ -0,0 +1,55 @@
package com.r3.corda.networkmanage.doorman
import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory
import com.r3.corda.networkmanage.common.utils.buildCertPath
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name
import net.corda.nodeapi.internal.crypto.X509Utilities
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import java.net.URI
@Ignore
// This is manual test for testing Jira API.
class JiraClientTest {
private lateinit var jiraClient: JiraClient
@Before
fun init() {
val jiraWebAPI = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI("http://jira.url.com"), "username", "password")
jiraClient = JiraClient(jiraWebAPI, "DOOR")
}
@Test
fun createRequestTicket() {
val request = X509Utilities.createCertificateSigningRequest(CordaX500Name("JiraAPITest", "R3 Ltd 3", "London", "GB").x500Principal, "test@test.com", Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME))
jiraClient.createRequestTicket(SecureHash.randomSHA256().toString(), request)
}
@Test
fun getApprovedRequests() {
jiraClient.getApprovedRequests().forEach { println(it) }
}
@Test
fun getRejectedRequests() {
val requests = jiraClient.getRejectedRequests()
requests.forEach { println(it) }
}
@Test
fun updateSignedRequests() {
val requests = jiraClient.getApprovedRequests()
val selfSignedCA = X509Utilities.createSelfSignedCACertificate(CordaX500Name("test", "london", "GB").x500Principal, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME))
jiraClient.updateSignedRequests(requests.map { it.requestId to buildCertPath(selfSignedCA) }.toMap())
}
@Test
fun updateRejectedRequests() {
val requests = jiraClient.getRejectedRequests()
jiraClient.updateRejectedRequests(requests.map { it.requestId })
assert(jiraClient.getRejectedRequests().isEmpty())
}
}

View File

@ -1,87 +0,0 @@
package com.r3.corda.networkmanage.doorman.signer
import com.nhaarman.mockito_kotlin.*
import com.r3.corda.networkmanage.common.persistence.CertificateResponse
import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequest
import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage
import com.r3.corda.networkmanage.common.persistence.RequestStatus
import com.r3.corda.networkmanage.doorman.JiraClient
import net.corda.core.crypto.Crypto
import net.corda.core.identity.CordaX500Name
import net.corda.nodeapi.internal.crypto.X509Utilities
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mock
import org.mockito.junit.MockitoJUnit
import java.security.cert.CertPath
class JiraCsrHandlerTest {
@Rule
@JvmField
val mockitoRule = MockitoJUnit.rule()
@Mock
lateinit var jiraClient: JiraClient
@Mock
lateinit var certificationRequestStorage: CertificationRequestStorage
@Mock
lateinit var defaultCsrHandler: DefaultCsrHandler
@Mock
var certPath: CertPath = mock()
private lateinit var jiraCsrHandler: JiraCsrHandler
private val requestId = "id"
private lateinit var certificateResponse: CertificateResponse.Ready
private val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
private val pkcS10CertificationRequest = X509Utilities.createCertificateSigningRequest(
CordaX500Name(locality = "London", organisation = "LegalName", country = "GB").x500Principal,
"my@mail.com",
keyPair)
@Before
fun setup() {
jiraCsrHandler = JiraCsrHandler(jiraClient, certificationRequestStorage, defaultCsrHandler)
certificateResponse = CertificateResponse.Ready(certPath)
}
@Test
fun `If jira connection fails we don't mark the ticket as created`() {
whenever(defaultCsrHandler.saveRequest(any())).thenReturn(requestId)
whenever(defaultCsrHandler.getResponse(requestId)).thenReturn(certificateResponse)
whenever(jiraClient.createRequestTicket(eq(requestId), any())).thenThrow(IllegalStateException("something broke"))
// Test
jiraCsrHandler.saveRequest(pkcS10CertificationRequest)
verify(certificationRequestStorage, never()).markRequestTicketCreated(requestId)
}
@Test
fun `If jira connection works we mark the ticket as created`() {
whenever(defaultCsrHandler.saveRequest(any())).thenReturn(requestId)
whenever(defaultCsrHandler.getResponse(requestId)).thenReturn(certificateResponse)
// Test
jiraCsrHandler.saveRequest(pkcS10CertificationRequest)
verify(certificationRequestStorage, times(1)).markRequestTicketCreated(requestId)
}
@Test
fun `create tickets`() {
val csr = CertificateSigningRequest(requestId, "name", RequestStatus.NEW, pkcS10CertificationRequest, null, emptyList(), null)
whenever(certificationRequestStorage.getRequests(RequestStatus.NEW)).thenReturn(listOf(csr))
// Test
jiraCsrHandler.createTickets()
verify(jiraClient).createRequestTicket(requestId, csr.request)
verify(certificationRequestStorage).markRequestTicketCreated(requestId)
}
}

View File

@ -0,0 +1,85 @@
package com.r3.corda.networkmanage.doorman.signer
import com.nhaarman.mockito_kotlin.*
import com.r3.corda.networkmanage.TestBase
import com.r3.corda.networkmanage.common.persistence.CertificateResponse
import com.r3.corda.networkmanage.common.persistence.CertificateStatus
import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage
import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage.Companion.DOORMAN_SIGNATURE
import com.r3.corda.networkmanage.common.persistence.RequestStatus
import com.r3.corda.networkmanage.common.utils.CertPathAndKey
import com.r3.corda.networkmanage.common.utils.buildCertPath
import net.corda.core.crypto.Crypto
import net.corda.core.internal.CertRole
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.testing.internal.createDevIntermediateCaCertPath
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import java.security.cert.CertPath
import java.security.cert.X509Certificate
import javax.security.auth.x500.X500Principal
import kotlin.test.assertEquals
class DefaultCsrHandlerTest : TestBase() {
@Test
fun getResponse() {
val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val cert = X509Utilities.createSelfSignedCACertificate(X500Principal("O=Test,L=London,C=GB"), keyPair)
val requestStorage: CertificationRequestStorage = mock {
on { getRequest("New") }.thenReturn(certificateSigningRequest())
on { getRequest("Signed") }.thenReturn(certificateSigningRequest(status = RequestStatus.SIGNED, certData = certificateData(CertificateStatus.VALID, buildCertPath(cert))))
on { getRequest("Rejected") }.thenReturn(certificateSigningRequest(status = RequestStatus.REJECTED, remark = "Random reason"))
}
val requestProcessor = DefaultCsrHandler(requestStorage, null)
assertEquals(CertificateResponse.NotReady, requestProcessor.getResponse("random"))
assertEquals(CertificateResponse.NotReady, requestProcessor.getResponse("New"))
assertEquals(CertificateResponse.Ready(buildCertPath(cert)), requestProcessor.getResponse("Signed"))
assertEquals(CertificateResponse.Unauthorised("Random reason"), requestProcessor.getResponse("Rejected"))
}
@Test
fun processApprovedRequests() {
val requests = (1..3).map {
X509Utilities.createCertificateSigningRequest(
X500Principal("O=Test$it,L=London,C=GB"),
"my@email.com",
Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME))
}
val requestStorage: CertificationRequestStorage = mock {
on { getRequests(RequestStatus.APPROVED) }.thenReturn(listOf(
certificateSigningRequest(requestId = "1", request = requests[0], status = RequestStatus.APPROVED),
certificateSigningRequest(requestId = "2", request = requests[1], status = RequestStatus.APPROVED)
))
on { getRequests(RequestStatus.REJECTED) }.thenReturn(listOf(
certificateSigningRequest(requestId = "3", request = requests[2], status = RequestStatus.REJECTED)
))
}
val (rootCa, csrCa) = createDevIntermediateCaCertPath()
val csrCertPathAndKey = CertPathAndKey(listOf(csrCa.certificate, rootCa.certificate), csrCa.keyPair.private)
val requestProcessor = DefaultCsrHandler(requestStorage, csrCertPathAndKey)
requestProcessor.processRequests()
val certPathCapture = argumentCaptor<CertPath>()
// Verify only the approved requests are taken
verify(requestStorage, times(1)).getRequests(RequestStatus.APPROVED)
verify(requestStorage, times(1)).putCertificatePath(eq("1"), certPathCapture.capture(), eq(listOf(DOORMAN_SIGNATURE)))
verify(requestStorage, times(1)).putCertificatePath(eq("2"), certPathCapture.capture(), eq(listOf(DOORMAN_SIGNATURE)))
// Then make sure the generated node cert paths are correct
certPathCapture.allValues.forEachIndexed { index, certPath ->
X509Utilities.validateCertificateChain(rootCa.certificate, *certPath.certificates.toTypedArray())
assertThat(certPath.certificates).hasSize(3).element(1).isEqualTo(csrCa.certificate)
(certPath.certificates[0] as X509Certificate).apply {
assertThat(CertRole.extract(this)).isEqualTo(CertRole.NODE_CA)
assertThat(publicKey).isEqualTo(Crypto.toSupportedPublicKey(requests[index].subjectPublicKeyInfo))
assertThat(subjectX500Principal).isEqualTo(X500Principal("O=Test${index + 1},L=London,C=GB"))
}
}
}
}

View File

@ -0,0 +1,151 @@
package com.r3.corda.networkmanage.doorman.signer
import com.nhaarman.mockito_kotlin.*
import com.r3.corda.networkmanage.TestBase
import com.r3.corda.networkmanage.common.persistence.*
import com.r3.corda.networkmanage.doorman.ApprovedRequest
import com.r3.corda.networkmanage.doorman.JiraClient
import com.r3.corda.networkmanage.doorman.RejectedRequest
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name
import net.corda.nodeapi.internal.crypto.X509Utilities
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mock
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import java.security.cert.CertPath
import kotlin.test.assertEquals
class JiraCsrHandlerTest : TestBase() {
@Rule
@JvmField
val mockitoRule: MockitoRule = MockitoJUnit.rule()
@Mock
private lateinit var jiraClient: JiraClient
@Mock
private lateinit var certificationRequestStorage: CertificationRequestStorage
@Mock
private lateinit var defaultCsrHandler: DefaultCsrHandler
@Mock
private val certPath: CertPath = mock()
private lateinit var jiraCsrHandler: JiraCsrHandler
private val requestId = "id"
private lateinit var certificateResponse: CertificateResponse.Ready
private val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
private val pkcS10CertificationRequest = X509Utilities.createCertificateSigningRequest(
CordaX500Name(locality = "London", organisation = "LegalName", country = "GB").x500Principal,
"my@mail.com",
keyPair)
@Before
fun setup() {
jiraCsrHandler = JiraCsrHandler(jiraClient, certificationRequestStorage, defaultCsrHandler)
certificateResponse = CertificateResponse.Ready(certPath)
}
@Test
fun `If jira connection fails we don't mark the ticket as created`() {
whenever(defaultCsrHandler.saveRequest(any())).thenReturn(requestId)
whenever(defaultCsrHandler.getResponse(requestId)).thenReturn(certificateResponse)
whenever(jiraClient.createRequestTicket(eq(requestId), any())).thenThrow(IllegalStateException("something broke"))
// Test
jiraCsrHandler.saveRequest(pkcS10CertificationRequest)
verify(certificationRequestStorage, never()).markRequestTicketCreated(requestId)
}
@Test
fun `If jira connection works we mark the ticket as created`() {
whenever(defaultCsrHandler.saveRequest(any())).thenReturn(requestId)
whenever(defaultCsrHandler.getResponse(requestId)).thenReturn(certificateResponse)
// Test
jiraCsrHandler.saveRequest(pkcS10CertificationRequest)
verify(certificationRequestStorage, times(1)).markRequestTicketCreated(requestId)
}
@Test
fun `create tickets`() {
val csr = certificateSigningRequest(
requestId = requestId,
legalName = "name",
status = RequestStatus.NEW,
request = pkcS10CertificationRequest)
whenever(certificationRequestStorage.getRequests(RequestStatus.NEW)).thenReturn(listOf(csr))
// Test
jiraCsrHandler.processRequests()
verify(jiraClient).createRequestTicket(requestId, csr.request)
verify(certificationRequestStorage).markRequestTicketCreated(requestId)
}
@Test
fun `sync tickets status`() {
val id1 = SecureHash.randomSHA256().toString()
val id2 = SecureHash.randomSHA256().toString()
val csr1 = CertificateSigningRequest(id1, "name1", SecureHash.randomSHA256(), RequestStatus.NEW, pkcS10CertificationRequest, null, emptyList(), null)
val csr2 = CertificateSigningRequest(id2, "name2", SecureHash.randomSHA256(), RequestStatus.NEW, pkcS10CertificationRequest, null, emptyList(), null)
val requests = mutableMapOf(id1 to csr1, id2 to csr2)
// Mocking storage behaviour.
whenever(certificationRequestStorage.getRequests(RequestStatus.NEW)).thenReturn(requests.values.filter { it.status == RequestStatus.NEW })
whenever(certificationRequestStorage.getRequest(any())).thenAnswer { requests[it.getArgument(0)] }
whenever(certificationRequestStorage.approveRequest(any(), any())).then {
val id = it.getArgument<String>(0)
if (requests[id]?.status == RequestStatus.NEW) {
requests[id] = requests[id]!!.copy(status = RequestStatus.APPROVED, modifiedBy = listOf(it.getArgument(1)))
}
null
}
whenever(certificationRequestStorage.rejectRequest(any(), any(), any())).then {
val id = it.getArgument<String>(0)
requests[id] = requests[id]!!.copy(status = RequestStatus.REJECTED, modifiedBy = listOf(it.getArgument(1)), remark = it.getArgument(2))
null
}
// Status change from jira.
whenever(jiraClient.getApprovedRequests()).thenReturn(listOf(ApprovedRequest(id1, "Me")))
whenever(jiraClient.getRejectedRequests()).thenReturn(listOf(RejectedRequest(id2, "Me", "Test reject")))
// Test.
jiraCsrHandler.processRequests()
verify(jiraClient).createRequestTicket(id1, csr1.request)
verify(jiraClient).createRequestTicket(id2, csr2.request)
verify(certificationRequestStorage).markRequestTicketCreated(id1)
verify(certificationRequestStorage).markRequestTicketCreated(id2)
// Verify request has the correct status in DB.
assertEquals(RequestStatus.APPROVED, requests[id1]!!.status)
assertEquals(RequestStatus.REJECTED, requests[id2]!!.status)
// Verify jira client get the correct call.
verify(jiraClient).updateRejectedRequests(listOf(id2))
verify(jiraClient).updateSignedRequests(emptyMap())
// Sign request 1
val certPath = mock<CertPath>()
val certData = CertificateData(CertificateStatus.VALID, certPath)
requests[id1] = requests[id1]!!.copy(status = RequestStatus.SIGNED, certData = certData)
// Process request again.
jiraCsrHandler.processRequests()
// Update signed request should be called.
verify(jiraClient).updateSignedRequests(mapOf(id1 to certPath))
}
}

View File

@ -1,4 +1,4 @@
package com.r3.corda.networkmanage.doorman package com.r3.corda.networkmanage.doorman.webservice
import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.times import com.nhaarman.mockito_kotlin.times
@ -7,7 +7,8 @@ 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.SignedNetworkMap import com.r3.corda.networkmanage.common.utils.SignedNetworkMap
import com.r3.corda.networkmanage.common.utils.SignedNetworkParameters import com.r3.corda.networkmanage.common.utils.SignedNetworkParameters
import com.r3.corda.networkmanage.doorman.webservice.NodeInfoWebService import com.r3.corda.networkmanage.doorman.NetworkManagementWebServer
import com.r3.corda.networkmanage.doorman.NetworkMapConfig
import net.corda.core.crypto.SecureHash.Companion.randomSHA256 import net.corda.core.crypto.SecureHash.Companion.randomSHA256
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.checkOkResponse import net.corda.core.internal.checkOkResponse
@ -36,7 +37,7 @@ 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
class NodeInfoWebServiceTest { class NetworkMapWebServiceTest {
@Rule @Rule
@JvmField @JvmField
val testSerialization = SerializationEnvironmentRule(true) val testSerialization = SerializationEnvironmentRule(true)
@ -56,12 +57,12 @@ class NodeInfoWebServiceTest {
@Test @Test
fun `submit nodeInfo`() { fun `submit nodeInfo`() {
val networkMapStorage: NetworkMapStorage = mock { val networkMapStorage: NetworkMapStorage = mock {
on { getCurrentSignedNetworkParameters() }.thenReturn(testNetworkParameters(emptyList()).signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)) on { getNetworkParametersOfNetworkMap() }.thenReturn(testNetworkParameters(emptyList()).signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate))
} }
// Create node info. // Create node info.
val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB")) val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB"))
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use {
it.start() it.start()
val nodeInfoAndSignature = signedNodeInfo.serialize().bytes val nodeInfoAndSignature = signedNodeInfo.serialize().bytes
// Post node info and signature to doorman, this should pass without any exception. // Post node info and signature to doorman, this should pass without any exception.
@ -72,12 +73,12 @@ class NodeInfoWebServiceTest {
@Test @Test
fun `submit old nodeInfo`() { fun `submit old nodeInfo`() {
val networkMapStorage: NetworkMapStorage = mock { val networkMapStorage: NetworkMapStorage = mock {
on { getCurrentSignedNetworkParameters() }.thenReturn(testNetworkParameters(emptyList(), minimumPlatformVersion = 2).signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)) on { getNetworkParametersOfNetworkMap() }.thenReturn(testNetworkParameters(emptyList(), minimumPlatformVersion = 2).signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate))
} }
// Create node info. // Create node info.
val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB"), platformVersion = 1) val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB"), platformVersion = 1)
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use {
it.start() it.start()
val nodeInfoAndSignature = signedNodeInfo.serialize().bytes val nodeInfoAndSignature = signedNodeInfo.serialize().bytes
assertThatThrownBy { it.doPost("publish", nodeInfoAndSignature) } assertThatThrownBy { it.doPost("publish", nodeInfoAndSignature) }
@ -88,12 +89,12 @@ class NodeInfoWebServiceTest {
@Test @Test
fun `submit nodeInfo when no network parameters`() { fun `submit nodeInfo when no network parameters`() {
val networkMapStorage: NetworkMapStorage = mock { val networkMapStorage: NetworkMapStorage = mock {
on { getCurrentSignedNetworkParameters() }.thenReturn(null) on { getNetworkParametersOfNetworkMap() }.thenReturn(null)
} }
// Create node info. // Create node info.
val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB"), platformVersion = 1) val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB"), platformVersion = 1)
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use {
it.start() it.start()
val nodeInfoAndSignature = signedNodeInfo.serialize().bytes val nodeInfoAndSignature = signedNodeInfo.serialize().bytes
assertThatThrownBy { it.doPost("publish", nodeInfoAndSignature) } assertThatThrownBy { it.doPost("publish", nodeInfoAndSignature) }
@ -110,7 +111,7 @@ class NodeInfoWebServiceTest {
on { getCurrentNetworkMap() }.thenReturn(signedNetworkMap) on { getCurrentNetworkMap() }.thenReturn(signedNetworkMap)
} }
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use {
it.start() it.start()
val signedNetworkMapResponse = it.doGet<SignedNetworkMap>("") val signedNetworkMapResponse = it.doGet<SignedNetworkMap>("")
verify(networkMapStorage, times(1)).getCurrentNetworkMap() verify(networkMapStorage, times(1)).getCurrentNetworkMap()
@ -127,7 +128,7 @@ class NodeInfoWebServiceTest {
on { getNodeInfo(nodeInfoHash) }.thenReturn(signedNodeInfo) on { getNodeInfo(nodeInfoHash) }.thenReturn(signedNodeInfo)
} }
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage, mock(), testNetworkMapConfig)).use { NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(nodeInfoStorage, mock(), testNetworkMapConfig)).use {
it.start() it.start()
val nodeInfoResponse = it.doGet<SignedNodeInfo>("node-info/$nodeInfoHash") val nodeInfoResponse = it.doGet<SignedNodeInfo>("node-info/$nodeInfoHash")
verify(nodeInfoStorage, times(1)).getNodeInfo(nodeInfoHash) verify(nodeInfoStorage, times(1)).getNodeInfo(nodeInfoHash)
@ -149,7 +150,7 @@ class NodeInfoWebServiceTest {
on { getSignedNetworkParameters(networkParametersHash) }.thenReturn(signedNetworkParameters) on { getSignedNetworkParameters(networkParametersHash) }.thenReturn(signedNetworkParameters)
} }
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use {
it.start() it.start()
val netParamsResponse = it.doGet<SignedNetworkParameters>("network-parameters/$networkParametersHash") val netParamsResponse = it.doGet<SignedNetworkParameters>("network-parameters/$networkParametersHash")
verify(networkMapStorage, times(1)).getSignedNetworkParameters(networkParametersHash) verify(networkMapStorage, times(1)).getSignedNetworkParameters(networkParametersHash)

View File

@ -1,11 +1,11 @@
package com.r3.corda.networkmanage.doorman package com.r3.corda.networkmanage.doorman.webservice
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.CertificateResponse import com.r3.corda.networkmanage.common.persistence.CertificateResponse
import com.r3.corda.networkmanage.common.utils.buildCertPath import com.r3.corda.networkmanage.common.utils.buildCertPath
import com.r3.corda.networkmanage.doorman.NetworkManagementWebServer
import com.r3.corda.networkmanage.doorman.signer.CsrHandler import com.r3.corda.networkmanage.doorman.signer.CsrHandler
import com.r3.corda.networkmanage.doorman.webservice.RegistrationWebService
import net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
@ -100,7 +100,7 @@ class RegistrationWebServiceTest : TestBase() {
CertificateResponse.Ready(it) CertificateResponse.Ready(it)
} ?: CertificateResponse.NotReady } ?: CertificateResponse.NotReady
} }
on { processApprovedRequests() }.then { on { processRequests() }.then {
val request = X509Utilities.createCertificateSigningRequest(subject, "my@mail.com", keyPair) val request = X509Utilities.createCertificateSigningRequest(subject, "my@mail.com", keyPair)
certificateStore[id] = JcaPKCS10CertificationRequest(request).run { certificateStore[id] = JcaPKCS10CertificationRequest(request).run {
val tlsCert = X509Utilities.createCertificate( val tlsCert = X509Utilities.createCertificate(
@ -118,7 +118,7 @@ class RegistrationWebServiceTest : TestBase() {
startSigningServer(requestProcessor) startSigningServer(requestProcessor)
assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady) assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady)
requestProcessor.processApprovedRequests() requestProcessor.processRequests()
val certificates = (pollForResponse(id) as PollResponse.Ready).certChain val certificates = (pollForResponse(id) as PollResponse.Ready).certChain
verify(requestProcessor, times(2)).getResponse(any()) verify(requestProcessor, times(2)).getResponse(any())
@ -141,7 +141,7 @@ class RegistrationWebServiceTest : TestBase() {
CertificateResponse.Ready(it) CertificateResponse.Ready(it)
} ?: CertificateResponse.NotReady } ?: CertificateResponse.NotReady
} }
on { processApprovedRequests() }.then { on { processRequests() }.then {
val request = X509Utilities.createCertificateSigningRequest( val request = X509Utilities.createCertificateSigningRequest(
CordaX500Name(locality = "London", organisation = "Legal Name", country = "GB").x500Principal, CordaX500Name(locality = "London", organisation = "Legal Name", country = "GB").x500Principal,
"my@mail.com", "my@mail.com",
@ -165,7 +165,7 @@ class RegistrationWebServiceTest : TestBase() {
startSigningServer(storage) startSigningServer(storage)
assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady) assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady)
storage.processApprovedRequests() storage.processRequests()
val certificates = (pollForResponse(id) as PollResponse.Ready).certChain val certificates = (pollForResponse(id) as PollResponse.Ready).certChain
verify(storage, times(2)).getResponse(any()) verify(storage, times(2)).getResponse(any())

View File

@ -1,11 +1,12 @@
package net.corda.nodeapi.internal package net.corda.nodeapi.internal
import net.corda.core.crypto.toStringShort
import net.corda.core.identity.Party
import net.corda.core.messaging.MessageRecipientGroup import net.corda.core.messaging.MessageRecipientGroup
import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.MessageRecipients
import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.messaging.SingleMessageRecipient
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.toBase58String
import java.security.PublicKey import java.security.PublicKey
/** /**
@ -23,7 +24,7 @@ class ArtemisMessagingComponent {
const val PEER_USER = "SystemUsers/Peer" const val PEER_USER = "SystemUsers/Peer"
const val INTERNAL_PREFIX = "internal." const val INTERNAL_PREFIX = "internal."
const val PEERS_PREFIX = "${INTERNAL_PREFIX}peers." //TODO Come up with better name for common peers/services queue const val PEERS_PREFIX = "${INTERNAL_PREFIX}peers." //TODO Come up with better name for common peers/services queue
const val P2P_QUEUE = "p2p.inbound" const val P2P_PREFIX = "p2p.inbound."
const val NOTIFICATIONS_ADDRESS = "${INTERNAL_PREFIX}activemq.notifications" const val NOTIFICATIONS_ADDRESS = "${INTERNAL_PREFIX}activemq.notifications"
} }
@ -49,7 +50,7 @@ class ArtemisMessagingComponent {
@CordaSerializable @CordaSerializable
data class NodeAddress(override val queueName: String, override val hostAndPort: NetworkHostAndPort) : ArtemisPeerAddress { data class NodeAddress(override val queueName: String, override val hostAndPort: NetworkHostAndPort) : ArtemisPeerAddress {
constructor(peerIdentity: PublicKey, hostAndPort: NetworkHostAndPort) : constructor(peerIdentity: PublicKey, hostAndPort: NetworkHostAndPort) :
this("$PEERS_PREFIX${peerIdentity.toBase58String()}", hostAndPort) this("$PEERS_PREFIX${peerIdentity.toStringShort()}", hostAndPort)
} }
/** /**
@ -62,6 +63,30 @@ class ArtemisMessagingComponent {
* @param identity The service identity's owning key. * @param identity The service identity's owning key.
*/ */
data class ServiceAddress(val identity: PublicKey) : ArtemisAddress, MessageRecipientGroup { data class ServiceAddress(val identity: PublicKey) : ArtemisAddress, MessageRecipientGroup {
override val queueName: String = "$PEERS_PREFIX${identity.toBase58String()}" override val queueName: String = "$PEERS_PREFIX${identity.toStringShort()}"
} }
/**
* [RemoteInboxAddress] implements [SingleMessageRecipient]. It represents the non-local address of a remote inbox.
* @param identity The Node public identity
*/
data class RemoteInboxAddress(val identity: PublicKey) : ArtemisAddress, SingleMessageRecipient {
constructor(party: Party) : this(party.owningKey)
companion object {
/**
* When transferring a message from the local holding queue to the remote inbox queue
* this method provides a simple translation of the address string.
* The topics are distinct so that proper segregation of internal
* and external access permissions can be made.
*/
fun translateLocalQueueToInboxAddress(address: String): String {
require(address.startsWith(PEERS_PREFIX)) { "Failed to map address: $address to a remote topic as it is not in the $PEERS_PREFIX namespace" }
return P2P_PREFIX + address.substring(PEERS_PREFIX.length)
}
}
override val queueName: String = "$P2P_PREFIX${identity.toStringShort()}"
}
} }

View File

@ -1,6 +1,7 @@
package net.corda.nodeapi.internal package net.corda.nodeapi.internal
import net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto
import net.corda.core.crypto.Crypto.generateKeyPair
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.config.SSLConfiguration import net.corda.nodeapi.internal.config.SSLConfiguration
@ -8,6 +9,7 @@ import net.corda.nodeapi.internal.crypto.*
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
import java.security.KeyPair
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import javax.security.auth.x500.X500Principal import javax.security.auth.x500.X500Principal
@ -31,7 +33,7 @@ fun SSLConfiguration.createDevKeyStores(legalName: CordaX500Name,
save(nodeKeystore, keyStorePassword) save(nodeKeystore, keyStorePassword)
} }
val tlsKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val tlsKeyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, nodeCaCert, nodeCaKeyPair, legalName.x500Principal, tlsKeyPair.public) val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, nodeCaCert, nodeCaKeyPair, legalName.x500Principal, tlsKeyPair.public)
loadOrCreateKeyStore(sslKeystore, keyStorePassword).apply { loadOrCreateKeyStore(sslKeystore, keyStorePassword).apply {
@ -59,17 +61,18 @@ fun createDevNetworkMapCa(rootCa: CertificateAndKeyPair = DEV_ROOT_CA): Certific
* 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.
*/ */
fun createDevNodeCa(intermediateCa: CertificateAndKeyPair, legalName: CordaX500Name): CertificateAndKeyPair { fun createDevNodeCa(intermediateCa: CertificateAndKeyPair,
val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) legalName: CordaX500Name,
nodeKeyPair: KeyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)): CertificateAndKeyPair {
val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName.x500Name))), arrayOf()) val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName.x500Name))), arrayOf())
val cert = X509Utilities.createCertificate( val cert = X509Utilities.createCertificate(
CertificateType.NODE_CA, CertificateType.NODE_CA,
intermediateCa.certificate, intermediateCa.certificate,
intermediateCa.keyPair, intermediateCa.keyPair,
legalName.x500Principal, legalName.x500Principal,
keyPair.public, nodeKeyPair.public,
nameConstraints = nameConstraints) nameConstraints = nameConstraints)
return CertificateAndKeyPair(cert, keyPair) return CertificateAndKeyPair(cert, nodeKeyPair)
} }
val DEV_INTERMEDIATE_CA: CertificateAndKeyPair get() = DevCaHelper.loadDevCa(X509Utilities.CORDA_INTERMEDIATE_CA) val DEV_INTERMEDIATE_CA: CertificateAndKeyPair get() = DevCaHelper.loadDevCa(X509Utilities.CORDA_INTERMEDIATE_CA)

View File

@ -1,13 +1,16 @@
package net.corda.nodeapi.internal.persistence package net.corda.nodeapi.internal.persistence
import co.paralleluniverse.strands.Strand
import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.MappedSchema
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import rx.Observable import rx.Observable
import rx.Subscriber import rx.Subscriber
import rx.subjects.PublishSubject
import rx.subjects.UnicastSubject import rx.subjects.UnicastSubject
import java.io.Closeable import java.io.Closeable
import java.sql.Connection import java.sql.Connection
import java.sql.SQLException import java.sql.SQLException
import java.util.*
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
import javax.persistence.AttributeConverter import javax.persistence.AttributeConverter
import javax.sql.DataSource import javax.sql.DataSource
@ -40,6 +43,11 @@ enum class TransactionIsolationLevel {
val jdbcValue: Int = java.sql.Connection::class.java.getField(jdbcString).get(null) as Int val jdbcValue: Int = java.sql.Connection::class.java.getField(jdbcString).get(null) as Int
} }
private val _contextDatabase = ThreadLocal<CordaPersistence>()
var contextDatabase: CordaPersistence
get() = _contextDatabase.get() ?: error("Was expecting to find CordaPersistence set on current thread: ${Strand.currentStrand()}")
set(database) = _contextDatabase.set(database)
class CordaPersistence( class CordaPersistence(
val dataSource: DataSource, val dataSource: DataSource,
databaseConfig: DatabaseConfig, databaseConfig: DatabaseConfig,
@ -51,7 +59,7 @@ class CordaPersistence(
private val log = contextLogger() private val log = contextLogger()
} }
val defaultIsolationLevel = databaseConfig.transactionIsolationLevel private val defaultIsolationLevel = databaseConfig.transactionIsolationLevel
val hibernateConfig: HibernateConfiguration by lazy { val hibernateConfig: HibernateConfiguration by lazy {
transaction { transaction {
@ -60,8 +68,19 @@ class CordaPersistence(
} }
val entityManagerFactory get() = hibernateConfig.sessionFactoryForRegisteredSchemas val entityManagerFactory get() = hibernateConfig.sessionFactoryForRegisteredSchemas
data class Boundary(val txId: UUID)
internal val transactionBoundaries = PublishSubject.create<Boundary>().toSerialized()
init { init {
DatabaseTransactionManager(this) // Found a unit test that was forgetting to close the database transactions. When you close() on the top level
// database transaction it will reset the threadLocalTx back to null, so if it isn't then there is still a
// database transaction open. The [transaction] helper above handles this in a finally clause for you
// but any manual database transaction management is liable to have this problem.
contextTransactionOrNull?.let {
error("Was not expecting to find existing database transaction on current strand when setting database: ${Strand.currentStrand()}, $it")
}
_contextDatabase.set(this)
// Check not in read-only mode. // Check not in read-only mode.
transaction { transaction {
check(!connection.metaData.isReadOnly) { "Database should not be readonly." } check(!connection.metaData.isReadOnly) { "Database should not be readonly." }
@ -72,25 +91,29 @@ class CordaPersistence(
const val DATA_SOURCE_URL = "dataSource.url" const val DATA_SOURCE_URL = "dataSource.url"
} }
/** fun currentOrNew(isolation: TransactionIsolationLevel = defaultIsolationLevel): DatabaseTransaction {
* Creates an instance of [DatabaseTransaction], with the given transaction isolation level. return contextTransactionOrNull ?: newTransaction(isolation)
*/ }
fun createTransaction(isolationLevel: TransactionIsolationLevel): DatabaseTransaction {
// We need to set the database for the current [Thread] or [Fiber] here as some tests share threads across databases. fun newTransaction(isolation: TransactionIsolationLevel = defaultIsolationLevel): DatabaseTransaction {
DatabaseTransactionManager.dataSource = this return DatabaseTransaction(isolation.jdbcValue, contextTransactionOrNull, this).also {
return DatabaseTransactionManager.currentOrNew(isolationLevel) contextTransactionOrNull = it
}
} }
/** /**
* Creates an instance of [DatabaseTransaction], with the default transaction isolation level. * Creates an instance of [DatabaseTransaction], with the given transaction isolation level.
*/ */
fun createTransaction(): DatabaseTransaction = createTransaction(defaultIsolationLevel) fun createTransaction(isolationLevel: TransactionIsolationLevel = defaultIsolationLevel): DatabaseTransaction {
// We need to set the database for the current [Thread] or [Fiber] here as some tests share threads across databases.
_contextDatabase.set(this)
return currentOrNew(isolationLevel)
}
fun createSession(): Connection { fun createSession(): Connection {
// We need to set the database for the current [Thread] or [Fiber] here as some tests share threads across databases. // We need to set the database for the current [Thread] or [Fiber] here as some tests share threads across databases.
DatabaseTransactionManager.dataSource = this _contextDatabase.set(this)
val ctx = DatabaseTransactionManager.currentOrNull() return contextTransaction.connection
return ctx?.connection ?: throw IllegalStateException("Was expecting to find database transaction: must wrap calling code within a transaction.")
} }
/** /**
@ -99,7 +122,7 @@ class CordaPersistence(
* @param statement to be executed in the scope of this transaction. * @param statement to be executed in the scope of this transaction.
*/ */
fun <T> transaction(isolationLevel: TransactionIsolationLevel, statement: DatabaseTransaction.() -> T): T { fun <T> transaction(isolationLevel: TransactionIsolationLevel, statement: DatabaseTransaction.() -> T): T {
DatabaseTransactionManager.dataSource = this _contextDatabase.set(this)
return transaction(isolationLevel, 2, statement) return transaction(isolationLevel, 2, statement)
} }
@ -110,7 +133,7 @@ class CordaPersistence(
fun <T> transaction(statement: DatabaseTransaction.() -> T): T = transaction(defaultIsolationLevel, statement) fun <T> transaction(statement: DatabaseTransaction.() -> T): T = transaction(defaultIsolationLevel, statement)
private fun <T> transaction(isolationLevel: TransactionIsolationLevel, recoverableFailureTolerance: Int, statement: DatabaseTransaction.() -> T): T { private fun <T> transaction(isolationLevel: TransactionIsolationLevel, recoverableFailureTolerance: Int, statement: DatabaseTransaction.() -> T): T {
val outer = DatabaseTransactionManager.currentOrNull() val outer = contextTransactionOrNull
return if (outer != null) { return if (outer != null) {
outer.statement() outer.statement()
} else { } else {
@ -126,7 +149,7 @@ class CordaPersistence(
log.warn("Cleanup task failed:", t) log.warn("Cleanup task failed:", t)
} }
while (true) { while (true) {
val transaction = DatabaseTransactionManager.currentOrNew(isolationLevel) val transaction = contextDatabase.currentOrNew(isolationLevel) // XXX: Does this code really support statement changing the contextDatabase?
try { try {
val answer = transaction.statement() val answer = transaction.statement()
transaction.commit() transaction.commit()
@ -160,8 +183,8 @@ class CordaPersistence(
* For examples, see the call hierarchy of this function. * For examples, see the call hierarchy of this function.
*/ */
fun <T : Any> rx.Observer<T>.bufferUntilDatabaseCommit(): rx.Observer<T> { fun <T : Any> rx.Observer<T>.bufferUntilDatabaseCommit(): rx.Observer<T> {
val currentTxId = DatabaseTransactionManager.transactionId val currentTxId = contextTransaction.id
val databaseTxBoundary: Observable<DatabaseTransactionManager.Boundary> = DatabaseTransactionManager.transactionBoundaries.first { it.txId == currentTxId } val databaseTxBoundary: Observable<CordaPersistence.Boundary> = contextDatabase.transactionBoundaries.first { it.txId == currentTxId }
val subject = UnicastSubject.create<T>() val subject = UnicastSubject.create<T>()
subject.delaySubscription(databaseTxBoundary).subscribe(this) subject.delaySubscription(databaseTxBoundary).subscribe(this)
databaseTxBoundary.doOnCompleted { subject.onCompleted() } databaseTxBoundary.doOnCompleted { subject.onCompleted() }
@ -169,12 +192,12 @@ fun <T : Any> rx.Observer<T>.bufferUntilDatabaseCommit(): rx.Observer<T> {
} }
// A subscriber that delegates to multiple others, wrapping a database transaction around the combination. // A subscriber that delegates to multiple others, wrapping a database transaction around the combination.
private class DatabaseTransactionWrappingSubscriber<U>(val db: CordaPersistence?) : Subscriber<U>() { private class DatabaseTransactionWrappingSubscriber<U>(private val db: CordaPersistence?) : Subscriber<U>() {
// Some unsubscribes happen inside onNext() so need something that supports concurrent modification. // Some unsubscribes happen inside onNext() so need something that supports concurrent modification.
val delegates = CopyOnWriteArrayList<Subscriber<in U>>() val delegates = CopyOnWriteArrayList<Subscriber<in U>>()
fun forEachSubscriberWithDbTx(block: Subscriber<in U>.() -> Unit) { fun forEachSubscriberWithDbTx(block: Subscriber<in U>.() -> Unit) {
(db ?: DatabaseTransactionManager.dataSource).transaction { (db ?: contextDatabase).transaction {
delegates.filter { !it.isUnsubscribed }.forEach { delegates.filter { !it.isUnsubscribed }.forEach {
it.block() it.block()
} }

View File

@ -1,23 +1,29 @@
package net.corda.nodeapi.internal.persistence package net.corda.nodeapi.internal.persistence
import co.paralleluniverse.strands.Strand
import org.hibernate.Session import org.hibernate.Session
import org.hibernate.Transaction import org.hibernate.Transaction
import rx.subjects.Subject
import java.sql.Connection import java.sql.Connection
import java.util.* import java.util.*
fun currentDBSession(): Session = contextTransaction.session
private val _contextTransaction = ThreadLocal<DatabaseTransaction>()
var contextTransactionOrNull: DatabaseTransaction?
get() = _contextTransaction.get()
set(transaction) = _contextTransaction.set(transaction)
val contextTransaction get() = contextTransactionOrNull ?: error("Was expecting to find transaction set on current strand: ${Strand.currentStrand()}")
class DatabaseTransaction( class DatabaseTransaction(
isolation: Int, isolation: Int,
private val threadLocal: ThreadLocal<DatabaseTransaction>, val outerTransaction: DatabaseTransaction?,
private val transactionBoundaries: Subject<DatabaseTransactionManager.Boundary, DatabaseTransactionManager.Boundary>, val database: CordaPersistence
val cordaPersistence: CordaPersistence
) { ) {
val id: UUID = UUID.randomUUID() val id: UUID = UUID.randomUUID()
private var _connectionCreated = false private var _connectionCreated = false
val connectionCreated get() = _connectionCreated val connectionCreated get() = _connectionCreated
val connection: Connection by lazy(LazyThreadSafetyMode.NONE) { val connection: Connection by lazy(LazyThreadSafetyMode.NONE) {
cordaPersistence.dataSource.connection database.dataSource.connection
.apply { .apply {
_connectionCreated = true _connectionCreated = true
// only set the transaction isolation level if it's actually changed - setting isn't free. // only set the transaction isolation level if it's actually changed - setting isn't free.
@ -28,16 +34,13 @@ class DatabaseTransaction(
} }
private val sessionDelegate = lazy { private val sessionDelegate = lazy {
val session = cordaPersistence.entityManagerFactory.withOptions().connection(connection).openSession() val session = database.entityManagerFactory.withOptions().connection(connection).openSession()
hibernateTransaction = session.beginTransaction() hibernateTransaction = session.beginTransaction()
session session
} }
val session: Session by sessionDelegate val session: Session by sessionDelegate
private lateinit var hibernateTransaction: Transaction private lateinit var hibernateTransaction: Transaction
val outerTransaction: DatabaseTransaction? = threadLocal.get()
fun commit() { fun commit() {
if (sessionDelegate.isInitialized()) { if (sessionDelegate.isInitialized()) {
hibernateTransaction.commit() hibernateTransaction.commit()
@ -63,9 +66,9 @@ class DatabaseTransaction(
if (_connectionCreated) { if (_connectionCreated) {
connection.close() connection.close()
} }
threadLocal.set(outerTransaction) contextTransactionOrNull = outerTransaction
if (outerTransaction == null) { if (outerTransaction == null) {
transactionBoundaries.onNext(DatabaseTransactionManager.Boundary(id)) database.transactionBoundaries.onNext(CordaPersistence.Boundary(id))
} }
} }
} }

View File

@ -1,77 +0,0 @@
package net.corda.nodeapi.internal.persistence
import co.paralleluniverse.strands.Strand
import org.hibernate.Session
import rx.subjects.PublishSubject
import rx.subjects.Subject
import java.util.*
import java.util.concurrent.ConcurrentHashMap
fun currentDBSession(): Session = DatabaseTransactionManager.current().session
class DatabaseTransactionManager(initDataSource: CordaPersistence) {
companion object {
private val threadLocalDb = ThreadLocal<CordaPersistence>()
private val threadLocalTx = ThreadLocal<DatabaseTransaction>()
private val databaseToInstance = ConcurrentHashMap<CordaPersistence, DatabaseTransactionManager>()
fun setThreadLocalTx(tx: DatabaseTransaction?): DatabaseTransaction? {
val oldTx = threadLocalTx.get()
threadLocalTx.set(tx)
return oldTx
}
fun restoreThreadLocalTx(context: DatabaseTransaction?) {
if (context != null) {
threadLocalDb.set(context.cordaPersistence)
}
threadLocalTx.set(context)
}
var dataSource: CordaPersistence
get() = threadLocalDb.get() ?: throw IllegalStateException("Was expecting to find CordaPersistence set on current thread: ${Strand.currentStrand()}")
set(value) = threadLocalDb.set(value)
val transactionId: UUID
get() = threadLocalTx.get()?.id ?: throw IllegalStateException("Was expecting to find transaction set on current strand: ${Strand.currentStrand()}")
val manager: DatabaseTransactionManager get() = databaseToInstance[dataSource]!!
val transactionBoundaries: Subject<Boundary, Boundary> get() = manager._transactionBoundaries
fun currentOrNull(): DatabaseTransaction? = manager.currentOrNull()
fun currentOrNew(isolation: TransactionIsolationLevel = dataSource.defaultIsolationLevel): DatabaseTransaction {
return currentOrNull() ?: manager.newTransaction(isolation.jdbcValue)
}
fun current(): DatabaseTransaction = currentOrNull() ?: error("No transaction in context.")
fun newTransaction(isolation: TransactionIsolationLevel = dataSource.defaultIsolationLevel): DatabaseTransaction {
return manager.newTransaction(isolation.jdbcValue)
}
}
data class Boundary(val txId: UUID)
private val _transactionBoundaries = PublishSubject.create<Boundary>().toSerialized()
init {
// Found a unit test that was forgetting to close the database transactions. When you close() on the top level
// database transaction it will reset the threadLocalTx back to null, so if it isn't then there is still a
// database transaction open. The [transaction] helper above handles this in a finally clause for you
// but any manual database transaction management is liable to have this problem.
if (threadLocalTx.get() != null) {
throw IllegalStateException("Was not expecting to find existing database transaction on current strand when setting database: ${Strand.currentStrand()}, ${threadLocalTx.get()}")
}
dataSource = initDataSource
databaseToInstance[dataSource] = this
}
private fun newTransaction(isolation: Int) =
DatabaseTransaction(isolation, threadLocalTx, transactionBoundaries, dataSource).apply {
threadLocalTx.set(this)
}
private fun currentOrNull(): DatabaseTransaction? = threadLocalTx.get()
}

View File

@ -128,15 +128,16 @@ class HibernateConfiguration(
class NodeDatabaseConnectionProvider : ConnectionProvider { class NodeDatabaseConnectionProvider : ConnectionProvider {
override fun closeConnection(conn: Connection) { override fun closeConnection(conn: Connection) {
conn.autoCommit = false conn.autoCommit = false
val tx = DatabaseTransactionManager.current() contextTransaction.run {
tx.commit() commit()
tx.close() close()
}
} }
override fun supportsAggressiveRelease(): Boolean = true override fun supportsAggressiveRelease(): Boolean = true
override fun getConnection(): Connection { override fun getConnection(): Connection {
return DatabaseTransactionManager.newTransaction().connection return contextDatabase.newTransaction().connection
} }
override fun <T : Any?> unwrap(unwrapType: Class<T>): T { override fun <T : Any?> unwrap(unwrapType: Class<T>): T {

View File

@ -3,18 +3,18 @@ package net.corda.node.amqp
import com.nhaarman.mockito_kotlin.any import com.nhaarman.mockito_kotlin.any
import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.crypto.toStringShort
import net.corda.core.internal.div import net.corda.core.internal.div
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.NetworkMapCache
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.toBase58String
import net.corda.node.internal.protonwrapper.netty.AMQPServer import net.corda.node.internal.protonwrapper.netty.AMQPServer
import net.corda.node.internal.security.RPCSecurityManager import net.corda.node.internal.security.RPCSecurityManager
import net.corda.node.services.api.NetworkMapCacheInternal
import net.corda.node.services.config.* import net.corda.node.services.config.*
import net.corda.node.services.messaging.ArtemisMessagingClient import net.corda.node.services.messaging.ArtemisMessagingClient
import net.corda.node.services.messaging.ArtemisMessagingServer import net.corda.node.services.messaging.ArtemisMessagingServer
import net.corda.nodeapi.internal.ArtemisMessagingComponent import net.corda.nodeapi.internal.ArtemisMessagingComponent
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE
import net.corda.nodeapi.internal.crypto.loadKeyStore import net.corda.nodeapi.internal.crypto.loadKeyStore
import net.corda.testing.* import net.corda.testing.*
import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.rigorousMock
@ -53,7 +53,7 @@ class AMQPBridgeTest {
@Test @Test
fun `test acked and nacked messages`() { fun `test acked and nacked messages`() {
// Create local queue // Create local queue
val sourceQueueName = "internal.peers." + BOB.publicKey.toBase58String() val sourceQueueName = "internal.peers." + BOB.publicKey.toStringShort()
val (artemisServer, artemisClient) = createArtemis(sourceQueueName) val (artemisServer, artemisClient) = createArtemis(sourceQueueName)
// Pre-populate local queue with 3 messages // Pre-populate local queue with 3 messages
@ -133,11 +133,13 @@ class AMQPBridgeTest {
@Test @Test
fun `Test legacy bridge still works`() { fun `Test legacy bridge still works`() {
// Create local queue // Create local queue
val sourceQueueName = "internal.peers." + ALICE.publicKey.toBase58String() val sourceQueueName = "internal.peers." + BOB.publicKey.toStringShort()
val (artemisLegacyServer, artemisLegacyClient) = createLegacyArtemis(sourceQueueName) val (artemisLegacyServer, artemisLegacyClient) = createLegacyArtemis(sourceQueueName)
val (artemisServer, artemisClient) = createArtemis(null) val (artemisServer, artemisClient) = createArtemis(null)
val inbox = ArtemisMessagingComponent.RemoteInboxAddress(BOB.party).queueName
artemisClient.started!!.session.createQueue(inbox, RoutingType.ANYCAST, inbox, true)
val artemis = artemisLegacyClient.started!! val artemis = artemisLegacyClient.started!!
for (i in 0 until 3) { for (i in 0 until 3) {
@ -150,8 +152,7 @@ class AMQPBridgeTest {
artemis.producer.send(sourceQueueName, artemisMessage) artemis.producer.send(sourceQueueName, artemisMessage)
} }
val subs = artemisClient.started!!.session.createConsumer(inbox)
val subs = artemisClient.started!!.session.createConsumer(P2P_QUEUE)
for (i in 0 until 3) { for (i in 0 until 3) {
val msg = subs.receive() val msg = subs.receive()
val messageBody = ByteArray(msg.bodySize).apply { msg.bodyBuffer.readBytes(this) } val messageBody = ByteArray(msg.bodySize).apply { msg.bodyBuffer.readBytes(this) }
@ -177,9 +178,9 @@ class AMQPBridgeTest {
doReturn(true).whenever(it).useAMQPBridges doReturn(true).whenever(it).useAMQPBridges
} }
artemisConfig.configureWithDevSSLCertificate() artemisConfig.configureWithDevSSLCertificate()
val networkMap = rigorousMock<NetworkMapCache>().also { val networkMap = rigorousMock<NetworkMapCacheInternal>().also {
doReturn(Observable.never<NetworkMapCache.MapChange>()).whenever(it).changed doReturn(Observable.never<NetworkMapCache.MapChange>()).whenever(it).changed
doReturn(listOf(NodeInfo(listOf(amqpAddress), listOf(BOB.identity), 1, 1L))).whenever(it).getNodesByLegalIdentityKey(any()) doReturn(listOf(NodeInfo(listOf(amqpAddress), listOf(BOB.identity), 1, 1L))).whenever(it).getNodesByOwningKeyIndex(any())
} }
val userService = rigorousMock<RPCSecurityManager>() val userService = rigorousMock<RPCSecurityManager>()
val artemisServer = ArtemisMessagingServer(artemisConfig, artemisPort, null, networkMap, userService, MAX_MESSAGE_SIZE) val artemisServer = ArtemisMessagingServer(artemisConfig, artemisPort, null, networkMap, userService, MAX_MESSAGE_SIZE)
@ -189,7 +190,7 @@ class AMQPBridgeTest {
val artemis = artemisClient.started!! val artemis = artemisClient.started!!
if (sourceQueueName != null) { if (sourceQueueName != null) {
// Local queue for outgoing messages // Local queue for outgoing messages
artemis.session.createQueue(sourceQueueName, RoutingType.MULTICAST, sourceQueueName, true) artemis.session.createQueue(sourceQueueName, RoutingType.ANYCAST, sourceQueueName, true)
} }
return Pair(artemisServer, artemisClient) return Pair(artemisServer, artemisClient)
} }
@ -206,9 +207,9 @@ class AMQPBridgeTest {
doReturn(ActiveMqServerConfiguration(BridgeConfiguration(0, 0, 0.0))).whenever(it).activeMQServer doReturn(ActiveMqServerConfiguration(BridgeConfiguration(0, 0, 0.0))).whenever(it).activeMQServer
} }
artemisConfig.configureWithDevSSLCertificate() artemisConfig.configureWithDevSSLCertificate()
val networkMap = rigorousMock<NetworkMapCache>().also { val networkMap = rigorousMock<NetworkMapCacheInternal>().also {
doReturn(Observable.never<NetworkMapCache.MapChange>()).whenever(it).changed doReturn(Observable.never<NetworkMapCache.MapChange>()).whenever(it).changed
doReturn(listOf(NodeInfo(listOf(artemisAddress), listOf(ALICE.identity), 1, 1L))).whenever(it).getNodesByLegalIdentityKey(any()) doReturn(listOf(NodeInfo(listOf(artemisAddress), listOf(ALICE.identity), 1, 1L))).whenever(it).getNodesByOwningKeyIndex(any())
} }
val userService = rigorousMock<RPCSecurityManager>() val userService = rigorousMock<RPCSecurityManager>()
val artemisServer = ArtemisMessagingServer(artemisConfig, artemisPort2, null, networkMap, userService, MAX_MESSAGE_SIZE) val artemisServer = ArtemisMessagingServer(artemisConfig, artemisPort2, null, networkMap, userService, MAX_MESSAGE_SIZE)
@ -217,7 +218,7 @@ class AMQPBridgeTest {
artemisClient.start() artemisClient.start()
val artemis = artemisClient.started!! val artemis = artemisClient.started!!
// Local queue for outgoing messages // Local queue for outgoing messages
artemis.session.createQueue(sourceQueueName, RoutingType.MULTICAST, sourceQueueName, true) artemis.session.createQueue(sourceQueueName, RoutingType.ANYCAST, sourceQueueName, true)
return Pair(artemisServer, artemisClient) return Pair(artemisServer, artemisClient)
} }

View File

@ -13,11 +13,13 @@ import net.corda.node.internal.protonwrapper.messages.MessageStatus
import net.corda.node.internal.protonwrapper.netty.AMQPClient import net.corda.node.internal.protonwrapper.netty.AMQPClient
import net.corda.node.internal.protonwrapper.netty.AMQPServer import net.corda.node.internal.protonwrapper.netty.AMQPServer
import net.corda.node.internal.security.RPCSecurityManager import net.corda.node.internal.security.RPCSecurityManager
import net.corda.node.services.api.NetworkMapCacheInternal
import net.corda.node.services.config.CertChainPolicyConfig import net.corda.node.services.config.CertChainPolicyConfig
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.configureWithDevSSLCertificate import net.corda.node.services.config.configureWithDevSSLCertificate
import net.corda.node.services.messaging.ArtemisMessagingClient import net.corda.node.services.messaging.ArtemisMessagingClient
import net.corda.node.services.messaging.ArtemisMessagingServer import net.corda.node.services.messaging.ArtemisMessagingServer
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
import net.corda.nodeapi.internal.crypto.loadKeyStore import net.corda.nodeapi.internal.crypto.loadKeyStore
import net.corda.testing.* import net.corda.testing.*
@ -48,7 +50,7 @@ class ProtonWrapperTests {
amqpServer.start() amqpServer.start()
val receiveSubs = amqpServer.onReceive.subscribe { val receiveSubs = amqpServer.onReceive.subscribe {
assertEquals(BOB_NAME.toString(), it.sourceLegalName) assertEquals(BOB_NAME.toString(), it.sourceLegalName)
assertEquals("p2p.inbound", it.topic) assertEquals(P2P_PREFIX + "Test", it.topic)
assertEquals("Test", String(it.payload)) assertEquals("Test", String(it.payload))
it.complete(true) it.complete(true)
} }
@ -64,7 +66,7 @@ class ProtonWrapperTests {
assertEquals(true, clientConnect.connected) assertEquals(true, clientConnect.connected)
assertEquals(ALICE_NAME, CordaX500Name.build(clientConnect.remoteCert!!.subjectX500Principal)) assertEquals(ALICE_NAME, CordaX500Name.build(clientConnect.remoteCert!!.subjectX500Principal))
val msg = amqpClient.createMessage("Test".toByteArray(), val msg = amqpClient.createMessage("Test".toByteArray(),
"p2p.inbound", P2P_PREFIX + "Test",
ALICE_NAME.toString(), ALICE_NAME.toString(),
emptyMap()) emptyMap())
amqpClient.write(msg) amqpClient.write(msg)
@ -151,8 +153,8 @@ class ProtonWrapperTests {
assertEquals(true, clientConnected.get().connected) assertEquals(true, clientConnected.get().connected)
assertEquals(CHARLIE_NAME, CordaX500Name.build(clientConnected.get().remoteCert!!.subjectX500Principal)) assertEquals(CHARLIE_NAME, CordaX500Name.build(clientConnected.get().remoteCert!!.subjectX500Principal))
val artemis = artemisClient.started!! val artemis = artemisClient.started!!
val sendAddress = "p2p.inbound" val sendAddress = P2P_PREFIX + "Test"
artemis.session.createQueue(sendAddress, RoutingType.MULTICAST, "queue", true) artemis.session.createQueue(sendAddress, RoutingType.ANYCAST, "queue", true)
val consumer = artemis.session.createConsumer("queue") val consumer = artemis.session.createConsumer("queue")
val testData = "Test".toByteArray() val testData = "Test".toByteArray()
val testProperty = mutableMapOf<Any?, Any?>() val testProperty = mutableMapOf<Any?, Any?>()
@ -230,7 +232,7 @@ class ProtonWrapperTests {
} }
artemisConfig.configureWithDevSSLCertificate() artemisConfig.configureWithDevSSLCertificate()
val networkMap = rigorousMock<NetworkMapCache>().also { val networkMap = rigorousMock<NetworkMapCacheInternal>().also {
doReturn(never<NetworkMapCache.MapChange>()).whenever(it).changed doReturn(never<NetworkMapCache.MapChange>()).whenever(it).changed
} }
val userService = rigorousMock<RPCSecurityManager>() val userService = rigorousMock<RPCSecurityManager>()

View File

@ -13,6 +13,7 @@ import net.corda.core.identity.Party
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.node.internal.StartedNode import net.corda.node.internal.StartedNode
import net.corda.node.services.config.MySQLConfiguration
import net.corda.node.services.config.NotaryConfig import net.corda.node.services.config.NotaryConfig
import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.DevIdentityGenerator
import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.nodeapi.internal.network.NetworkParametersCopier
@ -135,7 +136,7 @@ class MySQLNotaryServiceTests : IntegrationTest() {
legalName = notaryName, legalName = notaryName,
entropyRoot = BigInteger.valueOf(60L), entropyRoot = BigInteger.valueOf(60L),
configOverrides = { configOverrides = {
val notaryConfig = NotaryConfig(validating = false, mysql = dataStoreProperties) val notaryConfig = NotaryConfig(validating = false, mysql = MySQLConfiguration(dataStoreProperties))
doReturn(notaryConfig).whenever(it).notary doReturn(notaryConfig).whenever(it).notary
} }
) )

View File

@ -1,5 +1,6 @@
package net.corda.node.services.network package net.corda.node.services.network
import net.corda.core.crypto.generateKeyPair
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
@ -15,6 +16,7 @@ import org.junit.ClassRule
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
// TODO Clean up these tests, they were written with old network map design in place.
class PersistentNetworkMapCacheTest : NodeBasedTest() { class PersistentNetworkMapCacheTest : NodeBasedTest() {
companion object { companion object {
val ALICE = TestIdentity(ALICE_NAME, 70).party val ALICE = TestIdentity(ALICE_NAME, 70).party
@ -60,6 +62,19 @@ class PersistentNetworkMapCacheTest : NodeBasedTest() {
} }
} }
// This test has to be done as normal node not mock, because MockNodes don't have addresses.
@Test
fun `insert two node infos with the same host and port`() {
val aliceNode = startNode(ALICE_NAME)
val charliePartyCert = getTestPartyAndCertificate(CHARLIE_NAME, generateKeyPair().public)
val aliceCache = aliceNode.services.networkMapCache
aliceCache.addNode(aliceNode.info.copy(legalIdentitiesAndCerts = listOf(charliePartyCert)))
val res = aliceNode.database.transaction {
aliceCache.allNodes.filter { aliceNode.info.addresses[0] in it.addresses }
}
assertEquals(2, res.size)
}
@Test @Test
fun `restart node with DB map cache`() { fun `restart node with DB map cache`() {
val alice = startNodesWithPort(listOf(ALICE))[0] val alice = startNodesWithPort(listOf(ALICE))[0]

View File

@ -5,6 +5,7 @@ import net.corda.client.rpc.CordaRPCClient
import net.corda.client.rpc.CordaRPCConnection import net.corda.client.rpc.CordaRPCConnection
import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.generateKeyPair
import net.corda.core.crypto.random63BitValue import net.corda.core.crypto.random63BitValue
import net.corda.core.crypto.toStringShort
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession import net.corda.core.flows.FlowSession
import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatedBy
@ -14,14 +15,13 @@ import net.corda.core.identity.Party
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.toBase58String
import net.corda.core.utilities.unwrap import net.corda.core.utilities.unwrap
import net.corda.node.internal.Node import net.corda.node.internal.Node
import net.corda.node.internal.StartedNode import net.corda.node.internal.StartedNode
import net.corda.nodeapi.RPCApi import net.corda.nodeapi.RPCApi
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX
import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.testing.ALICE_NAME import net.corda.testing.ALICE_NAME
@ -79,30 +79,30 @@ abstract class MQSecurityTest : NodeBasedTest() {
@Test @Test
fun `consume message from P2P queue`() { fun `consume message from P2P queue`() {
assertConsumeAttackFails(P2P_QUEUE) assertConsumeAttackFails("$P2P_PREFIX${alice.info.chooseIdentity().owningKey.toStringShort()}")
} }
@Test @Test
fun `consume message from peer queue`() { fun `consume message from peer queue`() {
val bobParty = startBobAndCommunicateWithAlice() val bobParty = startBobAndCommunicateWithAlice()
assertConsumeAttackFails("$PEERS_PREFIX${bobParty.owningKey.toBase58String()}") assertConsumeAttackFails("$PEERS_PREFIX${bobParty.owningKey.toStringShort()}")
} }
@Test @Test
fun `send message to address of peer which has been communicated with`() { fun `send message to address of peer which has been communicated with`() {
val bobParty = startBobAndCommunicateWithAlice() val bobParty = startBobAndCommunicateWithAlice()
assertSendAttackFails("$PEERS_PREFIX${bobParty.owningKey.toBase58String()}") assertSendAttackFails("$PEERS_PREFIX${bobParty.owningKey.toStringShort()}")
} }
@Test @Test
fun `create queue for peer which has not been communicated with`() { fun `create queue for peer which has not been communicated with`() {
val bob = startNode(BOB_NAME) val bob = startNode(BOB_NAME)
assertAllQueueCreationAttacksFail("$PEERS_PREFIX${bob.info.chooseIdentity().owningKey.toBase58String()}") assertAllQueueCreationAttacksFail("$PEERS_PREFIX${bob.info.chooseIdentity().owningKey.toStringShort()}")
} }
@Test @Test
fun `create queue for unknown peer`() { fun `create queue for unknown peer`() {
val invalidPeerQueue = "$PEERS_PREFIX${generateKeyPair().public.toBase58String()}" val invalidPeerQueue = "$PEERS_PREFIX${generateKeyPair().public.toStringShort()}"
assertAllQueueCreationAttacksFail(invalidPeerQueue) assertAllQueueCreationAttacksFail(invalidPeerQueue)
} }

View File

@ -222,7 +222,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
check(started == null) { "Node has already been started" } check(started == null) { "Node has already been started" }
log.info("Node starting up ...") log.info("Node starting up ...")
initCertificate() initCertificate()
val schemaService = NodeSchemaService(cordappLoader.cordappSchemas) val schemaService = NodeSchemaService(cordappLoader.cordappSchemas, configuration.notary != null)
val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null) val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null)
val lh = lazyHub() val lh = lazyHub()
configure(lh) configure(lh)

View File

@ -40,6 +40,7 @@ import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import rx.Scheduler import rx.Scheduler
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import java.security.PublicKey
import java.time.Clock import java.time.Clock
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import javax.management.ObjectName import javax.management.ObjectName
@ -166,11 +167,14 @@ open class Node(configuration: NodeConfiguration,
VerifierType.OutOfProcess -> VerifierMessagingClient(configuration, serverAddress, services.monitoringService.metrics, networkParameters.maxMessageSize) VerifierType.OutOfProcess -> VerifierMessagingClient(configuration, serverAddress, services.monitoringService.metrics, networkParameters.maxMessageSize)
VerifierType.InMemory -> null VerifierType.InMemory -> null
} }
require(info.legalIdentities.size in 1..2) { "Currently nodes must have a primary address and optionally one serviced address" }
val serviceIdentity: PublicKey? = if (info.legalIdentities.size == 1) null else info.legalIdentities[1].owningKey
return P2PMessagingClient( return P2PMessagingClient(
configuration, configuration,
versionInfo, versionInfo,
serverAddress, serverAddress,
info.legalIdentities[0].owningKey, info.legalIdentities[0].owningKey,
serviceIdentity,
serverThread, serverThread,
database, database,
advertisedAddress, advertisedAddress,

View File

@ -29,6 +29,10 @@ interface NetworkMapCacheBaseInternal : NetworkMapCacheBase {
fun getNodeByHash(nodeHash: SecureHash): NodeInfo? fun getNodeByHash(nodeHash: SecureHash): NodeInfo?
/** Find nodes from the [PublicKey] toShortString representation.
* This is used for Artemis bridge lookup process. */
fun getNodesByOwningKeyIndex(identityKeyIndex: String): List<NodeInfo>
/** Adds a node to the local cache (generally only used for adding ourselves). */ /** Adds a node to the local cache (generally only used for adding ourselves). */
fun addNode(node: NodeInfo) fun addNode(node: NodeInfo)

View File

@ -18,8 +18,6 @@ import java.util.*
val Int.MB: Long get() = this * 1024L * 1024L val Int.MB: Long get() = this * 1024L * 1024L
interface NodeConfiguration : NodeSSLConfiguration { interface NodeConfiguration : NodeSSLConfiguration {
// myLegalName should be only used in the initial network registration, we should use the name from the certificate instead of this.
// TODO: Remove this so we don't accidentally use this identity in the code?
val myLegalName: CordaX500Name val myLegalName: CordaX500Name
val emailAddress: String val emailAddress: String
val exportJMXto: String val exportJMXto: String
@ -35,8 +33,6 @@ interface NodeConfiguration : NodeSSLConfiguration {
val notary: NotaryConfig? val notary: NotaryConfig?
val activeMQServer: ActiveMqServerConfiguration val activeMQServer: ActiveMqServerConfiguration
val additionalNodeInfoPollingFrequencyMsec: Long val additionalNodeInfoPollingFrequencyMsec: Long
// TODO Remove as this is only used by the driver
val useHTTPS: Boolean
val p2pAddress: NetworkHostAndPort val p2pAddress: NetworkHostAndPort
val rpcAddress: NetworkHostAndPort? val rpcAddress: NetworkHostAndPort?
val messagingServerAddress: NetworkHostAndPort? val messagingServerAddress: NetworkHostAndPort?
@ -71,7 +67,7 @@ data class NotaryConfig(val validating: Boolean,
val raft: RaftConfig? = null, val raft: RaftConfig? = null,
val bftSMaRt: BFTSMaRtConfiguration? = null, val bftSMaRt: BFTSMaRtConfiguration? = null,
val custom: Boolean = false, val custom: Boolean = false,
val mysql: Properties? = null val mysql: MySQLConfiguration? = null
) { ) {
init { init {
require(raft == null || bftSMaRt == null || !custom || mysql == null) { require(raft == null || bftSMaRt == null || !custom || mysql == null) {
@ -81,6 +77,15 @@ data class NotaryConfig(val validating: Boolean,
val isClusterConfig: Boolean get() = raft != null || bftSMaRt != null val isClusterConfig: Boolean get() = raft != null || bftSMaRt != null
} }
data class MySQLConfiguration(
val dataSource: Properties,
val connectionRetries: Int = 0
) {
init {
require(connectionRetries >= 0) { "connectionRetries cannot be negative" }
}
}
data class RaftConfig(val nodeAddress: NetworkHostAndPort, val clusterAddresses: List<NetworkHostAndPort>) data class RaftConfig(val nodeAddress: NetworkHostAndPort, val clusterAddresses: List<NetworkHostAndPort>)
/** @param exposeRaces for testing only, so its default is not in reference.conf but here. */ /** @param exposeRaces for testing only, so its default is not in reference.conf but here. */
@ -118,7 +123,6 @@ data class NodeConfigurationImpl(
// TODO typesafe config supports the notion of durations. Make use of that by mapping it to java.time.Duration. // TODO typesafe config supports the notion of durations. Make use of that by mapping it to java.time.Duration.
// Then rename this to messageRedeliveryDelay and make it of type Duration // Then rename this to messageRedeliveryDelay and make it of type Duration
override val messageRedeliveryDelaySeconds: Int = 30, override val messageRedeliveryDelaySeconds: Int = 30,
override val useHTTPS: Boolean,
override val p2pAddress: NetworkHostAndPort, override val p2pAddress: NetworkHostAndPort,
override val rpcAddress: NetworkHostAndPort?, override val rpcAddress: NetworkHostAndPort?,
override val relay: RelayConfiguration?, override val relay: RelayConfiguration?,

View File

@ -12,8 +12,8 @@ import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.messaging.AMQPBridgeManager.AMQPBridge.Companion.getBridgeName import net.corda.node.services.messaging.AMQPBridgeManager.AMQPBridge.Companion.getBridgeName
import net.corda.nodeapi.internal.ArtemisMessagingComponent import net.corda.nodeapi.internal.ArtemisMessagingComponent
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.P2P_QUEUE
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
import net.corda.nodeapi.internal.ArtemisMessagingComponent.RemoteInboxAddress.Companion.translateLocalQueueToInboxAddress
import net.corda.nodeapi.internal.crypto.loadKeyStore import net.corda.nodeapi.internal.crypto.loadKeyStore
import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE
@ -132,7 +132,8 @@ internal class AMQPBridgeManager(val config: NodeConfiguration, val p2pAddress:
properties[key.toString()] = value properties[key.toString()] = value
} }
log.debug { "Bridged Send to ${legalNames.first()} uuid: ${artemisMessage.getObjectProperty("_AMQ_DUPL_ID")}" } log.debug { "Bridged Send to ${legalNames.first()} uuid: ${artemisMessage.getObjectProperty("_AMQ_DUPL_ID")}" }
val sendableMessage = amqpClient.createMessage(data, P2P_QUEUE, val peerInbox = translateLocalQueueToInboxAddress(queueName)
val sendableMessage = amqpClient.createMessage(data, peerInbox,
legalNames.first().toString(), legalNames.first().toString(),
properties) properties)
sendableMessage.onComplete.then { sendableMessage.onComplete.then {

View File

@ -8,16 +8,15 @@ import net.corda.core.internal.div
import net.corda.core.internal.noneOrSingle import net.corda.core.internal.noneOrSingle
import net.corda.core.internal.uncheckedCast import net.corda.core.internal.uncheckedCast
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.services.NetworkMapCache
import net.corda.core.node.services.NetworkMapCache.MapChange import net.corda.core.node.services.NetworkMapCache.MapChange
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug import net.corda.core.utilities.debug
import net.corda.core.utilities.parsePublicKeyBase58
import net.corda.node.internal.Node import net.corda.node.internal.Node
import net.corda.node.internal.security.Password import net.corda.node.internal.security.Password
import net.corda.node.internal.security.RPCSecurityManager import net.corda.node.internal.security.RPCSecurityManager
import net.corda.node.services.api.NetworkMapCacheInternal
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.messaging.NodeLoginModule.Companion.NODE_ROLE import net.corda.node.services.messaging.NodeLoginModule.Companion.NODE_ROLE
import net.corda.node.services.messaging.NodeLoginModule.Companion.PEER_ROLE import net.corda.node.services.messaging.NodeLoginModule.Companion.PEER_ROLE
@ -31,7 +30,7 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.ArtemisPeerAddress
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX
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.NOTIFICATIONS_ADDRESS import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
import net.corda.nodeapi.internal.ArtemisMessagingComponent.NodeAddress import net.corda.nodeapi.internal.ArtemisMessagingComponent.NodeAddress
@ -94,7 +93,7 @@ import javax.security.cert.CertificateException
class ArtemisMessagingServer(private val config: NodeConfiguration, class ArtemisMessagingServer(private val config: NodeConfiguration,
private val p2pPort: Int, private val p2pPort: Int,
val rpcPort: Int?, val rpcPort: Int?,
val networkMapCache: NetworkMapCache, val networkMapCache: NetworkMapCacheInternal,
val securityManager: RPCSecurityManager, val securityManager: RPCSecurityManager,
val maxMessageSize: Int) : SingletonSerializeAsToken() { val maxMessageSize: Int) : SingletonSerializeAsToken() {
companion object { companion object {
@ -191,7 +190,6 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
// by having its password be an unknown securely random 128-bit value. // by having its password be an unknown securely random 128-bit value.
clusterPassword = BigInteger(128, newSecureRandom()).toString(16) clusterPassword = BigInteger(128, newSecureRandom()).toString(16)
queueConfigurations = listOf( queueConfigurations = listOf(
queueConfig(P2P_QUEUE, durable = true),
// Create an RPC queue: this will service locally connected clients only (not via a bridge) and those // Create an RPC queue: this will service locally connected clients only (not via a bridge) and those
// clients must have authenticated. We could use a single consumer for everything and perhaps we should, // clients must have authenticated. We could use a single consumer for everything and perhaps we should,
// but these queues are not worth persisting. // but these queues are not worth persisting.
@ -243,7 +241,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
private fun ConfigurationImpl.configureAddressSecurity(): Pair<Configuration, LoginListener> { private fun ConfigurationImpl.configureAddressSecurity(): Pair<Configuration, LoginListener> {
val nodeInternalRole = Role(NODE_ROLE, true, true, true, true, true, true, true, true) val nodeInternalRole = Role(NODE_ROLE, true, true, true, true, true, true, true, true)
securityRoles["$INTERNAL_PREFIX#"] = setOf(nodeInternalRole) // Do not add any other roles here as it's only for the node securityRoles["$INTERNAL_PREFIX#"] = setOf(nodeInternalRole) // Do not add any other roles here as it's only for the node
securityRoles[P2P_QUEUE] = setOf(nodeInternalRole, restrictedRole(PEER_ROLE, send = true)) securityRoles["$P2P_PREFIX#"] = setOf(nodeInternalRole, restrictedRole(PEER_ROLE, send = true))
securityRoles[RPCApi.RPC_SERVER_QUEUE_NAME] = setOf(nodeInternalRole, restrictedRole(RPC_ROLE, send = true)) securityRoles[RPCApi.RPC_SERVER_QUEUE_NAME] = setOf(nodeInternalRole, restrictedRole(RPC_ROLE, send = true))
// Each RPC user must have its own role and its own queue. This prevents users accessing each other's queues // Each RPC user must have its own role and its own queue. This prevents users accessing each other's queues
// and stealing RPC responses. // and stealing RPC responses.
@ -309,8 +307,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
if (queueName.startsWith(PEERS_PREFIX)) { if (queueName.startsWith(PEERS_PREFIX)) {
try { try {
val identity = parsePublicKeyBase58(queueName.substring(PEERS_PREFIX.length)) val nodeInfos = networkMapCache.getNodesByOwningKeyIndex(queueName.substring(PEERS_PREFIX.length))
val nodeInfos = networkMapCache.getNodesByLegalIdentityKey(identity)
if (nodeInfos.isNotEmpty()) { if (nodeInfos.isNotEmpty()) {
nodeInfos.forEach { deployBridgeToPeer(it) } nodeInfos.forEach { deployBridgeToPeer(it) }
} else { } else {

View File

@ -10,15 +10,17 @@ import net.corda.node.services.config.NodeConfiguration
import net.corda.nodeapi.ArtemisTcpTransport import net.corda.nodeapi.ArtemisTcpTransport
import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.ConnectionDirection
import net.corda.nodeapi.internal.ArtemisMessagingComponent import net.corda.nodeapi.internal.ArtemisMessagingComponent
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
import net.corda.nodeapi.internal.ArtemisMessagingComponent.RemoteInboxAddress.Companion.translateLocalQueueToInboxAddress
import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.crypto.X509Utilities
import org.apache.activemq.artemis.api.core.Message
import org.apache.activemq.artemis.core.config.BridgeConfiguration import org.apache.activemq.artemis.core.config.BridgeConfiguration
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnection import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnection
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnector import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnector
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants
import org.apache.activemq.artemis.core.server.ActiveMQServer import org.apache.activemq.artemis.core.server.ActiveMQServer
import org.apache.activemq.artemis.core.server.cluster.Transformer
import org.apache.activemq.artemis.spi.core.remoting.* import org.apache.activemq.artemis.spi.core.remoting.*
import org.apache.activemq.artemis.utils.ConfigurationHelper import org.apache.activemq.artemis.utils.ConfigurationHelper
import java.time.Duration import java.time.Duration
@ -46,7 +48,7 @@ internal class CoreBridgeManager(val config: NodeConfiguration, val activeMQServ
/** /**
* All nodes are expected to have a public facing address called [ArtemisMessagingComponent.P2P_QUEUE] for receiving * All nodes are expected to have a public facing address called p2p.inbound.$identity for receiving
* messages from other nodes. When we want to send a message to a node we send it to our internal address/queue for it, * messages from other nodes. When we want to send a message to a node we send it to our internal address/queue for it,
* as defined by ArtemisAddress.queueName. A bridge is then created to forward messages from this queue to the node's * as defined by ArtemisAddress.queueName. A bridge is then created to forward messages from this queue to the node's
* P2P address. * P2P address.
@ -64,7 +66,6 @@ internal class CoreBridgeManager(val config: NodeConfiguration, val activeMQServ
activeMQServer.deployBridge(BridgeConfiguration().apply { activeMQServer.deployBridge(BridgeConfiguration().apply {
name = getBridgeName(queueName, target) name = getBridgeName(queueName, target)
this.queueName = queueName this.queueName = queueName
forwardingAddress = P2P_QUEUE
staticConnectors = listOf(target.toString()) staticConnectors = listOf(target.toString())
confirmationWindowSize = 100000 // a guess confirmationWindowSize = 100000 // a guess
isUseDuplicateDetection = true // Enable the bridge's automatic deduplication logic isUseDuplicateDetection = true // Enable the bridge's automatic deduplication logic
@ -77,6 +78,7 @@ internal class CoreBridgeManager(val config: NodeConfiguration, val activeMQServ
// our TLS certificate. // our TLS certificate.
user = PEER_USER user = PEER_USER
password = PEER_USER password = PEER_USER
transformerClassName = InboxTopicTransformer::class.java.name
}) })
} }
@ -99,6 +101,13 @@ internal class CoreBridgeManager(val config: NodeConfiguration, val activeMQServ
} }
} }
class InboxTopicTransformer : Transformer {
override fun transform(message: Message): Message {
message.address = translateLocalQueueToInboxAddress(message.address)
return message
}
}
class VerifyingNettyConnectorFactory : NettyConnectorFactory() { class VerifyingNettyConnectorFactory : NettyConnectorFactory() {
override fun createConnector(configuration: MutableMap<String, Any>, override fun createConnector(configuration: MutableMap<String, Any>,
handler: BufferHandler?, handler: BufferHandler?,

View File

@ -19,7 +19,6 @@ import net.corda.node.utilities.AffinityExecutor
import net.corda.node.utilities.AppendOnlyPersistentMap import net.corda.node.utilities.AppendOnlyPersistentMap
import net.corda.node.utilities.PersistentMap import net.corda.node.utilities.PersistentMap
import net.corda.nodeapi.internal.ArtemisMessagingComponent.* import net.corda.nodeapi.internal.ArtemisMessagingComponent.*
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
import org.apache.activemq.artemis.api.core.ActiveMQObjectClosedException import org.apache.activemq.artemis.api.core.ActiveMQObjectClosedException
@ -54,21 +53,28 @@ import javax.persistence.Lob
* invoke methods on the provided implementation. There is more documentation on this in the docsite and the * invoke methods on the provided implementation. There is more documentation on this in the docsite and the
* CordaRPCClient class. * CordaRPCClient class.
* *
* @param serverAddress The address of the broker instance to connect to (might be running in the same process). * @param config The configuration of the node, which is used for controlling the message redelivery options.
* @param myIdentity The public key to be used as the ArtemisMQ address and queue name for the node. * @param versionInfo All messages from the node carry the version info and received messages are checked against this for compatibility.
* @param nodeExecutor An executor to run received message tasks upon. * @param serverAddress The host and port of the Artemis broker.
* @param advertisedAddress The node address for inbound connections, advertised to the network map service and peers. * @param myIdentity The primary identity of the node, which defines the messaging address for externally received messages.
* If not provided, will default to [serverAddress]. * It is also used to construct the myAddress field, which is ultimately advertised in the network map.
* @param serviceIdentity An optional second identity if the node is also part of a group address, for example a notary.
* @param nodeExecutor The received messages are marshalled onto the server executor to prevent Netty buffers leaking during fiber suspends.
* @param database The nodes database, which is used to deduplicate messages.
* @param advertisedAddress The externally advertised version of the Artemis broker address used to construct myAddress and included
* in the network map data.
* @param maxMessageSize A bound applied to the message size.
*/ */
@ThreadSafe @ThreadSafe
class P2PMessagingClient(config: NodeConfiguration, class P2PMessagingClient(config: NodeConfiguration,
private val versionInfo: VersionInfo, private val versionInfo: VersionInfo,
serverAddress: NetworkHostAndPort, serverAddress: NetworkHostAndPort,
private val myIdentity: PublicKey, private val myIdentity: PublicKey,
private val serviceIdentity: PublicKey?,
private val nodeExecutor: AffinityExecutor.ServiceAffinityExecutor, private val nodeExecutor: AffinityExecutor.ServiceAffinityExecutor,
private val database: CordaPersistence, private val database: CordaPersistence,
advertisedAddress: NetworkHostAndPort = serverAddress, advertisedAddress: NetworkHostAndPort = serverAddress,
private val maxMessageSize: Int maxMessageSize: Int
) : SingletonSerializeAsToken(), MessagingService { ) : SingletonSerializeAsToken(), MessagingService {
companion object { companion object {
private val log = contextLogger() private val log = contextLogger()
@ -126,6 +132,7 @@ class P2PMessagingClient(config: NodeConfiguration,
private class InnerState { private class InnerState {
var running = false var running = false
var p2pConsumer: ClientConsumer? = null var p2pConsumer: ClientConsumer? = null
var serviceConsumer: ClientConsumer? = null
} }
private val messagesToRedeliver = database.transaction { private val messagesToRedeliver = database.transaction {
@ -181,8 +188,20 @@ class P2PMessagingClient(config: NodeConfiguration,
fun start() { fun start() {
state.locked { state.locked {
val session = artemis.start().session val session = artemis.start().session
val inbox = RemoteInboxAddress(myIdentity).queueName
// Create a queue, consumer and producer for handling P2P network messages. // Create a queue, consumer and producer for handling P2P network messages.
p2pConsumer = session.createConsumer(P2P_QUEUE) createQueueIfAbsent(inbox)
p2pConsumer = session.createConsumer(inbox)
if (serviceIdentity != null) {
val serviceAddress = RemoteInboxAddress(serviceIdentity).queueName
createQueueIfAbsent(serviceAddress)
val serviceHandler = session.createConsumer(serviceAddress)
serviceHandler.setMessageHandler { msg ->
val message: ReceivedMessage? = artemisToCordaMessage(msg)
if (message != null)
deliver(msg, message)
}
}
} }
resumeMessageRedelivery() resumeMessageRedelivery()
@ -331,6 +350,13 @@ class P2PMessagingClient(config: NodeConfiguration,
// Ignore it: this can happen if the server has gone away before we do. // Ignore it: this can happen if the server has gone away before we do.
} }
p2pConsumer = null p2pConsumer = null
val s = serviceConsumer
try {
s?.close()
} catch (e: ActiveMQObjectClosedException) {
// Ignore it: this can happen if the server has gone away before we do.
}
serviceConsumer = null
prevRunning prevRunning
} }
if (running && !nodeExecutor.isOnThread) { if (running && !nodeExecutor.isOnThread) {
@ -430,7 +456,7 @@ class P2PMessagingClient(config: NodeConfiguration,
private fun getMQAddress(target: MessageRecipients): String { private fun getMQAddress(target: MessageRecipients): String {
return if (target == myAddress) { return if (target == myAddress) {
// If we are sending to ourselves then route the message directly to our P2P queue. // If we are sending to ourselves then route the message directly to our P2P queue.
P2P_QUEUE RemoteInboxAddress(myIdentity).queueName
} else { } else {
// Otherwise we send the message to an internal queue for the target residing on our broker. It's then the // Otherwise we send the message to an internal queue for the target residing on our broker. It's then the
// broker's job to route the message to the target's P2P queue. // broker's job to route the message to the target's P2P queue.
@ -447,7 +473,7 @@ class P2PMessagingClient(config: NodeConfiguration,
val queueQuery = session.queueQuery(SimpleString(queueName)) val queueQuery = session.queueQuery(SimpleString(queueName))
if (!queueQuery.isExists) { if (!queueQuery.isExists) {
log.info("Create fresh queue $queueName bound on same address") log.info("Create fresh queue $queueName bound on same address")
session.createQueue(queueName, RoutingType.MULTICAST, queueName, true) session.createQueue(queueName, RoutingType.ANYCAST, queueName, true)
} }
} }
} }

View File

@ -7,15 +7,15 @@ import net.corda.core.transactions.LedgerTransaction
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.node.services.transactions.OutOfProcessTransactionVerifierService import net.corda.node.services.transactions.OutOfProcessTransactionVerifierService
import net.corda.node.utilities.* import net.corda.node.utilities.AffinityExecutor
import net.corda.nodeapi.VerifierApi import net.corda.nodeapi.VerifierApi
import net.corda.nodeapi.VerifierApi.VERIFICATION_REQUESTS_QUEUE_NAME import net.corda.nodeapi.VerifierApi.VERIFICATION_REQUESTS_QUEUE_NAME
import net.corda.nodeapi.VerifierApi.VERIFICATION_RESPONSES_QUEUE_NAME_PREFIX import net.corda.nodeapi.VerifierApi.VERIFICATION_RESPONSES_QUEUE_NAME_PREFIX
import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.config.SSLConfiguration
import org.apache.activemq.artemis.api.core.RoutingType import org.apache.activemq.artemis.api.core.RoutingType
import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.client.* import org.apache.activemq.artemis.api.core.client.ClientConsumer
import java.util.concurrent.* import java.util.concurrent.TimeUnit
class VerifierMessagingClient(config: SSLConfiguration, serverAddress: NetworkHostAndPort, metrics: MetricRegistry, private val maxMessageSize: Int) : SingletonSerializeAsToken() { class VerifierMessagingClient(config: SSLConfiguration, serverAddress: NetworkHostAndPort, metrics: MetricRegistry, private val maxMessageSize: Int) : SingletonSerializeAsToken() {
companion object { companion object {
@ -40,7 +40,7 @@ class VerifierMessagingClient(config: SSLConfiguration, serverAddress: NetworkHo
val queueQuery = session.queueQuery(SimpleString(queueName)) val queueQuery = session.queueQuery(SimpleString(queueName))
if (!queueQuery.isExists) { if (!queueQuery.isExists) {
log.info("Create fresh queue $queueName bound on same address") log.info("Create fresh queue $queueName bound on same address")
session.createQueue(queueName, RoutingType.MULTICAST, queueName, true) session.createQueue(queueName, RoutingType.ANYCAST, queueName, true)
} }
} }
createQueueIfAbsent(VERIFICATION_REQUESTS_QUEUE_NAME) createQueueIfAbsent(VERIFICATION_REQUESTS_QUEUE_NAME)

View File

@ -160,6 +160,12 @@ open class PersistentNetworkMapCache(
private val nodesByKeyCache = NonInvalidatingCache<PublicKey, List<NodeInfo>>(1024, 8, { key -> database.transaction { queryByIdentityKey(session, key) } }) private val nodesByKeyCache = NonInvalidatingCache<PublicKey, List<NodeInfo>>(1024, 8, { key -> database.transaction { queryByIdentityKey(session, key) } })
override fun getNodesByOwningKeyIndex(identityKeyIndex: String): List<NodeInfo> {
return database.transaction {
queryByIdentityKeyIndex(session, identityKeyIndex)
}
}
override fun getNodeByAddress(address: NetworkHostAndPort): NodeInfo? = database.transaction { queryByAddress(session, address) } override fun getNodeByAddress(address: NetworkHostAndPort): NodeInfo? = database.transaction { queryByAddress(session, address) }
override fun getPeerCertificateByLegalName(name: CordaX500Name): PartyAndCertificate? = identityByLegalNameCache.get(name).orElse(null) override fun getPeerCertificateByLegalName(name: CordaX500Name): PartyAndCertificate? = identityByLegalNameCache.get(name).orElse(null)
@ -245,15 +251,23 @@ open class PersistentNetworkMapCache(
} }
private fun findByIdentityKey(session: Session, identityKey: PublicKey): List<NodeInfoSchemaV1.PersistentNodeInfo> { private fun findByIdentityKey(session: Session, identityKey: PublicKey): List<NodeInfoSchemaV1.PersistentNodeInfo> {
return findByIdentityKeyIndex(session, identityKey.toStringShort())
}
private fun findByIdentityKeyIndex(session: Session, identityKeyIndex: String): List<NodeInfoSchemaV1.PersistentNodeInfo> {
val query = session.createQuery( val query = session.createQuery(
"SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.legalIdentitiesAndCerts l WHERE l.owningKeyHash = :owningKeyHash", "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.legalIdentitiesAndCerts l WHERE l.owningKeyHash = :owningKeyHash",
NodeInfoSchemaV1.PersistentNodeInfo::class.java) NodeInfoSchemaV1.PersistentNodeInfo::class.java)
query.setParameter("owningKeyHash", identityKey.toStringShort()) query.setParameter("owningKeyHash", identityKeyIndex)
return query.resultList return query.resultList
} }
private fun queryByIdentityKey(session: Session, identityKey: PublicKey): List<NodeInfo> { private fun queryByIdentityKey(session: Session, identityKey: PublicKey): List<NodeInfo> {
val result = findByIdentityKey(session, identityKey) return queryByIdentityKeyIndex(session, identityKey.toStringShort())
}
private fun queryByIdentityKeyIndex(session: Session, identityKeyIndex: String): List<NodeInfo> {
val result = findByIdentityKeyIndex(session, identityKeyIndex)
return result.map { it.toNodeInfo() } return result.map { it.toNodeInfo() }
} }
@ -279,13 +293,13 @@ open class PersistentNetworkMapCache(
private fun queryByAddress(session: Session, hostAndPort: NetworkHostAndPort): NodeInfo? { private fun queryByAddress(session: Session, hostAndPort: NetworkHostAndPort): NodeInfo? {
val query = session.createQuery( val query = session.createQuery(
"SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.addresses a WHERE a.pk.host = :host AND a.pk.port = :port", "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.addresses a WHERE a.host = :host AND a.port = :port",
NodeInfoSchemaV1.PersistentNodeInfo::class.java) NodeInfoSchemaV1.PersistentNodeInfo::class.java)
query.setParameter("host", hostAndPort.host) query.setParameter("host", hostAndPort.host)
query.setParameter("port", hostAndPort.port) query.setParameter("port", hostAndPort.port)
query.setMaxResults(1)
val result = query.resultList val result = query.resultList
return if (result.isEmpty()) null return result.map { it.toNodeInfo() }.singleOrNull()
else result.map { it.toNodeInfo() }.singleOrNull() ?: throw IllegalStateException("More than one node with the same host and port")
} }
/** Object Relational Mapping support. */ /** Object Relational Mapping support. */

View File

@ -5,7 +5,6 @@ import net.corda.core.serialization.SerializedBytes
import net.corda.core.utilities.debug import net.corda.core.utilities.debug
import net.corda.node.services.api.CheckpointStorage import net.corda.node.services.api.CheckpointStorage
import net.corda.node.services.statemachine.Checkpoint import net.corda.node.services.statemachine.Checkpoint
import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
import net.corda.nodeapi.internal.persistence.currentDBSession import net.corda.nodeapi.internal.persistence.currentDBSession
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@ -43,7 +42,7 @@ class DBCheckpointStorage : CheckpointStorage {
} }
override fun removeCheckpoint(id: StateMachineRunId): Boolean { override fun removeCheckpoint(id: StateMachineRunId): Boolean {
val session = DatabaseTransactionManager.current().session val session = currentDBSession()
val criteriaBuilder = session.criteriaBuilder val criteriaBuilder = session.criteriaBuilder
val delete = criteriaBuilder.createCriteriaDelete(DBCheckpoint::class.java) val delete = criteriaBuilder.createCriteriaDelete(DBCheckpoint::class.java)
val root = delete.from(DBCheckpoint::class.java) val root = delete.from(DBCheckpoint::class.java)

View File

@ -17,7 +17,6 @@ import net.corda.core.node.services.vault.AttachmentSort
import net.corda.core.serialization.* import net.corda.core.serialization.*
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.node.services.vault.HibernateAttachmentQueryCriteriaParser import net.corda.node.services.vault.HibernateAttachmentQueryCriteriaParser
import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
import net.corda.nodeapi.internal.persistence.currentDBSession import net.corda.nodeapi.internal.persistence.currentDBSession
import java.io.* import java.io.*
@ -242,8 +241,7 @@ class NodeAttachmentService(metrics: MetricRegistry) : AttachmentStorage, Single
override fun queryAttachments(criteria: AttachmentQueryCriteria, sorting: AttachmentSort?): List<AttachmentId> { override fun queryAttachments(criteria: AttachmentQueryCriteria, sorting: AttachmentSort?): List<AttachmentId> {
log.info("Attachment query criteria: $criteria, sorting: $sorting") log.info("Attachment query criteria: $criteria, sorting: $sorting")
val session = currentDBSession()
val session = DatabaseTransactionManager.current().session
val criteriaBuilder = session.criteriaBuilder val criteriaBuilder = session.criteriaBuilder
val criteriaQuery = criteriaBuilder.createQuery(DBAttachment::class.java) val criteriaQuery = criteriaBuilder.createQuery(DBAttachment::class.java)

View File

@ -10,8 +10,8 @@ import net.corda.core.schemas.PersistentStateRef
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug import net.corda.core.utilities.debug
import net.corda.node.services.api.SchemaService import net.corda.node.services.api.SchemaService
import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager
import net.corda.nodeapi.internal.persistence.HibernateConfiguration import net.corda.nodeapi.internal.persistence.HibernateConfiguration
import net.corda.nodeapi.internal.persistence.contextTransaction
import org.hibernate.FlushMode import org.hibernate.FlushMode
import rx.Observable import rx.Observable
@ -54,7 +54,7 @@ class HibernateObserver private constructor(private val config: HibernateConfigu
internal fun persistStatesWithSchema(statesAndRefs: List<ContractStateAndRef>, schema: MappedSchema) { internal fun persistStatesWithSchema(statesAndRefs: List<ContractStateAndRef>, schema: MappedSchema) {
val sessionFactory = config.sessionFactoryForSchemas(setOf(schema)) val sessionFactory = config.sessionFactoryForSchemas(setOf(schema))
val session = sessionFactory.withOptions(). val session = sessionFactory.withOptions().
connection(DatabaseTransactionManager.current().connection). connection(contextTransaction.connection).
flushMode(FlushMode.MANUAL). flushMode(FlushMode.MANUAL).
openSession() openSession()
session.use { thisSession -> session.use { thisSession ->

View File

@ -29,24 +29,20 @@ import net.corda.node.services.vault.VaultSchemaV1
* TODO: support plugins for schema version upgrading or custom mapping not supported by original [QueryableState]. * TODO: support plugins for schema version upgrading or custom mapping not supported by original [QueryableState].
* TODO: create whitelisted tables when a CorDapp is first installed * TODO: create whitelisted tables when a CorDapp is first installed
*/ */
class NodeSchemaService(extraSchemas: Set<MappedSchema> = emptySet()) : SchemaService, SingletonSerializeAsToken() { class NodeSchemaService(extraSchemas: Set<MappedSchema> = emptySet(), includeNotarySchemas: Boolean = false) : SchemaService, SingletonSerializeAsToken() {
// Entities for compulsory services // Core Entities used by a Node
object NodeServices object NodeCore
object NodeServicesV1 : MappedSchema(schemaFamily = NodeServices.javaClass, version = 1, object NodeCoreV1 : MappedSchema(schemaFamily = NodeCore.javaClass, version = 1,
mappedTypes = listOf(DBCheckpointStorage.DBCheckpoint::class.java, mappedTypes = listOf(DBCheckpointStorage.DBCheckpoint::class.java,
DBTransactionStorage.DBTransaction::class.java, DBTransactionStorage.DBTransaction::class.java,
DBTransactionMappingStorage.DBTransactionMapping::class.java, DBTransactionMappingStorage.DBTransactionMapping::class.java,
PersistentKeyManagementService.PersistentKey::class.java, PersistentKeyManagementService.PersistentKey::class.java,
PersistentUniquenessProvider.PersistentUniqueness::class.java,
PersistentUniquenessProvider.PersistentNotaryCommit::class.java,
NodeSchedulerService.PersistentScheduledState::class.java, NodeSchedulerService.PersistentScheduledState::class.java,
NodeAttachmentService.DBAttachment::class.java, NodeAttachmentService.DBAttachment::class.java,
P2PMessagingClient.ProcessedMessage::class.java, P2PMessagingClient.ProcessedMessage::class.java,
P2PMessagingClient.RetryMessage::class.java, P2PMessagingClient.RetryMessage::class.java,
NodeAttachmentService.DBAttachment::class.java, NodeAttachmentService.DBAttachment::class.java,
RaftUniquenessProvider.RaftState::class.java,
BFTNonValidatingNotaryService.PersistedCommittedState::class.java,
PersistentIdentityService.PersistentIdentity::class.java, PersistentIdentityService.PersistentIdentity::class.java,
PersistentIdentityService.PersistentIdentityNames::class.java, PersistentIdentityService.PersistentIdentityNames::class.java,
ContractUpgradeServiceImpl.DBContractUpgrade::class.java, ContractUpgradeServiceImpl.DBContractUpgrade::class.java,
@ -67,15 +63,25 @@ class NodeSchemaService(extraSchemas: Set<MappedSchema> = emptySet()) : SchemaSe
override val migrationResource = "node-notary.changelog-master" override val migrationResource = "node-notary.changelog-master"
} }
// Entities used by a Notary
object NodeNotary
object NodeNotaryV1 : MappedSchema(schemaFamily = NodeNotary.javaClass, version = 1,
mappedTypes = listOf(PersistentUniquenessProvider.PersistentUniqueness::class.java,
PersistentUniquenessProvider.PersistentNotaryCommit::class.java,
RaftUniquenessProvider.RaftState::class.java,
BFTNonValidatingNotaryService.PersistedCommittedState::class.java
))
// Required schemas are those used by internal Corda services // Required schemas are those used by internal Corda services
// For example, cash is used by the vault for coin selection (but will be extracted as a standalone CorDapp in future)
private val requiredSchemas: Map<MappedSchema, SchemaService.SchemaOptions> = private val requiredSchemas: Map<MappedSchema, SchemaService.SchemaOptions> =
mapOf(Pair(CommonSchemaV1, SchemaOptions()), mapOf(Pair(CommonSchemaV1, SchemaOptions()),
Pair(VaultSchemaV1, SchemaOptions()), Pair(VaultSchemaV1, SchemaOptions()),
Pair(NodeInfoSchemaV1, SchemaOptions()), Pair(NodeInfoSchemaV1, SchemaOptions()),
Pair(NodeServicesV1, SchemaOptions())) Pair(NodeCoreV1, SchemaOptions()))
private val notarySchemas = if (includeNotarySchemas) mapOf(Pair(NodeNotaryV1, SchemaOptions())) else emptyMap<MappedSchema, SchemaService.SchemaOptions>()
override val schemaOptions: Map<MappedSchema, SchemaService.SchemaOptions> = requiredSchemas + extraSchemas.associateBy({ it }, { SchemaOptions() }) override val schemaOptions: Map<MappedSchema, SchemaService.SchemaOptions> = requiredSchemas + notarySchemas + extraSchemas.associateBy({ it }, { SchemaOptions() })
// Currently returns all schemas supported by the state, with no filtering or enrichment. // Currently returns all schemas supported by the state, with no filtering or enrichment.
override fun selectSchemas(state: ContractState): Iterable<MappedSchema> { override fun selectSchemas(state: ContractState): Iterable<MappedSchema> {

View File

@ -10,7 +10,9 @@ import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.trace import net.corda.core.utilities.trace
import net.corda.node.services.api.CheckpointStorage import net.corda.node.services.api.CheckpointStorage
import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.api.ServiceHubInternal
import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager import net.corda.nodeapi.internal.persistence.contextDatabase
import net.corda.nodeapi.internal.persistence.contextTransaction
import net.corda.nodeapi.internal.persistence.contextTransactionOrNull
import java.time.Duration import java.time.Duration
import java.time.Instant import java.time.Instant
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -163,24 +165,24 @@ class ActionExecutorImpl(
@Suspendable @Suspendable
private fun executeCreateTransaction() { private fun executeCreateTransaction() {
if (DatabaseTransactionManager.currentOrNull() != null) { if (contextTransactionOrNull != null) {
throw IllegalStateException("Refusing to create a second transaction") throw IllegalStateException("Refusing to create a second transaction")
} }
DatabaseTransactionManager.newTransaction() contextDatabase.newTransaction()
} }
@Suspendable @Suspendable
private fun executeRollbackTransaction() { private fun executeRollbackTransaction() {
DatabaseTransactionManager.currentOrNull()?.close() contextTransactionOrNull?.close()
} }
@Suspendable @Suspendable
private fun executeCommitTransaction() { private fun executeCommitTransaction() {
try { try {
DatabaseTransactionManager.current().commit() contextTransaction.commit()
} finally { } finally {
DatabaseTransactionManager.current().close() contextTransaction.close()
DatabaseTransactionManager.setThreadLocalTx(null) contextTransactionOrNull = null
} }
} }

View File

@ -27,7 +27,8 @@ import net.corda.node.services.statemachine.transitions.FlowContinuation
import net.corda.node.services.statemachine.transitions.StateMachine import net.corda.node.services.statemachine.transitions.StateMachine
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseTransaction import net.corda.nodeapi.internal.persistence.DatabaseTransaction
import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager import net.corda.nodeapi.internal.persistence.contextTransaction
import net.corda.nodeapi.internal.persistence.contextTransactionOrNull
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.nio.file.Paths import java.nio.file.Paths
@ -58,8 +59,8 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
} }
private fun extractThreadLocalTransaction(): TransientReference<DatabaseTransaction> { private fun extractThreadLocalTransaction(): TransientReference<DatabaseTransaction> {
val transaction = DatabaseTransactionManager.current() val transaction = contextTransaction
DatabaseTransactionManager.setThreadLocalTx(null) contextTransactionOrNull = null
return TransientReference(transaction) return TransientReference(transaction)
} }
} }
@ -234,7 +235,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
parkAndSerialize { _, _ -> parkAndSerialize { _, _ ->
logger.trace { "Suspended on $ioRequest" } logger.trace { "Suspended on $ioRequest" }
DatabaseTransactionManager.setThreadLocalTx(transaction.value) contextTransactionOrNull = transaction.value
val event = try { val event = try {
Event.Suspend( Event.Suspend(
ioRequest = ioRequest, ioRequest = ioRequest,

View File

@ -5,7 +5,8 @@ import net.corda.core.utilities.contextLogger
import net.corda.node.services.statemachine.transitions.FlowContinuation import net.corda.node.services.statemachine.transitions.FlowContinuation
import net.corda.node.services.statemachine.transitions.TransitionResult import net.corda.node.services.statemachine.transitions.TransitionResult
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager import net.corda.nodeapi.internal.persistence.contextDatabase
import net.corda.nodeapi.internal.persistence.contextTransactionOrNull
import java.security.SecureRandom import java.security.SecureRandom
/** /**
@ -31,12 +32,12 @@ class TransitionExecutorImpl(
transition: TransitionResult, transition: TransitionResult,
actionExecutor: ActionExecutor actionExecutor: ActionExecutor
): Pair<FlowContinuation, StateMachineState> { ): Pair<FlowContinuation, StateMachineState> {
DatabaseTransactionManager.dataSource = database contextDatabase = database
for (action in transition.actions) { for (action in transition.actions) {
try { try {
actionExecutor.executeAction(fiber, action) actionExecutor.executeAction(fiber, action)
} catch (exception: Throwable) { } catch (exception: Throwable) {
DatabaseTransactionManager.currentOrNull()?.close() contextTransactionOrNull?.close()
if (transition.newState.checkpoint.errorState is ErrorState.Errored) { if (transition.newState.checkpoint.errorState is ErrorState.Errored) {
// If we errored while transitioning to an error state then we cannot record the additional // If we errored while transitioning to an error state then we cannot record the additional
// error as that may result in an infinite loop, e.g. error propagation fails -> record error -> propagate fails again. // error as that may result in an infinite loop, e.g. error propagation fails -> record error -> propagate fails again.

View File

@ -5,6 +5,7 @@ import net.corda.core.flows.FlowSession
import net.corda.core.node.services.TimeWindowChecker import net.corda.core.node.services.TimeWindowChecker
import net.corda.core.node.services.TrustedAuthorityNotaryService import net.corda.core.node.services.TrustedAuthorityNotaryService
import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.api.ServiceHubInternal
import net.corda.node.services.config.MySQLConfiguration
import java.security.PublicKey import java.security.PublicKey
import java.util.* import java.util.*
@ -12,14 +13,14 @@ import java.util.*
abstract class MySQLNotaryService( abstract class MySQLNotaryService(
final override val services: ServiceHubInternal, final override val services: ServiceHubInternal,
override val notaryIdentityKey: PublicKey, override val notaryIdentityKey: PublicKey,
dataSourceProperties: Properties, configuration: MySQLConfiguration,
/** Database table will be automatically created in dev mode */ /** Database table will be automatically created in dev mode */
val devMode: Boolean) : TrustedAuthorityNotaryService() { val devMode: Boolean) : TrustedAuthorityNotaryService() {
override val timeWindowChecker = TimeWindowChecker(services.clock) override val timeWindowChecker = TimeWindowChecker(services.clock)
override val uniquenessProvider = MySQLUniquenessProvider( override val uniquenessProvider = MySQLUniquenessProvider(
services.monitoringService.metrics, services.monitoringService.metrics,
dataSourceProperties configuration
) )
override fun start() { override fun start() {
@ -33,14 +34,14 @@ abstract class MySQLNotaryService(
class MySQLNonValidatingNotaryService(services: ServiceHubInternal, class MySQLNonValidatingNotaryService(services: ServiceHubInternal,
notaryIdentityKey: PublicKey, notaryIdentityKey: PublicKey,
dataSourceProperties: Properties, configuration: MySQLConfiguration,
devMode: Boolean = false) : MySQLNotaryService(services, notaryIdentityKey, dataSourceProperties, devMode) { devMode: Boolean = false) : MySQLNotaryService(services, notaryIdentityKey, configuration, devMode) {
override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic<Void?> = NonValidatingNotaryFlow(otherPartySession, this) override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic<Void?> = NonValidatingNotaryFlow(otherPartySession, this)
} }
class MySQLValidatingNotaryService(services: ServiceHubInternal, class MySQLValidatingNotaryService(services: ServiceHubInternal,
notaryIdentityKey: PublicKey, notaryIdentityKey: PublicKey,
dataSourceProperties: Properties, configuration: MySQLConfiguration,
devMode: Boolean = false) : MySQLNotaryService(services, notaryIdentityKey, dataSourceProperties, devMode) { devMode: Boolean = false) : MySQLNotaryService(services, notaryIdentityKey, configuration, devMode) {
override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic<Void?> = ValidatingNotaryFlow(otherPartySession, this) override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic<Void?> = ValidatingNotaryFlow(otherPartySession, this)
} }

View File

@ -14,9 +14,11 @@ import net.corda.core.serialization.SingletonSerializeAsToken
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.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.node.services.config.MySQLConfiguration
import java.security.PublicKey import java.security.PublicKey
import java.sql.BatchUpdateException import java.sql.BatchUpdateException
import java.sql.Connection import java.sql.Connection
import java.sql.SQLTransientConnectionException
import java.util.* import java.util.*
/** /**
@ -27,7 +29,7 @@ import java.util.*
*/ */
class MySQLUniquenessProvider( class MySQLUniquenessProvider(
metrics: MetricRegistry, metrics: MetricRegistry,
dataSourceProperties: Properties configuration: MySQLConfiguration
) : UniquenessProvider, SingletonSerializeAsToken() { ) : UniquenessProvider, SingletonSerializeAsToken() {
companion object { companion object {
private val log = loggerFor<MySQLUniquenessProvider>() private val log = loggerFor<MySQLUniquenessProvider>()
@ -57,13 +59,29 @@ class MySQLUniquenessProvider(
* This is a useful heath metric. * This is a useful heath metric.
*/ */
private val rollbackCounter = metrics.counter("$metricPrefix.Rollback") private val rollbackCounter = metrics.counter("$metricPrefix.Rollback")
/** Incremented when we can not obtain a DB connection. */
private val connectionExceptionCounter = metrics.counter("$metricPrefix.ConnectionException")
/** Track double spend attempts. Note that this will also include notarisation retries. */ /** Track double spend attempts. Note that this will also include notarisation retries. */
private val conflictCounter = metrics.counter("$metricPrefix.Conflicts") private val conflictCounter = metrics.counter("$metricPrefix.Conflicts")
val dataSource = HikariDataSource(HikariConfig(dataSourceProperties)) val dataSource = HikariDataSource(HikariConfig(configuration.dataSource))
private val connectionRetries = configuration.connectionRetries
private val connection: Connection private val connection: Connection
get() = dataSource.connection get() = getConnection()
private fun getConnection(nRetries: Int = 0): Connection =
try {
dataSource.connection
} catch (e: SQLTransientConnectionException) {
if (nRetries == connectionRetries) {
log.warn("Couldn't obtain connection with {} retries, giving up, {}", nRetries, e)
throw e
}
log.warn("Connection exception, retrying", nRetries+1)
connectionExceptionCounter.inc()
getConnection(nRetries + 1)
}
fun createTable() { fun createTable() {
log.debug("Attempting to create DB table if it does not yet exist: $createTableStatement") log.debug("Attempting to create DB table if it does not yet exist: $createTableStatement")

View File

@ -17,12 +17,8 @@ import net.corda.core.transactions.NotaryChangeWireTransaction
import net.corda.core.transactions.WireTransaction import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.* import net.corda.core.utilities.*
import net.corda.node.services.api.VaultServiceInternal import net.corda.node.services.api.VaultServiceInternal
import net.corda.nodeapi.internal.persistence.HibernateConfiguration
import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.FlowStateMachineImpl
import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager import net.corda.nodeapi.internal.persistence.*
import net.corda.nodeapi.internal.persistence.bufferUntilDatabaseCommit
import net.corda.nodeapi.internal.persistence.currentDBSession
import net.corda.nodeapi.internal.persistence.wrapWithDatabaseTransaction
import org.hibernate.Session import org.hibernate.Session
import rx.Observable import rx.Observable
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
@ -479,8 +475,7 @@ class NodeVaultService(
} }
} }
private fun getSession() = DatabaseTransactionManager.currentOrNew().session private fun getSession() = contextDatabase.currentOrNew().session
/** /**
* Derive list from existing vault states and then incrementally update using vault observables * Derive list from existing vault states and then incrementally update using vault observables
*/ */

View File

@ -10,7 +10,7 @@ import net.corda.core.utilities.toNonEmptySet
import net.corda.core.utilities.trace import net.corda.core.utilities.trace
import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.FlowStateMachineImpl
import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.services.statemachine.StateMachineManager
import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager import net.corda.nodeapi.internal.persistence.contextDatabase
import java.util.* import java.util.*
class VaultSoftLockManager private constructor(private val vault: VaultService) { class VaultSoftLockManager private constructor(private val vault: VaultService) {
@ -52,14 +52,14 @@ class VaultSoftLockManager private constructor(private val vault: VaultService)
private fun registerSoftLocks(flowId: UUID, stateRefs: NonEmptySet<StateRef>) { private fun registerSoftLocks(flowId: UUID, stateRefs: NonEmptySet<StateRef>) {
log.trace { "Reserving soft locks for flow id $flowId and states $stateRefs" } log.trace { "Reserving soft locks for flow id $flowId and states $stateRefs" }
DatabaseTransactionManager.dataSource.transaction { contextDatabase.transaction {
vault.softLockReserve(flowId, stateRefs) vault.softLockReserve(flowId, stateRefs)
} }
} }
private fun unregisterSoftLocks(flowId: UUID, logic: FlowLogic<*>) { private fun unregisterSoftLocks(flowId: UUID, logic: FlowLogic<*>) {
log.trace { "Releasing soft locks for flow ${logic.javaClass.simpleName} with flow id $flowId" } log.trace { "Releasing soft locks for flow ${logic.javaClass.simpleName} with flow id $flowId" }
DatabaseTransactionManager.dataSource.transaction { contextDatabase.transaction {
vault.softLockRelease(flowId) vault.softLockRelease(flowId)
} }
} }

View File

@ -67,4 +67,17 @@
constraintName="FK__info_hosts__infos" constraintName="FK__info_hosts__infos"
referencedColumnNames="node_info_id" referencedTableName="node_infos"/> referencedColumnNames="node_info_id" referencedTableName="node_infos"/>
</changeSet> </changeSet>
<changeSet author="R3.Corda" id="remove_host_port_pk">
<delete tableName="node_infos"/>
<delete tableName="node_link_nodeinfo_party"/>
<delete tableName="node_info_hosts"/>
<delete tableName="node_info_party_cert"/>
<dropPrimaryKey tableName="node_info_hosts" constraintName="node_info_hosts_pkey"/>
<addColumn tableName="node_info_hosts">
<column name="hosts_id" type="INT">
<constraints nullable="false"/>
</column>
</addColumn>
<addPrimaryKey columnNames="hosts_id" constraintName="node_info_hosts_pkey_id" tableName="node_info_hosts"/>
</changeSet>
</databaseChangeLog> </databaseChangeLog>

View File

@ -14,7 +14,6 @@ database = {
exportHibernateJMXStatistics = "false" exportHibernateJMXStatistics = "false"
} }
devMode = true devMode = true
useHTTPS = false
h2port = 0 h2port = 0
useTestClock = false useTestClock = false
verifierType = InMemory verifierType = InMemory

View File

@ -76,7 +76,6 @@ class NodeConfigurationImplTest {
dataSourceProperties = makeTestDataSourceProperties(ALICE_NAME.organisation), dataSourceProperties = makeTestDataSourceProperties(ALICE_NAME.organisation),
rpcUsers = emptyList(), rpcUsers = emptyList(),
verifierType = VerifierType.InMemory, verifierType = VerifierType.InMemory,
useHTTPS = false,
p2pAddress = NetworkHostAndPort("localhost", 0), p2pAddress = NetworkHostAndPort("localhost", 0),
rpcAddress = NetworkHostAndPort("localhost", 1), rpcAddress = NetworkHostAndPort("localhost", 1),
messagingServerAddress = null, messagingServerAddress = null,

View File

@ -178,6 +178,7 @@ class ArtemisMessagingTests {
MOCK_VERSION_INFO.copy(platformVersion = platformVersion), MOCK_VERSION_INFO.copy(platformVersion = platformVersion),
server, server,
identity.public, identity.public,
null,
ServiceAffinityExecutor("ArtemisMessagingTests", 1), ServiceAffinityExecutor("ArtemisMessagingTests", 1),
database, database,
maxMessageSize = maxMessageSize).apply { maxMessageSize = maxMessageSize).apply {

View File

@ -1,9 +1,11 @@
package net.corda.node.services.network package net.corda.node.services.network
import net.corda.core.crypto.generateKeyPair
import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.NetworkMapCache
import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.services.api.NetworkMapCacheInternal
import net.corda.testing.ALICE_NAME import net.corda.testing.ALICE_NAME
import net.corda.testing.BOB_NAME import net.corda.testing.BOB_NAME
import net.corda.testing.getTestPartyAndCertificate
import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockNodeParameters import net.corda.testing.node.MockNodeParameters
import net.corda.testing.singleIdentity import net.corda.testing.singleIdentity
@ -106,4 +108,14 @@ class NetworkMapCacheTest {
assertThat(bobCache.getNodeByLegalName(alice.name) == null) assertThat(bobCache.getNodeByLegalName(alice.name) == null)
} }
} }
@Test
fun `add two nodes the same name different keys`() {
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
val aliceCache = aliceNode.services.networkMapCache
val alicePartyAndCert2 = getTestPartyAndCertificate(ALICE_NAME, generateKeyPair().public)
aliceCache.addNode(aliceNode.info.copy(legalIdentitiesAndCerts = listOf(alicePartyAndCert2)))
// This is correct behaviour as we may have distributed service nodes.
assertEquals(2, aliceCache.getNodesByLegalName(ALICE_NAME).size)
}
} }

View File

@ -15,7 +15,6 @@ import net.corda.core.schemas.QueryableState
import net.corda.node.services.api.SchemaService import net.corda.node.services.api.SchemaService
import net.corda.node.internal.configureDatabase import net.corda.node.internal.configureDatabase
import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager
import net.corda.testing.internal.LogHelper import net.corda.testing.internal.LogHelper
import net.corda.testing.TestIdentity import net.corda.testing.TestIdentity
import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyContract
@ -74,11 +73,11 @@ class HibernateObserverTests {
database.transaction { database.transaction {
val MEGA_CORP = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party val MEGA_CORP = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party
rawUpdatesPublisher.onNext(Vault.Update(emptySet(), setOf(StateAndRef(TransactionState(TestState(), DummyContract.PROGRAM_ID, MEGA_CORP), StateRef(SecureHash.sha256("dummy"), 0))))) rawUpdatesPublisher.onNext(Vault.Update(emptySet(), setOf(StateAndRef(TransactionState(TestState(), DummyContract.PROGRAM_ID, MEGA_CORP), StateRef(SecureHash.sha256("dummy"), 0)))))
val parentRowCountResult = DatabaseTransactionManager.current().connection.prepareStatement("select count(*) from Parents").executeQuery() val parentRowCountResult = connection.prepareStatement("select count(*) from Parents").executeQuery()
parentRowCountResult.next() parentRowCountResult.next()
val parentRows = parentRowCountResult.getInt(1) val parentRows = parentRowCountResult.getInt(1)
parentRowCountResult.close() parentRowCountResult.close()
val childrenRowCountResult = DatabaseTransactionManager.current().connection.prepareStatement("select count(*) from Children").executeQuery() val childrenRowCountResult = connection.prepareStatement("select count(*) from Children").executeQuery()
childrenRowCountResult.next() childrenRowCountResult.next()
val childrenRows = childrenRowCountResult.getInt(1) val childrenRows = childrenRowCountResult.getInt(1)
childrenRowCountResult.close() childrenRowCountResult.close()

Some files were not shown because too many files have changed in this diff Show More