From b031e66ab98d7296d1b7771f6856958fc2d2556d Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Tue, 15 May 2018 17:02:43 +0100 Subject: [PATCH] CORDA-1238: Updated JacksonSupport to support SerializedBytes, CertPath, X509Certificate and the signature classes (#3145) SerializedBytes are first converted to the object it represents before being serialised as a pojo. These changes will be needed to support the the blob inspector when it will output to YAML/JSON. --- .idea/compiler.xml | 117 +++++ .../corda/client/jackson/JacksonSupport.kt | 400 +++++++++++------- .../client/jackson/internal/JacksonUtils.kt | 5 + .../client/jackson/JacksonSupportTest.kt | 258 ++++++++--- .../jackson/class-not-on-classpath-data | Bin 0 -> 742 bytes docs/source/changelog.rst | 5 +- docs/source/shell.rst | 4 +- 7 files changed, 582 insertions(+), 207 deletions(-) create mode 100644 client/jackson/src/test/resources/net/corda/client/jackson/class-not-on-classpath-data diff --git a/.idea/compiler.xml b/.idea/compiler.xml index e409e551c9..6891e6e828 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -4,28 +4,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -33,15 +85,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -51,11 +156,23 @@ + + + + + + + + + + + + diff --git a/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt index 23dd98f274..52fb4e141d 100644 --- a/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt @@ -1,21 +1,20 @@ package net.corda.client.jackson -import com.fasterxml.jackson.annotation.JsonIgnore -import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.* import com.fasterxml.jackson.core.* import com.fasterxml.jackson.databind.* +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize import com.fasterxml.jackson.databind.deser.std.NumberDeserializers -import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.node.ObjectNode -import com.fasterxml.jackson.databind.ser.std.StdSerializer import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.module.kotlin.KotlinModule import com.fasterxml.jackson.module.kotlin.convertValue -import net.corda.client.jackson.internal.addSerAndDeser import net.corda.client.jackson.internal.jsonObject import net.corda.client.jackson.internal.readValueAs import net.corda.core.CordaInternal +import net.corda.core.CordaOID import net.corda.core.DoNotImplement import net.corda.core.contracts.Amount import net.corda.core.contracts.ContractState @@ -23,24 +22,30 @@ import net.corda.core.contracts.StateRef import net.corda.core.crypto.* import net.corda.core.crypto.TransactionSignature import net.corda.core.identity.* +import net.corda.core.internal.CertRole +import net.corda.core.internal.DigitalSignatureWithCert import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.CordaRPCOps import net.corda.core.node.NodeInfo import net.corda.core.node.services.IdentityService import net.corda.core.serialization.SerializedBytes +import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.transactions.CoreTransaction import net.corda.core.transactions.NotaryChangeWireTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.NetworkHostAndPort -import net.corda.core.utilities.OpaqueBytes -import net.corda.core.utilities.parsePublicKeyBase58 -import net.corda.core.utilities.toBase58String +import net.corda.core.utilities.* +import org.bouncycastle.asn1.x509.KeyPurposeId +import java.lang.reflect.Modifier import java.math.BigDecimal import java.security.PublicKey +import java.security.cert.CertPath +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate import java.util.* +import javax.security.auth.x500.X500Principal /** * Utilities and serialisers for working with JSON representations of basic types. This adds Jackson support for @@ -90,25 +95,26 @@ object JacksonSupport { val cordaModule: Module by lazy { SimpleModule("core").apply { - addSerAndDeser(AnonymousPartySerializer, AnonymousPartyDeserializer) - addSerAndDeser(PartySerializer, PartyDeserializer) - addDeserializer(AbstractParty::class.java, PartyDeserializer) - addSerAndDeser(toStringSerializer, NumberDeserializers.BigDecimalDeserializer()) - addSerAndDeser(toStringSerializer, SecureHashDeserializer()) - addSerAndDeser(toStringSerializer, AmountDeserializer) - addSerAndDeser(OpaqueBytesSerializer, OpaqueBytesDeserializer) - addSerAndDeser(toStringSerializer, CordaX500NameDeserializer) - addSerAndDeser(PublicKeySerializer, PublicKeyDeserializer) - addDeserializer(CompositeKey::class.java, CompositeKeyDeseriaizer) - addSerAndDeser(toStringSerializer, NetworkHostAndPortDeserializer) - // TODO Add deserialization which follows the same lookup logic as Party - addSerializer(PartyAndCertificate::class.java, PartyAndCertificateSerializer) - addDeserializer(NodeInfo::class.java, NodeInfoDeserializer) - - listOf(TransactionSignatureSerde, SignedTransactionSerde).forEach { serde -> serde.applyTo(this) } - - // Using mixins to fine-tune the default serialised output + setMixInAnnotation(BigDecimal::class.java, BigDecimalMixin::class.java) + setMixInAnnotation(X500Principal::class.java, X500PrincipalMixin::class.java) + setMixInAnnotation(X509Certificate::class.java, X509CertificateMixin::class.java) + setMixInAnnotation(PartyAndCertificate::class.java, PartyAndCertificateSerializerMixin::class.java) + setMixInAnnotation(NetworkHostAndPort::class.java, NetworkHostAndPortMixin::class.java) + setMixInAnnotation(CordaX500Name::class.java, CordaX500NameMixin::class.java) + setMixInAnnotation(Amount::class.java, AmountMixin::class.java) + setMixInAnnotation(AbstractParty::class.java, AbstractPartyMixin::class.java) + setMixInAnnotation(AnonymousParty::class.java, AnonymousPartyMixin::class.java) + setMixInAnnotation(Party::class.java, PartyMixin::class.java) + setMixInAnnotation(PublicKey::class.java, PublicKeyMixin::class.java) + setMixInAnnotation(ByteSequence::class.java, ByteSequenceMixin::class.java) + setMixInAnnotation(SecureHash.SHA256::class.java, SecureHashSHA256Mixin::class.java) + setMixInAnnotation(SerializedBytes::class.java, SerializedBytesMixin::class.java) + setMixInAnnotation(DigitalSignature.WithKey::class.java, ByteSequenceWithPropertiesMixin::class.java) + setMixInAnnotation(DigitalSignatureWithCert::class.java, ByteSequenceWithPropertiesMixin::class.java) + setMixInAnnotation(TransactionSignature::class.java, ByteSequenceWithPropertiesMixin::class.java) + setMixInAnnotation(SignedTransaction::class.java, SignedTransactionMixin2::class.java) setMixInAnnotation(WireTransaction::class.java, WireTransactionMixin::class.java) + setMixInAnnotation(CertPath::class.java, CertPathMixin::class.java) setMixInAnnotation(NodeInfo::class.java, NodeInfoMixin::class.java) } } @@ -171,7 +177,13 @@ object JacksonSupport { } } - private val toStringSerializer = com.fasterxml.jackson.databind.ser.std.ToStringSerializer.instance + @JacksonAnnotationsInside + @JsonSerialize(using = com.fasterxml.jackson.databind.ser.std.ToStringSerializer::class) + private annotation class ToStringSerialize + + @ToStringSerialize + @JsonDeserialize(using = NumberDeserializers.BigDecimalDeserializer::class) + private interface BigDecimalMixin private object DateSerializer : JsonSerializer() { override fun serialize(value: Date, gen: JsonGenerator, serializers: SerializerProvider) { @@ -179,20 +191,21 @@ object JacksonSupport { } } - private object NetworkHostAndPortDeserializer : JsonDeserializer() { + @ToStringSerialize + @JsonDeserialize(using = NetworkHostAndPortDeserializer::class) + private interface NetworkHostAndPortMixin + + private class NetworkHostAndPortDeserializer : JsonDeserializer() { override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): NetworkHostAndPort { return NetworkHostAndPort.parse(parser.text) } } - private object CompositeKeyDeseriaizer : JsonDeserializer() { - override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): CompositeKey { - val publicKey = parser.readValueAs() - return publicKey as? CompositeKey ?: throw JsonParseException(parser, "Not a CompositeKey: $publicKey") - } - } + @JsonSerialize(using = PartyAndCertificateSerializer::class) + // TODO Add deserialization which follows the same lookup logic as Party + private interface PartyAndCertificateSerializerMixin - private object PartyAndCertificateSerializer : JsonSerializer() { + private class PartyAndCertificateSerializer : JsonSerializer() { override fun serialize(value: PartyAndCertificate, gen: JsonGenerator, serializers: SerializerProvider) { gen.jsonObject { writeObjectField("name", value.name) @@ -202,100 +215,146 @@ object JacksonSupport { } } - @Suppress("unused") - private interface NodeInfoMixin { - @get:JsonIgnore val legalIdentities: Any // This is already covered by legalIdentitiesAndCerts + @JsonSerialize(using = SignedTransactionSerializer::class) + @JsonDeserialize(using = SignedTransactionDeserializer::class) + private interface SignedTransactionMixin2 + + private class SignedTransactionSerializer : JsonSerializer() { + override fun serialize(value: SignedTransaction, gen: JsonGenerator, serializers: SerializerProvider) { + gen.writeObject(SignedTransactionWrapper(value.txBits.bytes, value.sigs)) + } } - private interface JsonSerde { - val type: Class - val serializer: JsonSerializer - val deserializer: JsonDeserializer + private class SignedTransactionDeserializer : JsonDeserializer() { + override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): SignedTransaction { + val wrapper = parser.readValueAs() + return SignedTransaction(SerializedBytes(wrapper.txBits), wrapper.signatures) + } + } - fun applyTo(module: SimpleModule) { - with(module) { - addSerializer(type, serializer) - addDeserializer(type, deserializer) + private class SignedTransactionWrapper(val txBits: ByteArray, val signatures: List) + + @JsonSerialize(using = SerializedBytesSerializer::class) + @JsonDeserialize(using = SerializedBytesDeserializer::class) + private class SerializedBytesMixin + + private class SerializedBytesSerializer : JsonSerializer>() { + override fun serialize(value: SerializedBytes<*>, gen: JsonGenerator, serializers: SerializerProvider) { + val deserialized = value.deserialize() + gen.jsonObject { + writeStringField("class", deserialized.javaClass.name) + writeObjectField("deserialized", deserialized) } } } - private inline fun JsonNode.get(fieldName: String, condition: (JsonNode) -> Boolean, mapper: ObjectMapper, parser: JsonParser): RESULT { - if (get(fieldName)?.let(condition) != true) { - JsonParseException(parser, "Missing required object field \"$fieldName\".") - } - return mapper.treeToValue(get(fieldName), RESULT::class.java) - } - - private object TransactionSignatureSerde : JsonSerde { - override val type: Class = TransactionSignature::class.java - - override val serializer = object : StdSerializer(type) { - override fun serialize(value: TransactionSignature, gen: JsonGenerator, serializers: SerializerProvider) { - gen.jsonObject { - writeObjectField("by", value.by) - writeObjectField("signatureMetadata", value.signatureMetadata) - writeObjectField("bytes", value.bytes) - writeObjectField("partialMerkleTree", value.partialMerkleTree) - } - } - } - - override val deserializer = object : StdDeserializer(type) { - override fun deserialize(parser: JsonParser, context: DeserializationContext): TransactionSignature { + private class SerializedBytesDeserializer : JsonDeserializer>() { + override fun deserialize(parser: JsonParser, context: DeserializationContext): SerializedBytes { + return if (parser.currentToken == JsonToken.START_OBJECT) { val mapper = parser.codec as ObjectMapper - val json = mapper.readTree(parser) - val by = mapper.convertValue(json["by"]) - val signatureMetadata = json.get("signatureMetadata", JsonNode::isObject, mapper, parser) - val bytes = json.get("bytes", JsonNode::isObject, mapper, parser) - val partialMerkleTree = json.get("partialMerkleTree", JsonNode::isObject, mapper, parser) - - return TransactionSignature(bytes, by, signatureMetadata, partialMerkleTree) + val json = parser.readValueAsTree() + val clazz = context.findClass(json["class"].textValue()) + val pojo = mapper.convertValue(json["deserialized"], clazz) + pojo.serialize() + } else { + SerializedBytes(parser.binaryValue) } } } - private object SignedTransactionSerde : JsonSerde { - override val type: Class = SignedTransaction::class.java + @ToStringSerialize + private interface X500PrincipalMixin - override val serializer = object : StdSerializer(type) { - override fun serialize(value: SignedTransaction, gen: JsonGenerator, serializers: SerializerProvider) { - gen.jsonObject { - writeObjectField("txBits", value.txBits.bytes) - writeObjectField("signatures", value.sigs) + @JsonSerialize(using = X509CertificateSerializer::class) + @JsonDeserialize(using = X509CertificateDeserializer::class) + private interface X509CertificateMixin + + private object X509CertificateSerializer : JsonSerializer() { + val keyUsages = arrayOf( + "digitalSignature", + "nonRepudiation", + "keyEncipherment", + "dataEncipherment", + "keyAgreement", + "keyCertSign", + "cRLSign", + "encipherOnly", + "decipherOnly" + ) + + val keyPurposeIds = KeyPurposeId::class.java + .fields + .filter { Modifier.isStatic(it.modifiers) && it.type == KeyPurposeId::class.java } + .associateBy({ (it.get(null) as KeyPurposeId).id }, { it.name }) + + val knownExtensions = setOf("2.5.29.15", "2.5.29.37", "2.5.29.19", "2.5.29.17", "2.5.29.18", CordaOID.X509_EXTENSION_CORDA_ROLE) + + override fun serialize(value: X509Certificate, gen: JsonGenerator, serializers: SerializerProvider) { + gen.jsonObject { + writeNumberField("version", value.version) + writeObjectField("serialNumber", value.serialNumber) + writeObjectField("subject", value.subjectX500Principal) + writeObjectField("publicKey", value.publicKey) + writeObjectField("issuer", value.issuerX500Principal) + writeObjectField("notBefore", value.notBefore) + writeObjectField("notAfter", value.notAfter) + writeObjectField("issuerUniqueID", value.issuerUniqueID) + writeObjectField("subjectUniqueID", value.subjectUniqueID) + writeObjectField("keyUsage", value.keyUsage?.asList()?.mapIndexedNotNull { i, flag -> if (flag) keyUsages[i] else null }) + writeObjectField("extendedKeyUsage", value.extendedKeyUsage.map { keyPurposeIds.getOrDefault(it, it) }) + jsonObject("basicConstraints") { + writeBooleanField("isCA", value.basicConstraints != -1) + writeObjectField("pathLength", value.basicConstraints.let { if (it != Int.MAX_VALUE) it else null }) } + writeObjectField("subjectAlternativeNames", value.subjectAlternativeNames) + writeObjectField("issuerAlternativeNames", value.issuerAlternativeNames) + writeObjectField("cordaCertRole", CertRole.extract(value)) + writeObjectField("otherCriticalExtensions", value.criticalExtensionOIDs - knownExtensions) + writeObjectField("otherNonCriticalExtensions", value.nonCriticalExtensionOIDs - knownExtensions) + writeBinaryField("encoded", value.encoded) } } - - override val deserializer = object : StdDeserializer(type) { - override fun deserialize(parser: JsonParser, context: DeserializationContext): SignedTransaction { - val mapper = parser.codec as ObjectMapper - val json = mapper.readTree(parser) - - val txBits = json.get("txBits", JsonNode::isTextual, mapper, parser) - val signatures = json.get("signatures", JsonNode::isArray, mapper, parser) - - return SignedTransaction(SerializedBytes(txBits), signatures) - } - } - - private class TransactionSignatures : ArrayList() } - - - // - // The following should not have been made public and are thus deprecated with warnings. - // - - @Deprecated("No longer used as jackson already has a toString serializer", - replaceWith = ReplaceWith("com.fasterxml.jackson.databind.ser.std.ToStringSerializer.instance")) - object ToStringSerializer : JsonSerializer() { - override fun serialize(obj: Any, generator: JsonGenerator, provider: SerializerProvider) { - generator.writeString(obj.toString()) + private class X509CertificateDeserializer : JsonDeserializer() { + private val certFactory = CertificateFactory.getInstance("X.509") + override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): X509Certificate { + val encoded = parser.readValueAsTree()["encoded"] + return certFactory.generateCertificate(encoded.binaryValue().inputStream()) as X509Certificate } } + @JsonSerialize(using = CertPathSerializer::class) + @JsonDeserialize(using = CertPathDeserializer::class) + private interface CertPathMixin + + private class CertPathSerializer : JsonSerializer() { + override fun serialize(value: CertPath, gen: JsonGenerator, serializers: SerializerProvider) { + gen.writeObject(CertPathWrapper(value.type, uncheckedCast(value.certificates))) + } + } + + private class CertPathDeserializer : JsonDeserializer() { + private val certFactory = CertificateFactory.getInstance("X.509") + override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): CertPath { + val wrapper = parser.readValueAs() + return certFactory.generateCertPath(wrapper.certificates) + } + } + + private data class CertPathWrapper(val type: String, val certificates: List) { + init { + require(type == "X.509") { "Only X.509 cert paths are supported" } + } + } + + @JsonDeserialize(using = PartyDeserializer::class) + private interface AbstractPartyMixin + + @JsonSerialize(using = AnonymousPartySerializer::class) + @JsonDeserialize(using = AnonymousPartyDeserializer::class) + private interface AnonymousPartyMixin + @Deprecated("This is an internal class, do not use") object AnonymousPartySerializer : JsonSerializer() { override fun serialize(value: AnonymousParty, generator: JsonGenerator, provider: SerializerProvider) { @@ -310,6 +369,9 @@ object JacksonSupport { } } + @JsonSerialize(using = PartySerializer::class) + private interface PartyMixin + @Deprecated("This is an internal class, do not use") object PartySerializer : JsonSerializer() { override fun serialize(value: Party, generator: JsonGenerator, provider: SerializerProvider) { @@ -344,13 +406,9 @@ object JacksonSupport { } } - @Deprecated("This is an internal class, do not use") - // This is no longer used - object CordaX500NameSerializer : JsonSerializer() { - override fun serialize(obj: CordaX500Name, generator: JsonGenerator, provider: SerializerProvider) { - generator.writeString(obj.toString()) - } - } + @ToStringSerialize + @JsonDeserialize(using = CordaX500NameDeserializer::class) + private interface CordaX500NameMixin @Deprecated("This is an internal class, do not use") object CordaX500NameDeserializer : JsonDeserializer() { @@ -363,13 +421,9 @@ object JacksonSupport { } } - @Deprecated("This is an internal class, do not use") - // This is no longer used - object NodeInfoSerializer : JsonSerializer() { - override fun serialize(value: NodeInfo, gen: JsonGenerator, serializers: SerializerProvider) { - gen.writeString(Base58.encode(value.serialize().bytes)) - } - } + @JsonIgnoreProperties("legalIdentities") // This is already covered by legalIdentitiesAndCerts + @JsonDeserialize(using = NodeInfoDeserializer::class) + private interface NodeInfoMixin @Deprecated("This is an internal class, do not use") object NodeInfoDeserializer : JsonDeserializer() { @@ -380,17 +434,10 @@ object JacksonSupport { } } - @Deprecated("This is an internal class, do not use") - // This is no longer used - object SecureHashSerializer : JsonSerializer() { - override fun serialize(obj: SecureHash, generator: JsonGenerator, provider: SerializerProvider) { - generator.writeString(obj.toString()) - } - } + @ToStringSerialize + @JsonDeserialize(using = SecureHashDeserializer::class) + private interface SecureHashSHA256Mixin - /** - * Implemented as a class so that we can instantiate for T. - */ @Deprecated("This is an internal class, do not use") class SecureHashDeserializer : JsonDeserializer() { override fun deserialize(parser: JsonParser, context: DeserializationContext): T { @@ -402,6 +449,10 @@ object JacksonSupport { } } + @JsonSerialize(using = PublicKeySerializer::class) + @JsonDeserialize(using = PublicKeyDeserializer::class) + private interface PublicKeyMixin + @Deprecated("This is an internal class, do not use") object PublicKeySerializer : JsonSerializer() { override fun serialize(value: PublicKey, generator: JsonGenerator, provider: SerializerProvider) { @@ -420,13 +471,9 @@ object JacksonSupport { } } - @Deprecated("This is an internal class, do not use") - // This is no longer used - object AmountSerializer : JsonSerializer>() { - override fun serialize(value: Amount<*>, gen: JsonGenerator, serializers: SerializerProvider) { - gen.writeString(value.toString()) - } - } + @ToStringSerialize + @JsonDeserialize(using = AmountDeserializer::class) + private interface AmountMixin @Deprecated("This is an internal class, do not use") object AmountDeserializer : JsonDeserializer>() { @@ -434,20 +481,30 @@ object JacksonSupport { return if (parser.currentToken == JsonToken.VALUE_STRING) { Amount.parseCurrency(parser.text) } else { - try { - val tree = parser.readValueAsTree() - val quantity = tree["quantity"].apply { require(canConvertToLong()) } - val token = tree["token"] - // Attempt parsing as a currency token. TODO: This needs thought about how to extend to other token types. - val currency = (parser.codec as ObjectMapper).convertValue(token) - Amount(quantity.longValue(), currency) - } catch (e: Exception) { - throw JsonParseException(parser, "Invalid amount", e) - } + val wrapper = parser.readValueAs() + Amount(wrapper.quantity, wrapper.token) } } } + private data class CurrencyAmountWrapper(val quantity: Long, val token: Currency) + + @JsonDeserialize(using = OpaqueBytesDeserializer::class) + private interface ByteSequenceMixin { + @Suppress("unused") + @JsonValue + fun copyBytes(): ByteArray + } + + @JsonIgnoreProperties("offset", "size") + @JsonSerialize + @JsonDeserialize + private interface ByteSequenceWithPropertiesMixin { + @Suppress("unused") + @JsonValue(false) + fun copyBytes(): ByteArray + } + @Deprecated("This is an internal class, do not use") object OpaqueBytesDeserializer : JsonDeserializer() { override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): OpaqueBytes { @@ -455,6 +512,47 @@ object JacksonSupport { } } + + // + // Everything below this point is no longer used but can't be deleted as they leaked into the public API + // + + @Deprecated("No longer used as jackson already has a toString serializer", + replaceWith = ReplaceWith("com.fasterxml.jackson.databind.ser.std.ToStringSerializer.instance")) + object ToStringSerializer : JsonSerializer() { + override fun serialize(obj: Any, generator: JsonGenerator, provider: SerializerProvider) { + generator.writeString(obj.toString()) + } + } + + @Deprecated("This is an internal class, do not use") + object CordaX500NameSerializer : JsonSerializer() { + override fun serialize(obj: CordaX500Name, generator: JsonGenerator, provider: SerializerProvider) { + generator.writeString(obj.toString()) + } + } + + @Deprecated("This is an internal class, do not use") + object NodeInfoSerializer : JsonSerializer() { + override fun serialize(value: NodeInfo, gen: JsonGenerator, serializers: SerializerProvider) { + gen.writeString(Base58.encode(value.serialize().bytes)) + } + } + + @Deprecated("This is an internal class, do not use") + object SecureHashSerializer : JsonSerializer() { + override fun serialize(obj: SecureHash, generator: JsonGenerator, provider: SerializerProvider) { + generator.writeString(obj.toString()) + } + } + + @Deprecated("This is an internal class, do not use") + object AmountSerializer : JsonSerializer>() { + override fun serialize(value: Amount<*>, gen: JsonGenerator, serializers: SerializerProvider) { + gen.writeString(value.toString()) + } + } + @Deprecated("This is an internal class, do not use") object OpaqueBytesSerializer : JsonSerializer() { override fun serialize(value: OpaqueBytes, gen: JsonGenerator, serializers: SerializerProvider) { @@ -467,7 +565,7 @@ object JacksonSupport { abstract class SignedTransactionMixin { @JsonIgnore abstract fun getTxBits(): SerializedBytes @JsonProperty("signatures") protected abstract fun getSigs(): List - @JsonProperty protected abstract fun getTransaction(): CoreTransaction // TODO It seems this should be coreTransaction + @JsonProperty protected abstract fun getTransaction(): CoreTransaction @JsonIgnore abstract fun getTx(): WireTransaction @JsonIgnore abstract fun getNotaryChangeTx(): NotaryChangeWireTransaction @JsonIgnore abstract fun getInputs(): List diff --git a/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/JacksonUtils.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/JacksonUtils.kt index 04284557e9..a1d718193a 100644 --- a/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/JacksonUtils.kt +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/JacksonUtils.kt @@ -3,8 +3,11 @@ package net.corda.client.jackson.internal import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.JsonSerializer +import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.module.SimpleModule +import com.fasterxml.jackson.module.kotlin.convertValue inline fun SimpleModule.addSerAndDeser(serializer: JsonSerializer, deserializer: JsonDeserializer) { addSerializer(T::class.java, serializer) @@ -19,3 +22,5 @@ inline fun JsonGenerator.jsonObject(fieldName: String? = null, gen: JsonGenerato } inline fun JsonParser.readValueAs(): T = readValueAs(T::class.java) + +inline fun JsonNode.valueAs(mapper: ObjectMapper): T = mapper.convertValue(this) diff --git a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt index 196e610164..7c1eda8cce 100644 --- a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt +++ b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt @@ -1,13 +1,15 @@ package net.corda.client.jackson -import com.fasterxml.jackson.databind.SerializationFeature -import com.fasterxml.jackson.databind.node.ArrayNode +import com.fasterxml.jackson.core.JsonFactory +import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.node.BinaryNode import com.fasterxml.jackson.databind.node.ObjectNode import com.fasterxml.jackson.databind.node.TextNode +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory import com.fasterxml.jackson.module.kotlin.convertValue import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.whenever +import net.corda.client.jackson.internal.valueAs import net.corda.core.contracts.Amount import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.* @@ -16,14 +18,16 @@ import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party +import net.corda.core.internal.DigitalSignatureWithCert import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub +import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.SerializedBytes +import net.corda.core.serialization.serialize import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.NetworkHostAndPort -import net.corda.core.utilities.OpaqueBytes -import net.corda.core.utilities.toBase58String -import net.corda.core.utilities.toBase64 +import net.corda.core.utilities.* import net.corda.finance.USD +import net.corda.nodeapi.internal.crypto.x509Certificates import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.contracts.DummyContract import net.corda.testing.core.* @@ -34,19 +38,29 @@ import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Before import org.junit.Rule import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameters import java.math.BigInteger import java.security.PublicKey +import java.security.cert.CertPath +import java.security.cert.X509Certificate import java.util.* +import javax.security.auth.x500.X500Principal import kotlin.collections.ArrayList -import kotlin.test.assertEquals -class JacksonSupportTest { +@RunWith(Parameterized::class) +class JacksonSupportTest(@Suppress("unused") private val name: String, factory: JsonFactory) { private companion object { val SEED: BigInteger = BigInteger.valueOf(20170922L) val ALICE_PUBKEY = TestIdentity(ALICE_NAME, 70).publicKey val BOB_PUBKEY = TestIdentity(BOB_NAME, 70).publicKey val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party val MINI_CORP = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")) + + @Parameters(name = "{0}") + @JvmStatic + fun factories() = arrayOf(arrayOf("JSON", JsonFactory()), arrayOf("YAML", YAMLFactory())) } @Rule @@ -54,7 +68,7 @@ class JacksonSupportTest { val testSerialization = SerializationEnvironmentRule() private val partyObjectMapper = TestPartyObjectMapper() - private val mapper = JacksonSupport.createPartyObjectMapper(partyObjectMapper) + private val mapper = JacksonSupport.createPartyObjectMapper(partyObjectMapper, factory) private lateinit var services: ServiceHub private lateinit var cordappProvider: CordappProvider @@ -66,44 +80,29 @@ class JacksonSupportTest { doReturn(cordappProvider).whenever(services).cordappProvider } - private class Dummy(val notional: Amount) - @Test - fun `read Amount`() { - val oldJson = """ - { - "notional": { - "quantity": 2500000000, - "token": "USD" - } - } - """ - val newJson = """ { "notional" : "$25000000" } """ - - assertEquals(Amount(2500000000L, USD), mapper.readValue(newJson, Dummy::class.java).notional) - assertEquals(Amount(2500000000L, USD), mapper.readValue(oldJson, Dummy::class.java).notional) + fun `Amount(Currency) serialization`() { + assertThat(mapper.valueToTree(Amount.parseCurrency("£25000000")).textValue()).isEqualTo("25000000.00 GBP") + assertThat(mapper.valueToTree(Amount.parseCurrency("$250000")).textValue()).isEqualTo("250000.00 USD") } @Test - fun `write Amount`() { - val writer = mapper.writer().without(SerializationFeature.INDENT_OUTPUT) - assertEquals("""{"notional":"25000000.00 GBP"}""", writer.writeValueAsString(Dummy(Amount.parseCurrency("£25000000")))) - assertEquals("""{"notional":"250000.00 USD"}""", writer.writeValueAsString(Dummy(Amount.parseCurrency("$250000")))) + fun `Amount(Currency) deserialization`() { + val old = mapOf( + "quantity" to 2500000000, + "token" to "USD" + ) + assertThat(mapper.convertValue>(old)).isEqualTo(Amount(2_500_000_000, USD)) + assertThat(mapper.convertValue>(TextNode("$25000000"))).isEqualTo(Amount(2_500_000_000, USD)) } @Test - fun SignedTransaction() { - val attachmentRef = SecureHash.randomSHA256() - doReturn(attachmentRef).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID) - doReturn(testNetworkParameters()).whenever(services).networkParameters - - val writer = mapper.writer() - val stx = makeDummyStx() - val json = writer.writeValueAsString(stx) - - val deserializedTransaction = mapper.readValue(json, SignedTransaction::class.java) - - assertThat(deserializedTransaction).isEqualTo(stx) + fun ByteSequence() { + val byteSequence: ByteSequence = OpaqueBytes.of(1, 2, 3, 4).subSequence(0, 2) + val json = mapper.valueToTree(byteSequence) + assertThat(json.binaryValue()).containsExactly(1, 2) + assertThat(json.asText()).isEqualTo(byteArrayOf(1, 2).toBase64()) + assertThat(mapper.convertValue(json)).isEqualTo(byteSequence) } @Test @@ -115,6 +114,105 @@ class JacksonSupportTest { assertThat(mapper.convertValue(json)).isEqualTo(opaqueBytes) } + @Test + fun SerializedBytes() { + val data = TestData(BOB_NAME, "Summary", SubTestData(1234)) + val serializedBytes = data.serialize() + val json = mapper.valueToTree(serializedBytes) + println(mapper.writeValueAsString(json)) + assertThat(json["class"].textValue()).isEqualTo(TestData::class.java.name) + assertThat(json["deserialized"].valueAs(mapper)).isEqualTo(data) + // Check that the entire JSON object can be converted back to the same SerializedBytes + assertThat(mapper.convertValue>(json)).isEqualTo(serializedBytes) + assertThat(mapper.convertValue>(BinaryNode(serializedBytes.bytes))).isEqualTo(serializedBytes) + } + + // This is the class that was used to serialise the message for the test below. It's commented out so that it's no + // longer on the classpath. +// @CordaSerializable +// data class ClassNotOnClasspath(val name: CordaX500Name, val value: Int) + + @Test + fun `SerializedBytes of class not on classpath`() { + // The contents of the file were written out as follows: +// ClassNotOnClasspath(BOB_NAME, 54321).serialize().open().copyTo("build" / "class-not-on-classpath-data") + + val serializedBytes = SerializedBytes(javaClass.getResource("class-not-on-classpath-data").readBytes()) + val json = mapper.valueToTree(serializedBytes) + println(mapper.writeValueAsString(json)) + assertThat(json["class"].textValue()).isEqualTo("net.corda.client.jackson.JacksonSupportTest\$ClassNotOnClasspath") + assertThat(json["deserialized"].valueAs>(mapper)).isEqualTo(mapOf( + "name" to BOB_NAME.toString(), + "value" to 54321 + )) + assertThat(mapper.convertValue>(BinaryNode(serializedBytes.bytes))).isEqualTo(serializedBytes) + } + + @Test + fun DigitalSignature() { + val digitalSignature = DigitalSignature(secureRandomBytes(128)) + val json = mapper.valueToTree(digitalSignature) + assertThat(json.binaryValue()).isEqualTo(digitalSignature.bytes) + assertThat(json.asText()).isEqualTo(digitalSignature.bytes.toBase64()) + assertThat(mapper.convertValue(json)).isEqualTo(digitalSignature) + } + + @Test + fun `DigitalSignature WithKey`() { + val digitalSignature = DigitalSignature.WithKey(BOB_PUBKEY, secureRandomBytes(128)) + val json = mapper.valueToTree(digitalSignature) + val (by, bytes) = json.assertHasOnlyFields("by", "bytes") + assertThat(by.valueAs(mapper)).isEqualTo(BOB_PUBKEY) + assertThat(bytes.binaryValue()).isEqualTo(digitalSignature.bytes) + assertThat(mapper.convertValue(json)).isEqualTo(digitalSignature) + } + + @Test + fun DigitalSignatureWithCert() { + val digitalSignature = DigitalSignatureWithCert(MINI_CORP.identity.certificate, secureRandomBytes(128)) + val json = mapper.valueToTree(digitalSignature) + val (by, bytes) = json.assertHasOnlyFields("by", "bytes") + assertThat(by.valueAs(mapper)).isEqualTo(MINI_CORP.identity.certificate) + assertThat(bytes.binaryValue()).isEqualTo(digitalSignature.bytes) + assertThat(mapper.convertValue(json)).isEqualTo(digitalSignature) + } + + @Test + fun TransactionSignature() { + val metadata = SignatureMetadata(1, 1) + val transactionSignature = TransactionSignature(secureRandomBytes(128), BOB_PUBKEY, metadata) + val json = mapper.valueToTree(transactionSignature) + val (bytes, by, signatureMetadata, partialMerkleTree) = json.assertHasOnlyFields( + "bytes", + "by", + "signatureMetadata", + "partialMerkleTree" + ) + assertThat(bytes.binaryValue()).isEqualTo(transactionSignature.bytes) + assertThat(by.valueAs(mapper)).isEqualTo(BOB_PUBKEY) + assertThat(signatureMetadata.valueAs(mapper)).isEqualTo(metadata) + assertThat(partialMerkleTree.isNull).isTrue() + assertThat(mapper.convertValue(json)).isEqualTo(transactionSignature) + } + + // TODO Add test for PartialMerkleTree + + @Test + fun SignedTransaction() { + val attachmentRef = SecureHash.randomSHA256() + doReturn(attachmentRef).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID) + doReturn(testNetworkParameters()).whenever(services).networkParameters + + val stx = makeDummyStx() + val json = mapper.valueToTree(stx) + println(mapper.writeValueAsString(json)) + val (txBits, signatures) = json.assertHasOnlyFields("txBits", "signatures") + assertThat(txBits.binaryValue()).isEqualTo(stx.txBits.bytes) + val sigs = signatures.elements().asSequence().map { it.valueAs(mapper) }.toList() + assertThat(sigs).isEqualTo(stx.sigs) + assertThat(mapper.convertValue(json)).isEqualTo(stx) + } + @Test fun CordaX500Name() { testToStringSerialisation(CordaX500Name(commonName = "COMMON", organisationUnit = "ORG UNIT", organisation = "ORG", locality = "NYC", state = "NY", country = "US")) @@ -211,31 +309,40 @@ class JacksonSupportTest { @Test fun AnonymousParty() { - val anon = AnonymousParty(ALICE_PUBKEY) - val json = mapper.valueToTree(anon) + val anonymousParty = AnonymousParty(ALICE_PUBKEY) + val json = mapper.valueToTree(anonymousParty) assertThat(json.textValue()).isEqualTo(ALICE_PUBKEY.toBase58String()) - assertThat(mapper.convertValue(json)).isEqualTo(anon) + assertThat(mapper.convertValue(json)).isEqualTo(anonymousParty) } @Test fun `PartyAndCertificate serialisation`() { val json = mapper.valueToTree(MINI_CORP.identity) - assertThat(json.fieldNames()).containsOnly("name", "owningKey") - assertThat(mapper.convertValue(json["name"])).isEqualTo(MINI_CORP.name) - assertThat(mapper.convertValue(json["owningKey"])).isEqualTo(MINI_CORP.publicKey) + val (name, owningKey) = json.assertHasOnlyFields("name", "owningKey") + assertThat(name.valueAs(mapper)).isEqualTo(MINI_CORP.name) + assertThat(owningKey.valueAs(mapper)).isEqualTo(MINI_CORP.publicKey) } @Test fun `NodeInfo serialisation`() { val (nodeInfo) = createNodeInfoAndSigned(ALICE_NAME) val json = mapper.valueToTree(nodeInfo) - assertThat(json.fieldNames()).containsOnly("addresses", "legalIdentitiesAndCerts", "platformVersion", "serial") - val address = (json["addresses"] as ArrayNode).also { assertThat(it).hasSize(1) }[0] - assertThat(mapper.convertValue(address)).isEqualTo(nodeInfo.addresses[0]) - val identity = (json["legalIdentitiesAndCerts"] as ArrayNode).also { assertThat(it).hasSize(1) }[0] - assertThat(mapper.convertValue(identity["name"])).isEqualTo(ALICE_NAME) - assertThat(mapper.convertValue(json["platformVersion"])).isEqualTo(nodeInfo.platformVersion) - assertThat(mapper.convertValue(json["serial"])).isEqualTo(nodeInfo.serial) + val (addresses, legalIdentitiesAndCerts, platformVersion, serial) = json.assertHasOnlyFields( + "addresses", + "legalIdentitiesAndCerts", + "platformVersion", + "serial" + ) + addresses.run { + assertThat(this).hasSize(1) + assertThat(this[0].valueAs(mapper)).isEqualTo(nodeInfo.addresses[0]) + } + legalIdentitiesAndCerts.run { + assertThat(this).hasSize(1) + assertThat(this[0]["name"].valueAs(mapper)).isEqualTo(ALICE_NAME) + } + assertThat(platformVersion.intValue()).isEqualTo(nodeInfo.platformVersion) + assertThat(serial.longValue()).isEqualTo(nodeInfo.serial) } @Test @@ -264,6 +371,40 @@ class JacksonSupportTest { assertThat(convertToNodeInfo()).isEqualTo(nodeInfo) } + @Test + fun CertPath() { + val certPath = MINI_CORP.identity.certPath + val json = mapper.valueToTree(certPath) + println(mapper.writeValueAsString(json)) + val (type, certificates) = json.assertHasOnlyFields("type", "certificates") + assertThat(type.textValue()).isEqualTo(certPath.type) + certificates.run { + val serialNumbers = elements().asSequence().map { it["serialNumber"].bigIntegerValue() }.toList() + assertThat(serialNumbers).isEqualTo(certPath.x509Certificates.map { it.serialNumber }) + } + assertThat(mapper.convertValue(json).encoded).isEqualTo(certPath.encoded) + } + + @Test + fun X509Certificate() { + val cert: X509Certificate = MINI_CORP.identity.certificate + val json = mapper.valueToTree(cert) + println(mapper.writeValueAsString(json)) + assertThat(json["serialNumber"].bigIntegerValue()).isEqualTo(cert.serialNumber) + assertThat(json["issuer"].valueAs(mapper)).isEqualTo(cert.issuerX500Principal) + assertThat(json["subject"].valueAs(mapper)).isEqualTo(cert.subjectX500Principal) + assertThat(json["publicKey"].valueAs(mapper)).isEqualTo(cert.publicKey) + assertThat(json["notAfter"].valueAs(mapper)).isEqualTo(cert.notAfter) + assertThat(json["notBefore"].valueAs(mapper)).isEqualTo(cert.notBefore) + assertThat(json["encoded"].binaryValue()).isEqualTo(cert.encoded) + assertThat(mapper.convertValue(json).encoded).isEqualTo(cert.encoded) + } + + @Test + fun X500Principal() { + testToStringSerialisation(X500Principal("CN=Common,L=London,O=Org,C=UK")) + } + private fun makeDummyStx(): SignedTransaction { val wtx = DummyContract.generateInitial(1, DUMMY_NOTARY, MINI_CORP.ref(1)) .toWireTransaction(services) @@ -280,6 +421,17 @@ class JacksonSupportTest { assertThat(mapper.convertValue(json)).isEqualTo(value) } + private fun JsonNode.assertHasOnlyFields(vararg fieldNames: String): List { + assertThat(fieldNames()).containsOnly(*fieldNames) + return fieldNames.map { this[it] } + } + + @CordaSerializable + private data class TestData(val name: CordaX500Name, val summary: String, val subData: SubTestData) + + @CordaSerializable + private data class SubTestData(val value: Int) + private class TestPartyObjectMapper : JacksonSupport.PartyObjectMapper { val identities = ArrayList() val nodes = ArrayList() diff --git a/client/jackson/src/test/resources/net/corda/client/jackson/class-not-on-classpath-data b/client/jackson/src/test/resources/net/corda/client/jackson/class-not-on-classpath-data new file mode 100644 index 0000000000000000000000000000000000000000..dd639112c39b612596104d48f94cceadee1018a5 GIT binary patch literal 742 zcma)(PfNov7>CoYT~s`HP$uGGh zN3Oh z*k;{9p4X=Alzdl1#P0NF^j<31mS>5m2RDBbN>GGcAxLD{im6c7)a;(nvDrkcha0=v z&F9+!YwCKn7R^+GWysYB2EaXdZ X?t<7X#!jiNh>dy-u>|+|X9;`({z2hF literal 0 HcmV?d00001 diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 61a4ffdf91..331b6b5158 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -22,7 +22,10 @@ Unreleased * ``NodeInfo`` objects are serialised as an object and can be looked up using the same mechanism as ``Party`` * ``NetworkHostAndPort`` serialised according to its ``toString()`` * ``PartyAndCertificate`` is serialised as an object containing the name and owning key - * ``SignedTransaction`` can now be serialized to JSON and deserialized back into an object. + * ``SerializedBytes`` is serialised by converting the bytes into the object it represents, which is then serialised into + a JSON/YAML object + * ``CertPath`` and ``X509Certificate`` are serialised as objects and can be deserialised back + * ``SignedTransaction`` is serialised into its ``txBits`` and ``signatures`` and can be deserialised back * Several members of ``JacksonSupport`` have been deprecated to highlight that they are internal and not to be used. diff --git a/docs/source/shell.rst b/docs/source/shell.rst index da7bef0e8f..b03bf70601 100644 --- a/docs/source/shell.rst +++ b/docs/source/shell.rst @@ -267,8 +267,8 @@ SecureHash ~~~~~~~~~~ A parameter of type ``SecureHash`` can be written as a hexadecimal string: ``F69A7626ACC27042FEEAE187E6BFF4CE666E6F318DC2B32BE9FAF87DF687930C`` -OpaqueBytes -~~~~~~~~~~~ +OpaqueBytes and SerializedBytes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A parameter of type ``OpaqueBytes`` can be provided as a string in Base64. PublicKey and CompositeKey