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

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

3
.idea/compiler.xml generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -66,26 +66,24 @@ object NodeInfoSchemaV1 : MappedSchema(
}
}
@Embeddable
data class PKHostAndPort(
val host: String? = null,
val port: Int? = null
) : Serializable
@Entity
@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!!)
}
}

View File

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

View File

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

View File

@ -46,7 +46,6 @@ handling, and ensures the Corda service is run at boot.
myLegalName : "O=Bank of Breakfast Tea, L=London, C=GB"
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=[
{

View File

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

View File

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

View File

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

View File

@ -46,7 +46,7 @@ Message queues
The node makes use of various queues for its operation. The more important ones are described below. Others are used
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.

View File

@ -37,7 +37,7 @@ It's reproduced here as an example of both ways you can do this for a couple of
:end-before: END 7
.. 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``.

View File

@ -1,3 +1,9 @@
.. highlight:: kotlin
.. raw:: html
<script type="text/javascript" src="_static/jquery.js"></script>
<script type="text/javascript" src="_static/codesets.js"></script>
Upgrading a CorDapp (outside of platform version upgrades)
==========================================================
@ -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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ package com.r3.corda.networkmanage.common.persistence
import com.r3.corda.networkmanage.common.persistence.entity.CertificateDataEntity
import com.r3.corda.networkmanage.common.persistence.entity.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")
}
}

View File

@ -6,10 +6,7 @@ import com.r3.corda.networkmanage.common.utils.SignedNetworkParameters
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.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()
}
}

View File

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

View File

@ -4,6 +4,7 @@ import com.r3.corda.networkmanage.common.persistence.CertificateData
import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequest
import com.r3.corda.networkmanage.common.persistence.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()
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,41 +1,41 @@
package com.r3.corda.networkmanage.doorman.signer
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)
}
}

View File

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

View File

@ -1,48 +1,20 @@
package com.r3.corda.networkmanage.doorman.signer
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))
}
}

View File

@ -7,9 +7,8 @@ import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
import com.r3.corda.networkmanage.common.persistence.NodeInfoStorage
import com.r3.corda.networkmanage.common.persistence.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()
}

View File

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

View File

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

View File

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

View File

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

View File

@ -2,5 +2,7 @@
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd" >
<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>

View File

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

View File

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

View File

@ -20,6 +20,7 @@ abstract class TestBase {
requestId: String = SecureHash.randomSHA256().toString(),
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
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
package com.r3.corda.networkmanage.doorman
package com.r3.corda.networkmanage.doorman.webservice
import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.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)

View File

@ -1,11 +1,11 @@
package com.r3.corda.networkmanage.doorman
package com.r3.corda.networkmanage.doorman.webservice
import com.nhaarman.mockito_kotlin.*
import com.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())

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -128,15 +128,16 @@ class HibernateConfiguration(
class NodeDatabaseConnectionProvider : ConnectionProvider {
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 {

View File

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

View File

@ -13,11 +13,13 @@ import net.corda.node.internal.protonwrapper.messages.MessageStatus
import net.corda.node.internal.protonwrapper.netty.AMQPClient
import net.corda.node.internal.protonwrapper.netty.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>()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,8 +12,8 @@ import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.messaging.AMQPBridgeManager.AMQPBridge.Companion.getBridgeName
import net.corda.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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,24 +29,20 @@ import net.corda.node.services.vault.VaultSchemaV1
* TODO: support plugins for schema version upgrading or custom mapping not supported by original [QueryableState].
* TODO: 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> {

View File

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

View File

@ -27,7 +27,8 @@ import net.corda.node.services.statemachine.transitions.FlowContinuation
import net.corda.node.services.statemachine.transitions.StateMachine
import net.corda.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,

View File

@ -5,7 +5,8 @@ import net.corda.core.utilities.contextLogger
import net.corda.node.services.statemachine.transitions.FlowContinuation
import net.corda.node.services.statemachine.transitions.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.

View File

@ -5,6 +5,7 @@ import net.corda.core.flows.FlowSession
import net.corda.core.node.services.TimeWindowChecker
import net.corda.core.node.services.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)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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