ENT-9583 Public key caching of encoded form (OS) (#7332)

This commit is contained in:
Rick Parker 2023-04-26 17:49:52 +01:00 committed by GitHub
parent f4917e08e1
commit 9ba3919980
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 215 additions and 47 deletions

View File

@ -82,6 +82,7 @@ def patchCore = tasks.register('patchCore', Zip) {
exclude 'net/corda/core/serialization/internal/CheckpointSerializationFactory*.class'
exclude 'net/corda/core/internal/rules/*.class'
exclude 'net/corda/core/internal/utilities/PrivateInterner*.class'
exclude 'net/corda/core/crypto/internal/PublicKeyCache*.class'
exclude 'net/corda/core/internal/ContractStateClassCache*.class'
}

View File

@ -0,0 +1,21 @@
package net.corda.core.crypto.internal
import net.corda.core.utilities.ByteSequence
import java.security.PublicKey
@Suppress("unused")
object PublicKeyCache {
@Suppress("UNUSED_PARAMETER")
fun bytesForCachedPublicKey(key: PublicKey): ByteSequence? {
return null
}
@Suppress("UNUSED_PARAMETER")
fun publicKeyForCachedBytes(bytes: ByteSequence): PublicKey? {
return null
}
fun cachePublicKey(key: PublicKey): PublicKey {
return key
}
}

View File

@ -4,7 +4,14 @@ import net.corda.core.KeepForDJVM
import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.exactAdd
import net.corda.core.utilities.sequence
import org.bouncycastle.asn1.*
import org.bouncycastle.asn1.ASN1EncodableVector
import org.bouncycastle.asn1.ASN1Encoding
import org.bouncycastle.asn1.ASN1Integer
import org.bouncycastle.asn1.ASN1Object
import org.bouncycastle.asn1.ASN1Primitive
import org.bouncycastle.asn1.ASN1Sequence
import org.bouncycastle.asn1.DERBitString
import org.bouncycastle.asn1.DERSequence
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
import java.security.PublicKey
@ -162,7 +169,7 @@ class CompositeKey private constructor(val threshold: Int, children: List<NodeAn
override fun toASN1Primitive(): ASN1Primitive {
val vector = ASN1EncodableVector()
vector.add(DERBitString(node.encoded))
vector.add(DERBitString(Crypto.encodePublicKey(node)))
vector.add(ASN1Integer(weight.toLong()))
return DERSequence(vector)
}

View File

@ -5,6 +5,7 @@ import net.corda.core.DeleteForDJVM
import net.corda.core.KeepForDJVM
import net.corda.core.crypto.internal.AliasPrivateKey
import net.corda.core.crypto.internal.Instances.withSignature
import net.corda.core.crypto.internal.PublicKeyCache
import net.corda.core.crypto.internal.bouncyCastlePQCProvider
import net.corda.core.crypto.internal.cordaBouncyCastleProvider
import net.corda.core.crypto.internal.cordaSecurityProvider
@ -12,6 +13,7 @@ import net.corda.core.crypto.internal.`id-Curve25519ph`
import net.corda.core.crypto.internal.providerMap
import net.corda.core.internal.utilities.PrivateInterner
import net.corda.core.serialization.serialize
import net.corda.core.utilities.ByteSequence
import net.i2p.crypto.eddsa.EdDSAEngine
import net.i2p.crypto.eddsa.EdDSAPrivateKey
import net.i2p.crypto.eddsa.EdDSAPublicKey
@ -281,7 +283,7 @@ object Crypto {
*/
@JvmStatic
fun findSignatureScheme(key: PublicKey): SignatureScheme {
val keyInfo = SubjectPublicKeyInfo.getInstance(key.encoded)
val keyInfo = SubjectPublicKeyInfo.getInstance(encodePublicKey(key))
return findSignatureScheme(keyInfo.algorithm)
}
@ -373,10 +375,17 @@ object Crypto {
*/
@JvmStatic
fun decodePublicKey(encodedKey: ByteArray): PublicKey {
val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(encodedKey)
val signatureScheme = findSignatureScheme(subjectPublicKeyInfo.algorithm)
val keyFactory = keyFactory(signatureScheme)
return convertIfBCEdDSAPublicKey(keyFactory.generatePublic(X509EncodedKeySpec(encodedKey)))
return PublicKeyCache.publicKeyForCachedBytes(ByteSequence.of(encodedKey)) ?: {
val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(encodedKey)
val signatureScheme = findSignatureScheme(subjectPublicKeyInfo.algorithm)
val keyFactory = keyFactory(signatureScheme)
convertIfBCEdDSAPublicKey(keyFactory.generatePublic(X509EncodedKeySpec(encodedKey)))
}()
}
@JvmStatic
fun encodePublicKey(key: PublicKey): ByteArray {
return PublicKeyCache.bytesForCachedPublicKey(key)?.bytes ?: key.encoded
}
/**
@ -993,7 +1002,8 @@ object Crypto {
}
private val interner = PrivateInterner<PublicKey>()
private fun internPublicKey(key: PublicKey): PublicKey = interner.intern(key)
private fun internPublicKey(key: PublicKey): PublicKey = PublicKeyCache.cachePublicKey(interner.intern(key))
private fun convertIfBCEdDSAPublicKey(key: PublicKey): PublicKey {
return internPublicKey(when (key) {

View File

@ -13,7 +13,6 @@ import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.parseAsHex
import net.corda.core.utilities.toHexString
import java.nio.ByteBuffer
import java.security.MessageDigest
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap
@ -39,9 +38,10 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
return true
}
// This is an efficient hashCode, because there is no point in performing a hash calculation on a cryptographic hash.
// It just takes the first 4 bytes and transforms them into an Int.
override fun hashCode() = ByteBuffer.wrap(bytes).int
override fun hashCode(): Int {
// Hash code not overridden on purpose (super class impl will do), but don't delete or have to deal with detekt and API checker.
return super.hashCode()
}
/**
* Convert the hash value to an uppercase hexadecimal [String].
@ -62,7 +62,10 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
}
}
override fun hashCode() = ByteBuffer.wrap(bytes).int
override fun hashCode(): Int {
// Hash code not overridden on purpose (super class impl will do), but don't delete or have to deal with detekt and API checker.
return super.hashCode()
}
override fun toString(): String {
return "$algorithm$DELIMITER${toHexString()}"

View File

@ -0,0 +1,54 @@
package net.corda.core.crypto.internal
import net.corda.core.utilities.ByteSequence
import java.lang.ref.ReferenceQueue
import java.lang.ref.WeakReference
import java.security.PublicKey
import java.util.concurrent.ConcurrentHashMap
object PublicKeyCache {
private val collectedWeakPubKeys = ReferenceQueue<PublicKey>()
private class WeakPubKey(key: PublicKey, val bytes: ByteSequence? = null) : WeakReference<PublicKey>(key, collectedWeakPubKeys) {
private val hashCode = key.hashCode()
override fun hashCode(): Int = hashCode
override fun equals(other: Any?): Boolean {
if(this === other) return true
if(other !is WeakPubKey) return false
if(this.hashCode != other.hashCode) return false
val thisGet = this.get()
val otherGet = other.get()
if(thisGet == null || otherGet == null) return false
return thisGet == otherGet
}
}
private val pubKeyToBytes = ConcurrentHashMap<WeakPubKey, ByteSequence>()
private val bytesToPubKey = ConcurrentHashMap<ByteSequence, WeakPubKey>()
private fun reapCollectedWeakPubKeys() {
while(true) {
val weakPubKey = (collectedWeakPubKeys.poll() as? WeakPubKey) ?: break
pubKeyToBytes.remove(weakPubKey)
bytesToPubKey.remove(weakPubKey.bytes!!)
}
}
fun bytesForCachedPublicKey(key: PublicKey): ByteSequence? {
val weakPubKey = WeakPubKey(key)
return pubKeyToBytes[weakPubKey]
}
fun publicKeyForCachedBytes(bytes: ByteSequence): PublicKey? {
return bytesToPubKey[bytes]?.get()
}
fun cachePublicKey(key: PublicKey): PublicKey {
reapCollectedWeakPubKeys()
val weakPubKey = WeakPubKey(key, ByteSequence.of(key.encoded))
pubKeyToBytes.putIfAbsent(weakPubKey, weakPubKey.bytes!!)
bytesToPubKey.putIfAbsent(weakPubKey.bytes, weakPubKey)
return key
}
}

View File

@ -555,7 +555,7 @@ fun <T : Any> SerializedBytes<T>.sign(keyPair: KeyPair): SignedData<T> = SignedD
fun ByteBuffer.copyBytes(): ByteArray = ByteArray(remaining()).also { get(it) }
val PublicKey.hash: SecureHash get() = encoded.sha256()
val PublicKey.hash: SecureHash get() = Crypto.encodePublicKey(this).sha256()
/**
* Extension method for providing a sumBy method that processes and returns a Long

View File

@ -1,9 +1,16 @@
package net.corda.core.internal
import net.corda.core.DeleteForDJVM
import net.corda.core.crypto.Crypto
import net.i2p.crypto.eddsa.EdDSAEngine
import net.i2p.crypto.eddsa.EdDSAPublicKey
import java.security.*
import java.security.AlgorithmParameters
import java.security.InvalidKeyException
import java.security.MessageDigest
import java.security.PrivateKey
import java.security.PublicKey
import java.security.SecureRandom
import java.security.Signature
import java.security.spec.AlgorithmParameterSpec
import java.security.spec.X509EncodedKeySpec
@ -30,7 +37,7 @@ class X509EdDSAEngine : Signature {
override fun engineInitVerify(publicKey: PublicKey) {
val parsedKey = try {
publicKey as? EdDSAPublicKey ?: EdDSAPublicKey(X509EncodedKeySpec(publicKey.encoded))
publicKey as? EdDSAPublicKey ?: EdDSAPublicKey(X509EncodedKeySpec(Crypto.encodePublicKey(publicKey)))
} catch (e: Exception) {
throw (InvalidKeyException(e.message))
}

View File

@ -166,7 +166,7 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
fun data(): ByteArray? {
return when (type()) {
Type.HASH -> (constraint as HashAttachmentConstraint).attachmentId.bytes
Type.SIGNATURE -> (constraint as SignatureAttachmentConstraint).key.encoded
Type.SIGNATURE -> Crypto.encodePublicKey((constraint as SignatureAttachmentConstraint).key)
else -> null
}
}

View File

@ -112,6 +112,7 @@ sealed class ByteSequence(private val _bytes: ByteArray, val offset: Int, val si
if (this === other) return true
if (other !is ByteSequence) return false
if (this.size != other.size) return false
if (this.hashCode() != other.hashCode()) return false
return subArraysEqual(this._bytes, this.offset, this.size, other._bytes, other.offset)
}
@ -125,13 +126,18 @@ sealed class ByteSequence(private val _bytes: ByteArray, val offset: Int, val si
return true
}
private var _hashCode: Int = 0;
override fun hashCode(): Int {
val thisBytes = _bytes
var result = 1
for (index in offset until (offset + size)) {
result = 31 * result + thisBytes[index]
}
return result
return if (_hashCode == 0) {
val thisBytes = _bytes
var result = 1
for (index in offset until (offset + size)) {
result = 31 * result + thisBytes[index]
}
_hashCode = result
result
} else _hashCode
}
override fun toString(): String = "[${copyBytes().toHexString()}]"

View File

@ -9,7 +9,7 @@ import net.corda.core.crypto.Crypto
import net.corda.core.internal.hash
import java.nio.charset.Charset
import java.security.PublicKey
import java.util.*
import java.util.Base64
// This file includes useful encoding methods and extension functions for the most common encoding/decoding operations.
@ -81,7 +81,7 @@ fun String.hexToBase64(): String = hexToByteArray().toBase64()
fun parsePublicKeyBase58(base58String: String): PublicKey = Crypto.decodePublicKey(base58String.base58ToByteArray())
/** Return the Base58 representation of the serialised public key. */
fun PublicKey.toBase58String(): String = this.encoded.toBase58()
fun PublicKey.toBase58String(): String = Crypto.encodePublicKey(this).toBase58()
/** Return the bytes of the SHA-256 output for this public key. */
fun PublicKey.toSHA256Bytes(): ByteArray = this.hash.bytes

View File

@ -3,14 +3,33 @@ package net.corda.nodeapi.internal.crypto
import net.corda.core.CordaOID
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.newSecureRandom
import net.corda.core.internal.*
import net.corda.core.internal.CertRole
import net.corda.core.internal.SignedDataWithCert
import net.corda.core.internal.reader
import net.corda.core.internal.signWithCert
import net.corda.core.internal.uncheckedCast
import net.corda.core.internal.validate
import net.corda.core.internal.writer
import net.corda.core.utilities.days
import net.corda.core.utilities.millis
import org.bouncycastle.asn1.*
import org.bouncycastle.asn1.ASN1EncodableVector
import org.bouncycastle.asn1.ASN1ObjectIdentifier
import org.bouncycastle.asn1.ASN1Sequence
import org.bouncycastle.asn1.DERSequence
import org.bouncycastle.asn1.DERUTF8String
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x500.style.BCStyle
import org.bouncycastle.asn1.x509.*
import org.bouncycastle.asn1.x509.BasicConstraints
import org.bouncycastle.asn1.x509.CRLDistPoint
import org.bouncycastle.asn1.x509.DistributionPoint
import org.bouncycastle.asn1.x509.DistributionPointName
import org.bouncycastle.asn1.x509.Extension
import org.bouncycastle.asn1.x509.GeneralName
import org.bouncycastle.asn1.x509.GeneralNames
import org.bouncycastle.asn1.x509.KeyPurposeId
import org.bouncycastle.asn1.x509.KeyUsage
import org.bouncycastle.asn1.x509.NameConstraints
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.cert.X509v3CertificateBuilder
import org.bouncycastle.cert.bc.BcX509ExtensionUtils
@ -28,12 +47,18 @@ import java.nio.file.Path
import java.security.KeyPair
import java.security.PublicKey
import java.security.SignatureException
import java.security.cert.*
import java.security.cert.CertPath
import java.security.cert.Certificate
import java.security.cert.CertificateException
import java.security.cert.CertificateFactory
import java.security.cert.TrustAnchor
import java.security.cert.X509CRL
import java.security.cert.X509Certificate
import java.time.Duration
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.*
import java.util.ArrayList
import java.util.Date
import javax.security.auth.x500.X500Principal
import kotlin.experimental.and
import kotlin.experimental.or
@ -189,7 +214,7 @@ object X509Utilities {
crlIssuer: X500Name? = null): X509v3CertificateBuilder {
val serial = generateCertificateSerialNumber()
val keyPurposes = DERSequence(ASN1EncodableVector().apply { certificateType.purposes.forEach { add(it) } })
val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(ASN1Sequence.getInstance(subjectPublicKey.encoded))
val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(ASN1Sequence.getInstance(Crypto.encodePublicKey(subjectPublicKey)))
val role = certificateType.role
val builder = JcaX509v3CertificateBuilder(issuer, serial, validityWindow.first, validityWindow.second, subject, subjectPublicKey)

View File

@ -1,6 +1,10 @@
package net.corda.nodeapi.internal.serialization.kryo
import com.esotericsoftware.kryo.*
import com.esotericsoftware.kryo.ClassResolver
import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.KryoException
import com.esotericsoftware.kryo.Registration
import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.kryo.factories.ReflectionSerializerFactory
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
@ -17,7 +21,13 @@ import net.corda.core.internal.LazyMappedList
import net.corda.core.internal.uncheckedCast
import net.corda.core.serialization.SerializeAsTokenContext
import net.corda.core.serialization.SerializedBytes
import net.corda.core.transactions.*
import net.corda.core.transactions.ComponentGroup
import net.corda.core.transactions.ContractUpgradeFilteredTransaction
import net.corda.core.transactions.ContractUpgradeWireTransaction
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.OpaqueBytes
import net.corda.core.utilities.SgxSupport
import net.corda.serialization.internal.serializationContextKey
@ -302,7 +312,7 @@ object PrivateKeySerializer : Serializer<PrivateKey>() {
object PublicKeySerializer : Serializer<PublicKey>() {
override fun write(kryo: Kryo, output: Output, obj: PublicKey) {
// TODO: Instead of encoding to the default X509 format, we could have a custom per key type (space-efficient) serialiser.
output.writeBytesWithLength(obj.encoded)
output.writeBytesWithLength(Crypto.encodePublicKey(obj))
}
override fun read(kryo: Kryo, input: Input, type: Class<PublicKey>): PublicKey {

View File

@ -47,7 +47,9 @@ import java.security.cert.CertificateNotYetValidException
import java.security.cert.CollectionCertStoreParameters
import java.security.cert.TrustAnchor
import java.security.cert.X509Certificate
import java.util.*
import java.util.HashSet
import java.util.Optional
import java.util.UUID
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.stream.Stream
@ -136,7 +138,7 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri
)
},
toPersistentEntity = { key: String, value: PublicKey ->
PersistentHashToPublicKey(key, value.encoded)
PersistentHashToPublicKey(key, Crypto.encodePublicKey(value))
},
persistentEntityClass = PersistentHashToPublicKey::class.java)
}

View File

@ -1,6 +1,14 @@
package net.corda.node.services.keys
import net.corda.core.crypto.*
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SignableData
import net.corda.core.crypto.SignatureScheme
import net.corda.core.crypto.TransactionSignature
import net.corda.core.crypto.generateKeyPair
import net.corda.core.crypto.keys
import net.corda.core.crypto.sign
import net.corda.core.crypto.toStringShort
import net.corda.core.internal.NamedCacheFactory
import net.corda.core.internal.telemetry.TelemetryServiceImpl
import net.corda.core.serialization.SingletonSerializeAsToken
@ -16,9 +24,12 @@ import org.bouncycastle.operator.ContentSigner
import java.security.KeyPair
import java.security.PrivateKey
import java.security.PublicKey
import java.util.*
import javax.persistence.*
import kotlin.collections.LinkedHashSet
import java.util.UUID
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.Id
import javax.persistence.Lob
import javax.persistence.Table
/**
* A persistent implementation of [KeyManagementServiceInternal] to support CryptoService for initial keys and
@ -52,7 +63,7 @@ class BasicHSMKeyManagementService(
var privateKey: ByteArray = EMPTY_BYTE_ARRAY
) {
constructor(publicKey: PublicKey, privateKey: PrivateKey)
: this(publicKey.toStringShort(), publicKey.encoded, privateKey.encoded)
: this(publicKey.toStringShort(), Crypto.encodePublicKey(publicKey), privateKey.encoded)
}
private companion object {

View File

@ -13,6 +13,6 @@ import javax.persistence.Converter
*/
@Converter(autoApply = true)
class PublicKeyToTextConverter : AttributeConverter<PublicKey, String> {
override fun convertToDatabaseColumn(key: PublicKey?): String? = key?.encoded?.toHex()
override fun convertToDatabaseColumn(key: PublicKey?): String? = key?.let { Crypto.encodePublicKey(key).toHex() }
override fun convertToEntityAttribute(text: String?): PublicKey? = text?.let { Crypto.decodePublicKey(it.hexToByteArray()) }
}

View File

@ -1,7 +1,6 @@
package sandbox.net.corda.core.crypto
import sandbox.net.corda.core.crypto.DJVM.fromDJVM
import sandbox.net.corda.core.crypto.DJVM.toDJVM
import sandbox.java.lang.Object
import sandbox.java.lang.String
import sandbox.java.lang.doCatch
import sandbox.java.math.BigInteger
@ -10,7 +9,8 @@ import sandbox.java.security.PrivateKey
import sandbox.java.security.PublicKey
import sandbox.java.util.ArrayList
import sandbox.java.util.List
import sandbox.java.lang.Object
import sandbox.net.corda.core.crypto.DJVM.fromDJVM
import sandbox.net.corda.core.crypto.DJVM.toDJVM
import sandbox.org.bouncycastle.asn1.x509.AlgorithmIdentifier
import sandbox.org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
import java.security.GeneralSecurityException
@ -149,6 +149,11 @@ object Crypto : Object() {
return decodePublicKey(signatureScheme.schemeCodeName, encodedKey)
}
@JvmStatic
fun encodePublicKey(key: java.security.PublicKey): ByteArray {
return key.encoded
}
@JvmStatic
fun deriveKeyPair(signatureScheme: SignatureScheme, privateKey: PrivateKey, seed: ByteArray): KeyPair {
throw sandbox.java.lang.failApi("Crypto.deriveKeyPair(SignatureScheme, PrivateKey, byte[])")

View File

@ -3,7 +3,13 @@ package net.corda.serialization.internal.amqp.custom
import net.corda.core.crypto.Crypto
import net.corda.core.serialization.DESERIALIZATION_CACHE_PROPERTY
import net.corda.core.serialization.SerializationContext
import net.corda.serialization.internal.amqp.*
import net.corda.serialization.internal.amqp.AMQPTypeIdentifiers
import net.corda.serialization.internal.amqp.CustomSerializer
import net.corda.serialization.internal.amqp.DeserializationInput
import net.corda.serialization.internal.amqp.RestrictedType
import net.corda.serialization.internal.amqp.Schema
import net.corda.serialization.internal.amqp.SerializationOutput
import net.corda.serialization.internal.amqp.SerializationSchemas
import org.apache.qpid.proton.codec.Data
import java.lang.reflect.Type
import java.security.PublicKey
@ -28,7 +34,7 @@ object PublicKeySerializer
context: SerializationContext
) {
// TODO: Instead of encoding to the default X509 format, we could have a custom per key type (space-efficient) serialiser.
output.writeObject(obj.encoded, data, clazz, context)
output.writeObject(Crypto.encodePublicKey(obj), data, clazz, context)
}
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,