Support fuzzy matching for identities.

Matching can be done with case insensitive substrings in the identity service, RPC and shell. In future cleverer matching should be possible, e.g. using Lucene or RDBMS free text search features.
This commit is contained in:
Mike Hearn 2017-05-17 23:42:00 +02:00
parent a86c5ba2dd
commit ccf43a8e17
7 changed files with 94 additions and 17 deletions

View File

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

View File

@ -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<Party>
/** Enumerates the class names of the flows that this node knows about. */
fun registeredFlows(): List<String>
}

View File

@ -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<Party>
class UnknownAnonymousPartyException(msg: String) : Exception(msg)
}

View File

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

View File

@ -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<Party> = services.identityService.partiesFromName(query, exactMatch)
override fun registeredFlows(): List<String> = services.rpcFlows.map { it.name }.sorted()

View File

@ -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<PartyAndCertificate>,
return partyFromAnonymous(party) ?: throw IllegalStateException("Could not deanonymise party ${party.owningKey.toStringShort()}")
}
override fun partiesFromName(query: String, exactMatch: Boolean): Set<Party> {
val results = HashSet<Party>()
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()}")

View File

@ -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<KeyPair, CertPath, PartyAndCertificate> {
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.
*/