Skip to content

Commit

Permalink
Add ECDSA support for importing private key from PEM (#2941)
Browse files Browse the repository at this point in the history
* Add ECDSA support for reading private key from PEM

* Padded x, y EC point coordinates and the d private key with leading zeros to match keySize

* CreateCertificateWithPEMPrivateKey creates either ECDSA or RSA certificate depending on certificate type
  • Loading branch information
mrsuciu authored Jan 17, 2025
1 parent 432c69f commit 62b4f05
Show file tree
Hide file tree
Showing 7 changed files with 387 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,27 @@ public static byte[] CreatePfxWithRSAPrivateKey(
}
}

/// <summary>
/// Create a Pfx with a private key by combining
/// an existing X509Certificate2 and a RSA private key.
/// </summary>
public static byte[] CreatePfxWithECdsaPrivateKey(
X509Certificate2 certificate,
string friendlyName,
ECDsa privateKey,
string passcode)
{
Org.BouncyCastle.X509.X509Certificate x509 = new X509CertificateParser().ReadCertificate(certificate.RawData);
using (var cfrg = new CryptoApiRandomGenerator())
{
return X509Utils.CreatePfxWithPrivateKey(
x509, friendlyName,
X509Utils.GetECDsaPrivateKeyParameter(privateKey),
passcode,
new SecureRandom(cfrg));
}
}

