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.WireTransaction
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.parsePublicKeyBase58
import net.corda.core.utilities.toBase58String
import net.corda.core.utilities.base58ToByteArray
import net.corda.core.utilities.base64ToByteArray
import net.corda.core.utilities.toBase64
import net.i2p.crypto.eddsa.EdDSAPublicKey
import java.math.BigDecimal
import java.security.PublicKey
@ -83,13 +84,9 @@ object JacksonSupport {
addDeserializer(SecureHash::class.java, SecureHashDeserializer())
addDeserializer(SecureHash.SHA256::class.java, SecureHashDeserializer())
// For ed25519 pubkeys
addSerializer(EdDSAPublicKey::class.java, PublicKeySerializer)
addDeserializer(EdDSAPublicKey::class.java, PublicKeyDeserializer)
// For composite keys
addSerializer(CompositeKey::class.java, CompositeKeySerializer)
addDeserializer(CompositeKey::class.java, CompositeKeyDeserializer)
// Public key types
addSerializer(PublicKey::class.java, PublicKeySerializer)
addDeserializer(PublicKey::class.java, PublicKeyDeserializer)
// For NodeInfo
// 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>() {
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()
}
// TODO this needs to use some industry identifier(s) instead of these keys
val key = parsePublicKeyBase58(parser.text)
val key = PublicKeyDeserializer.deserialize(parser, context)
return AnonymousParty(key)
}
}
@ -187,19 +183,24 @@ object JacksonSupport {
}
val mapper = parser.codec as PartyObjectMapper
// TODO: We should probably have a better specified way of identifying X.500 names vs keys
// Base58 keys never include an equals character, while X.500 names always will, so we use that to determine
// how to parse the content
return if (parser.text.contains("=")) {
// The comma character is invalid in base64, and required as a separator for X.500 names. As Corda
// X.500 names all involve at least three attributes (organisation, locality, country), they must
// include a comma. As such we can use it as a distinguisher between the two types.
return if (parser.text.contains(",")) {
val principal = CordaX500Name.parse(parser.text)
mapper.wellKnownPartyFromX500Name(principal) ?: throw JsonParseException(parser, "Could not find a Party with name $principal")
} else {
val nameMatches = mapper.partiesFromName(parser.text)
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 {
parsePublicKeyBase58(parser.text)
Crypto.decodePublicKey(derBytes)
} 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()}")
} else if (nameMatches.size == 1) {
@ -273,39 +274,23 @@ object JacksonSupport {
}
}
object PublicKeySerializer : JsonSerializer<EdDSAPublicKey>() {
override fun serialize(obj: EdDSAPublicKey, generator: JsonGenerator, provider: SerializerProvider) {
check(obj.params == Crypto.EDDSA_ED25519_SHA512.algSpec)
generator.writeString(obj.toBase58String())
object PublicKeySerializer : JsonSerializer<PublicKey>() {
override fun serialize(obj: PublicKey, generator: JsonGenerator, provider: SerializerProvider) {
generator.writeString(obj.encoded.toBase64())
}
}
object PublicKeyDeserializer : JsonDeserializer<EdDSAPublicKey>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): EdDSAPublicKey {
object PublicKeyDeserializer : JsonDeserializer<PublicKey>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): PublicKey {
return try {
parsePublicKeyBase58(parser.text) as EdDSAPublicKey
val derBytes = parser.text.base64ToByteArray()
Crypto.decodePublicKey(derBytes)
} catch (e: Exception) {
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<*>>() {
override fun serialize(value: Amount<*>, gen: JsonGenerator, serializers: SerializerProvider) {
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.TestDependencyInjectionBase
import net.corda.testing.contracts.DummyContract
import net.i2p.crypto.eddsa.EdDSAPublicKey
import org.junit.Before
import org.junit.Test
import java.math.BigInteger
import java.security.PublicKey
import java.util.*
import kotlin.reflect.jvm.jvmName
import kotlin.test.assertEquals
class JacksonSupportTest : TestDependencyInjectionBase() {
companion object {
private val SEED = BigInteger.valueOf(20170922L)
val mapper = JacksonSupport.createNonRpcMapper()
}
@ -37,15 +38,34 @@ class JacksonSupportTest : TestDependencyInjectionBase() {
}
@Test
fun publicKeySerializingWorks() {
val publicKey = generateKeyPair().public
fun `should serialize Composite keys`() {
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 parsedKey = mapper.readValue(serialized, EdDSAPublicKey::class.java)
assertEquals(expected, serialized)
val parsedKey = mapper.readValue(serialized, PublicKey::class.java)
assertEquals(publicKey, parsedKey)
}
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
fun readAmount() {
val oldJson = """

View File

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