Skip to content

Commit 62b4f05

Browse files
authored
Add ECDSA support for importing private key from PEM (#2941)
* 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
1 parent 432c69f commit 62b4f05

File tree

7 files changed

+387
-84
lines changed

7 files changed

+387
-84
lines changed

Libraries/Opc.Ua.Security.Certificates/Org.BouncyCastle/CertificateBuilder.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,27 @@ public static byte[] CreatePfxWithRSAPrivateKey(
155155
}
156156
}
157157

158+
/// <summary>
159+
/// Create a Pfx with a private key by combining
160+
/// an existing X509Certificate2 and a RSA private key.
161+
/// </summary>
162+
public static byte[] CreatePfxWithECdsaPrivateKey(
163+
X509Certificate2 certificate,
164+
string friendlyName,
165+
ECDsa privateKey,
166+
string passcode)
167+
{
168+
Org.BouncyCastle.X509.X509Certificate x509 = new X509CertificateParser().ReadCertificate(certificate.RawData);
169+
using (var cfrg = new CryptoApiRandomGenerator())
170+
{
171+
return X509Utils.CreatePfxWithPrivateKey(
172+
x509, friendlyName,
173+
X509Utils.GetECDsaPrivateKeyParameter(privateKey),
174+
passcode,
175+
new SecureRandom(cfrg));
176+
}
177+
}
178+
158179
/// <summary>
159180
/// Creates a certificate signing request from an
160181
/// existing certificate with a private key.

Libraries/Opc.Ua.Security.Certificates/Org.BouncyCastle/PEMReader.cs

