diff --git a/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt b/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt index 4439cb0be5..358b5ed947 100644 --- a/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt +++ b/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt @@ -40,6 +40,7 @@ object JacksonSupport { fun partyFromName(partyName: String): Party? fun partyFromX500Name(name: X500Name): Party? fun partyFromKey(owningKey: PublicKey): Party? + fun partiesFromName(query: String, exactMatch: Boolean): Set } class RpcObjectMapper(val rpc: CordaRPCOps, factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) { @@ -47,6 +48,7 @@ object JacksonSupport { override fun partyFromName(partyName: String): Party? = rpc.partyFromName(partyName) override fun partyFromX500Name(name: X500Name): Party? = rpc.partyFromX500Name(name) override fun partyFromKey(owningKey: PublicKey): Party? = rpc.partyFromKey(owningKey) + override fun partiesFromName(query: String, exactMatch: Boolean) = rpc.partiesFromName(query, exactMatch) } class IdentityObjectMapper(val identityService: IdentityService, factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) { @@ -54,6 +56,7 @@ object JacksonSupport { override fun partyFromName(partyName: String): Party? = identityService.partyFromName(partyName) override fun partyFromX500Name(name: X500Name): Party? = identityService.partyFromX500Name(name) override fun partyFromKey(owningKey: PublicKey): Party? = identityService.partyFromKey(owningKey) + override fun partiesFromName(query: String, exactMatch: Boolean) = identityService.partiesFromName(query, exactMatch) } class NoPartyObjectMapper(factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) { @@ -61,6 +64,7 @@ object JacksonSupport { override fun partyFromName(partyName: String): Party? = throw UnsupportedOperationException() override fun partyFromX500Name(name: X500Name): Party? = throw UnsupportedOperationException() override fun partyFromKey(owningKey: PublicKey): Party? = throw UnsupportedOperationException() + override fun partiesFromName(query: String, exactMatch: Boolean) = throw UnsupportedOperationException() } val cordaModule: Module by lazy { @@ -169,14 +173,21 @@ object JacksonSupport { // how to parse the content return if (parser.text.contains("=")) { val principal = X500Name(parser.text) - mapper.partyFromX500Name(principal) ?: throw JsonParseException(parser, "Could not find a Party with name ${principal}") + mapper.partyFromX500Name(principal) ?: throw JsonParseException(parser, "Could not find a Party with name $principal") } else { - val key = try { - parsePublicKeyBase58(parser.text) - } catch (e: Exception) { - throw JsonParseException(parser, "Could not interpret ${parser.text} as a base58 encoded public key") + val nameMatches = mapper.partiesFromName(parser.text, false) + if (nameMatches.isEmpty()) { + val key = try { + parsePublicKeyBase58(parser.text) + } catch (e: Exception) { + throw JsonParseException(parser, "Could not find a matching party for '${parser.text}' and is not a base58 encoded public key") + } + mapper.partyFromKey(key) ?: throw JsonParseException(parser, "Could not find a Party with key ${key.toStringShort()}") + } else if (nameMatches.size == 1) { + nameMatches.first() + } else { + throw JsonParseException(parser, "Ambiguous name match '${parser.text}': could be any of " + nameMatches.map { it.name }.joinToString(" ... or ...")) } - mapper.partyFromKey(key) ?: throw JsonParseException(parser, "Could not find a Party with key ${key.toStringShort()}") } } } diff --git a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt index 190f2456a0..ebdb291174 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -244,6 +244,16 @@ interface CordaRPCOps : RPCOps { */ fun partyFromX500Name(x500Name: X500Name): Party? + /** + * Returns a list of candidate matches for a given string, with optional fuzzy(ish) matching. Fuzzy matching may + * get smarter with time e.g. to correct spelling errors, so you should not hard-code indexes into the results + * but rather show them via a user interface and let the user pick the one they wanted. + * + * @param query The string to check against the X.500 name components + * @param exactMatch If true, a case sensitive match is done against each component of each X.500 name. + */ + fun partiesFromName(query: String, exactMatch: Boolean): Set + /** Enumerates the class names of the flows that this node knows about. */ fun registeredFlows(): List } diff --git a/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt b/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt index 586ad52ce2..ce5d5f9262 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt @@ -64,7 +64,7 @@ interface IdentityService { // but for now this is not supported. fun partyFromKey(key: PublicKey): Party? - @Deprecated("Use partyFromX500Name") + @Deprecated("Use partyFromX500Name or partiesFromName") fun partyFromName(name: String): Party? fun partyFromX500Name(principal: X500Name): Party? @@ -98,5 +98,15 @@ interface IdentityService { */ fun pathForAnonymous(anonymousParty: AnonymousParty): CertPath? + /** + * Returns a list of candidate matches for a given string, with optional fuzzy(ish) matching. Fuzzy matching may + * get smarter with time e.g. to correct spelling errors, so you should not hard-code indexes into the results + * but rather show them via a user interface and let the user pick the one they wanted. + * + * @param query The string to check against the X.500 name components + * @param exactMatch If true, a case sensitive match is done against each component of each X.500 name. + */ + fun partiesFromName(query: String, exactMatch: Boolean): Set + class UnknownAnonymousPartyException(msg: String) : Exception(msg) } diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 8d5bfe7e2d..a7afd59e23 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -7,8 +7,11 @@ from the previous milestone release. UNRELEASED ---------- -Milestone 12.0 - First Public Beta ----------------------------------- +* A new RPC has been added to support fuzzy matching of X.500 names, for instance, to translate from user input to + an unambiguous identity by searching the network map. + +Milestone 12 +------------ * Quite a few changes have been made to the flow API which should make things simpler when writing CorDapps: @@ -97,6 +100,7 @@ Milestone 12.0 - First Public Beta * The certificate hierarchy has been changed in order to allow corda node to sign keys with proper certificate chain. * The corda node will now be issued a restricted client CA for identity/transaction key signing. * TLS certificate are now stored in `sslkeystore.jks` and identity keys are stored in `nodekeystore.jks` + .. warning:: The old keystore will need to be removed when upgrading to this version. Milestone 11.1 diff --git a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt index acdc90af16..b0fced2732 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -8,6 +8,7 @@ import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowLogic import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.Party import net.corda.core.messaging.* import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache @@ -174,7 +175,8 @@ class CordaRPCOpsImpl( @Suppress("DEPRECATION") @Deprecated("Use partyFromX500Name instead") override fun partyFromName(name: String) = services.identityService.partyFromName(name) - override fun partyFromX500Name(x500Name: X500Name)= services.identityService.partyFromX500Name(x500Name) + override fun partyFromX500Name(x500Name: X500Name) = services.identityService.partyFromX500Name(x500Name) + override fun partiesFromName(query: String, exactMatch: Boolean): Set = services.identityService.partiesFromName(query, exactMatch) override fun registeredFlows(): List = services.rpcFlows.map { it.name }.sorted() diff --git a/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt index ad8742f9fa..0f06a21490 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt @@ -4,7 +4,10 @@ import net.corda.core.contracts.PartyAndReference import net.corda.core.contracts.requireThat import net.corda.core.crypto.subject import net.corda.core.crypto.toStringShort -import net.corda.core.identity.* +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.Party +import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.services.IdentityService import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.loggerFor @@ -18,6 +21,7 @@ import java.security.cert.* import java.util.* import java.util.concurrent.ConcurrentHashMap import javax.annotation.concurrent.ThreadSafe +import kotlin.collections.ArrayList /** * Simple identity service which caches parties and provides functionality for efficient lookup. @@ -95,6 +99,24 @@ class InMemoryIdentityService(identities: Iterable, return partyFromAnonymous(party) ?: throw IllegalStateException("Could not deanonymise party ${party.owningKey.toStringShort()}") } + override fun partiesFromName(query: String, exactMatch: Boolean): Set { + val results = HashSet() + for ((x500name, partyAndCertificate) in principalToParties) { + val party = partyAndCertificate.party + for (rdn in x500name.rdNs) { + val component = rdn.first.value.toString() + if (exactMatch && component == query) { + results += party + } else if (!exactMatch) { + // We can imagine this being a query over a lucene index in future. + if (component.contains(query, ignoreCase = true)) + results += party + } + } + } + return results + } + @Throws(IdentityService.UnknownAnonymousPartyException::class) override fun assertOwnership(party: Party, anonymousParty: AnonymousParty) { val path = partyToPath[anonymousParty] ?: throw IdentityService.UnknownAnonymousPartyException("Unknown anonymous party ${anonymousParty.owningKey.toStringShort()}") diff --git a/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt index c0fff9bbd2..e18309d578 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt @@ -11,6 +11,8 @@ import net.corda.testing.ALICE_PUBKEY import net.corda.testing.BOB_PUBKEY import org.bouncycastle.asn1.x500.X500Name import org.junit.Test +import java.security.KeyPair +import java.security.cert.CertPath import java.security.cert.X509Certificate import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -53,6 +55,18 @@ class InMemoryIdentityServiceTests { assertNull(service.partyFromX500Name(ALICE.name)) } + @Test + fun `get identity by substring match`() { + val service = InMemoryIdentityService(trustRoot = DUMMY_CA.certificate) + service.registerIdentity(ALICE_IDENTITY) + service.registerIdentity(BOB_IDENTITY) + val (_, _, alicente) = createParty(X500Name("O=Alicente Worldwide,L=London,C=UK")) + service.registerIdentity(alicente) + assertEquals(setOf(ALICE, alicente.party), service.partiesFromName("Alice", false)) + assertEquals(setOf(ALICE), service.partiesFromName("Alice Corp", true)) + assertEquals(setOf(BOB), service.partiesFromName("Bob Plc", true)) + } + @Test fun `get identity by name`() { val service = InMemoryIdentityService(trustRoot = DUMMY_CA.certificate) @@ -87,12 +101,7 @@ class InMemoryIdentityServiceTests { */ @Test fun `assert ownership`() { - val aliceRootKey = Crypto.generateKeyPair() - val aliceRootCert = X509Utilities.createSelfSignedCACertificate(ALICE.name, aliceRootKey) - val aliceTxKey = Crypto.generateKeyPair() - val aliceTxCert = X509Utilities.createCertificate(CertificateType.IDENTITY, aliceRootCert, aliceRootKey, ALICE.name, aliceTxKey.public) - val aliceCertPath = X509Utilities.createCertificatePath(aliceRootCert, aliceTxCert, revocationEnabled = false) - val alice = PartyAndCertificate(ALICE.name, aliceRootKey.public, aliceRootCert, aliceCertPath) + val (aliceTxKey, aliceCertPath, alice) = createParty(ALICE.name) val bobRootKey = Crypto.generateKeyPair() val bobRootCert = X509Utilities.createSelfSignedCACertificate(BOB.name, bobRootKey) @@ -120,6 +129,15 @@ class InMemoryIdentityServiceTests { } } + private fun createParty(x500Name: X500Name): Triple { + val rootKey = Crypto.generateKeyPair() + val rootCert = X509Utilities.createSelfSignedCACertificate(x500Name, rootKey) + val txKey = Crypto.generateKeyPair() + val txCert = X509Utilities.createCertificate(CertificateType.IDENTITY, rootCert, rootKey, x500Name, txKey.public) + val certPath = X509Utilities.createCertificatePath(rootCert, txCert, revocationEnabled = false) + return Triple(txKey, certPath, PartyAndCertificate(x500Name, rootKey.public, rootCert, certPath)) + } + /** * Ensure if we feed in a full identity, we get the same identity back. */