/// <summary>
/// Creates a certificate signing request from an
/// existing certificate with a private key.
Expand Down
137 changes: 123 additions & 14 deletions Libraries/Opc.Ua.Security.Certificates/Org.BouncyCastle/PEMReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,53 @@ public static class PEMReader
{
#region Public Methods
/// <summary>
/// Import an RSA private key from PEM.
/// </summary>
public static RSA ImportRsaPrivateKeyFromPEM(
byte[] pemDataBlob,
string password = null)
{
AsymmetricAlgorithm key = ImportPrivateKey(pemDataBlob, password);
if (key is RSA rsaKey)
{
return rsaKey;
}
else
{
throw new CryptographicException("PEM data does not contain a valid RSA private key");
}
}

/// <summary>
/// Import an ECDSa private key from PEM.
/// </summary>
public static ECDsa ImportECDsaPrivateKeyFromPEM(
byte[] pemDataBlob,
string password = null)
{
AsymmetricAlgorithm key = ImportPrivateKey(pemDataBlob, password);
if (key is ECDsa ecKey)
{
return ecKey;
}
else
{
throw new CryptographicException("PEM data does not contain a valid RSA private key");
}
}


#endregion

#region Private
/// <summary>
/// Import a private key from PEM.
/// </summary>
public static RSA ImportPrivateKeyFromPEM(
private static AsymmetricAlgorithm ImportPrivateKey(
byte[] pemDataBlob,
string password = null)
{
RSA rsaPrivateKey = null;

Org.BouncyCastle.OpenSsl.PemReader pemReader;
using (var pemStreamReader = new StreamReader(new MemoryStream(pemDataBlob), Encoding.UTF8, true))
{
Expand All @@ -64,30 +104,35 @@ public static RSA ImportPrivateKeyFromPEM(
var pwFinder = new Password(password.ToCharArray());
pemReader = new Org.BouncyCastle.OpenSsl.PemReader(pemStreamReader, pwFinder);
}

AsymmetricAlgorithm key = null;
try
{
// find the private key in the PEM blob
object pemObject = pemReader.ReadObject();
while (pemObject != null)
{
RsaPrivateCrtKeyParameters privateKey = null;
if (pemObject is Org.BouncyCastle.Crypto.AsymmetricCipherKeyPair keypair)
{
privateKey = keypair.Private as RsaPrivateCrtKeyParameters;
pemObject = keypair.Private;
}

if (privateKey == null)
// Check for an RSA private key
if (pemObject is RsaPrivateCrtKeyParameters rsaParams)
{
privateKey = pemObject as RsaPrivateCrtKeyParameters;
var rsa = RSA.Create();
rsa.ImportParameters(DotNetUtilities.ToRSAParameters(rsaParams));
key = rsa;
break;
}

if (privateKey != null)
// Check for an EC private key
if (pemObject is ECPrivateKeyParameters ecParams)
{
rsaPrivateKey = RSA.Create();
rsaPrivateKey.ImportParameters(DotNetUtilities.ToRSAParameters(privateKey));
var ecdsa = CreateECDsaFromECPrivateKey(ecParams);
key = ecdsa;
break;
}

// read next object
pemObject = pemReader.ReadObject();
}
Expand All @@ -96,14 +141,78 @@ public static RSA ImportPrivateKeyFromPEM(
{
pemReader.Reader.Dispose();
}
if (key == null)
{
throw new CryptographicException("PEM data blob does not contain a private key.");
}
return key;
}
}

private static ECDsa CreateECDsaFromECPrivateKey(ECPrivateKeyParameters eCPrivateKeyParameters)
{
var domainParams = eCPrivateKeyParameters.Parameters;

// calculate keySize round up (bitLength + 7) / 8
int keySizeBytes = (domainParams.N.BitLength + 7) / 8;

var curveOid = eCPrivateKeyParameters.PublicKeyParamSet.Id;
var curve = ECCurve.CreateFromOid(new Oid(curveOid));

var q = domainParams.G.Multiply(eCPrivateKeyParameters.D).Normalize();
var x = q.AffineXCoord.ToBigInteger().ToByteArrayUnsigned();
var y = q.AffineYCoord.ToBigInteger().ToByteArrayUnsigned();
var d = eCPrivateKeyParameters.D.ToByteArrayUnsigned();

// pad all to the same length since ToByteArrayUnsigned might drop leading zeroes
x = PadWithLeadingZeros(x, keySizeBytes);
y = PadWithLeadingZeros(y, keySizeBytes);
d = PadWithLeadingZeros(d, keySizeBytes);


var ecParams = new ECParameters {
Curve = curve,
Q =
{
X = x,
Y = y
},
D = d
};

var ecdsa = ECDsa.Create();
ecdsa.ImportParameters(ecParams);

return ecdsa;
}

/// <summary>
/// Pads a byte array with leading zeros to reach the specifieed size
/// If the input is allready the given size, it just returns it
/// </summary>
/// <param name="arrayToPad">Provided array to pad</param>
/// <param name="desiredSize">The desired total length of byte array after padding</param>
/// <returns></returns>
private static byte[] PadWithLeadingZeros(byte[] arrayToPad, int desiredSize)
{
if (arrayToPad.Length == desiredSize)
{
return arrayToPad;
}

if (rsaPrivateKey == null)
int paddingLength = desiredSize - arrayToPad.Length;
if (paddingLength < 0)
{
throw new CryptographicException("PEM data blob does not contain a private key.");
throw new ArgumentException($"Input byte array is larger than the desired size {desiredSize} bytes.");
}

return rsaPrivateKey;
var paddedArray = new byte[desiredSize];

// Right-align the arrayToPad into paddedArray
Buffer.BlockCopy(arrayToPad, 0, paddedArray, paddingLength, arrayToPad.Length);

return paddedArray;

}
#endregion

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public static byte[] ExportPrivateKeyAsPEM(
else
{
if (!String.IsNullOrEmpty(password)) throw new ArgumentException("Export with password not supported on this platform.", nameof(password));
ECPrivateKeyParameters privateKeyParameter = X509Utils.GetECPrivateKeyParameter(certificate.GetECDsaPrivateKey());
ECPrivateKeyParameters privateKeyParameter = X509Utils.GetECDsaPrivateKeyParameter(certificate.GetECDsaPrivateKey());
// write private key as PKCS#8
PrivateKeyInfo privateKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(privateKeyParameter);
byte[] serializedPrivateBytes = privateKeyInfo.ToAsn1Object().GetDerEncoded();
Expand Down
Loading

0 comments on commit 62b4f05

Please sign in to comment.