Lines changed: 123 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,53 @@ public static class PEMReader
4545
{
4646
#region Public Methods
4747
/// <summary>
48+
/// Import an RSA private key from PEM.
49+
/// </summary>
50+
public static RSA ImportRsaPrivateKeyFromPEM(
51+
byte[] pemDataBlob,
52+
string password = null)
53+
{
54+
AsymmetricAlgorithm key = ImportPrivateKey(pemDataBlob, password);
55+
if (key is RSA rsaKey)
56+
{
57+
return rsaKey;
58+
}
59+
else
60+
{
61+
throw new CryptographicException("PEM data does not contain a valid RSA private key");
62+
}
63+
}
64+
65+
/// <summary>
66+
/// Import an ECDSa private key from PEM.
67+
/// </summary>
68+
public static ECDsa ImportECDsaPrivateKeyFromPEM(
69+
byte[] pemDataBlob,
70+
string password = null)
71+
{
72+
AsymmetricAlgorithm key = ImportPrivateKey(pemDataBlob, password);
73+
if (key is ECDsa ecKey)
74+
{
75+
return ecKey;
76+
}
77+
else
78+
{
79+
throw new CryptographicException("PEM data does not contain a valid RSA private key");
80+
}
81+
}
82+
83+
84+
#endregion
85+
86+
#region Private
87+
/// <summary>
4888
/// Import a private key from PEM.
4989
/// </summary>
50-
public static RSA ImportPrivateKeyFromPEM(
90+
private static AsymmetricAlgorithm ImportPrivateKey(
5191
byte[] pemDataBlob,
5292
string password = null)
5393
{
54-
RSA rsaPrivateKey = null;
94+
5595
Org.BouncyCastle.OpenSsl.PemReader pemReader;
5696
using (var pemStreamReader = new StreamReader(new MemoryStream(pemDataBlob), Encoding.UTF8, true))
5797
{
@@ -64,30 +104,35 @@ public static RSA ImportPrivateKeyFromPEM(
64104
var pwFinder = new Password(password.ToCharArray());
65105
pemReader = new Org.BouncyCastle.OpenSsl.PemReader(pemStreamReader, pwFinder);
66106
}
107+
108+
AsymmetricAlgorithm key = null;
67109
try
68110
{
69111
// find the private key in the PEM blob
70112
object pemObject = pemReader.ReadObject();
71113
while (pemObject != null)
72114
{
73-
RsaPrivateCrtKeyParameters privateKey = null;
74115
if (pemObject is Org.BouncyCastle.Crypto.AsymmetricCipherKeyPair keypair)
75116
{
76-
privateKey = keypair.Private as RsaPrivateCrtKeyParameters;
117+
pemObject = keypair.Private;
77118
}
78119

79-
if (privateKey == null)
120+
// Check for an RSA private key
121+
if (pemObject is RsaPrivateCrtKeyParameters rsaParams)
80122
{
81-
privateKey = pemObject as RsaPrivateCrtKeyParameters;
123+
var rsa = RSA.Create();
124+
rsa.ImportParameters(DotNetUtilities.ToRSAParameters(rsaParams));
125+
key = rsa;
126+
break;
82127
}
83-
84-
if (privateKey != null)
128+
// Check for an EC private key
129+
if (pemObject is ECPrivateKeyParameters ecParams)
85130
{
86-
rsaPrivateKey = RSA.Create();
87-
rsaPrivateKey.ImportParameters(DotNetUtilities.ToRSAParameters(privateKey));
131+
var ecdsa = CreateECDsaFromECPrivateKey(ecParams);
132+
key = ecdsa;
88133
break;
89134
}
90-
135+
91136
// read next object
92137
pemObject = pemReader.ReadObject();
93138
}
@@ -96,14 +141,78 @@ public static RSA ImportPrivateKeyFromPEM(
96141
{
97142
pemReader.Reader.Dispose();
98143
}
144+
if (key == null)
145+
{
146+
throw new CryptographicException("PEM data blob does not contain a private key.");
147+
}
148+
return key;
149+
}
150+
}
151+
152+
private static ECDsa CreateECDsaFromECPrivateKey(ECPrivateKeyParameters eCPrivateKeyParameters)
153+
{
154+
var domainParams = eCPrivateKeyParameters.Parameters;
155+
156+
// calculate keySize round up (bitLength + 7) / 8
157+
int keySizeBytes = (domainParams.N.BitLength + 7) / 8;
158+
159+
var curveOid = eCPrivateKeyParameters.PublicKeyParamSet.Id;
160+
var curve = ECCurve.CreateFromOid(new Oid(curveOid));
161+
162+
var q = domainParams.G.Multiply(eCPrivateKeyParameters.D).Normalize();
163+
var x = q.AffineXCoord.ToBigInteger().ToByteArrayUnsigned();
164+
var y = q.AffineYCoord.ToBigInteger().ToByteArrayUnsigned();
165+
var d = eCPrivateKeyParameters.D.ToByteArrayUnsigned();
166+
167+
// pad all to the same length since ToByteArrayUnsigned might drop leading zeroes
168+
x = PadWithLeadingZeros(x, keySizeBytes);
169+
y = PadWithLeadingZeros(y, keySizeBytes);
170+
d = PadWithLeadingZeros(d, keySizeBytes);
171+
172+
173+
var ecParams = new ECParameters {
174+
Curve = curve,
175+
Q =
176+
{
177+
X = x,
178+
Y = y
179+
},
180+
D = d
181+
};
182+
183+
var ecdsa = ECDsa.Create();
184+
ecdsa.ImportParameters(ecParams);
185+
186+
return ecdsa;
187+
}
188+
189+
/// <summary>
190+
/// Pads a byte array with leading zeros to reach the specifieed size
191+
/// If the input is allready the given size, it just returns it
192+
/// </summary>
193+
/// <param name="arrayToPad">Provided array to pad</param>
194+
/// <param name="desiredSize">The desired total length of byte array after padding</param>
195+
/// <returns></returns>
196+
private static byte[] PadWithLeadingZeros(byte[] arrayToPad, int desiredSize)
197+
{
198+
if (arrayToPad.Length == desiredSize)
199+
{
200+
return arrayToPad;
99201
}
100202

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

106-
return rsaPrivateKey;
209+
var paddedArray = new byte[desiredSize];
210+
211+
// Right-align the arrayToPad into paddedArray
212+
Buffer.BlockCopy(arrayToPad, 0, paddedArray, paddingLength, arrayToPad.Length);
213+
214+
return paddedArray;
215+
107216
}
108217
#endregion
109218

Libraries/Opc.Ua.Security.Certificates/Org.BouncyCastle/PEMWriter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public static byte[] ExportPrivateKeyAsPEM(
6767
else
6868
{
6969
if (!String.IsNullOrEmpty(password)) throw new ArgumentException("Export with password not supported on this platform.", nameof(password));
70-
ECPrivateKeyParameters privateKeyParameter = X509Utils.GetECPrivateKeyParameter(certificate.GetECDsaPrivateKey());
70+
ECPrivateKeyParameters privateKeyParameter = X509Utils.GetECDsaPrivateKeyParameter(certificate.GetECDsaPrivateKey());
7171
// write private key as PKCS#8
7272
PrivateKeyInfo privateKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(privateKeyParameter);
7373
byte[] serializedPrivateBytes = privateKeyInfo.ToAsn1Object().GetDerEncoded();

0 commit comments

Comments
 (0)