diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/cryptoservice/CryptoService.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/cryptoservice/CryptoService.kt index 0eea108376..aa20f93591 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/cryptoservice/CryptoService.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/cryptoservice/CryptoService.kt @@ -143,4 +143,4 @@ enum class WrappingMode { WRAPPED } -class WrappedPrivateKey(val keyMaterial: ByteArray, val signatureScheme: SignatureScheme) \ No newline at end of file +class WrappedPrivateKey(val keyMaterial: ByteArray, val signatureScheme: SignatureScheme, val encodingVersion: Int? = null) \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/cryptoservice/bouncycastle/BCCryptoService.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/cryptoservice/bouncycastle/BCCryptoService.kt index 8fcdbb2bfd..d57f750b96 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/cryptoservice/bouncycastle/BCCryptoService.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/cryptoservice/bouncycastle/BCCryptoService.kt @@ -162,20 +162,30 @@ class BCCryptoService(private val legalName: X500Principal, wrappingKeyStore.save(wrappingKeyStorePath!!, certificateStore.password) } + /** + * Using "AESWRAPPAD" cipher spec for key wrapping defined by [RFC 5649](https://tools.ietf.org/html/rfc5649). + * "AESWRAPPAD" (same as "AESKWP" or "AESRFC5649WRAP") is implemented in [org.bouncycastle.jcajce.provider.symmetric.AES.WrapPad] using + * [org.bouncycastle.crypto.engines.RFC5649WrapEngine]. See: + * - https://www.bouncycastle.org/docs/docs1.5on/org/bouncycastle/crypto/engines/AESWrapPadEngine.html + * - https://www.bouncycastle.org/docs/docs1.5on/org/bouncycastle/crypto/engines/RFC5649WrapEngine.html + * + * Keys encoded with "AESWRAPPAD" are stored with encodingVersion = 1. Previously used cipher spec ("AES" == "AES/ECB/PKCS5Padding") + * corresponds to encodingVersion = null. + */ override fun generateWrappedKeyPair(masterKeyAlias: String, childKeyScheme: SignatureScheme): Pair { if (!wrappingKeyStore.containsAlias(masterKeyAlias)) { throw IllegalStateException("There is no master key under the alias: $masterKeyAlias") } val wrappingKey = wrappingKeyStore.getKey(masterKeyAlias, certificateStore.entryPassword.toCharArray()) - val cipher = Cipher.getInstance("AES", cordaBouncyCastleProvider) + val cipher = Cipher.getInstance("AESWRAPPAD", cordaBouncyCastleProvider) cipher.init(Cipher.WRAP_MODE, wrappingKey) val keyPairGenerator = keyPairGeneratorFromScheme(childKeyScheme) val keyPair = keyPairGenerator.generateKeyPair() val privateKeyMaterialWrapped = cipher.wrap(keyPair.private) - return Pair(keyPair.public, WrappedPrivateKey(privateKeyMaterialWrapped, childKeyScheme)) + return Pair(keyPair.public, WrappedPrivateKey(privateKeyMaterialWrapped, childKeyScheme, encodingVersion = 1)) } override fun sign(masterKeyAlias: String, wrappedPrivateKey: WrappedPrivateKey, payloadToSign: ByteArray): ByteArray { @@ -184,7 +194,12 @@ class BCCryptoService(private val legalName: X500Principal, } val wrappingKey = wrappingKeyStore.getKey(masterKeyAlias, certificateStore.entryPassword.toCharArray()) - val cipher = Cipher.getInstance("AES", cordaBouncyCastleProvider) + // Keeping backwards compatibility with previous encoding algorithms + val algorithm = when(wrappedPrivateKey.encodingVersion) { + 1 -> "AESWRAPPAD" + else -> "AES" + } + val cipher = Cipher.getInstance(algorithm, cordaBouncyCastleProvider) cipher.init(Cipher.UNWRAP_MODE, wrappingKey) val privateKey = cipher.unwrap(wrappedPrivateKey.keyMaterial, keyAlgorithmFromScheme(wrappedPrivateKey.signatureScheme), Cipher.PRIVATE_KEY) as PrivateKey diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/cryptoservice/bouncycastle/BCCryptoServiceTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/cryptoservice/bouncycastle/BCCryptoServiceTests.kt index f56477cef6..364ef13ce4 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/cryptoservice/bouncycastle/BCCryptoServiceTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/cryptoservice/bouncycastle/BCCryptoServiceTests.kt @@ -2,6 +2,7 @@ package net.corda.nodeapi.internal.cryptoservice.bouncycastle import net.corda.core.crypto.Crypto import net.corda.core.crypto.SignatureScheme +import net.corda.core.crypto.internal.cordaBouncyCastleProvider import net.corda.core.internal.div import net.corda.core.utilities.days import net.corda.nodeapi.internal.config.CertificateStoreSupplier @@ -13,6 +14,7 @@ import net.corda.nodeapi.internal.cryptoservice.WrappedPrivateKey import net.corda.nodeapi.internal.cryptoservice.WrappingMode import net.corda.testing.core.ALICE_NAME import net.corda.coretesting.internal.stubs.CertificateStoreStubs +import net.corda.nodeapi.internal.crypto.loadOrCreateKeyStore import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.bouncycastle.jce.provider.BouncyCastleProvider @@ -23,8 +25,10 @@ import org.junit.rules.TemporaryFolder import java.io.FileOutputStream import java.nio.file.Path import java.security.* +import java.security.spec.ECGenParameterSpec import java.time.Duration import java.util.* +import javax.crypto.Cipher import javax.security.auth.x500.X500Principal import kotlin.test.assertFailsWith import kotlin.test.assertFalse @@ -253,4 +257,27 @@ class BCCryptoServiceTests { Crypto.doVerify(publicKey, signature, data) } + + @Test(timeout=300_000) + fun `cryptoService can sign with previously encoded version of wrapped key`() { + val cryptoService = BCCryptoService(ALICE_NAME.x500Principal, signingCertificateStore, wrappingKeyStorePath) + + val wrappingKeyAlias = UUID.randomUUID().toString() + cryptoService.createWrappingKey(wrappingKeyAlias) + + val wrappingKeyStore = loadOrCreateKeyStore(wrappingKeyStorePath, cryptoService.certificateStore.password, "PKCS12") + val wrappingKey = wrappingKeyStore.getKey(wrappingKeyAlias, cryptoService.certificateStore.entryPassword.toCharArray()) + val cipher = Cipher.getInstance("AES", cordaBouncyCastleProvider) + cipher.init(Cipher.WRAP_MODE, wrappingKey) + + val keyPairGenerator = KeyPairGenerator.getInstance("EC", cordaBouncyCastleProvider) + keyPairGenerator.initialize(ECGenParameterSpec("secp256r1")) + val keyPair = keyPairGenerator.generateKeyPair() + val privateKeyMaterialWrapped = cipher.wrap(keyPair.private) + val wrappedPrivateKey = WrappedPrivateKey(privateKeyMaterialWrapped, Crypto.ECDSA_SECP256R1_SHA256, encodingVersion = null) + + val data = "data".toByteArray() + val signature = cryptoService.sign(wrappingKeyAlias, wrappedPrivateKey, data) + Crypto.doVerify(keyPair.public, signature, data) + } }