Enforce JSON serialized key format (#1606)

Move Jackson public key encode/decode support away from Kryo serialization format for compactness and to DER format encoded as base64 for compatibility with other systems.
This commit is contained in:
Ross Nicoll 2017-09-26 18:22:35 +01:00 committed by GitHub
parent 24ff7efd5f
commit aff4d35ccb
3 changed files with 54 additions and 49 deletions

View File

@ -28,8 +28,9 @@ import net.corda.core.transactions.NotaryChangeWireTransaction
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.parsePublicKeyBase58 import net.corda.core.utilities.base58ToByteArray
import net.corda.core.utilities.toBase58String import net.corda.core.utilities.base64ToByteArray
import net.corda.core.utilities.toBase64
import net.i2p.crypto.eddsa.EdDSAPublicKey import net.i2p.crypto.eddsa.EdDSAPublicKey
import java.math.BigDecimal import java.math.BigDecimal
import java.security.PublicKey import java.security.PublicKey
@ -83,13 +84,9 @@ object JacksonSupport {
addDeserializer(SecureHash::class.java, SecureHashDeserializer()) addDeserializer(SecureHash::class.java, SecureHashDeserializer())
addDeserializer(SecureHash.SHA256::class.java, SecureHashDeserializer()) addDeserializer(SecureHash.SHA256::class.java, SecureHashDeserializer())
// For ed25519 pubkeys // Public key types
addSerializer(EdDSAPublicKey::class.java, PublicKeySerializer) addSerializer(PublicKey::class.java, PublicKeySerializer)
addDeserializer(EdDSAPublicKey::class.java, PublicKeyDeserializer) addDeserializer(PublicKey::class.java, PublicKeyDeserializer)
// For composite keys
addSerializer(CompositeKey::class.java, CompositeKeySerializer)
addDeserializer(CompositeKey::class.java, CompositeKeyDeserializer)
// For NodeInfo // For NodeInfo
// TODO this tunnels the Kryo representation as a Base58 encoded string. Replace when RPC supports this. // TODO this tunnels the Kryo representation as a Base58 encoded string. Replace when RPC supports this.
@ -158,7 +155,7 @@ object JacksonSupport {
object AnonymousPartySerializer : JsonSerializer<AnonymousParty>() { object AnonymousPartySerializer : JsonSerializer<AnonymousParty>() {
override fun serialize(obj: AnonymousParty, generator: JsonGenerator, provider: SerializerProvider) { override fun serialize(obj: AnonymousParty, generator: JsonGenerator, provider: SerializerProvider) {
generator.writeString(obj.owningKey.toBase58String()) PublicKeySerializer.serialize(obj.owningKey, generator, provider)
} }
} }
@ -168,8 +165,7 @@ object JacksonSupport {
parser.nextToken() parser.nextToken()
} }
// TODO this needs to use some industry identifier(s) instead of these keys val key = PublicKeyDeserializer.deserialize(parser, context)
val key = parsePublicKeyBase58(parser.text)
return AnonymousParty(key) return AnonymousParty(key)
} }
} }
@ -187,19 +183,24 @@ object JacksonSupport {
} }
val mapper = parser.codec as PartyObjectMapper val mapper = parser.codec as PartyObjectMapper
// TODO: We should probably have a better specified way of identifying X.500 names vs keys // The comma character is invalid in base64, and required as a separator for X.500 names. As Corda
// Base58 keys never include an equals character, while X.500 names always will, so we use that to determine // X.500 names all involve at least three attributes (organisation, locality, country), they must
// how to parse the content // include a comma. As such we can use it as a distinguisher between the two types.
return if (parser.text.contains("=")) { return if (parser.text.contains(",")) {
val principal = CordaX500Name.parse(parser.text) val principal = CordaX500Name.parse(parser.text)
mapper.wellKnownPartyFromX500Name(principal) ?: throw JsonParseException(parser, "Could not find a Party with name $principal") mapper.wellKnownPartyFromX500Name(principal) ?: throw JsonParseException(parser, "Could not find a Party with name $principal")
} else { } else {
val nameMatches = mapper.partiesFromName(parser.text) val nameMatches = mapper.partiesFromName(parser.text)
if (nameMatches.isEmpty()) { if (nameMatches.isEmpty()) {
val derBytes = try {
parser.text.base64ToByteArray()
} catch (e: AddressFormatException) {
throw JsonParseException(parser, "Could not find a matching party for '${parser.text}' and is not a base64 encoded public key: " + e.message)
}
val key = try { val key = try {
parsePublicKeyBase58(parser.text) Crypto.decodePublicKey(derBytes)
} catch (e: Exception) { } catch (e: Exception) {
throw JsonParseException(parser, "Could not find a matching party for '${parser.text}' and is not a base58 encoded public key") throw JsonParseException(parser, "Could not find a matching party for '${parser.text}' and is not a valid public key: " + e.message)
} }
mapper.partyFromKey(key) ?: throw JsonParseException(parser, "Could not find a Party with key ${key.toStringShort()}") mapper.partyFromKey(key) ?: throw JsonParseException(parser, "Could not find a Party with key ${key.toStringShort()}")
} else if (nameMatches.size == 1) { } else if (nameMatches.size == 1) {
@ -273,39 +274,23 @@ object JacksonSupport {
} }
} }
object PublicKeySerializer : JsonSerializer<EdDSAPublicKey>() { object PublicKeySerializer : JsonSerializer<PublicKey>() {
override fun serialize(obj: EdDSAPublicKey, generator: JsonGenerator, provider: SerializerProvider) { override fun serialize(obj: PublicKey, generator: JsonGenerator, provider: SerializerProvider) {
check(obj.params == Crypto.EDDSA_ED25519_SHA512.algSpec) generator.writeString(obj.encoded.toBase64())
generator.writeString(obj.toBase58String())
} }
} }
object PublicKeyDeserializer : JsonDeserializer<EdDSAPublicKey>() { object PublicKeyDeserializer : JsonDeserializer<PublicKey>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): EdDSAPublicKey { override fun deserialize(parser: JsonParser, context: DeserializationContext): PublicKey {
return try { return try {
parsePublicKeyBase58(parser.text) as EdDSAPublicKey val derBytes = parser.text.base64ToByteArray()
Crypto.decodePublicKey(derBytes)
} catch (e: Exception) { } catch (e: Exception) {
throw JsonParseException(parser, "Invalid public key ${parser.text}: ${e.message}") throw JsonParseException(parser, "Invalid public key ${parser.text}: ${e.message}")
} }
} }
} }
object CompositeKeySerializer : JsonSerializer<CompositeKey>() {
override fun serialize(obj: CompositeKey, generator: JsonGenerator, provider: SerializerProvider) {
generator.writeString(obj.toBase58String())
}
}
object CompositeKeyDeserializer : JsonDeserializer<CompositeKey>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): CompositeKey {
return try {
parsePublicKeyBase58(parser.text) as CompositeKey
} catch (e: Exception) {
throw JsonParseException(parser, "Invalid composite key ${parser.text}: ${e.message}")
}
}
}
object AmountSerializer : JsonSerializer<Amount<*>>() { object AmountSerializer : JsonSerializer<Amount<*>>() {
override fun serialize(value: Amount<*>, gen: JsonGenerator, serializers: SerializerProvider) { override fun serialize(value: Amount<*>, gen: JsonGenerator, serializers: SerializerProvider) {
gen.writeString(value.toString()) gen.writeString(value.toString())

View File

@ -14,15 +14,16 @@ import net.corda.testing.DUMMY_NOTARY
import net.corda.testing.MINI_CORP import net.corda.testing.MINI_CORP
import net.corda.testing.TestDependencyInjectionBase import net.corda.testing.TestDependencyInjectionBase
import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyContract
import net.i2p.crypto.eddsa.EdDSAPublicKey
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import java.math.BigInteger
import java.security.PublicKey
import java.util.* import java.util.*
import kotlin.reflect.jvm.jvmName
import kotlin.test.assertEquals import kotlin.test.assertEquals
class JacksonSupportTest : TestDependencyInjectionBase() { class JacksonSupportTest : TestDependencyInjectionBase() {
companion object { companion object {
private val SEED = BigInteger.valueOf(20170922L)
val mapper = JacksonSupport.createNonRpcMapper() val mapper = JacksonSupport.createNonRpcMapper()
} }
@ -37,15 +38,34 @@ class JacksonSupportTest : TestDependencyInjectionBase() {
} }
@Test @Test
fun publicKeySerializingWorks() { fun `should serialize Composite keys`() {
val publicKey = generateKeyPair().public val expected = "\"MIHAMBUGE2mtoq+J1bjir/ONk6yd5pab0FoDgaYAMIGiAgECMIGcMDIDLQAwKjAFBgMrZXADIQAgIX1QlJRgaLlD0ttLlJF5kNqT/7P7QwCvrWc9+/248gIBATAyAy0AMCowBQYDK2VwAyEAqS0JPGlzdviBZjB9FaNY+w6cVs3/CQ2A5EimE9Lyng4CAQEwMgMtADAqMAUGAytlcAMhALq4GG0gBQZIlaKE6ucooZsuoKUbH4MtGSmA6cwj136+AgEB\""
val innerKeys = (1..3).map { i ->
Crypto.deriveKeyPairFromEntropy(Crypto.EDDSA_ED25519_SHA512, SEED.plus(BigInteger.valueOf(i.toLong()))).public
}
// Build a 2 of 3 composite key
val publicKey = CompositeKey.Builder().let {
innerKeys.forEach { key -> it.addKey(key, 1) }
it.build(2)
}
val serialized = mapper.writeValueAsString(publicKey) val serialized = mapper.writeValueAsString(publicKey)
val parsedKey = mapper.readValue(serialized, EdDSAPublicKey::class.java) assertEquals(expected, serialized)
val parsedKey = mapper.readValue(serialized, PublicKey::class.java)
assertEquals(publicKey, parsedKey) assertEquals(publicKey, parsedKey)
} }
private class Dummy(val notional: Amount<Currency>) private class Dummy(val notional: Amount<Currency>)
@Test
fun `should serialize EdDSA keys`() {
val expected = "\"MCowBQYDK2VwAyEACFTgLk1NOqYXAfxLoR7ctSbZcl9KMXu58Mq31Kv1Dwk=\""
val publicKey = Crypto.deriveKeyPairFromEntropy(Crypto.EDDSA_ED25519_SHA512, SEED).public
val serialized = mapper.writeValueAsString(publicKey)
assertEquals(expected, serialized)
val parsedKey = mapper.readValue(serialized, PublicKey::class.java)
assertEquals(publicKey, parsedKey)
}
@Test @Test
fun readAmount() { fun readAmount() {
val oldJson = """ val oldJson = """

View File

@ -1,6 +1,6 @@
{ {
"fixedLeg": { "fixedLeg": {
"fixedRatePayer": "8Kqd4oWdx4KQGHGR7xcgpFf9JmP6HiXqTf85NpSgdSu431EGEhujA6ePaFD", "fixedRatePayer": "MCowBQYDK2VwAyEAzswVB9wd3XKVlRwpCIjwla25BE0bc9aW5t8GXWg71Pw=",
"notional": "$25000000", "notional": "$25000000",
"paymentFrequency": "SemiAnnual", "paymentFrequency": "SemiAnnual",
"effectiveDate": "2016-03-11", "effectiveDate": "2016-03-11",
@ -22,7 +22,7 @@
"interestPeriodAdjustment": "Adjusted" "interestPeriodAdjustment": "Adjusted"
}, },
"floatingLeg": { "floatingLeg": {
"floatingRatePayer": "8Kqd4oWdx4KQGHGJSFTX4kdZukmHohBRN3gvPekticL4eHTdmbJTVZNZJUj", "floatingRatePayer": "MCowBQYDK2VwAyEAa3nFfmoJUjkoLASBjpYRLz8DpAAbqXpWTCOFKj8epfw=",
"notional": { "notional": {
"quantity": 2500000000, "quantity": 2500000000,
"token": "USD" "token": "USD"
@ -71,7 +71,7 @@
"notificationTime": "2:00pm London", "notificationTime": "2:00pm London",
"resolutionTime": "2:00pm London time on the first LocalBusiness Day following the date on which the notice is given ", "resolutionTime": "2:00pm London time on the first LocalBusiness Day following the date on which the notice is given ",
"interestRate": { "interestRate": {
"oracle": "Rates Service Provider", "oracle": "C=ES,L=Madrid,O=Rates Service Provider",
"tenor": { "tenor": {
"name": "6M" "name": "6M"
}, },