mirror of
https://github.com/corda/corda.git
synced 2024-12-28 00:38:55 +00:00
Merge branch 'master' into colljos-schema-split-for-notaries
This commit is contained in:
commit
4bff002b41
3
.idea/compiler.xml
generated
3
.idea/compiler.xml
generated
@ -179,4 +179,7 @@
|
||||
<module name="webserver_test" target="1.8" />
|
||||
</bytecodeTargetLevel>
|
||||
</component>
|
||||
<component name="JavacSettings">
|
||||
<option name="ADDITIONAL_OPTIONS_STRING" value="-parameters" />
|
||||
</component>
|
||||
</project>
|
@ -29,7 +29,7 @@ import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.nodeapi.ArtemisConsumer
|
||||
import net.corda.nodeapi.ArtemisProducer
|
||||
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.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE
|
||||
import org.apache.activemq.artemis.api.core.client.ClientMessage
|
||||
@ -194,7 +194,7 @@ class RPCClientProxyHandler(
|
||||
TimeUnit.MILLISECONDS
|
||||
)
|
||||
sessionAndProducerPool.run {
|
||||
it.session.createTemporaryQueue(clientAddress, ActiveMQDefaultConfiguration.getDefaultRoutingType(), clientAddress)
|
||||
it.session.createTemporaryQueue(clientAddress, RoutingType.ANYCAST, clientAddress)
|
||||
}
|
||||
val sessionFactory = serverLocator.createSessionFactory()
|
||||
val session = sessionFactory.createSession(rpcUsername, rpcPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE)
|
||||
|
@ -4,4 +4,3 @@ trustStorePassword : "trustpass"
|
||||
p2pAddress : "localhost:10002"
|
||||
rpcAddress : "localhost:10003"
|
||||
webAddress : "localhost:10004"
|
||||
useHTTPS : false
|
||||
|
@ -4,4 +4,3 @@ trustStorePassword : "trustpass"
|
||||
p2pAddress : "localhost:10005"
|
||||
rpcAddress : "localhost:10006"
|
||||
webAddress : "localhost:10007"
|
||||
useHTTPS : false
|
||||
|
@ -6,4 +6,3 @@ webAddress : "localhost:10001"
|
||||
notary : {
|
||||
validating : true
|
||||
}
|
||||
useHTTPS : false
|
||||
|
@ -66,26 +66,24 @@ object NodeInfoSchemaV1 : MappedSchema(
|
||||
}
|
||||
}
|
||||
|
||||
@Embeddable
|
||||
data class PKHostAndPort(
|
||||
val host: String? = null,
|
||||
val port: Int? = null
|
||||
) : Serializable
|
||||
|
||||
@Entity
|
||||
@Table(name = "node_info_hosts")
|
||||
data class DBHostAndPort(
|
||||
@EmbeddedId
|
||||
private val pk: PKHostAndPort
|
||||
@Id
|
||||
@GeneratedValue
|
||||
@Column(name = "hosts_id")
|
||||
var id: Int,
|
||||
val host: String? = null,
|
||||
val port: Int? = null
|
||||
) {
|
||||
companion object {
|
||||
fun fromHostAndPort(hostAndPort: NetworkHostAndPort) = DBHostAndPort(
|
||||
PKHostAndPort(hostAndPort.host, hostAndPort.port)
|
||||
0, hostAndPort.host, hostAndPort.port
|
||||
)
|
||||
}
|
||||
|
||||
fun toHostAndPort(): NetworkHostAndPort {
|
||||
return NetworkHostAndPort(this.pk.host!!, this.pk.port!!)
|
||||
return NetworkHostAndPort(host!!, port!!)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -158,6 +158,13 @@ R3 Corda 3.0 Developer Preview
|
||||
* 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.
|
||||
|
||||
* 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: node configuration property ``database.schema`` and documented existing database properties.
|
||||
|
@ -43,7 +43,6 @@ Simple Notary configuration file.
|
||||
notary : {
|
||||
validating : false
|
||||
}
|
||||
useHTTPS : false
|
||||
devMode : true
|
||||
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.
|
||||
|
||||
: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
|
||||
following fields:
|
||||
|
||||
|
@ -46,7 +46,6 @@ handling, and ensures the Corda service is run at boot.
|
||||
myLegalName : "O=Bank of Breakfast Tea, L=London, C=GB"
|
||||
keyStorePassword : "cordacadevpass"
|
||||
trustStorePassword : "trustpass"
|
||||
useHTTPS : false
|
||||
devMode : false
|
||||
rpcUsers=[
|
||||
{
|
||||
@ -217,7 +216,6 @@ at boot, and means the Corda service stays running with no users connected to th
|
||||
keyStorePassword : "cordacadevpass"
|
||||
trustStorePassword : "trustpass"
|
||||
extraAdvertisedServiceIds: [ "" ]
|
||||
useHTTPS : false
|
||||
devMode : false
|
||||
rpcUsers=[
|
||||
{
|
||||
|
@ -4,4 +4,3 @@ trustStorePassword : "trustpass"
|
||||
p2pAddress : "my-network-map:10000"
|
||||
webAddress : "localhost:10001"
|
||||
sshdAddress : "localhost:10002"
|
||||
useHTTPS : false
|
||||
|
@ -10,7 +10,6 @@ dataSourceProperties : {
|
||||
p2pAddress : "my-corda-node:10002"
|
||||
rpcAddress : "my-corda-node:10003"
|
||||
webAddress : "localhost:10004"
|
||||
useHTTPS : false
|
||||
rpcUsers : [
|
||||
{ username=user1, password=letmein, permissions=[ StartFlow.net.corda.protocols.CashProtocol ] }
|
||||
]
|
||||
|
@ -1,6 +1,8 @@
|
||||
Glossary
|
||||
========
|
||||
|
||||
AMQP
|
||||
The serialisation mechanism used within Corda for everything except flow checkpoints and RPC.
|
||||
Artemis
|
||||
The message queuing middleware used within Corda
|
||||
Attachment
|
||||
@ -30,7 +32,7 @@ Gradle
|
||||
Kotlin
|
||||
The language used to code Corda. Fully compatible with any JVM language, including (obviously) Java.
|
||||
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
|
||||
In Corda terms, an input state is one that is used and consumed within a transaction. Once consumed, it cannot be re-used.
|
||||
JVM
|
||||
|
@ -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
|
||||
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
|
||||
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).
|
||||
@ -54,7 +54,7 @@ for maintenance and other minor purposes.
|
||||
:``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.
|
||||
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.
|
||||
|
||||
:``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
|
||||
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
|
||||
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
|
||||
is successful then they are only given sufficient permission to perform RPC, otherwise they are rejected.
|
||||
|
@ -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
|
||||
|
||||
.. 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
|
||||
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
|
||||
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
|
||||
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 support cross platform (non-JVM) interaction, where the format of a class file is not so easily interpreted.
|
||||
#. 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
|
||||
#. 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
|
||||
#. 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
|
||||
and do not need to be defined in the schema *upfront*, which is key to many Corda concepts, such as contract states.
|
||||
#. Increased security from deserialized objects being constructed through supported constructors rather than having
|
||||
data poked directly into their fields without an opportunity to validate consistency or intercept attempts to manipulate
|
||||
supposed invariants.
|
||||
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 by constructing deserialized objects through supported constructors, rather than having
|
||||
data inserted directly into their fields without an opportunity to validate consistency or intercept attempts to manipulate
|
||||
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
|
||||
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 peer to peer context, representing inter-node communication.
|
||||
#. The persistence layer, representing contract states persisted into the vault.
|
||||
#. The peer-to-peer context, representing inter-node communication
|
||||
#. 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
|
||||
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
|
||||
than our AMQP framework in what it can construct and serialize, 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
|
||||
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. However, that flexibility makes it exceptionally difficult to make secure. Conversely,
|
||||
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.
|
||||
|
||||
.. 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`.
|
||||
|
||||
We describe here what is and 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
|
||||
course continue to apply the whitelisting functionality that is already in place and described in :doc:`serialization`.
|
||||
This document describes what is currently and what will be supported in the Corda AMQP format from the perspective
|
||||
of CorDapp developers, to allow CorDapps to take into consideration the future state. The AMQP serialization format will
|
||||
continue to apply the whitelisting functionality that is already in place and described in :doc:`serialization`.
|
||||
|
||||
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
|
||||
````````````````
|
||||
|
||||
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,
|
||||
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
|
||||
in their generic form, the generic type parameters will be included in the schema, and the elements type checked against the
|
||||
generic parameters when deserialized.
|
||||
For example, if you use a Guava implementation of a collection, it will deserialize as the primitive collection type.
|
||||
|
||||
The declared types of properties should only use these types, and not any concrete implementation types (e.g.
|
||||
Guava implementations). Collections must specify their generic type, the generic type parameters will be included in
|
||||
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.NavigableMap
|
||||
|
||||
However, we will support the concrete implementation types below explicitly and also as the declared type of a field, as
|
||||
a convenience.
|
||||
However, as a convenience, we explicitly support the concrete implementation types below, and they can be used as the
|
||||
declared types of properties.
|
||||
|
||||
::
|
||||
|
||||
@ -151,12 +151,12 @@ All the primitive types are supported.
|
||||
Arrays
|
||||
``````
|
||||
|
||||
We also support arrays of any supported type, primitive or otherwise.
|
||||
Arrays of any type are supported, primitive or otherwise.
|
||||
|
||||
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.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
|
||||
```````````
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
not conform to ``CordaThrowable`` will be converted to a ``CordaRuntimeException``, with the original exception type
|
||||
and other properties retained within it.
|
||||
|
||||
.. _amqp_custom_types_ref:
|
||||
@ -227,7 +227,7 @@ and other properties retained within it.
|
||||
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
|
||||
```````
|
||||
@ -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
|
||||
but must be turned on in Java (``-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
|
||||
but must be turned on in Java using the ``-parameters`` command line option to ``javac``
|
||||
|
||||
.. 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
|
||||
widened to an open wildcard.
|
||||
#. 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.
|
||||
widened to an open wildcard
|
||||
#. 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
|
||||
|
||||
Constructor Instantiation
|
||||
'''''''''''''''''''''''''
|
||||
|
||||
The primary way the AMQP serialization framework for Corda instantiates objects is via a defined constructor. This is
|
||||
used to first determine which properties of an object are to be serialised then, on deserialization, it is used to
|
||||
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
|
||||
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
|
||||
be created
|
||||
It is recommended that serializable objects in Corda adhere to the following rules, as they allow immutable state
|
||||
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
|
||||
parameter ``foo``, there must be a getter called ``getFoo()``. If the type of ``foo`` is boolean, the getter may
|
||||
optionally be called ``isFoo()``. This is why the class must be compiled with parameter names turned on.
|
||||
#. 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 ``foo`` is a boolean, the getter may
|
||||
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
|
||||
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``
|
||||
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
|
||||
|
||||
@ -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)
|
||||
|
||||
Both properties a and b will be included in the serialised form. However, as stated above, 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
|
||||
Properties ``a`` and ``b`` will be included in 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
|
||||
|
||||
@ -291,14 +298,14 @@ serialised form
|
||||
Setter Instantiation
|
||||
''''''''''''''''''''
|
||||
|
||||
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
|
||||
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 the class. If a class has **only** a default
|
||||
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
|
||||
the classic *idiom* of mutable JavaBeans.
|
||||
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).
|
||||
|
||||
On deserialization, a default instance will first be created and then, in turn, the setters invoked
|
||||
on that object to populate the correct values.
|
||||
On deserialization, a default instance will first be created, and then the setters will be invoked on that object to
|
||||
populate it with the correct values.
|
||||
|
||||
For example:
|
||||
|
||||
@ -324,7 +331,7 @@ Inaccessible Private Properties
|
||||
```````````````````````````````
|
||||
|
||||
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.
|
||||
|
||||
@ -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,
|
||||
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
|
||||
visibility. Ignore this as whilst it will work, as discussed above there are many reasons why this isn't
|
||||
a good idea and those are beyond the scope of the IDEs inference rules
|
||||
.. warning:: IDEs will indicate erroneously that properties can be given something other than public visibility. Ignore
|
||||
this, as whilst it will work, as discussed above there are many reasons why this isn't a good idea.
|
||||
|
||||
Providing a public getter, as per the following example, is acceptable
|
||||
Providing a public getter, as per the following example, is acceptable:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
@ -392,7 +398,7 @@ Providing a public getter, as per the following example, is acceptable
|
||||
Enums
|
||||
`````
|
||||
|
||||
#. All enums are supported, provided they are annotated with ``@CordaSerializable``.
|
||||
#. All enums are supported, provided they are annotated with ``@CordaSerializable``
|
||||
|
||||
|
||||
Exceptions
|
||||
@ -401,24 +407,25 @@ Exceptions
|
||||
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
|
||||
``CordaException`` or ``CordaRuntimeException``.
|
||||
``CordaException`` or ``CordaRuntimeException``
|
||||
#. 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 ``object`` s are singletons and treated differently. They are recorded into the stream with no properties
|
||||
and deserialize back to the singleton instance. Currently, the same is not true of Java singletons,
|
||||
and they will deserialize to new instances of the class.
|
||||
#. Kotlin's anonymous ``object`` s are not currently supported. I.e. constructs like:
|
||||
``object : Contract {...}`` will not serialize correctly and need to be re-written as an explicit class declaration.
|
||||
#. Kotlin's non-anonymous ``object`` s (i.e. constructs like ``object foo : Contract {...}``) are singletons and
|
||||
treated differently. They are recorded into the stream with no properties, and deserialize back to the
|
||||
singleton instance. Currently, the same is not true of Java singletons, which will deserialize to new instances
|
||||
of the class
|
||||
#. 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
|
||||
`````````````
|
||||
|
||||
We will 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
|
||||
We support a class carpenter that can dynamically manufacture classes from the supplied schema when deserializing,
|
||||
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
|
||||
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.
|
||||
@ -427,25 +434,25 @@ Future Enhancements
|
||||
```````````````````
|
||||
|
||||
#. 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
|
||||
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 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
|
||||
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
|
||||
``````````````
|
||||
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``.
|
||||
|
||||
|
||||
|
||||
|
@ -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)
|
||||
==========================================================
|
||||
|
||||
@ -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
|
||||
programmatically evolve flows across versions. For example:
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
.. container:: codeset
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val otherFlowVersion = otherSession.getCounterpartyFlowInfo().flowVersion
|
||||
val receivedString = if (otherFlowVersion == 1) {
|
||||
receive<Int>(otherParty).unwrap { it.toString() }
|
||||
} else {
|
||||
receive<String>(otherParty).unwrap { it }
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val otherFlowVersion = otherSession.getCounterpartyFlowInfo().flowVersion
|
||||
val receivedString = if (otherFlowVersion == 1) {
|
||||
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
|
||||
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:
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
.. container:: codeset
|
||||
|
||||
@StartableByRPC
|
||||
@InitiatingFlow
|
||||
class FlowA(val recipient: Party) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
subFlow(FlowB(recipient))
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
@StartableByRPC
|
||||
@InitiatingFlow
|
||||
class FlowA(val recipient: Party) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
subFlow(FlowB(recipient))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(FlowA::class)
|
||||
class FlowC(val otherSession: FlowSession) : FlowLogic() {
|
||||
// Omitted.
|
||||
}
|
||||
|
||||
// Note: No annotations. This is used as an inlined subflow.
|
||||
class FlowB(val recipient: Party) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val message = "I'm an inlined subflow, so I inherit the @InitiatingFlow's session ID and type."
|
||||
initiateFlow(recipient).send(message)
|
||||
@InitiatedBy(FlowA::class)
|
||||
class FlowC(val otherSession: FlowSession) : FlowLogic() {
|
||||
// Omitted.
|
||||
}
|
||||
|
||||
// Note: No annotations. This is used as an inlined subflow.
|
||||
class FlowB(val recipient: Party) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val message = "I'm an inlined subflow, so I inherit the @InitiatingFlow's session ID and type."
|
||||
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.
|
||||
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
|
||||
existing object relational mapper. For example, we can update:
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
.. container:: codeset
|
||||
|
||||
object ObligationSchemaV1 : MappedSchema(Obligation::class.java, 1, listOf(ObligationEntity::class.java)) {
|
||||
@Entity @Table(name = "obligations")
|
||||
class ObligationEntity(obligation: Obligation) : PersistentState() {
|
||||
@Column var currency: String = obligation.amount.token.toString()
|
||||
@Column var amount: Long = obligation.amount.quantity
|
||||
@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:: kotlin
|
||||
|
||||
object ObligationSchemaV1 : MappedSchema(Obligation::class.java, 1, listOf(ObligationEntity::class.java)) {
|
||||
@Entity @Table(name = "obligations")
|
||||
class ObligationEntity(obligation: Obligation) : PersistentState() {
|
||||
@Column var currency: String = obligation.amount.token.toString()
|
||||
@Column var amount: Long = obligation.amount.quantity
|
||||
@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:
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
.. container:: codeset
|
||||
|
||||
object ObligationSchemaV1 : MappedSchema(Obligation::class.java, 1, listOf(ObligationEntity::class.java)) {
|
||||
@Entity @Table(name = "obligations")
|
||||
class ObligationEntity(obligation: Obligation) : PersistentState() {
|
||||
@Column var currency: String = obligation.amount.token.toString()
|
||||
@Column var amount: Long = obligation.amount.quantity
|
||||
@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()
|
||||
@Column var defaulted: Bool = obligation.amount.inDefault // NEW COLUNM!
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
object ObligationSchemaV1 : MappedSchema(Obligation::class.java, 1, listOf(ObligationEntity::class.java)) {
|
||||
@Entity @Table(name = "obligations")
|
||||
class ObligationEntity(obligation: Obligation) : PersistentState() {
|
||||
@Column var currency: String = obligation.amount.token.toString()
|
||||
@Column var amount: Long = obligation.amount.quantity
|
||||
@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()
|
||||
@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.
|
||||
|
||||
@ -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
|
||||
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:
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
.. container:: codeset
|
||||
|
||||
override fun generateMappedObject(schema: MappedSchema): PersistentState {
|
||||
return when (schema) {
|
||||
is DummyLinearStateSchemaV1 -> // Omitted.
|
||||
is DummyLinearStateSchemaV2 -> // Omitted.
|
||||
else -> throw IllegalArgumentException("Unrecognised schema $schema")
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
override fun generateMappedObject(schema: MappedSchema): PersistentState {
|
||||
return when (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
|
||||
separate database tables where possible - one for each supported schema.
|
@ -2,7 +2,7 @@ package net.corda.flowhook
|
||||
|
||||
import co.paralleluniverse.fibers.Fiber
|
||||
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
|
||||
|
||||
@Suppress("UNUSED")
|
||||
@ -156,7 +156,7 @@ object FlowHookContainer {
|
||||
|
||||
private fun currentTransactionOrThread(): Any {
|
||||
return try {
|
||||
DatabaseTransactionManager.currentOrNull()
|
||||
contextTransactionOrNull
|
||||
} catch (exception: IllegalStateException) {
|
||||
null
|
||||
} ?: Thread.currentThread()
|
||||
|
@ -65,13 +65,15 @@ The doorman service can use JIRA to manage the certificate signing request appro
|
||||
projectCode = "TD"
|
||||
username = "username"
|
||||
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
|
||||
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"
|
||||
username = "username"
|
||||
password = "password"
|
||||
doneTransitionCode = 41
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,7 +88,7 @@ dependencies {
|
||||
testCompile "com.nhaarman:mockito-kotlin:0.6.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.
|
||||
exclude module: 'jersey-core'
|
||||
}
|
||||
|
@ -23,7 +23,6 @@ doormanConfig{
|
||||
projectCode = "TD"
|
||||
username = "username"
|
||||
password = "password"
|
||||
doneTransitionCode = 41
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package com.r3.corda.networkmanage.doorman
|
||||
|
||||
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 net.corda.cordform.CordformNode
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
@ -141,11 +142,11 @@ class NodeRegistrationTest : IntegrationTest() {
|
||||
start(
|
||||
serverAddress,
|
||||
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),
|
||||
networkParameters?.let {
|
||||
NetworkMapStartParams(
|
||||
LocalSigner(networkMapCa.keyPair, arrayOf(networkMapCa.certificate, rootCaCert)),
|
||||
LocalSigner(networkMapCa),
|
||||
networkParameters,
|
||||
NetworkMapConfig(cacheTimeout = timeoutMillis, signInterval = timeoutMillis)
|
||||
)
|
||||
|
@ -12,6 +12,7 @@ import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class HsmTest {
|
||||
@ -19,7 +20,12 @@ class HsmTest {
|
||||
@Rule
|
||||
@JvmField
|
||||
val hsmSimulator: HsmSimulator = HsmSimulator()
|
||||
val testParameters = Parameters(
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val tempFolder = TemporaryFolder()
|
||||
|
||||
private val testParameters = Parameters(
|
||||
dataSourceProperties = mock(),
|
||||
device = "${hsmSimulator.port}@${hsmSimulator.host}",
|
||||
keySpecifier = 1,
|
||||
@ -30,10 +36,6 @@ class HsmTest {
|
||||
validDays = 3650
|
||||
)
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val tempFolder = TemporaryFolder()
|
||||
|
||||
private lateinit var inputReader: InputReader
|
||||
|
||||
@Before
|
||||
@ -47,14 +49,12 @@ class HsmTest {
|
||||
fun `Authenticator executes the block once user is successfully authenticated`() {
|
||||
// given
|
||||
val authenticator = Authenticator(testParameters.createProvider(), inputReader = inputReader)
|
||||
var executed = false
|
||||
val executed = AtomicBoolean(false)
|
||||
|
||||
// when
|
||||
authenticator.connectAndAuthenticate({ provider, signers ->
|
||||
executed = true
|
||||
})
|
||||
authenticator.connectAndAuthenticate { _, _ -> executed.set(true) }
|
||||
|
||||
// then
|
||||
assertTrue(executed)
|
||||
assertTrue(executed.get())
|
||||
}
|
||||
}
|
@ -90,7 +90,12 @@ class SigningServiceIntegrationTest {
|
||||
val database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(runMigration = true))
|
||||
|
||||
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
|
||||
// Start Corda network registration.
|
||||
val config = createConfig().also {
|
||||
|
@ -1,12 +1,14 @@
|
||||
package com.r3.corda.networkmanage.common.persistence
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||
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,
|
||||
val legalName: String,
|
||||
val publicKeyHash: SecureHash,
|
||||
val status: RequestStatus,
|
||||
val request: PKCS10CertificationRequest,
|
||||
val remark: String?,
|
||||
@ -59,7 +61,7 @@ interface CertificationRequestStorage {
|
||||
* @param rejectedBy authority (its identifier) rejecting this request.
|
||||
* @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].
|
||||
|
@ -39,10 +39,9 @@ interface NetworkMapStorage {
|
||||
fun getSignedNetworkParameters(hash: SecureHash): SignedNetworkParameters?
|
||||
|
||||
/**
|
||||
* Retrieve network map parameters.
|
||||
* @return signed current network map parameters or null if they don't exist
|
||||
* Retrieve the network parameters of the current network map, or null if there's no network map.
|
||||
*/
|
||||
fun getCurrentSignedNetworkParameters(): SignedNetworkParameters?
|
||||
fun getNetworkParametersOfNetworkMap(): SignedNetworkParameters?
|
||||
|
||||
/**
|
||||
* Persists given network parameters with signature if provided.
|
||||
@ -55,5 +54,5 @@ interface NetworkMapStorage {
|
||||
* Note that they may not have been signed up yet.
|
||||
* @return latest network parameters
|
||||
*/
|
||||
fun getLatestUnsignedNetworkParameters(): NetworkParameters
|
||||
}
|
||||
fun getLatestNetworkParameters(): NetworkParameters?
|
||||
}
|
||||
|
@ -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.CertificateSigningRequestEntity
|
||||
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.identity.CordaX500Name
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
@ -26,14 +27,12 @@ class PersistentCertificateRequestStorage(private val database: CordaPersistence
|
||||
builder.and(requestIdEq, statusEq)
|
||||
}
|
||||
request ?: throw IllegalArgumentException("Cannot retrieve 'APPROVED' certificate signing request for request id: $requestId")
|
||||
val publicKeyHash = certificates.certificates.first().publicKey.hashString()
|
||||
val certificateSigningRequest = request.copy(
|
||||
modifiedBy = signedBy,
|
||||
modifiedAt = Instant.now(),
|
||||
status = RequestStatus.SIGNED)
|
||||
session.merge(certificateSigningRequest)
|
||||
val certificateDataEntity = CertificateDataEntity(
|
||||
publicKeyHash = publicKeyHash,
|
||||
certificateStatus = CertificateStatus.VALID,
|
||||
certificatePathBytes = certificates.encoded,
|
||||
certificateSigningRequest = certificateSigningRequest)
|
||||
@ -48,6 +47,7 @@ class PersistentCertificateRequestStorage(private val database: CordaPersistence
|
||||
session.save(CertificateSigningRequestEntity(
|
||||
requestId = requestId,
|
||||
legalName = legalName,
|
||||
publicKeyHash = toSupportedPublicKey(request.subjectPublicKeyInfo).hashString(),
|
||||
requestBytes = request.encoded,
|
||||
remark = rejectReason,
|
||||
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) {
|
||||
val request = findRequest(requestId)
|
||||
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}")
|
||||
}
|
||||
|
||||
val query = session.criteriaBuilder.run {
|
||||
val duplicateNameQuery = session.criteriaBuilder.run {
|
||||
val criteriaQuery = createQuery(CertificateSigningRequestEntity::class.java)
|
||||
criteriaQuery.from(CertificateSigningRequestEntity::class.java).run {
|
||||
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
|
||||
// Also, at the moment we assume that once the CSR is approved it cannot be rejected.
|
||||
// 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
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
@ -6,10 +6,7 @@ import com.r3.corda.networkmanage.common.utils.SignedNetworkParameters
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
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.nodeapi.internal.network.NetworkMap
|
||||
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||
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 {
|
||||
override fun getCurrentNetworkMap(): SignedNetworkMap? {
|
||||
return database.transaction {
|
||||
getCurrentNetworkMapEntity()?.let {
|
||||
val signatureAndCertPath = it.signatureAndCertificate()
|
||||
SignedNetworkMap(SerializedBytes(it.networkMap), signatureAndCertPath)
|
||||
}
|
||||
getCurrentNetworkMapEntity()?.toSignedNetworkMap()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCurrentSignedNetworkParameters(): SignedNetworkParameters? {
|
||||
override fun getNetworkParametersOfNetworkMap(): SignedNetworkParameters? {
|
||||
return database.transaction {
|
||||
getCurrentNetworkMapEntity()?.let {
|
||||
val netParamsHash = it.networkMap.deserialize<NetworkMap>().networkParameterHash
|
||||
getSignedNetworkParameters(netParamsHash)
|
||||
val netParamsHash = it.toNetworkMap().networkParameterHash
|
||||
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? {
|
||||
return getNetworkParametersEntity(hash.toString())?.signedParameters()
|
||||
return getNetworkParametersEntity(hash.toString())?.let {
|
||||
if (it.isSigned) it.toSignedNetworkParameters() else null
|
||||
}
|
||||
}
|
||||
|
||||
override fun getNodeInfoHashes(certificateStatus: CertificateStatus): List<SecureHash> {
|
||||
@ -79,18 +76,17 @@ class PersistentNetworkMapStorage(private val database: CordaPersistence) : Netw
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLatestUnsignedNetworkParameters(): NetworkParameters = getLatestNetworkParametersEntity().networkParameters()
|
||||
|
||||
private fun getLatestNetworkParametersEntity(): NetworkParametersEntity {
|
||||
override fun getLatestNetworkParameters(): NetworkParameters? {
|
||||
return database.transaction {
|
||||
val builder = session.criteriaBuilder
|
||||
val query = builder.createQuery(NetworkParametersEntity::class.java).run {
|
||||
from(NetworkParametersEntity::class.java).run {
|
||||
orderBy(builder.desc(get<String>(NetworkParametersEntity::created.name)))
|
||||
val query = session.criteriaBuilder.run {
|
||||
createQuery(NetworkParametersEntity::class.java).run {
|
||||
from(NetworkParametersEntity::class.java).run {
|
||||
orderBy(desc(get<String>(NetworkParametersEntity::created.name)))
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
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.NodeInfoEntity
|
||||
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.nodeapi.internal.SignedNodeInfo
|
||||
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
|
||||
|
||||
/**
|
||||
@ -21,16 +20,13 @@ class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeIn
|
||||
val nodeInfo = nodeInfoWithSigned.nodeInfo
|
||||
val signedNodeInfo = nodeInfoWithSigned.signedNodeInfo
|
||||
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
|
||||
val request = nodeCaCert?.let {
|
||||
singleRequestWhere(CertificateDataEntity::class.java) { builder, path ->
|
||||
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)
|
||||
}
|
||||
getSignedRequestByPublicHash(it.publicKey.encoded.sha256(), this)
|
||||
}
|
||||
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
|
||||
@ -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.
|
||||
*/
|
||||
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 hashedNodeInfo = NodeInfoEntity(
|
||||
nodeInfoHash = hash.toString(),
|
||||
certificateSigningRequest = request.certificateSigningRequest,
|
||||
certificateSigningRequest = request,
|
||||
signedNodeInfoBytes = signedNodeInfo.serialize().bytes)
|
||||
session.save(hashedNodeInfo)
|
||||
hash
|
||||
@ -61,16 +57,16 @@ class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeIn
|
||||
|
||||
override fun getCertificatePath(publicKeyHash: SecureHash): CertPath? {
|
||||
return database.transaction {
|
||||
val builder = session.criteriaBuilder
|
||||
val query = builder.createQuery(ByteArray::class.java).run {
|
||||
from(CertificateSigningRequestEntity::class.java).run {
|
||||
select(get<CertificateDataEntity>(CertificateSigningRequestEntity::certificateData.name)
|
||||
.get<ByteArray>(CertificateDataEntity::certificatePathBytes.name))
|
||||
where(builder.equal(get<CertificateDataEntity>(CertificateSigningRequestEntity::certificateData.name)
|
||||
.get<String>(CertificateDataEntity::publicKeyHash.name), publicKeyHash.toString()))
|
||||
}
|
||||
}
|
||||
session.createQuery(query).uniqueResultOptional().orElseGet { null }?.let { buildCertPath(it) }
|
||||
val request = getSignedRequestByPublicHash(publicKeyHash, this)
|
||||
request?.let { buildCertPath(it.certificateData!!.certificatePathBytes) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSignedRequestByPublicHash(publicKeyHash: SecureHash, transaction: DatabaseTransaction): CertificateSigningRequestEntity? {
|
||||
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)
|
||||
builder.and(publicKeyEq, statusEq)
|
||||
}
|
||||
}
|
||||
}
|
@ -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.CertificateStatus
|
||||
import com.r3.corda.networkmanage.common.persistence.RequestStatus
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||
import org.hibernate.envers.Audited
|
||||
import java.security.cert.CertPath
|
||||
@ -12,7 +13,7 @@ import java.time.Instant
|
||||
import javax.persistence.*
|
||||
|
||||
@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(
|
||||
@Id
|
||||
@Column(name = "request_id", length = 64)
|
||||
@ -22,6 +23,9 @@ class CertificateSigningRequestEntity(
|
||||
@Column(name = "legal_name", length = 256, nullable = false)
|
||||
val legalName: String,
|
||||
|
||||
@Column(name = "public_key_hash", length = 64)
|
||||
val publicKeyHash: String,
|
||||
|
||||
@Audited
|
||||
@Column(name = "status", nullable = false)
|
||||
@Enumerated(EnumType.STRING)
|
||||
@ -50,6 +54,7 @@ class CertificateSigningRequestEntity(
|
||||
fun toCertificateSigningRequest() = CertificateSigningRequest(
|
||||
requestId = requestId,
|
||||
legalName = legalName,
|
||||
publicKeyHash = SecureHash.parse(publicKeyHash),
|
||||
status = status,
|
||||
request = request(),
|
||||
remark = remark,
|
||||
@ -59,6 +64,7 @@ class CertificateSigningRequestEntity(
|
||||
|
||||
fun copy(requestId: String = this.requestId,
|
||||
legalName: String = this.legalName,
|
||||
publicKeyHash: String = this.publicKeyHash,
|
||||
status: RequestStatus = this.status,
|
||||
modifiedBy: List<String> = this.modifiedBy,
|
||||
modifiedAt: Instant = this.modifiedAt,
|
||||
@ -69,6 +75,7 @@ class CertificateSigningRequestEntity(
|
||||
return CertificateSigningRequestEntity(
|
||||
requestId = requestId,
|
||||
legalName = legalName,
|
||||
publicKeyHash = publicKeyHash,
|
||||
status = status,
|
||||
modifiedAt = modifiedAt,
|
||||
modifiedBy = modifiedBy,
|
||||
@ -82,16 +89,13 @@ class CertificateSigningRequestEntity(
|
||||
}
|
||||
|
||||
@Entity
|
||||
@Table(name = "certificate_data", indexes = arrayOf(Index(name = "IDX_PUB_KEY_HASH", columnList = "public_key_hash")))
|
||||
@Table(name = "certificate_data")
|
||||
class CertificateDataEntity(
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE)
|
||||
val id: Long? = null,
|
||||
|
||||
@Column(name = "public_key_hash", length = 64)
|
||||
val publicKeyHash: String,
|
||||
|
||||
@Column(name = "certificate_status")
|
||||
val certificateStatus: CertificateStatus,
|
||||
|
||||
@ -105,7 +109,6 @@ class CertificateDataEntity(
|
||||
) {
|
||||
fun toCertificateData(): CertificateData {
|
||||
return CertificateData(
|
||||
publicKeyHash = publicKeyHash,
|
||||
certStatus = certificateStatus,
|
||||
certPath = toCertificatePath()
|
||||
)
|
||||
|
@ -1,7 +1,11 @@
|
||||
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.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||
import net.corda.nodeapi.internal.network.NetworkMap
|
||||
import javax.persistence.*
|
||||
|
||||
@Entity
|
||||
@ -23,11 +27,12 @@ class NetworkMapEntity(
|
||||
@Column(name = "certificate")
|
||||
val certificate: ByteArray
|
||||
) {
|
||||
/**
|
||||
* Deserializes NetworkMapEntity.signatureBytes into the [DigitalSignatureWithCert] instance
|
||||
*/
|
||||
fun signatureAndCertificate(): DigitalSignatureWithCert {
|
||||
return DigitalSignatureWithCert(X509CertificateFactory().generateCertificate(certificate.inputStream()), signature)
|
||||
}
|
||||
fun toNetworkMap(): NetworkMap = networkMap.deserialize()
|
||||
|
||||
}
|
||||
fun toSignedNetworkMap(): SignedNetworkMap {
|
||||
return SignedNetworkMap(
|
||||
SerializedBytes(networkMap),
|
||||
DigitalSignatureWithCert(X509CertificateFactory().generateCertificate(certificate.inputStream()), signature)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -35,13 +35,15 @@ class NetworkParametersEntity(
|
||||
@Column(name = "certificate")
|
||||
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 signedParameters(): SignedNetworkParameters? {
|
||||
return if (certificate != null && signature != null) {
|
||||
val sigWithCert = DigitalSignatureWithCert(X509CertificateFactory().generateCertificate(certificate.inputStream()), signature)
|
||||
SignedDataWithCert(SerializedBytes(parametersBytes), sigWithCert)
|
||||
} else null
|
||||
fun toNetworkParameters(): NetworkParameters = parametersBytes.deserialize()
|
||||
|
||||
fun toSignedNetworkParameters(): SignedNetworkParameters {
|
||||
if (certificate == null || signature == null) throw IllegalStateException("Network parameters entity is not signed: $parametersHash")
|
||||
return SignedDataWithCert(
|
||||
SerializedBytes(parametersBytes),
|
||||
DigitalSignatureWithCert(X509CertificateFactory().generateCertificate(certificate.inputStream()), signature)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,20 +4,30 @@ import com.r3.corda.networkmanage.common.persistence.CertificateStatus
|
||||
import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
|
||||
import net.corda.core.internal.SignedDataWithCert
|
||||
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.NetworkParameters
|
||||
|
||||
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.
|
||||
*/
|
||||
fun signNetworkMap() {
|
||||
// TODO There is no network parameters update process in place yet. We assume that latest parameters are to be used
|
||||
// in current network map.
|
||||
val latestNetworkParameters = networkMapStorage.getLatestUnsignedNetworkParameters()
|
||||
val currentNetworkParameters = networkMapStorage.getCurrentSignedNetworkParameters()
|
||||
if (currentNetworkParameters?.verified() != latestNetworkParameters)
|
||||
signNetworkParameters(latestNetworkParameters)
|
||||
val latestNetworkParameters = networkMapStorage.getLatestNetworkParameters()
|
||||
if (latestNetworkParameters == null) {
|
||||
logger.debug("No network parameters present")
|
||||
return
|
||||
}
|
||||
val currentNetworkParameters = networkMapStorage.getNetworkParametersOfNetworkMap()
|
||||
if (currentNetworkParameters?.verified() != latestNetworkParameters) {
|
||||
persistSignedNetworkParameters(latestNetworkParameters)
|
||||
}
|
||||
val currentSignedNetworkMap = networkMapStorage.getCurrentNetworkMap()
|
||||
val nodeInfoHashes = networkMapStorage.getNodeInfoHashes(CertificateStatus.VALID)
|
||||
val serialisedNetworkMap = NetworkMap(nodeInfoHashes, latestNetworkParameters.serialize().hash).serialize()
|
||||
@ -27,10 +37,8 @@ class NetworkMapSigner(private val networkMapStorage: NetworkMapStorage, private
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs latest inserted network parameters.
|
||||
*/
|
||||
fun signNetworkParameters(networkParameters: NetworkParameters) {
|
||||
fun persistSignedNetworkParameters(networkParameters: NetworkParameters) {
|
||||
logger.info("Signing and persisting network parameters: $networkParameters")
|
||||
val digitalSignature = signer.signObject(networkParameters).sig
|
||||
networkMapStorage.saveNetworkParameters(networkParameters, digitalSignature)
|
||||
}
|
||||
|
@ -10,14 +10,20 @@ import net.corda.core.internal.SignedDataWithCert
|
||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||
import net.corda.nodeapi.internal.network.NetworkMap
|
||||
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||
import java.security.KeyPair
|
||||
import java.security.PrivateKey
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.CertPath
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
// TODO These should be defined in node-api
|
||||
typealias SignedNetworkParameters = SignedDataWithCert<NetworkParameters>
|
||||
typealias SignedNetworkMap = SignedDataWithCert<NetworkMap>
|
||||
|
||||
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.
|
||||
/**
|
||||
* 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()
|
||||
|
||||
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())
|
||||
|
||||
|
@ -65,8 +65,7 @@ data class JiraConfig(
|
||||
val address: String,
|
||||
val projectCode: String,
|
||||
val username: String,
|
||||
val password: String,
|
||||
val doneTransitionCode: Int
|
||||
val password: String
|
||||
)
|
||||
|
||||
/**
|
||||
|
@ -1,13 +1,15 @@
|
||||
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.domain.Comment
|
||||
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.IssueType
|
||||
import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder
|
||||
import com.atlassian.jira.rest.client.api.domain.input.TransitionInput
|
||||
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 org.bouncycastle.asn1.x500.style.BCStyle
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
|
||||
@ -17,15 +19,20 @@ import java.io.StringWriter
|
||||
import java.security.cert.CertPath
|
||||
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 {
|
||||
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 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 var doneTransitionId: Int = -1
|
||||
private var canceledTransitionId: Int = -1
|
||||
private var startProgressTransitionId: Int = -1
|
||||
|
||||
fun createRequestTicket(requestId: String, signingRequest: PKCS10CertificationRequest) {
|
||||
// Check there isn't already a ticket for this request.
|
||||
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()
|
||||
}
|
||||
|
||||
fun getApprovedRequests(): List<Pair<String, String>> {
|
||||
fun getApprovedRequests(): List<ApprovedRequest> {
|
||||
val issues = restClient.searchClient.searchJql("project = $projectCode AND status = Approved").claim().issues
|
||||
return issues.map { issue ->
|
||||
issue.getField(requestIdField.id)?.value?.toString().let {
|
||||
val requestId = it ?: throw IllegalArgumentException("RequestId cannot be null.")
|
||||
val approvedBy = issue.assignee?.displayName ?: "Unknown"
|
||||
Pair(requestId, approvedBy)
|
||||
}
|
||||
return issues.mapNotNull { issue ->
|
||||
val requestId = issue.getField(requestIdField.id)?.value?.toString() ?: throw IllegalArgumentException("Error processing request '${issue.key}' : RequestId cannot be null.")
|
||||
// Issue retrieved via search doesn't contain change logs.
|
||||
val fullIssue = restClient.issueClient.getIssue(issue.key, listOf(IssueRestClient.Expandos.CHANGELOG)).claim()
|
||||
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.
|
||||
signedRequests.forEach { (id, certPath) ->
|
||||
val certificate = certPath.certificates.first()
|
||||
// Jira only support ~ (contains) search for custom textfield.
|
||||
val issue = getIssueById(id)
|
||||
if (issue != null) {
|
||||
restClient.issueClient.transition(issue, TransitionInput(doneTransitionCode)).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("Exception when uploading attachment to JIRA.", it) }.claim()
|
||||
if (doneTransitionId == -1) {
|
||||
doneTransitionId = restClient.issueClient.getTransitions(issue.transitionsUri).claim().single { it.name == "Done" }.id
|
||||
}
|
||||
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? =
|
||||
restClient.searchClient.searchJql("'Request ID' ~ $requestId").claim().issues.firstOrNull()
|
||||
fun updateRejectedRequests(rejectedRequests: List<String>) {
|
||||
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?)
|
||||
|
@ -1,16 +1,11 @@
|
||||
package com.r3.corda.networkmanage.doorman
|
||||
|
||||
import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory
|
||||
import com.r3.corda.networkmanage.common.persistence.*
|
||||
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.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.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.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_NETWORK_MAP_CERTIFICATE_NAME
|
||||
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.nodeSerializationEnv
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.nodeapi.internal.crypto.*
|
||||
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.SerializationFactoryImpl
|
||||
import net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||
import java.io.Closeable
|
||||
import java.net.URI
|
||||
import java.nio.file.Path
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.security.auth.x500.X500Principal
|
||||
import kotlin.concurrent.thread
|
||||
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 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: ")
|
||||
|
||||
if (rootStore.containsAlias(X509Utilities.CORDA_ROOT_CA)) {
|
||||
val oldKey = loadOrCreateKeyStore(rootStoreFile, rootKeystorePassword).getCertificate(X509Utilities.CORDA_ROOT_CA).publicKey
|
||||
println("Key ${X509Utilities.CORDA_ROOT_CA} already exists in keystore, process will now terminate.")
|
||||
println(oldKey)
|
||||
println("${X509Utilities.CORDA_ROOT_CA} already exists in keystore, process will now terminate.")
|
||||
println(rootStore.getCertificate(X509Utilities.CORDA_ROOT_CA))
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
@ -203,7 +75,7 @@ fun generateRootKeyPair(rootStoreFile: Path, rootKeystorePass: String?, rootPriv
|
||||
println("Trust store for distribution to nodes created in $nodeTrustStore")
|
||||
|
||||
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?) {
|
||||
@ -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
|
||||
|
||||
// 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 keyStore = loadOrCreateKeyStore(parameters.keystorePath, keyStorePassword)
|
||||
|
||||
val (doormanSigner, networkMapSigner) = listOf(DEFAULT_CSR_CERTIFICATE_NAME, DEFAULT_NETWORK_MAP_CERTIFICATE_NAME).map {
|
||||
val keyPair = keyStore.getKeyPair(it, privateKeyPassword)
|
||||
val certPath = keyStore.getCertificateChain(it).map { it as X509Certificate }
|
||||
LocalSigner(keyPair, certPath.toTypedArray())
|
||||
val csrCertPathAndKey = keyStore.run {
|
||||
CertPathAndKey(
|
||||
keyStore.getCertificateChain(DEFAULT_CSR_CERTIFICATE_NAME).map { it as X509Certificate },
|
||||
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.
|
||||
*/
|
||||
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 {
|
||||
val requestId = delegate.saveRequest(request)
|
||||
delegate.markRequestTicketCreated(requestId)
|
||||
@ -311,9 +186,9 @@ fun main(args: Array<String>) {
|
||||
initialiseSerialization()
|
||||
val database = configureDatabase(dataSourceProperties)
|
||||
// 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")
|
||||
}
|
||||
|
||||
@ -325,10 +200,10 @@ fun main(args: Array<String>) {
|
||||
parseNetworkParametersFrom(it)
|
||||
}
|
||||
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) {
|
||||
networkManagementServer.close()
|
||||
|
@ -1,58 +1,140 @@
|
||||
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.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 net.corda.nodeapi.internal.network.NetworkParameters
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
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
|
||||
|
||||
/**
|
||||
* NetworkManagementWebServer runs on Jetty server and provides service via http.
|
||||
*/
|
||||
class NetworkManagementWebServer(hostAndPort: NetworkHostAndPort, private vararg val webServices: Any) : Closeable {
|
||||
class NetworkManagementServer : Closeable {
|
||||
companion object {
|
||||
val logger = loggerFor<NetworkManagementServer>()
|
||||
private val logger = loggerFor<NetworkManagementServer>()
|
||||
}
|
||||
|
||||
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()
|
||||
private val closeActions = mutableListOf<() -> Unit>()
|
||||
lateinit var hostAndPort: NetworkHostAndPort
|
||||
|
||||
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) }
|
||||
for (closeAction in closeActions) {
|
||||
try {
|
||||
closeAction()
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Discregarding exception thrown during close", e)
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
@ -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, "/*")
|
||||
}
|
||||
}
|
||||
}
|
@ -1,41 +1,41 @@
|
||||
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.CertificationRequestStorage.Companion.DOORMAN_SIGNATURE
|
||||
import com.r3.corda.networkmanage.common.persistence.RequestStatus
|
||||
import com.r3.corda.networkmanage.doorman.JiraClient
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import com.r3.corda.networkmanage.common.utils.CertPathAndKey
|
||||
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.jcajce.JcaPKCS10CertificationRequest
|
||||
import java.security.cert.CertPath
|
||||
import javax.security.auth.x500.X500Principal
|
||||
|
||||
interface CsrHandler {
|
||||
fun saveRequest(rawRequest: PKCS10CertificationRequest): String
|
||||
fun createTickets()
|
||||
fun processApprovedRequests()
|
||||
fun processRequests()
|
||||
fun getResponse(requestId: String): CertificateResponse
|
||||
}
|
||||
|
||||
class DefaultCsrHandler(private val storage: CertificationRequestStorage, private val signer: LocalSigner?) : CsrHandler {
|
||||
override fun processApprovedRequests() {
|
||||
storage.getRequests(RequestStatus.APPROVED)
|
||||
.forEach { processRequest(it.requestId, it.request) }
|
||||
}
|
||||
class DefaultCsrHandler(private val storage: CertificationRequestStorage,
|
||||
private val csrCertPathAndKey: CertPathAndKey?) : CsrHandler {
|
||||
|
||||
override fun createTickets() {}
|
||||
|
||||
private fun processRequest(requestId: String, request: PKCS10CertificationRequest) {
|
||||
if (signer != null) {
|
||||
val certs = signer.createSignedClientCertificate(request)
|
||||
override fun processRequests() {
|
||||
if (csrCertPathAndKey == null) return
|
||||
storage.getRequests(RequestStatus.APPROVED).forEach {
|
||||
val nodeCertPath = createSignedNodeCertificate(it.request, csrCertPathAndKey)
|
||||
// Since Doorman is deployed in the auto-signing mode (i.e. signer != null),
|
||||
// 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 {
|
||||
return storage.saveRequest(rawRequest)
|
||||
}
|
||||
override fun saveRequest(rawRequest: PKCS10CertificationRequest): String = storage.saveRequest(rawRequest)
|
||||
|
||||
override fun getResponse(requestId: String): CertificateResponse {
|
||||
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."))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class JiraCsrHandler(private val jiraClient: JiraClient, private val storage: CertificationRequestStorage, private val delegate: CsrHandler) : CsrHandler by delegate {
|
||||
private companion object {
|
||||
val log = loggerFor<JiraCsrHandler>()
|
||||
}
|
||||
|
||||
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 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)
|
||||
private fun createSignedNodeCertificate(certificationRequest: PKCS10CertificationRequest,
|
||||
csrCertPathAndKey: CertPathAndKey): 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,
|
||||
csrCertPathAndKey.certPath[0],
|
||||
csrCertPathAndKey.toKeyPair(),
|
||||
X500Principal(request.subject.encoded),
|
||||
request.publicKey,
|
||||
nameConstraints = nameConstraints)
|
||||
return X509CertificateFactory().generateCertPath(listOf(nodeCaCert) + csrCertPathAndKey.certPath)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -1,48 +1,20 @@
|
||||
package com.r3.corda.networkmanage.doorman.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.internal.DigitalSignatureWithCert
|
||||
import net.corda.nodeapi.internal.crypto.CertificateType
|
||||
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.jcajce.JcaPKCS10CertificationRequest
|
||||
import java.security.KeyPair
|
||||
import java.security.cert.CertPath
|
||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||
import java.security.KeyStore
|
||||
import java.security.PrivateKey
|
||||
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 is intended to be used in testing environment where hardware signing module is not available.
|
||||
* This local signer 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 signingKeyPair: KeyPair, private val signingCertPath: Array<X509Certificate>) : Signer {
|
||||
// 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)
|
||||
}
|
||||
class LocalSigner(private val signingKey: PrivateKey, private val signingCert: X509Certificate) : Signer {
|
||||
constructor(certAndKeyPair: CertificateAndKeyPair) : this(certAndKeyPair.keyPair.private, certAndKeyPair.certificate)
|
||||
|
||||
override fun signBytes(data: ByteArray): DigitalSignatureWithCert {
|
||||
return DigitalSignatureWithCert(signingCertPath[0], Crypto.doSign(signingKeyPair.private, data))
|
||||
return DigitalSignatureWithCert(signingCert, Crypto.doSign(signingKey, data))
|
||||
}
|
||||
}
|
||||
|
@ -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.NodeInfoWithSigned
|
||||
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.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.node.NodeInfo
|
||||
import net.corda.core.serialization.deserialize
|
||||
@ -31,9 +30,9 @@ import javax.ws.rs.core.Response.ok
|
||||
import javax.ws.rs.core.Response.status
|
||||
|
||||
@Path(NETWORK_MAP_PATH)
|
||||
class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage,
|
||||
private val networkMapStorage: NetworkMapStorage,
|
||||
private val config: NetworkMapConfig) {
|
||||
class NetworkMapWebService(private val nodeInfoStorage: NodeInfoStorage,
|
||||
private val networkMapStorage: NetworkMapStorage,
|
||||
private val config: NetworkMapConfig) {
|
||||
|
||||
companion object {
|
||||
val log = contextLogger()
|
||||
@ -42,7 +41,7 @@ class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage,
|
||||
|
||||
private val networkMapCache: LoadingCache<Boolean, Pair<SignedNetworkMap?, NetworkParameters?>> = CacheBuilder.newBuilder()
|
||||
.expireAfterWrite(config.cacheTimeout, TimeUnit.MILLISECONDS)
|
||||
.build(CacheLoader.from { _ -> Pair(networkMapStorage.getCurrentNetworkMap(), networkMapStorage.getCurrentSignedNetworkParameters()?.verified()) })
|
||||
.build(CacheLoader.from { _ -> Pair(networkMapStorage.getCurrentNetworkMap(), networkMapStorage.getNetworkParametersOfNetworkMap()?.verified()) })
|
||||
|
||||
@POST
|
||||
@Path("publish")
|
||||
@ -87,7 +86,6 @@ class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage,
|
||||
@GET
|
||||
@Path("my-ip")
|
||||
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()
|
||||
}
|
||||
|
@ -1,7 +1,10 @@
|
||||
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.configureDatabase
|
||||
import com.r3.corda.networkmanage.common.signer.NetworkMapSigner
|
||||
import com.r3.corda.networkmanage.common.utils.ShowHelpException
|
||||
import com.r3.corda.networkmanage.hsm.authentication.AuthMode
|
||||
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.HsmNetworkMapSigner
|
||||
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 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
|
||||
|
||||
private val log = LogManager.getLogger("com.r3.corda.networkmanage.hsm.Main")
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
// Grabbed from https://stackoverflow.com/questions/7953567/checking-if-unlimited-cryptography-is-available
|
||||
if (Cipher.getMaxAllowedKeyLength("AES") < 256) {
|
||||
@ -46,12 +58,15 @@ fun run(parameters: Parameters) {
|
||||
checkNotNull(dataSourceProperties)
|
||||
val database = configureDatabase(dataSourceProperties, databaseConfig)
|
||||
val csrStorage = DBSignedCertificateRequestStorage(database)
|
||||
val networkMapStorage = PersistentNetworkMapStorage(database)
|
||||
val hsmNetworkMapSigningThread = HsmNetworkMapSigner(
|
||||
networkMapStorage,
|
||||
val hsmSigner = HsmNetworkMapSigner(
|
||||
networkMapCertificateName,
|
||||
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 signer = HsmCsrSigner(
|
||||
csrStorage,
|
||||
@ -62,6 +77,7 @@ fun run(parameters: Parameters) {
|
||||
Authenticator(createProvider(), authMode, autoUsername, authKeyFilePath, authKeyFilePassword, signAuthThreshold))
|
||||
signer.sign(it)
|
||||
}
|
||||
|
||||
Menu().withExceptionHandler(::processError).addItem("1", "Generate root and intermediate certificates", {
|
||||
if (confirmedKeyGen()) {
|
||||
val generator = KeyCertificateGenerator(
|
||||
@ -104,13 +120,29 @@ fun run(parameters: Parameters) {
|
||||
println("There is no approved and unsigned CSR")
|
||||
}
|
||||
}).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) {
|
||||
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 {
|
||||
|
@ -1,78 +1,39 @@
|
||||
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.hsm.authentication.Authenticator
|
||||
import com.r3.corda.networkmanage.hsm.utils.X509Utilities
|
||||
import com.r3.corda.networkmanage.hsm.utils.X509Utilities.getAndInitializeKeyStore
|
||||
import com.r3.corda.networkmanage.hsm.utils.X509Utilities.verify
|
||||
import net.corda.core.internal.DigitalSignatureWithCert
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.core.utilities.minutes
|
||||
import net.corda.nodeapi.internal.crypto.getX509Certificate
|
||||
import java.security.PrivateKey
|
||||
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.
|
||||
* It uses HSM as the signing entity with keys and certificates specified at the construction time.
|
||||
* Signer which connects to a HSM using the given [authenticator] to sign bytes.
|
||||
*/
|
||||
// TODO Rename this to HsmSigner
|
||||
class HsmNetworkMapSigner(networkMapStorage: NetworkMapStorage,
|
||||
private val caCertificateKeyName: String,
|
||||
private val caPrivateKeyPass: String,
|
||||
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)
|
||||
}
|
||||
|
||||
class HsmNetworkMapSigner(private val certificateKeyName: String,
|
||||
private val privateKeyPassword: String,
|
||||
private val authenticator: Authenticator) : Signer {
|
||||
/**
|
||||
* Signs given data using [CryptoServerJCE.CryptoServerProvider], which connects to the underlying HSM.
|
||||
*/
|
||||
override fun signBytes(data: ByteArray): DigitalSignatureWithCert {
|
||||
return authenticator.connectAndAuthenticate { provider, _ ->
|
||||
val keyStore = getAndInitializeKeyStore(provider)
|
||||
val caCertificateChain = keyStore.getCertificateChain(caCertificateKeyName)
|
||||
val caKey = keyStore.getKey(caCertificateKeyName, caPrivateKeyPass.toCharArray()) as PrivateKey
|
||||
val certificate = keyStore.getX509Certificate(certificateKeyName)
|
||||
// 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 {
|
||||
initSign(caKey)
|
||||
initSign(key)
|
||||
update(data)
|
||||
sign()
|
||||
}
|
||||
verify(data, signature, caCertificateChain[0].publicKey)
|
||||
DigitalSignatureWithCert(caCertificateChain[0] as X509Certificate, signature)
|
||||
verify(data, signature, certificate.publicKey)
|
||||
DigitalSignatureWithCert(certificate, signature)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,9 +33,9 @@ object HsmErrors {
|
||||
fun mapCryptoServerException(exception: Exception): Exception {
|
||||
// Try to decode the error code
|
||||
val crypto = exception as? CryptoServerException ?: exception.cause as? CryptoServerException
|
||||
if (crypto != null) {
|
||||
return Exception("(CryptoServer) ${HsmErrors.errors[crypto.ErrorCode]}", exception)
|
||||
return if (crypto != null) {
|
||||
Exception("(CryptoServer) ${HsmErrors.errors[crypto.ErrorCode]}", exception)
|
||||
} else {
|
||||
return exception
|
||||
exception
|
||||
}
|
||||
}
|
@ -93,13 +93,11 @@
|
||||
</changeSet>
|
||||
<changeSet author="R3.Corda" id="1513267683777-8">
|
||||
<createTable tableName="network_parameters">
|
||||
<column name="hash" type="NVARCHAR(64)">
|
||||
<column name="version" type="BIGINT">
|
||||
<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"/>
|
||||
<column name="bytes" type="BLOB"/>
|
||||
<column name="hash" type="NVARCHAR(64)"/>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
<changeSet author="R3.Corda" id="1513267683777-9">
|
||||
@ -122,7 +120,7 @@
|
||||
</createTable>
|
||||
</changeSet>
|
||||
<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 author="R3.Corda" id="1513267683777-12">
|
||||
<addPrimaryKey columnNames="id" constraintName="CONSTRAINT_7" tableName="certificate_data"/>
|
||||
@ -169,11 +167,6 @@
|
||||
<column name="public_key_hash"/>
|
||||
</createIndex>
|
||||
</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">
|
||||
<addForeignKeyConstraint baseColumnNames="rev" baseTableName="certificate_signing_request_AUD"
|
||||
constraintName="FK5g5cagcrx7siu8lwtavirunxd"
|
||||
|
@ -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" >
|
||||
|
||||
<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>
|
||||
|
@ -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>
|
@ -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>
|
@ -20,6 +20,7 @@ abstract class TestBase {
|
||||
requestId: String = SecureHash.randomSHA256().toString(),
|
||||
status: RequestStatus = RequestStatus.NEW,
|
||||
legalName: String = "TestLegalName",
|
||||
publicKeyHash: SecureHash = SecureHash.randomSHA256(),
|
||||
remark: String = "Test remark",
|
||||
request: PKCS10CertificationRequest = mock(),
|
||||
certData: CertificateData = mock(),
|
||||
@ -29,6 +30,7 @@ abstract class TestBase {
|
||||
requestId = requestId,
|
||||
status = status,
|
||||
legalName = legalName,
|
||||
publicKeyHash = publicKeyHash,
|
||||
remark = remark,
|
||||
certData = certData,
|
||||
request = request,
|
||||
@ -36,11 +38,9 @@ abstract class TestBase {
|
||||
)
|
||||
}
|
||||
|
||||
protected fun certificateData(publicKeyHash: String = SecureHash.randomSHA256().toString(),
|
||||
certStatus: CertificateStatus = CertificateStatus.VALID,
|
||||
protected fun certificateData(certStatus: CertificateStatus = CertificateStatus.VALID,
|
||||
certPath: CertPath = mock()): CertificateData {
|
||||
return CertificateData(
|
||||
publicKeyHash = publicKeyHash,
|
||||
certStatus = certStatus,
|
||||
certPath = certPath
|
||||
)
|
||||
|
@ -19,6 +19,7 @@ import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.security.KeyPair
|
||||
import java.security.cert.CertPath
|
||||
import java.util.*
|
||||
import javax.security.auth.x500.X500Principal
|
||||
import kotlin.test.*
|
||||
@ -69,7 +70,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
|
||||
|
||||
@Test
|
||||
fun `sign request`() {
|
||||
val (csr, _) = createRequest("LegalName")
|
||||
val (csr, nodeKeyPair) = createRequest("LegalName")
|
||||
// Add request to DB.
|
||||
val requestId = storage.saveRequest(csr)
|
||||
// New request should equals to 1.
|
||||
@ -86,11 +87,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
|
||||
// Sign certificate
|
||||
storage.putCertificatePath(
|
||||
requestId,
|
||||
JcaPKCS10CertificationRequest(csr).run {
|
||||
// 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)
|
||||
},
|
||||
generateSignedCertPath(csr, nodeKeyPair),
|
||||
listOf(DOORMAN_SIGNATURE)
|
||||
)
|
||||
// Check request is ready
|
||||
@ -99,33 +96,51 @@ class PersistentCertificateRequestStorageTest : TestBase() {
|
||||
|
||||
@Test
|
||||
fun `sign request ignores subsequent sign requests`() {
|
||||
val (csr, _) = createRequest("LegalName")
|
||||
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,
|
||||
JcaPKCS10CertificationRequest(csr).run {
|
||||
val (rootCa, intermediateCa, nodeCa) = createDevNodeCaCertPath(CordaX500Name.build(X500Principal(subject.encoded)))
|
||||
buildCertPath(nodeCa.certificate, intermediateCa.certificate, rootCa.certificate)
|
||||
},
|
||||
generateSignedCertPath(csr, nodeKeyPair),
|
||||
listOf(DOORMAN_SIGNATURE)
|
||||
)
|
||||
// Sign certificate
|
||||
// When subsequent signature requested
|
||||
assertFailsWith(IllegalArgumentException::class) {
|
||||
storage.putCertificatePath(
|
||||
requestId,
|
||||
JcaPKCS10CertificationRequest(csr).run {
|
||||
val (rootCa, intermediateCa, nodeCa) = createDevNodeCaCertPath(CordaX500Name.build(X500Principal(subject.encoded)))
|
||||
buildCertPath(nodeCa.certificate, intermediateCa.certificate, rootCa.certificate)
|
||||
},
|
||||
generateSignedCertPath(csr, nodeKeyPair),
|
||||
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
|
||||
fun `reject request`() {
|
||||
val requestId = storage.saveRequest(createRequest("BankA").first)
|
||||
@ -159,18 +174,14 @@ class PersistentCertificateRequestStorageTest : TestBase() {
|
||||
|
||||
@Test
|
||||
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)
|
||||
storage.markRequestTicketCreated(requestId)
|
||||
storage.approveRequest(requestId, DOORMAN_SIGNATURE)
|
||||
// Sign certificate
|
||||
storage.putCertificatePath(
|
||||
requestId,
|
||||
JcaPKCS10CertificationRequest(csr).run {
|
||||
// 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)
|
||||
},
|
||||
generateSignedCertPath(csr, nodeKeyPair),
|
||||
listOf(DOORMAN_SIGNATURE)
|
||||
)
|
||||
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 {
|
||||
val props = Properties()
|
||||
props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource")
|
||||
@ -225,8 +244,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun createRequest(organisation: String): Pair<PKCS10CertificationRequest, KeyPair> {
|
||||
val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
internal fun createRequest(organisation: String, keyPair: KeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)): Pair<PKCS10CertificationRequest, KeyPair> {
|
||||
val request = X509Utilities.createCertificateSigningRequest(X500Principal("O=$organisation,L=London,C=GB"), "my@mail.com", keyPair)
|
||||
return Pair(request, keyPair)
|
||||
}
|
||||
}
|
@ -5,13 +5,11 @@ import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.signWithCert
|
||||
import net.corda.nodeapi.internal.createDevNetworkMapCa
|
||||
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.verifiedNetworkMapCert
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.internal.TestNodeInfoBuilder
|
||||
import net.corda.testing.internal.createDevIntermediateCaCertPath
|
||||
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
@ -50,7 +48,7 @@ class PersistentNetworkMapStorageTest : TestBase() {
|
||||
fun `saveNetworkMap and saveNetworkParameters create current network map and parameters`() {
|
||||
// given
|
||||
// Create node info.
|
||||
val signedNodeInfo = createValidSignedNodeInfo("Test")
|
||||
val (signedNodeInfo) = createValidSignedNodeInfo("Test", requestStorage)
|
||||
val nodeInfoHash = nodeInfoStorage.putNodeInfo(signedNodeInfo)
|
||||
|
||||
val networkParameters = testNetworkParameters(emptyList())
|
||||
@ -65,7 +63,7 @@ class PersistentNetworkMapStorageTest : TestBase() {
|
||||
|
||||
// then
|
||||
val persistedSignedNetworkMap = networkMapStorage.getCurrentNetworkMap()
|
||||
val persistedSignedParameters = networkMapStorage.getCurrentSignedNetworkParameters()
|
||||
val persistedSignedParameters = networkMapStorage.getNetworkParametersOfNetworkMap()
|
||||
|
||||
assertEquals(networkParameters, persistedSignedParameters?.verifiedNetworkMapCert(rootCaCert))
|
||||
assertEquals(parametersSignature, persistedSignedParameters?.sig)
|
||||
@ -84,13 +82,13 @@ class PersistentNetworkMapStorageTest : TestBase() {
|
||||
networkMapStorage.saveNetworkParameters(params2, null)
|
||||
|
||||
// when
|
||||
val latest = networkMapStorage.getLatestUnsignedNetworkParameters()
|
||||
val latest = networkMapStorage.getLatestNetworkParameters()?.minimumPlatformVersion
|
||||
// then
|
||||
assertEquals(2, latest.minimumPlatformVersion)
|
||||
assertEquals(2, latest)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCurrentNetworkParameters returns current network map parameters`() {
|
||||
fun `getNetworkParametersOfNetworkMap returns current network map parameters`() {
|
||||
// given
|
||||
// Create network parameters
|
||||
val testParameters1 = testNetworkParameters(emptyList())
|
||||
@ -107,7 +105,7 @@ class PersistentNetworkMapStorageTest : TestBase() {
|
||||
networkMapStorage.saveNetworkParameters(testParameters2, testParameters2.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate).sig)
|
||||
|
||||
// when
|
||||
val result = networkMapStorage.getCurrentSignedNetworkParameters()?.verifiedNetworkMapCert(rootCaCert)
|
||||
val result = networkMapStorage.getNetworkParametersOfNetworkMap()?.verifiedNetworkMapCert(rootCaCert)
|
||||
|
||||
// then
|
||||
assertEquals(1, result?.minimumPlatformVersion)
|
||||
@ -117,8 +115,8 @@ class PersistentNetworkMapStorageTest : TestBase() {
|
||||
fun `getValidNodeInfoHashes returns only valid and signed node info hashes`() {
|
||||
// given
|
||||
// Create node infos.
|
||||
val signedNodeInfoA = createValidSignedNodeInfo("TestA")
|
||||
val signedNodeInfoB = createValidSignedNodeInfo("TestB")
|
||||
val (signedNodeInfoA) = createValidSignedNodeInfo("TestA", requestStorage)
|
||||
val (signedNodeInfoB) = createValidSignedNodeInfo("TestB", requestStorage)
|
||||
|
||||
// Put signed node info data
|
||||
val nodeInfoHashA = nodeInfoStorage.putNodeInfo(signedNodeInfoA)
|
||||
@ -139,15 +137,4 @@ class PersistentNetworkMapStorageTest : TestBase() {
|
||||
// then
|
||||
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)
|
||||
}
|
||||
}
|
@ -9,7 +9,6 @@ import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||
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.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
@ -23,6 +22,7 @@ import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.security.auth.x500.X500Principal
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
@ -83,8 +83,8 @@ class PersistentNodeInfoStorageTest : TestBase() {
|
||||
@Test
|
||||
fun `getNodeInfo returns persisted SignedNodeInfo using the hash of just the NodeInfo`() {
|
||||
// given
|
||||
val (nodeA) = createValidSignedNodeInfo("TestA")
|
||||
val (nodeB) = createValidSignedNodeInfo("TestB")
|
||||
val (nodeA) = createValidSignedNodeInfo("TestA", requestStorage)
|
||||
val (nodeB) = createValidSignedNodeInfo("TestB", requestStorage)
|
||||
|
||||
// Put signed node info data
|
||||
nodeInfoStorage.putNodeInfo(nodeA)
|
||||
@ -102,7 +102,7 @@ class PersistentNodeInfoStorageTest : TestBase() {
|
||||
@Test
|
||||
fun `same public key with different 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 node2 = NodeInfoWithSigned(nodeInfo2.signWith(listOf(key)))
|
||||
|
||||
@ -120,7 +120,7 @@ class PersistentNodeInfoStorageTest : TestBase() {
|
||||
@Test
|
||||
fun `putNodeInfo persists SignedNodeInfo with its signature`() {
|
||||
// given
|
||||
val (nodeInfoWithSigned) = createValidSignedNodeInfo("Test")
|
||||
val (nodeInfoWithSigned) = createValidSignedNodeInfo("Test", requestStorage)
|
||||
|
||||
// when
|
||||
val nodeInfoHash = nodeInfoStorage.putNodeInfo(nodeInfoWithSigned)
|
||||
@ -129,16 +129,17 @@ class PersistentNodeInfoStorageTest : TestBase() {
|
||||
val persistedSignedNodeInfo = nodeInfoStorage.getNodeInfo(nodeInfoHash)
|
||||
assertThat(persistedSignedNodeInfo?.signatures).isEqualTo(nodeInfoWithSigned.signedNodeInfo.signatures)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createValidSignedNodeInfo(organisation: String, serial: Long = 1): Pair<NodeInfoWithSigned, PrivateKey> {
|
||||
val nodeInfoBuilder = TestNodeInfoBuilder()
|
||||
val requestId = requestStorage.saveRequest(createRequest(organisation).first)
|
||||
requestStorage.markRequestTicketCreated(requestId)
|
||||
requestStorage.approveRequest(requestId, "TestUser")
|
||||
val (identity, key) = nodeInfoBuilder.addIdentity(CordaX500Name(organisation, "London", "GB"))
|
||||
val nodeCaCertPath = X509CertificateFactory().generateCertPath(identity.certPath.certificates.drop(1))
|
||||
requestStorage.putCertificatePath(requestId, nodeCaCertPath, emptyList())
|
||||
val (_, signedNodeInfo) = nodeInfoBuilder.buildWithSigned(serial)
|
||||
return Pair(NodeInfoWithSigned(signedNodeInfo), key)
|
||||
}
|
||||
internal fun createValidSignedNodeInfo(organisation: String,
|
||||
storage: CertificationRequestStorage): Pair<NodeInfoWithSigned, PrivateKey> {
|
||||
val (csr, nodeKeyPair) = createRequest(organisation)
|
||||
val requestId = storage.saveRequest(csr)
|
||||
storage.markRequestTicketCreated(requestId)
|
||||
storage.approveRequest(requestId, "TestUser")
|
||||
val nodeInfoBuilder = TestNodeInfoBuilder()
|
||||
val (identity, key) = nodeInfoBuilder.addIdentity(CordaX500Name.build(X500Principal(csr.subject.encoded)), nodeKeyPair)
|
||||
storage.putCertificatePath(requestId, identity.certPath, listOf("Test"))
|
||||
val (_, signedNodeInfo) = nodeInfoBuilder.buildWithSigned(1)
|
||||
return Pair(NodeInfoWithSigned(signedNodeInfo), key)
|
||||
}
|
@ -51,8 +51,8 @@ class NetworkMapSignerTest : TestBase() {
|
||||
val signedNetworkMap = networkMap.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)
|
||||
whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(signedNetworkMap)
|
||||
whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(signedNodeInfoHashes)
|
||||
whenever(networkMapStorage.getLatestUnsignedNetworkParameters()).thenReturn(latestNetworkParameters)
|
||||
whenever(networkMapStorage.getCurrentSignedNetworkParameters()).thenReturn(currentParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate))
|
||||
whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(latestNetworkParameters)
|
||||
whenever(networkMapStorage.getNetworkParametersOfNetworkMap()).thenReturn(currentParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate))
|
||||
whenever(signer.signBytes(any())).then {
|
||||
DigitalSignatureWithCert(networkMapCa.certificate, Crypto.doSign(networkMapCa.keyPair.private, it.arguments[0] as ByteArray))
|
||||
}
|
||||
@ -67,8 +67,8 @@ class NetworkMapSignerTest : TestBase() {
|
||||
// then
|
||||
// Verify networkMapStorage calls
|
||||
verify(networkMapStorage).getNodeInfoHashes(any())
|
||||
verify(networkMapStorage).getLatestUnsignedNetworkParameters()
|
||||
verify(networkMapStorage).getCurrentSignedNetworkParameters()
|
||||
verify(networkMapStorage).getLatestNetworkParameters()
|
||||
verify(networkMapStorage).getNetworkParametersOfNetworkMap()
|
||||
argumentCaptor<SignedNetworkMap>().apply {
|
||||
verify(networkMapStorage).saveNetworkMap(capture())
|
||||
val capturedNetworkMap = firstValue.verifiedNetworkMapCert(rootCaCert)
|
||||
@ -87,8 +87,8 @@ class NetworkMapSignerTest : TestBase() {
|
||||
val signedNetworkMap = networkMap.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)
|
||||
whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(signedNetworkMap)
|
||||
whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(emptyList())
|
||||
whenever(networkMapStorage.getLatestUnsignedNetworkParameters()).thenReturn(networkParameters)
|
||||
whenever(networkMapStorage.getCurrentSignedNetworkParameters()).thenReturn(networkParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate))
|
||||
whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(networkParameters)
|
||||
whenever(networkMapStorage.getNetworkParametersOfNetworkMap()).thenReturn(networkParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate))
|
||||
|
||||
// when
|
||||
networkMapSigner.signNetworkMap()
|
||||
@ -104,7 +104,7 @@ class NetworkMapSignerTest : TestBase() {
|
||||
val networkParameters = testNetworkParameters(emptyList())
|
||||
whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(null)
|
||||
whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(emptyList())
|
||||
whenever(networkMapStorage.getLatestUnsignedNetworkParameters()).thenReturn(networkParameters)
|
||||
whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(networkParameters)
|
||||
whenever(signer.signBytes(any())).then {
|
||||
DigitalSignatureWithCert(networkMapCa.certificate, Crypto.doSign(networkMapCa.keyPair.private, it.arguments[0] as ByteArray))
|
||||
}
|
||||
@ -118,7 +118,7 @@ class NetworkMapSignerTest : TestBase() {
|
||||
// then
|
||||
// Verify networkMapStorage calls
|
||||
verify(networkMapStorage).getNodeInfoHashes(any())
|
||||
verify(networkMapStorage).getLatestUnsignedNetworkParameters()
|
||||
verify(networkMapStorage).getLatestNetworkParameters()
|
||||
argumentCaptor<SignedNetworkMap>().apply {
|
||||
verify(networkMapStorage).saveNetworkMap(capture())
|
||||
val networkMap = firstValue.verifiedNetworkMapCert(rootCaCert)
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
@ -52,6 +52,5 @@ class DoormanParametersTest {
|
||||
assertEquals("TD", parameter.jiraConfig?.projectCode)
|
||||
assertEquals("username", parameter.jiraConfig?.username)
|
||||
assertEquals("password", parameter.jiraConfig?.password)
|
||||
assertEquals(41, parameter.jiraConfig?.doneTransitionCode)
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
@ -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.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.utils.SignedNetworkMap
|
||||
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.identity.CordaX500Name
|
||||
import net.corda.core.internal.checkOkResponse
|
||||
@ -36,7 +37,7 @@ import java.security.cert.X509Certificate
|
||||
import javax.ws.rs.core.MediaType
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class NodeInfoWebServiceTest {
|
||||
class NetworkMapWebServiceTest {
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule(true)
|
||||
@ -56,12 +57,12 @@ class NodeInfoWebServiceTest {
|
||||
@Test
|
||||
fun `submit nodeInfo`() {
|
||||
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.
|
||||
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()
|
||||
val nodeInfoAndSignature = signedNodeInfo.serialize().bytes
|
||||
// Post node info and signature to doorman, this should pass without any exception.
|
||||
@ -72,12 +73,12 @@ class NodeInfoWebServiceTest {
|
||||
@Test
|
||||
fun `submit old nodeInfo`() {
|
||||
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.
|
||||
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()
|
||||
val nodeInfoAndSignature = signedNodeInfo.serialize().bytes
|
||||
assertThatThrownBy { it.doPost("publish", nodeInfoAndSignature) }
|
||||
@ -88,12 +89,12 @@ class NodeInfoWebServiceTest {
|
||||
@Test
|
||||
fun `submit nodeInfo when no network parameters`() {
|
||||
val networkMapStorage: NetworkMapStorage = mock {
|
||||
on { getCurrentSignedNetworkParameters() }.thenReturn(null)
|
||||
on { getNetworkParametersOfNetworkMap() }.thenReturn(null)
|
||||
}
|
||||
// Create node info.
|
||||
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()
|
||||
val nodeInfoAndSignature = signedNodeInfo.serialize().bytes
|
||||
assertThatThrownBy { it.doPost("publish", nodeInfoAndSignature) }
|
||||
@ -110,7 +111,7 @@ class NodeInfoWebServiceTest {
|
||||
on { getCurrentNetworkMap() }.thenReturn(signedNetworkMap)
|
||||
}
|
||||
|
||||
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), networkMapStorage, testNetworkMapConfig)).use {
|
||||
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use {
|
||||
it.start()
|
||||
val signedNetworkMapResponse = it.doGet<SignedNetworkMap>("")
|
||||
verify(networkMapStorage, times(1)).getCurrentNetworkMap()
|
||||
@ -127,7 +128,7 @@ class NodeInfoWebServiceTest {
|
||||
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()
|
||||
val nodeInfoResponse = it.doGet<SignedNodeInfo>("node-info/$nodeInfoHash")
|
||||
verify(nodeInfoStorage, times(1)).getNodeInfo(nodeInfoHash)
|
||||
@ -149,7 +150,7 @@ class NodeInfoWebServiceTest {
|
||||
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()
|
||||
val netParamsResponse = it.doGet<SignedNetworkParameters>("network-parameters/$networkParametersHash")
|
||||
verify(networkMapStorage, times(1)).getSignedNetworkParameters(networkParametersHash)
|
@ -1,11 +1,11 @@
|
||||
package com.r3.corda.networkmanage.doorman
|
||||
package com.r3.corda.networkmanage.doorman.webservice
|
||||
|
||||
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.utils.buildCertPath
|
||||
import com.r3.corda.networkmanage.doorman.NetworkManagementWebServer
|
||||
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.SecureHash
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
@ -100,7 +100,7 @@ class RegistrationWebServiceTest : TestBase() {
|
||||
CertificateResponse.Ready(it)
|
||||
} ?: CertificateResponse.NotReady
|
||||
}
|
||||
on { processApprovedRequests() }.then {
|
||||
on { processRequests() }.then {
|
||||
val request = X509Utilities.createCertificateSigningRequest(subject, "my@mail.com", keyPair)
|
||||
certificateStore[id] = JcaPKCS10CertificationRequest(request).run {
|
||||
val tlsCert = X509Utilities.createCertificate(
|
||||
@ -118,7 +118,7 @@ class RegistrationWebServiceTest : TestBase() {
|
||||
startSigningServer(requestProcessor)
|
||||
assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady)
|
||||
|
||||
requestProcessor.processApprovedRequests()
|
||||
requestProcessor.processRequests()
|
||||
|
||||
val certificates = (pollForResponse(id) as PollResponse.Ready).certChain
|
||||
verify(requestProcessor, times(2)).getResponse(any())
|
||||
@ -141,7 +141,7 @@ class RegistrationWebServiceTest : TestBase() {
|
||||
CertificateResponse.Ready(it)
|
||||
} ?: CertificateResponse.NotReady
|
||||
}
|
||||
on { processApprovedRequests() }.then {
|
||||
on { processRequests() }.then {
|
||||
val request = X509Utilities.createCertificateSigningRequest(
|
||||
CordaX500Name(locality = "London", organisation = "Legal Name", country = "GB").x500Principal,
|
||||
"my@mail.com",
|
||||
@ -165,7 +165,7 @@ class RegistrationWebServiceTest : TestBase() {
|
||||
|
||||
startSigningServer(storage)
|
||||
assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady)
|
||||
storage.processApprovedRequests()
|
||||
storage.processRequests()
|
||||
|
||||
val certificates = (pollForResponse(id) as PollResponse.Ready).certChain
|
||||
verify(storage, times(2)).getResponse(any())
|
@ -1,11 +1,12 @@
|
||||
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.MessageRecipients
|
||||
import net.corda.core.messaging.SingleMessageRecipient
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.toBase58String
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
@ -23,7 +24,7 @@ class ArtemisMessagingComponent {
|
||||
const val PEER_USER = "SystemUsers/Peer"
|
||||
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 P2P_QUEUE = "p2p.inbound"
|
||||
const val P2P_PREFIX = "p2p.inbound."
|
||||
const val NOTIFICATIONS_ADDRESS = "${INTERNAL_PREFIX}activemq.notifications"
|
||||
}
|
||||
|
||||
@ -49,7 +50,7 @@ class ArtemisMessagingComponent {
|
||||
@CordaSerializable
|
||||
data class NodeAddress(override val queueName: String, override val hostAndPort: NetworkHostAndPort) : ArtemisPeerAddress {
|
||||
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.
|
||||
*/
|
||||
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()}"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.nodeapi.internal
|
||||
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.Crypto.generateKeyPair
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.x500Name
|
||||
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.GeneralSubtree
|
||||
import org.bouncycastle.asn1.x509.NameConstraints
|
||||
import java.security.KeyPair
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.security.auth.x500.X500Principal
|
||||
|
||||
@ -31,7 +33,7 @@ fun SSLConfiguration.createDevKeyStores(legalName: CordaX500Name,
|
||||
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)
|
||||
|
||||
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
|
||||
* [CordaX500Name] as the cert subject.
|
||||
*/
|
||||
fun createDevNodeCa(intermediateCa: CertificateAndKeyPair, legalName: CordaX500Name): CertificateAndKeyPair {
|
||||
val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
fun createDevNodeCa(intermediateCa: CertificateAndKeyPair,
|
||||
legalName: CordaX500Name,
|
||||
nodeKeyPair: KeyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)): CertificateAndKeyPair {
|
||||
val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName.x500Name))), arrayOf())
|
||||
val cert = X509Utilities.createCertificate(
|
||||
CertificateType.NODE_CA,
|
||||
intermediateCa.certificate,
|
||||
intermediateCa.keyPair,
|
||||
legalName.x500Principal,
|
||||
keyPair.public,
|
||||
nodeKeyPair.public,
|
||||
nameConstraints = nameConstraints)
|
||||
return CertificateAndKeyPair(cert, keyPair)
|
||||
return CertificateAndKeyPair(cert, nodeKeyPair)
|
||||
}
|
||||
|
||||
val DEV_INTERMEDIATE_CA: CertificateAndKeyPair get() = DevCaHelper.loadDevCa(X509Utilities.CORDA_INTERMEDIATE_CA)
|
||||
|
@ -1,13 +1,16 @@
|
||||
package net.corda.nodeapi.internal.persistence
|
||||
|
||||
import co.paralleluniverse.strands.Strand
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import rx.Observable
|
||||
import rx.Subscriber
|
||||
import rx.subjects.PublishSubject
|
||||
import rx.subjects.UnicastSubject
|
||||
import java.io.Closeable
|
||||
import java.sql.Connection
|
||||
import java.sql.SQLException
|
||||
import java.util.*
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import javax.persistence.AttributeConverter
|
||||
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
|
||||
}
|
||||
|
||||
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(
|
||||
val dataSource: DataSource,
|
||||
databaseConfig: DatabaseConfig,
|
||||
@ -51,7 +59,7 @@ class CordaPersistence(
|
||||
private val log = contextLogger()
|
||||
}
|
||||
|
||||
val defaultIsolationLevel = databaseConfig.transactionIsolationLevel
|
||||
private val defaultIsolationLevel = databaseConfig.transactionIsolationLevel
|
||||
val hibernateConfig: HibernateConfiguration by lazy {
|
||||
|
||||
transaction {
|
||||
@ -60,8 +68,19 @@ class CordaPersistence(
|
||||
}
|
||||
val entityManagerFactory get() = hibernateConfig.sessionFactoryForRegisteredSchemas
|
||||
|
||||
data class Boundary(val txId: UUID)
|
||||
|
||||
internal val transactionBoundaries = PublishSubject.create<Boundary>().toSerialized()
|
||||
|
||||
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.
|
||||
transaction {
|
||||
check(!connection.metaData.isReadOnly) { "Database should not be readonly." }
|
||||
@ -72,25 +91,29 @@ class CordaPersistence(
|
||||
const val DATA_SOURCE_URL = "dataSource.url"
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of [DatabaseTransaction], with the given transaction isolation level.
|
||||
*/
|
||||
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.
|
||||
DatabaseTransactionManager.dataSource = this
|
||||
return DatabaseTransactionManager.currentOrNew(isolationLevel)
|
||||
fun currentOrNew(isolation: TransactionIsolationLevel = defaultIsolationLevel): DatabaseTransaction {
|
||||
return contextTransactionOrNull ?: newTransaction(isolation)
|
||||
}
|
||||
|
||||
fun newTransaction(isolation: TransactionIsolationLevel = defaultIsolationLevel): DatabaseTransaction {
|
||||
return DatabaseTransaction(isolation.jdbcValue, contextTransactionOrNull, this).also {
|
||||
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 {
|
||||
// We need to set the database for the current [Thread] or [Fiber] here as some tests share threads across databases.
|
||||
DatabaseTransactionManager.dataSource = this
|
||||
val ctx = DatabaseTransactionManager.currentOrNull()
|
||||
return ctx?.connection ?: throw IllegalStateException("Was expecting to find database transaction: must wrap calling code within a transaction.")
|
||||
_contextDatabase.set(this)
|
||||
return contextTransaction.connection
|
||||
}
|
||||
|
||||
/**
|
||||
@ -99,7 +122,7 @@ class CordaPersistence(
|
||||
* @param statement to be executed in the scope of this transaction.
|
||||
*/
|
||||
fun <T> transaction(isolationLevel: TransactionIsolationLevel, statement: DatabaseTransaction.() -> T): T {
|
||||
DatabaseTransactionManager.dataSource = this
|
||||
_contextDatabase.set(this)
|
||||
return transaction(isolationLevel, 2, statement)
|
||||
}
|
||||
|
||||
@ -110,7 +133,7 @@ class CordaPersistence(
|
||||
fun <T> transaction(statement: DatabaseTransaction.() -> T): T = transaction(defaultIsolationLevel, statement)
|
||||
|
||||
private fun <T> transaction(isolationLevel: TransactionIsolationLevel, recoverableFailureTolerance: Int, statement: DatabaseTransaction.() -> T): T {
|
||||
val outer = DatabaseTransactionManager.currentOrNull()
|
||||
val outer = contextTransactionOrNull
|
||||
return if (outer != null) {
|
||||
outer.statement()
|
||||
} else {
|
||||
@ -126,7 +149,7 @@ class CordaPersistence(
|
||||
log.warn("Cleanup task failed:", t)
|
||||
}
|
||||
while (true) {
|
||||
val transaction = DatabaseTransactionManager.currentOrNew(isolationLevel)
|
||||
val transaction = contextDatabase.currentOrNew(isolationLevel) // XXX: Does this code really support statement changing the contextDatabase?
|
||||
try {
|
||||
val answer = transaction.statement()
|
||||
transaction.commit()
|
||||
@ -160,8 +183,8 @@ class CordaPersistence(
|
||||
* For examples, see the call hierarchy of this function.
|
||||
*/
|
||||
fun <T : Any> rx.Observer<T>.bufferUntilDatabaseCommit(): rx.Observer<T> {
|
||||
val currentTxId = DatabaseTransactionManager.transactionId
|
||||
val databaseTxBoundary: Observable<DatabaseTransactionManager.Boundary> = DatabaseTransactionManager.transactionBoundaries.first { it.txId == currentTxId }
|
||||
val currentTxId = contextTransaction.id
|
||||
val databaseTxBoundary: Observable<CordaPersistence.Boundary> = contextDatabase.transactionBoundaries.first { it.txId == currentTxId }
|
||||
val subject = UnicastSubject.create<T>()
|
||||
subject.delaySubscription(databaseTxBoundary).subscribe(this)
|
||||
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.
|
||||
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.
|
||||
val delegates = CopyOnWriteArrayList<Subscriber<in U>>()
|
||||
|
||||
fun forEachSubscriberWithDbTx(block: Subscriber<in U>.() -> Unit) {
|
||||
(db ?: DatabaseTransactionManager.dataSource).transaction {
|
||||
(db ?: contextDatabase).transaction {
|
||||
delegates.filter { !it.isUnsubscribed }.forEach {
|
||||
it.block()
|
||||
}
|
||||
|
@ -1,23 +1,29 @@
|
||||
package net.corda.nodeapi.internal.persistence
|
||||
|
||||
import co.paralleluniverse.strands.Strand
|
||||
import org.hibernate.Session
|
||||
import org.hibernate.Transaction
|
||||
import rx.subjects.Subject
|
||||
import java.sql.Connection
|
||||
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(
|
||||
isolation: Int,
|
||||
private val threadLocal: ThreadLocal<DatabaseTransaction>,
|
||||
private val transactionBoundaries: Subject<DatabaseTransactionManager.Boundary, DatabaseTransactionManager.Boundary>,
|
||||
val cordaPersistence: CordaPersistence
|
||||
val outerTransaction: DatabaseTransaction?,
|
||||
val database: CordaPersistence
|
||||
) {
|
||||
val id: UUID = UUID.randomUUID()
|
||||
|
||||
private var _connectionCreated = false
|
||||
val connectionCreated get() = _connectionCreated
|
||||
val connection: Connection by lazy(LazyThreadSafetyMode.NONE) {
|
||||
cordaPersistence.dataSource.connection
|
||||
database.dataSource.connection
|
||||
.apply {
|
||||
_connectionCreated = true
|
||||
// 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 {
|
||||
val session = cordaPersistence.entityManagerFactory.withOptions().connection(connection).openSession()
|
||||
val session = database.entityManagerFactory.withOptions().connection(connection).openSession()
|
||||
hibernateTransaction = session.beginTransaction()
|
||||
session
|
||||
}
|
||||
|
||||
val session: Session by sessionDelegate
|
||||
private lateinit var hibernateTransaction: Transaction
|
||||
|
||||
val outerTransaction: DatabaseTransaction? = threadLocal.get()
|
||||
|
||||
fun commit() {
|
||||
if (sessionDelegate.isInitialized()) {
|
||||
hibernateTransaction.commit()
|
||||
@ -63,9 +66,9 @@ class DatabaseTransaction(
|
||||
if (_connectionCreated) {
|
||||
connection.close()
|
||||
}
|
||||
threadLocal.set(outerTransaction)
|
||||
contextTransactionOrNull = outerTransaction
|
||||
if (outerTransaction == null) {
|
||||
transactionBoundaries.onNext(DatabaseTransactionManager.Boundary(id))
|
||||
database.transactionBoundaries.onNext(CordaPersistence.Boundary(id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
@ -128,15 +128,16 @@ class HibernateConfiguration(
|
||||
class NodeDatabaseConnectionProvider : ConnectionProvider {
|
||||
override fun closeConnection(conn: Connection) {
|
||||
conn.autoCommit = false
|
||||
val tx = DatabaseTransactionManager.current()
|
||||
tx.commit()
|
||||
tx.close()
|
||||
contextTransaction.run {
|
||||
commit()
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
override fun supportsAggressiveRelease(): Boolean = true
|
||||
|
||||
override fun getConnection(): Connection {
|
||||
return DatabaseTransactionManager.newTransaction().connection
|
||||
return contextDatabase.newTransaction().connection
|
||||
}
|
||||
|
||||
override fun <T : Any?> unwrap(unwrapType: Class<T>): T {
|
||||
|
@ -3,18 +3,18 @@ package net.corda.node.amqp
|
||||
import com.nhaarman.mockito_kotlin.any
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.crypto.toStringShort
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.NetworkMapCache
|
||||
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.security.RPCSecurityManager
|
||||
import net.corda.node.services.api.NetworkMapCacheInternal
|
||||
import net.corda.node.services.config.*
|
||||
import net.corda.node.services.messaging.ArtemisMessagingClient
|
||||
import net.corda.node.services.messaging.ArtemisMessagingServer
|
||||
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.testing.*
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
@ -53,7 +53,7 @@ class AMQPBridgeTest {
|
||||
@Test
|
||||
fun `test acked and nacked messages`() {
|
||||
// Create local queue
|
||||
val sourceQueueName = "internal.peers." + BOB.publicKey.toBase58String()
|
||||
val sourceQueueName = "internal.peers." + BOB.publicKey.toStringShort()
|
||||
val (artemisServer, artemisClient) = createArtemis(sourceQueueName)
|
||||
|
||||
// Pre-populate local queue with 3 messages
|
||||
@ -133,11 +133,13 @@ class AMQPBridgeTest {
|
||||
@Test
|
||||
fun `Test legacy bridge still works`() {
|
||||
// Create local queue
|
||||
val sourceQueueName = "internal.peers." + ALICE.publicKey.toBase58String()
|
||||
val sourceQueueName = "internal.peers." + BOB.publicKey.toStringShort()
|
||||
val (artemisLegacyServer, artemisLegacyClient) = createLegacyArtemis(sourceQueueName)
|
||||
|
||||
|
||||
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!!
|
||||
for (i in 0 until 3) {
|
||||
@ -150,8 +152,7 @@ class AMQPBridgeTest {
|
||||
artemis.producer.send(sourceQueueName, artemisMessage)
|
||||
}
|
||||
|
||||
|
||||
val subs = artemisClient.started!!.session.createConsumer(P2P_QUEUE)
|
||||
val subs = artemisClient.started!!.session.createConsumer(inbox)
|
||||
for (i in 0 until 3) {
|
||||
val msg = subs.receive()
|
||||
val messageBody = ByteArray(msg.bodySize).apply { msg.bodyBuffer.readBytes(this) }
|
||||
@ -177,9 +178,9 @@ class AMQPBridgeTest {
|
||||
doReturn(true).whenever(it).useAMQPBridges
|
||||
}
|
||||
artemisConfig.configureWithDevSSLCertificate()
|
||||
val networkMap = rigorousMock<NetworkMapCache>().also {
|
||||
val networkMap = rigorousMock<NetworkMapCacheInternal>().also {
|
||||
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 artemisServer = ArtemisMessagingServer(artemisConfig, artemisPort, null, networkMap, userService, MAX_MESSAGE_SIZE)
|
||||
@ -189,7 +190,7 @@ class AMQPBridgeTest {
|
||||
val artemis = artemisClient.started!!
|
||||
if (sourceQueueName != null) {
|
||||
// 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)
|
||||
}
|
||||
@ -206,9 +207,9 @@ class AMQPBridgeTest {
|
||||
doReturn(ActiveMqServerConfiguration(BridgeConfiguration(0, 0, 0.0))).whenever(it).activeMQServer
|
||||
}
|
||||
artemisConfig.configureWithDevSSLCertificate()
|
||||
val networkMap = rigorousMock<NetworkMapCache>().also {
|
||||
val networkMap = rigorousMock<NetworkMapCacheInternal>().also {
|
||||
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 artemisServer = ArtemisMessagingServer(artemisConfig, artemisPort2, null, networkMap, userService, MAX_MESSAGE_SIZE)
|
||||
@ -217,7 +218,7 @@ class AMQPBridgeTest {
|
||||
artemisClient.start()
|
||||
val artemis = artemisClient.started!!
|
||||
// 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)
|
||||
}
|
||||
|
||||
|
@ -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.AMQPServer
|
||||
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.NodeConfiguration
|
||||
import net.corda.node.services.config.configureWithDevSSLCertificate
|
||||
import net.corda.node.services.messaging.ArtemisMessagingClient
|
||||
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.crypto.loadKeyStore
|
||||
import net.corda.testing.*
|
||||
@ -48,7 +50,7 @@ class ProtonWrapperTests {
|
||||
amqpServer.start()
|
||||
val receiveSubs = amqpServer.onReceive.subscribe {
|
||||
assertEquals(BOB_NAME.toString(), it.sourceLegalName)
|
||||
assertEquals("p2p.inbound", it.topic)
|
||||
assertEquals(P2P_PREFIX + "Test", it.topic)
|
||||
assertEquals("Test", String(it.payload))
|
||||
it.complete(true)
|
||||
}
|
||||
@ -64,7 +66,7 @@ class ProtonWrapperTests {
|
||||
assertEquals(true, clientConnect.connected)
|
||||
assertEquals(ALICE_NAME, CordaX500Name.build(clientConnect.remoteCert!!.subjectX500Principal))
|
||||
val msg = amqpClient.createMessage("Test".toByteArray(),
|
||||
"p2p.inbound",
|
||||
P2P_PREFIX + "Test",
|
||||
ALICE_NAME.toString(),
|
||||
emptyMap())
|
||||
amqpClient.write(msg)
|
||||
@ -151,8 +153,8 @@ class ProtonWrapperTests {
|
||||
assertEquals(true, clientConnected.get().connected)
|
||||
assertEquals(CHARLIE_NAME, CordaX500Name.build(clientConnected.get().remoteCert!!.subjectX500Principal))
|
||||
val artemis = artemisClient.started!!
|
||||
val sendAddress = "p2p.inbound"
|
||||
artemis.session.createQueue(sendAddress, RoutingType.MULTICAST, "queue", true)
|
||||
val sendAddress = P2P_PREFIX + "Test"
|
||||
artemis.session.createQueue(sendAddress, RoutingType.ANYCAST, "queue", true)
|
||||
val consumer = artemis.session.createConsumer("queue")
|
||||
val testData = "Test".toByteArray()
|
||||
val testProperty = mutableMapOf<Any?, Any?>()
|
||||
@ -230,7 +232,7 @@ class ProtonWrapperTests {
|
||||
}
|
||||
artemisConfig.configureWithDevSSLCertificate()
|
||||
|
||||
val networkMap = rigorousMock<NetworkMapCache>().also {
|
||||
val networkMap = rigorousMock<NetworkMapCacheInternal>().also {
|
||||
doReturn(never<NetworkMapCache.MapChange>()).whenever(it).changed
|
||||
}
|
||||
val userService = rigorousMock<RPCSecurityManager>()
|
||||
|
@ -13,6 +13,7 @@ import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.node.services.config.MySQLConfiguration
|
||||
import net.corda.node.services.config.NotaryConfig
|
||||
import net.corda.nodeapi.internal.DevIdentityGenerator
|
||||
import net.corda.nodeapi.internal.network.NetworkParametersCopier
|
||||
@ -135,7 +136,7 @@ class MySQLNotaryServiceTests : IntegrationTest() {
|
||||
legalName = notaryName,
|
||||
entropyRoot = BigInteger.valueOf(60L),
|
||||
configOverrides = {
|
||||
val notaryConfig = NotaryConfig(validating = false, mysql = dataStoreProperties)
|
||||
val notaryConfig = NotaryConfig(validating = false, mysql = MySQLConfiguration(dataStoreProperties))
|
||||
doReturn(notaryConfig).whenever(it).notary
|
||||
}
|
||||
)
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.node.services.network
|
||||
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.NodeInfo
|
||||
@ -15,6 +16,7 @@ import org.junit.ClassRule
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
// TODO Clean up these tests, they were written with old network map design in place.
|
||||
class PersistentNetworkMapCacheTest : NodeBasedTest() {
|
||||
companion object {
|
||||
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
|
||||
fun `restart node with DB map cache`() {
|
||||
val alice = startNodesWithPort(listOf(ALICE))[0]
|
||||
|
@ -5,6 +5,7 @@ import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.client.rpc.CordaRPCConnection
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.crypto.toStringShort
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
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.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.toBase58String
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.node.internal.Node
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.nodeapi.RPCApi
|
||||
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.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.config.SSLConfiguration
|
||||
import net.corda.testing.ALICE_NAME
|
||||
@ -79,30 +79,30 @@ abstract class MQSecurityTest : NodeBasedTest() {
|
||||
|
||||
@Test
|
||||
fun `consume message from P2P queue`() {
|
||||
assertConsumeAttackFails(P2P_QUEUE)
|
||||
assertConsumeAttackFails("$P2P_PREFIX${alice.info.chooseIdentity().owningKey.toStringShort()}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `consume message from peer queue`() {
|
||||
val bobParty = startBobAndCommunicateWithAlice()
|
||||
assertConsumeAttackFails("$PEERS_PREFIX${bobParty.owningKey.toBase58String()}")
|
||||
assertConsumeAttackFails("$PEERS_PREFIX${bobParty.owningKey.toStringShort()}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `send message to address of peer which has been communicated with`() {
|
||||
val bobParty = startBobAndCommunicateWithAlice()
|
||||
assertSendAttackFails("$PEERS_PREFIX${bobParty.owningKey.toBase58String()}")
|
||||
assertSendAttackFails("$PEERS_PREFIX${bobParty.owningKey.toStringShort()}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create queue for peer which has not been communicated with`() {
|
||||
val bob = startNode(BOB_NAME)
|
||||
assertAllQueueCreationAttacksFail("$PEERS_PREFIX${bob.info.chooseIdentity().owningKey.toBase58String()}")
|
||||
assertAllQueueCreationAttacksFail("$PEERS_PREFIX${bob.info.chooseIdentity().owningKey.toStringShort()}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create queue for unknown peer`() {
|
||||
val invalidPeerQueue = "$PEERS_PREFIX${generateKeyPair().public.toBase58String()}"
|
||||
val invalidPeerQueue = "$PEERS_PREFIX${generateKeyPair().public.toStringShort()}"
|
||||
assertAllQueueCreationAttacksFail(invalidPeerQueue)
|
||||
}
|
||||
|
||||
|
@ -222,7 +222,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
||||
check(started == null) { "Node has already been started" }
|
||||
log.info("Node starting up ...")
|
||||
initCertificate()
|
||||
val schemaService = NodeSchemaService(cordappLoader.cordappSchemas)
|
||||
val schemaService = NodeSchemaService(cordappLoader.cordappSchemas, configuration.notary != null)
|
||||
val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null)
|
||||
val lh = lazyHub()
|
||||
configure(lh)
|
||||
|
@ -40,6 +40,7 @@ import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import rx.Scheduler
|
||||
import rx.schedulers.Schedulers
|
||||
import java.security.PublicKey
|
||||
import java.time.Clock
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import javax.management.ObjectName
|
||||
@ -166,11 +167,14 @@ open class Node(configuration: NodeConfiguration,
|
||||
VerifierType.OutOfProcess -> VerifierMessagingClient(configuration, serverAddress, services.monitoringService.metrics, networkParameters.maxMessageSize)
|
||||
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(
|
||||
configuration,
|
||||
versionInfo,
|
||||
serverAddress,
|
||||
info.legalIdentities[0].owningKey,
|
||||
serviceIdentity,
|
||||
serverThread,
|
||||
database,
|
||||
advertisedAddress,
|
||||
|
@ -29,6 +29,10 @@ interface NetworkMapCacheBaseInternal : NetworkMapCacheBase {
|
||||
|
||||
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). */
|
||||
fun addNode(node: NodeInfo)
|
||||
|
||||
|
@ -18,8 +18,6 @@ import java.util.*
|
||||
val Int.MB: Long get() = this * 1024L * 1024L
|
||||
|
||||
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 emailAddress: String
|
||||
val exportJMXto: String
|
||||
@ -35,8 +33,6 @@ interface NodeConfiguration : NodeSSLConfiguration {
|
||||
val notary: NotaryConfig?
|
||||
val activeMQServer: ActiveMqServerConfiguration
|
||||
val additionalNodeInfoPollingFrequencyMsec: Long
|
||||
// TODO Remove as this is only used by the driver
|
||||
val useHTTPS: Boolean
|
||||
val p2pAddress: NetworkHostAndPort
|
||||
val rpcAddress: NetworkHostAndPort?
|
||||
val messagingServerAddress: NetworkHostAndPort?
|
||||
@ -71,7 +67,7 @@ data class NotaryConfig(val validating: Boolean,
|
||||
val raft: RaftConfig? = null,
|
||||
val bftSMaRt: BFTSMaRtConfiguration? = null,
|
||||
val custom: Boolean = false,
|
||||
val mysql: Properties? = null
|
||||
val mysql: MySQLConfiguration? = null
|
||||
) {
|
||||
init {
|
||||
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
|
||||
}
|
||||
|
||||
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>)
|
||||
|
||||
/** @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.
|
||||
// Then rename this to messageRedeliveryDelay and make it of type Duration
|
||||
override val messageRedeliveryDelaySeconds: Int = 30,
|
||||
override val useHTTPS: Boolean,
|
||||
override val p2pAddress: NetworkHostAndPort,
|
||||
override val rpcAddress: NetworkHostAndPort?,
|
||||
override val relay: RelayConfiguration?,
|
||||
|
@ -12,8 +12,8 @@ import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.messaging.AMQPBridgeManager.AMQPBridge.Companion.getBridgeName
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent
|
||||
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.RemoteInboxAddress.Companion.translateLocalQueueToInboxAddress
|
||||
import net.corda.nodeapi.internal.crypto.loadKeyStore
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
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
|
||||
}
|
||||
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(),
|
||||
properties)
|
||||
sendableMessage.onComplete.then {
|
||||
|
@ -8,16 +8,15 @@ import net.corda.core.internal.div
|
||||
import net.corda.core.internal.noneOrSingle
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
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.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.parsePublicKeyBase58
|
||||
import net.corda.node.internal.Node
|
||||
import net.corda.node.internal.security.Password
|
||||
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.messaging.NodeLoginModule.Companion.NODE_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.NODE_USER
|
||||
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.PEER_USER
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.NodeAddress
|
||||
@ -94,7 +93,7 @@ import javax.security.cert.CertificateException
|
||||
class ArtemisMessagingServer(private val config: NodeConfiguration,
|
||||
private val p2pPort: Int,
|
||||
val rpcPort: Int?,
|
||||
val networkMapCache: NetworkMapCache,
|
||||
val networkMapCache: NetworkMapCacheInternal,
|
||||
val securityManager: RPCSecurityManager,
|
||||
val maxMessageSize: Int) : SingletonSerializeAsToken() {
|
||||
companion object {
|
||||
@ -191,7 +190,6 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
|
||||
// by having its password be an unknown securely random 128-bit value.
|
||||
clusterPassword = BigInteger(128, newSecureRandom()).toString(16)
|
||||
queueConfigurations = listOf(
|
||||
queueConfig(P2P_QUEUE, durable = true),
|
||||
// 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,
|
||||
// but these queues are not worth persisting.
|
||||
@ -243,7 +241,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
|
||||
private fun ConfigurationImpl.configureAddressSecurity(): Pair<Configuration, LoginListener> {
|
||||
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[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))
|
||||
// Each RPC user must have its own role and its own queue. This prevents users accessing each other's queues
|
||||
// and stealing RPC responses.
|
||||
@ -309,8 +307,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
|
||||
|
||||
if (queueName.startsWith(PEERS_PREFIX)) {
|
||||
try {
|
||||
val identity = parsePublicKeyBase58(queueName.substring(PEERS_PREFIX.length))
|
||||
val nodeInfos = networkMapCache.getNodesByLegalIdentityKey(identity)
|
||||
val nodeInfos = networkMapCache.getNodesByOwningKeyIndex(queueName.substring(PEERS_PREFIX.length))
|
||||
if (nodeInfos.isNotEmpty()) {
|
||||
nodeInfos.forEach { deployBridgeToPeer(it) }
|
||||
} else {
|
||||
|
@ -10,15 +10,17 @@ import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.nodeapi.ArtemisTcpTransport
|
||||
import net.corda.nodeapi.ConnectionDirection
|
||||
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.RemoteInboxAddress.Companion.translateLocalQueueToInboxAddress
|
||||
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.remoting.impl.netty.NettyConnection
|
||||
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.TransportConstants
|
||||
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.utils.ConfigurationHelper
|
||||
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,
|
||||
* as defined by ArtemisAddress.queueName. A bridge is then created to forward messages from this queue to the node's
|
||||
* P2P address.
|
||||
@ -64,7 +66,6 @@ internal class CoreBridgeManager(val config: NodeConfiguration, val activeMQServ
|
||||
activeMQServer.deployBridge(BridgeConfiguration().apply {
|
||||
name = getBridgeName(queueName, target)
|
||||
this.queueName = queueName
|
||||
forwardingAddress = P2P_QUEUE
|
||||
staticConnectors = listOf(target.toString())
|
||||
confirmationWindowSize = 100000 // a guess
|
||||
isUseDuplicateDetection = true // Enable the bridge's automatic deduplication logic
|
||||
@ -77,6 +78,7 @@ internal class CoreBridgeManager(val config: NodeConfiguration, val activeMQServ
|
||||
// our TLS certificate.
|
||||
user = 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() {
|
||||
override fun createConnector(configuration: MutableMap<String, Any>,
|
||||
handler: BufferHandler?,
|
||||
|
@ -19,7 +19,6 @@ import net.corda.node.utilities.AffinityExecutor
|
||||
import net.corda.node.utilities.AppendOnlyPersistentMap
|
||||
import net.corda.node.utilities.PersistentMap
|
||||
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.NODE_DATABASE_PREFIX
|
||||
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
|
||||
* CordaRPCClient class.
|
||||
*
|
||||
* @param serverAddress The address of the broker instance to connect to (might be running in the same process).
|
||||
* @param myIdentity The public key to be used as the ArtemisMQ address and queue name for the node.
|
||||
* @param nodeExecutor An executor to run received message tasks upon.
|
||||
* @param advertisedAddress The node address for inbound connections, advertised to the network map service and peers.
|
||||
* If not provided, will default to [serverAddress].
|
||||
* @param config The configuration of the node, which is used for controlling the message redelivery options.
|
||||
* @param versionInfo All messages from the node carry the version info and received messages are checked against this for compatibility.
|
||||
* @param serverAddress The host and port of the Artemis broker.
|
||||
* @param myIdentity The primary identity of the node, which defines the messaging address for externally received messages.
|
||||
* 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
|
||||
class P2PMessagingClient(config: NodeConfiguration,
|
||||
private val versionInfo: VersionInfo,
|
||||
serverAddress: NetworkHostAndPort,
|
||||
private val myIdentity: PublicKey,
|
||||
private val serviceIdentity: PublicKey?,
|
||||
private val nodeExecutor: AffinityExecutor.ServiceAffinityExecutor,
|
||||
private val database: CordaPersistence,
|
||||
advertisedAddress: NetworkHostAndPort = serverAddress,
|
||||
private val maxMessageSize: Int
|
||||
maxMessageSize: Int
|
||||
) : SingletonSerializeAsToken(), MessagingService {
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
@ -126,6 +132,7 @@ class P2PMessagingClient(config: NodeConfiguration,
|
||||
private class InnerState {
|
||||
var running = false
|
||||
var p2pConsumer: ClientConsumer? = null
|
||||
var serviceConsumer: ClientConsumer? = null
|
||||
}
|
||||
|
||||
private val messagesToRedeliver = database.transaction {
|
||||
@ -181,8 +188,20 @@ class P2PMessagingClient(config: NodeConfiguration,
|
||||
fun start() {
|
||||
state.locked {
|
||||
val session = artemis.start().session
|
||||
val inbox = RemoteInboxAddress(myIdentity).queueName
|
||||
// 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()
|
||||
@ -331,6 +350,13 @@ class P2PMessagingClient(config: NodeConfiguration,
|
||||
// Ignore it: this can happen if the server has gone away before we do.
|
||||
}
|
||||
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
|
||||
}
|
||||
if (running && !nodeExecutor.isOnThread) {
|
||||
@ -430,7 +456,7 @@ class P2PMessagingClient(config: NodeConfiguration,
|
||||
private fun getMQAddress(target: MessageRecipients): String {
|
||||
return if (target == myAddress) {
|
||||
// If we are sending to ourselves then route the message directly to our P2P queue.
|
||||
P2P_QUEUE
|
||||
RemoteInboxAddress(myIdentity).queueName
|
||||
} else {
|
||||
// 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.
|
||||
@ -447,7 +473,7 @@ class P2PMessagingClient(config: NodeConfiguration,
|
||||
val queueQuery = session.queueQuery(SimpleString(queueName))
|
||||
if (!queueQuery.isExists) {
|
||||
log.info("Create fresh queue $queueName bound on same address")
|
||||
session.createQueue(queueName, RoutingType.MULTICAST, queueName, true)
|
||||
session.createQueue(queueName, RoutingType.ANYCAST, queueName, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,15 +7,15 @@ import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.loggerFor
|
||||
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.VERIFICATION_REQUESTS_QUEUE_NAME
|
||||
import net.corda.nodeapi.VerifierApi.VERIFICATION_RESPONSES_QUEUE_NAME_PREFIX
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import org.apache.activemq.artemis.api.core.RoutingType
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
import org.apache.activemq.artemis.api.core.client.*
|
||||
import java.util.concurrent.*
|
||||
import org.apache.activemq.artemis.api.core.client.ClientConsumer
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class VerifierMessagingClient(config: SSLConfiguration, serverAddress: NetworkHostAndPort, metrics: MetricRegistry, private val maxMessageSize: Int) : SingletonSerializeAsToken() {
|
||||
companion object {
|
||||
@ -40,7 +40,7 @@ class VerifierMessagingClient(config: SSLConfiguration, serverAddress: NetworkHo
|
||||
val queueQuery = session.queueQuery(SimpleString(queueName))
|
||||
if (!queueQuery.isExists) {
|
||||
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)
|
||||
|
@ -160,6 +160,12 @@ open class PersistentNetworkMapCache(
|
||||
|
||||
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 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> {
|
||||
return findByIdentityKeyIndex(session, identityKey.toStringShort())
|
||||
}
|
||||
|
||||
private fun findByIdentityKeyIndex(session: Session, identityKeyIndex: String): List<NodeInfoSchemaV1.PersistentNodeInfo> {
|
||||
val query = session.createQuery(
|
||||
"SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.legalIdentitiesAndCerts l WHERE l.owningKeyHash = :owningKeyHash",
|
||||
NodeInfoSchemaV1.PersistentNodeInfo::class.java)
|
||||
query.setParameter("owningKeyHash", identityKey.toStringShort())
|
||||
query.setParameter("owningKeyHash", identityKeyIndex)
|
||||
return query.resultList
|
||||
}
|
||||
|
||||
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() }
|
||||
}
|
||||
|
||||
@ -279,13 +293,13 @@ open class PersistentNetworkMapCache(
|
||||
|
||||
private fun queryByAddress(session: Session, hostAndPort: NetworkHostAndPort): NodeInfo? {
|
||||
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)
|
||||
query.setParameter("host", hostAndPort.host)
|
||||
query.setParameter("port", hostAndPort.port)
|
||||
query.setMaxResults(1)
|
||||
val result = query.resultList
|
||||
return if (result.isEmpty()) null
|
||||
else result.map { it.toNodeInfo() }.singleOrNull() ?: throw IllegalStateException("More than one node with the same host and port")
|
||||
return result.map { it.toNodeInfo() }.singleOrNull()
|
||||
}
|
||||
|
||||
/** Object Relational Mapping support. */
|
||||
|
@ -5,7 +5,6 @@ import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.node.services.api.CheckpointStorage
|
||||
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.currentDBSession
|
||||
import org.slf4j.LoggerFactory
|
||||
@ -43,7 +42,7 @@ class DBCheckpointStorage : CheckpointStorage {
|
||||
}
|
||||
|
||||
override fun removeCheckpoint(id: StateMachineRunId): Boolean {
|
||||
val session = DatabaseTransactionManager.current().session
|
||||
val session = currentDBSession()
|
||||
val criteriaBuilder = session.criteriaBuilder
|
||||
val delete = criteriaBuilder.createCriteriaDelete(DBCheckpoint::class.java)
|
||||
val root = delete.from(DBCheckpoint::class.java)
|
||||
|
@ -17,7 +17,6 @@ import net.corda.core.node.services.vault.AttachmentSort
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.utilities.contextLogger
|
||||
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.currentDBSession
|
||||
import java.io.*
|
||||
@ -242,8 +241,7 @@ class NodeAttachmentService(metrics: MetricRegistry) : AttachmentStorage, Single
|
||||
|
||||
override fun queryAttachments(criteria: AttachmentQueryCriteria, sorting: AttachmentSort?): List<AttachmentId> {
|
||||
log.info("Attachment query criteria: $criteria, sorting: $sorting")
|
||||
|
||||
val session = DatabaseTransactionManager.current().session
|
||||
val session = currentDBSession()
|
||||
val criteriaBuilder = session.criteriaBuilder
|
||||
|
||||
val criteriaQuery = criteriaBuilder.createQuery(DBAttachment::class.java)
|
||||
|
@ -10,8 +10,8 @@ import net.corda.core.schemas.PersistentStateRef
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
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.contextTransaction
|
||||
import org.hibernate.FlushMode
|
||||
import rx.Observable
|
||||
|
||||
@ -54,7 +54,7 @@ class HibernateObserver private constructor(private val config: HibernateConfigu
|
||||
internal fun persistStatesWithSchema(statesAndRefs: List<ContractStateAndRef>, schema: MappedSchema) {
|
||||
val sessionFactory = config.sessionFactoryForSchemas(setOf(schema))
|
||||
val session = sessionFactory.withOptions().
|
||||
connection(DatabaseTransactionManager.current().connection).
|
||||
connection(contextTransaction.connection).
|
||||
flushMode(FlushMode.MANUAL).
|
||||
openSession()
|
||||
session.use { thisSession ->
|
||||
|
@ -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: create whitelisted tables when a CorDapp is first installed
|
||||
*/
|
||||
class NodeSchemaService(extraSchemas: Set<MappedSchema> = emptySet()) : SchemaService, SingletonSerializeAsToken() {
|
||||
// Entities for compulsory services
|
||||
object NodeServices
|
||||
class NodeSchemaService(extraSchemas: Set<MappedSchema> = emptySet(), includeNotarySchemas: Boolean = false) : SchemaService, SingletonSerializeAsToken() {
|
||||
// Core Entities used by a Node
|
||||
object NodeCore
|
||||
|
||||
object NodeServicesV1 : MappedSchema(schemaFamily = NodeServices.javaClass, version = 1,
|
||||
object NodeCoreV1 : MappedSchema(schemaFamily = NodeCore.javaClass, version = 1,
|
||||
mappedTypes = listOf(DBCheckpointStorage.DBCheckpoint::class.java,
|
||||
DBTransactionStorage.DBTransaction::class.java,
|
||||
DBTransactionMappingStorage.DBTransactionMapping::class.java,
|
||||
PersistentKeyManagementService.PersistentKey::class.java,
|
||||
PersistentUniquenessProvider.PersistentUniqueness::class.java,
|
||||
PersistentUniquenessProvider.PersistentNotaryCommit::class.java,
|
||||
NodeSchedulerService.PersistentScheduledState::class.java,
|
||||
NodeAttachmentService.DBAttachment::class.java,
|
||||
P2PMessagingClient.ProcessedMessage::class.java,
|
||||
P2PMessagingClient.RetryMessage::class.java,
|
||||
NodeAttachmentService.DBAttachment::class.java,
|
||||
RaftUniquenessProvider.RaftState::class.java,
|
||||
BFTNonValidatingNotaryService.PersistedCommittedState::class.java,
|
||||
PersistentIdentityService.PersistentIdentity::class.java,
|
||||
PersistentIdentityService.PersistentIdentityNames::class.java,
|
||||
ContractUpgradeServiceImpl.DBContractUpgrade::class.java,
|
||||
@ -67,15 +63,25 @@ class NodeSchemaService(extraSchemas: Set<MappedSchema> = emptySet()) : SchemaSe
|
||||
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
|
||||
// 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> =
|
||||
mapOf(Pair(CommonSchemaV1, SchemaOptions()),
|
||||
Pair(VaultSchemaV1, SchemaOptions()),
|
||||
Pair(NodeInfoSchemaV1, SchemaOptions()),
|
||||
Pair(NodeServicesV1, SchemaOptions()))
|
||||
Pair(VaultSchemaV1, SchemaOptions()),
|
||||
Pair(NodeInfoSchemaV1, 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.
|
||||
override fun selectSchemas(state: ContractState): Iterable<MappedSchema> {
|
||||
|
@ -10,7 +10,9 @@ import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.trace
|
||||
import net.corda.node.services.api.CheckpointStorage
|
||||
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.Instant
|
||||
import java.util.concurrent.TimeUnit
|
||||
@ -163,24 +165,24 @@ class ActionExecutorImpl(
|
||||
|
||||
@Suspendable
|
||||
private fun executeCreateTransaction() {
|
||||
if (DatabaseTransactionManager.currentOrNull() != null) {
|
||||
if (contextTransactionOrNull != null) {
|
||||
throw IllegalStateException("Refusing to create a second transaction")
|
||||
}
|
||||
DatabaseTransactionManager.newTransaction()
|
||||
contextDatabase.newTransaction()
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun executeRollbackTransaction() {
|
||||
DatabaseTransactionManager.currentOrNull()?.close()
|
||||
contextTransactionOrNull?.close()
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun executeCommitTransaction() {
|
||||
try {
|
||||
DatabaseTransactionManager.current().commit()
|
||||
contextTransaction.commit()
|
||||
} finally {
|
||||
DatabaseTransactionManager.current().close()
|
||||
DatabaseTransactionManager.setThreadLocalTx(null)
|
||||
contextTransaction.close()
|
||||
contextTransactionOrNull = null
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,8 @@ import net.corda.node.services.statemachine.transitions.FlowContinuation
|
||||
import net.corda.node.services.statemachine.transitions.StateMachine
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
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.LoggerFactory
|
||||
import java.nio.file.Paths
|
||||
@ -58,8 +59,8 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
}
|
||||
|
||||
private fun extractThreadLocalTransaction(): TransientReference<DatabaseTransaction> {
|
||||
val transaction = DatabaseTransactionManager.current()
|
||||
DatabaseTransactionManager.setThreadLocalTx(null)
|
||||
val transaction = contextTransaction
|
||||
contextTransactionOrNull = null
|
||||
return TransientReference(transaction)
|
||||
}
|
||||
}
|
||||
@ -234,7 +235,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
parkAndSerialize { _, _ ->
|
||||
logger.trace { "Suspended on $ioRequest" }
|
||||
|
||||
DatabaseTransactionManager.setThreadLocalTx(transaction.value)
|
||||
contextTransactionOrNull = transaction.value
|
||||
val event = try {
|
||||
Event.Suspend(
|
||||
ioRequest = ioRequest,
|
||||
|
@ -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.TransitionResult
|
||||
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
|
||||
|
||||
/**
|
||||
@ -31,12 +32,12 @@ class TransitionExecutorImpl(
|
||||
transition: TransitionResult,
|
||||
actionExecutor: ActionExecutor
|
||||
): Pair<FlowContinuation, StateMachineState> {
|
||||
DatabaseTransactionManager.dataSource = database
|
||||
contextDatabase = database
|
||||
for (action in transition.actions) {
|
||||
try {
|
||||
actionExecutor.executeAction(fiber, action)
|
||||
} catch (exception: Throwable) {
|
||||
DatabaseTransactionManager.currentOrNull()?.close()
|
||||
contextTransactionOrNull?.close()
|
||||
if (transition.newState.checkpoint.errorState is ErrorState.Errored) {
|
||||
// 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.
|
||||
|
@ -5,6 +5,7 @@ import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.node.services.TimeWindowChecker
|
||||
import net.corda.core.node.services.TrustedAuthorityNotaryService
|
||||
import net.corda.node.services.api.ServiceHubInternal
|
||||
import net.corda.node.services.config.MySQLConfiguration
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
|
||||
@ -12,14 +13,14 @@ import java.util.*
|
||||
abstract class MySQLNotaryService(
|
||||
final override val services: ServiceHubInternal,
|
||||
override val notaryIdentityKey: PublicKey,
|
||||
dataSourceProperties: Properties,
|
||||
configuration: MySQLConfiguration,
|
||||
/** Database table will be automatically created in dev mode */
|
||||
val devMode: Boolean) : TrustedAuthorityNotaryService() {
|
||||
|
||||
override val timeWindowChecker = TimeWindowChecker(services.clock)
|
||||
override val uniquenessProvider = MySQLUniquenessProvider(
|
||||
services.monitoringService.metrics,
|
||||
dataSourceProperties
|
||||
configuration
|
||||
)
|
||||
|
||||
override fun start() {
|
||||
@ -33,14 +34,14 @@ abstract class MySQLNotaryService(
|
||||
|
||||
class MySQLNonValidatingNotaryService(services: ServiceHubInternal,
|
||||
notaryIdentityKey: PublicKey,
|
||||
dataSourceProperties: Properties,
|
||||
devMode: Boolean = false) : MySQLNotaryService(services, notaryIdentityKey, dataSourceProperties, devMode) {
|
||||
configuration: MySQLConfiguration,
|
||||
devMode: Boolean = false) : MySQLNotaryService(services, notaryIdentityKey, configuration, devMode) {
|
||||
override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic<Void?> = NonValidatingNotaryFlow(otherPartySession, this)
|
||||
}
|
||||
|
||||
class MySQLValidatingNotaryService(services: ServiceHubInternal,
|
||||
notaryIdentityKey: PublicKey,
|
||||
dataSourceProperties: Properties,
|
||||
devMode: Boolean = false) : MySQLNotaryService(services, notaryIdentityKey, dataSourceProperties, devMode) {
|
||||
configuration: MySQLConfiguration,
|
||||
devMode: Boolean = false) : MySQLNotaryService(services, notaryIdentityKey, configuration, devMode) {
|
||||
override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic<Void?> = ValidatingNotaryFlow(otherPartySession, this)
|
||||
}
|
@ -14,9 +14,11 @@ import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.services.config.MySQLConfiguration
|
||||
import java.security.PublicKey
|
||||
import java.sql.BatchUpdateException
|
||||
import java.sql.Connection
|
||||
import java.sql.SQLTransientConnectionException
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
@ -27,7 +29,7 @@ import java.util.*
|
||||
*/
|
||||
class MySQLUniquenessProvider(
|
||||
metrics: MetricRegistry,
|
||||
dataSourceProperties: Properties
|
||||
configuration: MySQLConfiguration
|
||||
) : UniquenessProvider, SingletonSerializeAsToken() {
|
||||
companion object {
|
||||
private val log = loggerFor<MySQLUniquenessProvider>()
|
||||
@ -57,13 +59,29 @@ class MySQLUniquenessProvider(
|
||||
* This is a useful heath metric.
|
||||
*/
|
||||
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. */
|
||||
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
|
||||
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() {
|
||||
log.debug("Attempting to create DB table if it does not yet exist: $createTableStatement")
|
||||
|
@ -17,12 +17,8 @@ import net.corda.core.transactions.NotaryChangeWireTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.*
|
||||
import net.corda.node.services.api.VaultServiceInternal
|
||||
import net.corda.nodeapi.internal.persistence.HibernateConfiguration
|
||||
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager
|
||||
import net.corda.nodeapi.internal.persistence.bufferUntilDatabaseCommit
|
||||
import net.corda.nodeapi.internal.persistence.currentDBSession
|
||||
import net.corda.nodeapi.internal.persistence.wrapWithDatabaseTransaction
|
||||
import net.corda.nodeapi.internal.persistence.*
|
||||
import org.hibernate.Session
|
||||
import rx.Observable
|
||||
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
|
||||
*/
|
||||
|
@ -10,7 +10,7 @@ import net.corda.core.utilities.toNonEmptySet
|
||||
import net.corda.core.utilities.trace
|
||||
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
||||
import net.corda.node.services.statemachine.StateMachineManager
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager
|
||||
import net.corda.nodeapi.internal.persistence.contextDatabase
|
||||
import java.util.*
|
||||
|
||||
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>) {
|
||||
log.trace { "Reserving soft locks for flow id $flowId and states $stateRefs" }
|
||||
DatabaseTransactionManager.dataSource.transaction {
|
||||
contextDatabase.transaction {
|
||||
vault.softLockReserve(flowId, stateRefs)
|
||||
}
|
||||
}
|
||||
|
||||
private fun unregisterSoftLocks(flowId: UUID, logic: FlowLogic<*>) {
|
||||
log.trace { "Releasing soft locks for flow ${logic.javaClass.simpleName} with flow id $flowId" }
|
||||
DatabaseTransactionManager.dataSource.transaction {
|
||||
contextDatabase.transaction {
|
||||
vault.softLockRelease(flowId)
|
||||
}
|
||||
}
|
||||
|
@ -67,4 +67,17 @@
|
||||
constraintName="FK__info_hosts__infos"
|
||||
referencedColumnNames="node_info_id" referencedTableName="node_infos"/>
|
||||
</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>
|
||||
|
@ -14,7 +14,6 @@ database = {
|
||||
exportHibernateJMXStatistics = "false"
|
||||
}
|
||||
devMode = true
|
||||
useHTTPS = false
|
||||
h2port = 0
|
||||
useTestClock = false
|
||||
verifierType = InMemory
|
||||
|
@ -76,7 +76,6 @@ class NodeConfigurationImplTest {
|
||||
dataSourceProperties = makeTestDataSourceProperties(ALICE_NAME.organisation),
|
||||
rpcUsers = emptyList(),
|
||||
verifierType = VerifierType.InMemory,
|
||||
useHTTPS = false,
|
||||
p2pAddress = NetworkHostAndPort("localhost", 0),
|
||||
rpcAddress = NetworkHostAndPort("localhost", 1),
|
||||
messagingServerAddress = null,
|
||||
|
@ -178,6 +178,7 @@ class ArtemisMessagingTests {
|
||||
MOCK_VERSION_INFO.copy(platformVersion = platformVersion),
|
||||
server,
|
||||
identity.public,
|
||||
null,
|
||||
ServiceAffinityExecutor("ArtemisMessagingTests", 1),
|
||||
database,
|
||||
maxMessageSize = maxMessageSize).apply {
|
||||
|
@ -1,9 +1,11 @@
|
||||
package net.corda.node.services.network
|
||||
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.node.services.NetworkMapCache
|
||||
import net.corda.node.services.api.NetworkMapCacheInternal
|
||||
import net.corda.testing.ALICE_NAME
|
||||
import net.corda.testing.BOB_NAME
|
||||
import net.corda.testing.getTestPartyAndCertificate
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.MockNodeParameters
|
||||
import net.corda.testing.singleIdentity
|
||||
@ -106,4 +108,14 @@ class NetworkMapCacheTest {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,6 @@ import net.corda.core.schemas.QueryableState
|
||||
import net.corda.node.services.api.SchemaService
|
||||
import net.corda.node.internal.configureDatabase
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager
|
||||
import net.corda.testing.internal.LogHelper
|
||||
import net.corda.testing.TestIdentity
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
@ -74,11 +73,11 @@ class HibernateObserverTests {
|
||||
database.transaction {
|
||||
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)))))
|
||||
val parentRowCountResult = DatabaseTransactionManager.current().connection.prepareStatement("select count(*) from Parents").executeQuery()
|
||||
val parentRowCountResult = connection.prepareStatement("select count(*) from Parents").executeQuery()
|
||||
parentRowCountResult.next()
|
||||
val parentRows = parentRowCountResult.getInt(1)
|
||||
parentRowCountResult.close()
|
||||
val childrenRowCountResult = DatabaseTransactionManager.current().connection.prepareStatement("select count(*) from Children").executeQuery()
|
||||
val childrenRowCountResult = connection.prepareStatement("select count(*) from Children").executeQuery()
|
||||
childrenRowCountResult.next()
|
||||
val childrenRows = childrenRowCountResult.getInt(1)
|
||||
childrenRowCountResult.close()
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user