Unfortunately I have never solved this issue. However, I came up with a different approach for my particular use case. My use case was to read the Pkcs8PrivateBlob .NET key in Kotlin and generate / validate signatures that are compatible with the ones generated in .NET (i.e. signatures generated in .NET should be valid in Kotlin and vice versa). I post my approach here for reference, but note that it is not a solution to the original problem. It is also not a very elegant and most likely not the simplest solution, but I had to get it working somehow.
Newer JDK versions support the P1363 signature format which means that at least I only had to convert the key blob into a format readable by Java, namely PKCS#8. Nonetheless I used BouncyCastle to create the initial keypair in Kotlin.
Converting the private key blob to PKCS#8 in C#
internal enum KeyBlobMagicNumber : int
{
BCRYPT_DSA_PUBLIC_MAGIC = 0x42505344,
BCRYPT_DSA_PRIVATE_MAGIC = 0x56505344,
BCRYPT_DSA_PUBLIC_MAGIC_V2 = 0x32425044,
BCRYPT_DSA_PRIVATE_MAGIC_V2 = 0x32565044,
BCRYPT_ECDH_PUBLIC_P256_MAGIC = 0x314B4345,
BCRYPT_ECDH_PRIVATE_P256_MAGIC = 0x324B4345,
BCRYPT_ECDH_PUBLIC_P384_MAGIC = 0x334B4345,
BCRYPT_ECDH_PRIVATE_P384_MAGIC = 0x344B4345,
BCRYPT_ECDH_PUBLIC_P521_MAGIC = 0x354B4345,
BCRYPT_ECDH_PRIVATE_P521_MAGIC = 0x364B4345,
BCRYPT_ECDH_PUBLIC_GENERIC_MAGIC = 0x504B4345,
BCRYPT_ECDH_PRIVATE_GENERIC_MAGIC = 0x564B4345,
BCRYPT_ECDSA_PUBLIC_P256_MAGIC = 0x31534345,
BCRYPT_ECDSA_PRIVATE_P256_MAGIC = 0x32534345,
BCRYPT_ECDSA_PUBLIC_P384_MAGIC = 0x33534345,
BCRYPT_ECDSA_PRIVATE_P384_MAGIC = 0x34534345,
BCRYPT_ECDSA_PUBLIC_P521_MAGIC = 0x35534345,
BCRYPT_ECDSA_PRIVATE_P521_MAGIC = 0x36534345,
BCRYPT_ECDSA_PUBLIC_GENERIC_MAGIC = 0x50444345,
BCRYPT_ECDSA_PRIVATE_GENERIC_MAGIC = 0x56444345,
BCRYPT_RSAPUBLIC_MAGIC = 0x31415352,
BCRYPT_RSAPRIVATE_MAGIC = 0x32415352,
BCRYPT_RSAFULLPRIVATE_MAGIC = 0x33415352,
BCRYPT_KEY_DATA_BLOB_MAGIC = 0x4d42444b,
}
[StructLayout(LayoutKind.Sequential)]
internal struct BCRYPT_ECCKEY_BLOB
{
internal KeyBlobMagicNumber Magic;
internal int cbKey;
}
// ===== THE CONVERSION PROCESS =====
byte[] d;
byte[] blobBytes = // store Pkcs8PrivateBlob here
// Parse the binary blob into a BCRYPT_ECCKEY_BLOB structure
// (see https://learn.microsoft.com/en-gb/windows/win32/api/bcrypt/ns-bcrypt-bcrypt_ecckey_blob?redirectedfrom=MSDN)
unsafe
{
fixed (byte* pBlob = blobBytes)
{
BCRYPT_ECCKEY_BLOB* pBcryptBlob = (BCRYPT_ECCKEY_BLOB*) pBlob;
var headerSize = sizeof(BCRYPT_ECCKEY_BLOB);
var keyData = blobBytes.Skip(headerSize).ToArray();
d = keyData.Skip(pBcryptBlob->cbKey * 2).ToArray();
// The values in the blob are big-endian. Check if we need to reverse them to little-endian
if (BitConverter.IsLittleEndian)
{
d = Arrays.Reverse(d);
}
// Append 0 to mark the data as unsigned as per the documentation of System.Numerics.BigInteger
var unsignedByteData = d.Concat(new byte[] {0}).ToArray();
var dValue = new System.Numerics.BigInteger(unsignedByteData);
// Create a BouncyCastle BigInteger from the string representation to make sure that we use the
// same endianess and sign
var dValueBC = new BigInteger(dValue.ToString());
var ecDomainParameters = TlsEccUtilities.GetParametersForNamedCurve(NamedCurve.secp384r1);
// Build a private key from the parsed d value
var privateKey = new ECPrivateKeyParameters(dValueBC, ecDomainParameters);
// Export the private key in PKCS#8 format
var privateKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(privateKey);
var result = privateKeyInfo.GetDerEncoded();
}
}
Creating a key pair from the PKCS#8 private key in Kotlin with BouncyCastle
class KeyConverter : IKeyConverter {
init {
Security.addProvider(BouncyCastleProvider())
}
override fun getKeyPair(pkcs8Base64Key: String): KeyPair {
val decoder = Base64.getDecoder()
val keyBytes = decoder.decode(pkcs8Base64Key)
val pkcS8EncodedKeySpec = PKCS8EncodedKeySpec(keyBytes)
val defaultKeyFactory = KeyFactory.getInstance("EC", "BC")
val privateKey = defaultKeyFactory.generatePrivate(pkcS8EncodedKeySpec) as ECPrivateKey
val publicKey = publicKeyFromPrivateKey(privateKey)
return KeyPair(publicKey, privateKey)
}
private fun publicKeyFromPrivateKey(key: ECPrivateKey): PublicKey {
val bcKeyFactory = KeyFactory.getInstance("EC", "BC")
val ecParameterSpec = ECNamedCurveTable.getParameterSpec("secp384r1")
val Q = ecParameterSpec.g.multiply(JCEECPrivateKey(key).d)
val publicKeySpec = ECPublicKeySpec(Q, ecParameterSpec)
return bcKeyFactory.generatePublic(publicKeySpec)
}
}
Using the key pair to create / validate P1363 signatures in Kotlin
class DefaultCryptoProvider(private val keyPair: KeyPair) {
private val randomSource: SecureRandom = SecureRandom()
override fun signData(data: ByteArray): ByteArray {
val signature = Signature.getInstance("SHA256withECDSAinP1363Format")
signature.initSign(keyPair.private)
signature.update(data)
return signature.sign()
}
override fun verifyData(data: ByteArray, signatureData: ByteArray): Boolean {
val signature = Signature.getInstance("SHA256withECDSAinP1363Format")
signature.initVerify(keyPair.public)
signature.update(data)
return signature.verify(signatureData)
}
}
Hopefully it helps someone with a similar problem at least.