Skip to content

Commit 40b9dbb

Browse files
romanettmarcschier
andauthored
Implement Support for PEM Public Keys in Directory Certificate Store (#3088)
* Implement search of certificates in PEM files on net 6 in directory store * Implement using Custom Certificate Store * Cleanup * Implement Support for PEM Public Keys in Directory Store * Throw exception if test file is not found * Skip Tests on Mac OS & Net < 8.0 * Add delay for slow CI Agents * Add more delay * Make test file with explicit message * Address review Feedback * fix test * Throw ex to show possible error in test * next try * Only throw on full load * Manually set LastWriteTimeUtc * make private key encrypted, use two separate tests instead of one --------- Co-authored-by: Marc Schier <[email protected]>
1 parent b50d7bc commit 40b9dbb

File tree

11 files changed

+878
-122
lines changed

11 files changed

+878
-122
lines changed

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

Lines changed: 88 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,14 @@
2929

3030
#if !NETSTANDARD2_1 && !NET5_0_OR_GREATER
3131
using System;
32-
using System.Security.Cryptography;
3332
using System.IO;
33+
using System.Security.Cryptography;
34+
using System.Security.Cryptography.X509Certificates;
3435
using System.Text;
36+
using Opc.Ua.Security.Certificates.BouncyCastle;
37+
using Org.BouncyCastle.Crypto.Parameters;
3538
using Org.BouncyCastle.OpenSsl;
3639
using Org.BouncyCastle.Security;
37-
using Org.BouncyCastle.Crypto.Parameters;
38-
using Opc.Ua.Security.Certificates.BouncyCastle;
3940

4041
namespace Opc.Ua.Security.Certificates
4142
{
@@ -45,6 +46,88 @@ namespace Opc.Ua.Security.Certificates
4546
public static class PEMReader
4647
{
4748
#region Public Methods
49+
/// <summary>
50+
/// Checks if the PEM data contains a private key.
51+
/// </summary>
52+
/// <param name="pemDataBlob">The PEM data as a byte span.</param>
53+
/// <returns>True if a private key is found.</returns>
54+
public static bool ContainsPrivateKey(byte[] pemDataBlob)
55+
{
56+
using (var ms = new MemoryStream(pemDataBlob))
57+
using (var reader = new StreamReader(ms, Encoding.UTF8, true))
58+
{
59+
var pemReader = new Org.BouncyCastle.OpenSsl.PemReader(reader);
60+
try
61+
{
62+
object pemObject = pemReader.ReadObject();
63+
while (pemObject != null)
64+
{
65+
// Check for AsymmetricCipherKeyPair (private key)
66+
if (pemObject is Org.BouncyCastle.Crypto.AsymmetricCipherKeyPair)
67+
{
68+
return true;
69+
}
70+
// Check for direct private key parameters
71+
if (pemObject is Org.BouncyCastle.Crypto.Parameters.RsaPrivateCrtKeyParameters)
72+
{
73+
return true;
74+
}
75+
#if NET472_OR_GREATER
76+
if (pemObject is Org.BouncyCastle.Crypto.Parameters.ECPrivateKeyParameters)
77+
{
78+
return true;
79+
}
80+
#endif
81+
pemObject = pemReader.ReadObject();
82+
}
83+
}
84+
finally
85+
{
86+
pemReader.Reader.Dispose();
87+
}
88+
}
89+
return false;
90+
}
91+
92+
93+
/// <summary>
94+
/// Import multiple X509 certificates from PEM data.
95+
/// Supports a maximum of 99 certificates in the PEM data.
96+
/// </summary>
97+
/// <param name="pemDataBlob">The PEM datablob as byte array.</param>
98+
/// <returns>The certificates.</returns>
99+
public static X509Certificate2Collection ImportPublicKeysFromPEM(
100+
byte[] pemDataBlob)
101+
{
102+
var certificates = new X509Certificate2Collection();
103+
using (var ms = new MemoryStream(pemDataBlob))
104+
using (var reader = new StreamReader(ms, Encoding.UTF8, true))
105+
{
106+
var pemReader = new Org.BouncyCastle.OpenSsl.PemReader(reader);
107+
int certCount = 0;
108+
try
109+
{
110+
object pemObject = pemReader.ReadObject();
111+
while (pemObject != null && certCount < 99)
112+
{
113+
if (pemObject is Org.BouncyCastle.X509.X509Certificate bcCert)
114+
{
115+
var rawData = bcCert.GetEncoded();
116+
var cert = new X509Certificate2(rawData);
117+
certificates.Add(cert);
118+
certCount++;
119+
}
120+
pemObject = pemReader.ReadObject();
121+
}
122+
}
123+
finally
124+
{
125+
pemReader.Reader.Dispose();
126+
}
127+
}
128+
return certificates;
129+
}
130+
48131
/// <summary>
49132
/// Import an RSA private key from PEM.
50133
/// </summary>
@@ -92,7 +175,7 @@ private static AsymmetricAlgorithm ImportPrivateKey(
92175
byte[] pemDataBlob,
93176
string password = null)
94177
{
95-
178+
96179
Org.BouncyCastle.OpenSsl.PemReader pemReader;
97180
using (var pemStreamReader = new StreamReader(new MemoryStream(pemDataBlob), Encoding.UTF8, true))
98181
{
@@ -191,7 +274,7 @@ private static ECDsa CreateECDsaFromECPrivateKey(ECPrivateKeyParameters eCPrivat
191274
}
192275
#endif
193276

194-
#endregion
277+
#endregion
195278

196279
#region Internal class
197280
/// <summary>

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

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,13 @@
3030
#if !NETSTANDARD2_1 && !NET5_0_OR_GREATER
3131

3232
using System;
33+
using System.Security.Cryptography;
3334
using System.Security.Cryptography.X509Certificates;
35+
using System.Text;
3436
using Opc.Ua.Security.Certificates.BouncyCastle;
37+
using Org.BouncyCastle.Asn1.Pkcs;
3538
using Org.BouncyCastle.Crypto.Parameters;
3639
using Org.BouncyCastle.Pkcs;
37-
using Org.BouncyCastle.Asn1.Pkcs;
3840

3941
namespace Opc.Ua.Security.Certificates
4042
{
@@ -77,7 +79,61 @@ public static byte[] ExportPrivateKeyAsPEM(
7779
throw new ArgumentException("ExportPrivateKeyAsPEM not supported on this platform."); // Only on NETSTANDARD2_0
7880
#endif
7981
}
80-
#endregion
82+
83+
/// <summary>
84+
/// Returns a byte array containing the private key in PEM format.
85+
/// </summary>
86+
public static bool TryRemovePublicKeyFromPEM(
87+
string thumbprint,
88+
byte[] pemDataBlob,
89+
out byte[] modifiedPemDataBlob
90+
)
91+
{
92+
modifiedPemDataBlob = null;
93+
string label = "CERTIFICATE";
94+
string beginlabel = $"-----BEGIN {label}-----";
95+
string endlabel = $"-----END {label}-----";
96+
try
97+
{
98+
string pemText = Encoding.UTF8.GetString(pemDataBlob);
99+
int searchPosition = 0;
100+
int count = 0;
101+
int endIndex = 0;
102+
while (endIndex > -1 && count < 99)
103+
{
104+
count++;
105+
int beginIndex = pemText.IndexOf(beginlabel, searchPosition, StringComparison.Ordinal);
106+
if (beginIndex < 0)
107+
{
108+
return false;
109+
}
110+
endIndex = pemText.IndexOf(endlabel, searchPosition, StringComparison.Ordinal);
111+
beginIndex += beginlabel.Length;
112+
if (endIndex < 0 || endIndex <= beginIndex)
113+
{
114+
return false;
115+
}
116+
var pemCertificateContent = pemText.Substring(beginIndex, endIndex - beginIndex);
117+
var pemCertificateDecoded = Convert.FromBase64CharArray(pemCertificateContent.ToCharArray(), 0, pemCertificateContent.Length);
118+
119+
var certificate = X509CertificateLoader.LoadCertificate(pemCertificateDecoded);
120+
if (thumbprint.Equals(certificate.Thumbprint, StringComparison.OrdinalIgnoreCase))
121+
{
122+
modifiedPemDataBlob = Encoding.ASCII.GetBytes(pemText.Replace(pemText.Substring(beginIndex -= beginlabel.Length, endIndex + endlabel.Length), string.Empty));
123+
return true;
124+
}
125+
126+
127+
searchPosition = endIndex + endlabel.Length;
128+
}
129+
}
130+
catch (Exception)
131+
{
132+
return false;
133+
}
134+
return false;
135+
}
136+
#endregion
81137
}
82138
}
83139
#endif

Libraries/Opc.Ua.Security.Certificates/PEM/PEMReader.cs

Lines changed: 91 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@
3030
#if NETSTANDARD2_1 || NET5_0_OR_GREATER
3131

3232
using System;
33-
using System.Security.Cryptography;
3433
using System.IO;
34+
using System.Linq;
35+
using System.Security.Cryptography;
36+
using System.Security.Cryptography.X509Certificates;
3537
using System.Text;
3638

3739
namespace Opc.Ua.Security.Certificates
@@ -41,7 +43,90 @@ namespace Opc.Ua.Security.Certificates
4143
/// </summary>
4244
public static class PEMReader
4345
{
44-
#region Public Methods
46+
#region Public Methods
47+
/// <summary>
48+
/// Checks if the PEM data contains a private key.
49+
/// </summary>
50+
/// <param name="pemDataBlob">The PEM data as a byte span.</param>
51+
/// <returns>True if a private key is found.</returns>
52+
public static bool ContainsPrivateKey(ReadOnlySpan<byte> pemDataBlob)
53+
{
54+
try
55+
{
56+
string pemText = Encoding.UTF8.GetString(pemDataBlob);
57+
58+
59+
string[] valuesToCheck = {
60+
"-----BEGIN PRIVATE KEY-----",
61+
"-----BEGIN RSA PRIVATE KEY-----",
62+
"-----BEGIN ENCRYPTED PRIVATE KEY-----",
63+
"-----BEGIN EC PRIVATE KEY-----"
64+
};
65+
66+
return valuesToCheck.Any(value => pemText.Contains(value, StringComparison.Ordinal));
67+
}
68+
catch
69+
{
70+
return false;
71+
}
72+
}
73+
/// <summary>
74+
/// Import multiple X509 certificates from PEM data.
75+
/// Supports a maximum of 99 certificates in the PEM data.
76+
/// </summary>
77+
/// <param name="pemDataBlob">The PEM datablob as byte array.</param>
78+
/// <returns>The certificates.</returns>
79+
public static X509Certificate2Collection ImportPublicKeysFromPEM(
80+
ReadOnlySpan<byte> pemDataBlob)
81+
{
82+
var certificates = new X509Certificate2Collection();
83+
string label = "CERTIFICATE";
84+
string beginlabel = $"-----BEGIN {label}-----";
85+
string endlabel = $"-----END {label}-----";
86+
try
87+
{
88+
ReadOnlySpan<char> pemText = Encoding.UTF8.GetString(pemDataBlob).AsSpan();
89+
int count = 0;
90+
int endIndex = 0;
91+
while (endIndex > -1 && count < 99)
92+
{
93+
count++;
94+
int beginIndex = pemText.IndexOf(beginlabel, StringComparison.Ordinal);
95+
if (beginIndex < 0)
96+
{
97+
return certificates;
98+
}
99+
endIndex = pemText.IndexOf(endlabel, StringComparison.Ordinal);
100+
beginIndex += beginlabel.Length;
101+
if (endIndex < 0 || endIndex <= beginIndex)
102+
{
103+
return certificates;
104+
}
105+
var pemCertificateContent = pemText.Slice(beginIndex, endIndex - beginIndex);
106+
Span<byte> pemCertificateDecoded = new Span<byte>(new byte[pemCertificateContent.Length]);
107+
if (Convert.TryFromBase64Chars(pemCertificateContent, pemCertificateDecoded, out var bytesWritten))
108+
{
109+
#if NET6_0_OR_GREATER
110+
certificates.Add(X509CertificateLoader.LoadCertificate(pemCertificateDecoded));
111+
#else
112+
certificates.Add(X509CertificateLoader.LoadCertificate(pemCertificateDecoded.ToArray()));
113+
#endif
114+
}
115+
116+
pemText = pemText.Slice(endIndex + endlabel.Length);
117+
}
118+
}
119+
catch (CryptographicException)
120+
{
121+
throw;
122+
}
123+
catch (Exception ex)
124+
{
125+
throw new CryptographicException("Failed to decode the PEM encoded Certificates.", ex);
126+
}
127+
return certificates;
128+
}
129+
45130
/// <summary>
46131
/// Import a PKCS#8 private key or RSA private key from PEM.
47132
/// The PKCS#8 private key may be encrypted using a password.
@@ -156,7 +241,7 @@ public static ECDsa ImportECDsaPrivateKeyFromPEM(
156241
// Extract the base64-encoded section
157242
string pemData = pemText.Substring(beginIndex, endIndex - beginIndex).Trim();
158243
byte[] decodedBytes = new byte[pemData.Length];
159-
if(Convert.TryFromBase64Chars(pemData, decodedBytes, out int bytesDecoded))
244+
if (Convert.TryFromBase64Chars(pemData, decodedBytes, out int bytesDecoded))
160245
{
161246
// Resize array to actual decoded length
162247
Array.Resize(ref decodedBytes, bytesDecoded);
@@ -201,10 +286,10 @@ public static ECDsa ImportECDsaPrivateKeyFromPEM(
201286
// If no recognized PEM label was found
202287
throw new ArgumentException("No ECDSA private PEM key found.");
203288
}
204-
#endregion
289+
#endregion
205290

206-
#region Private Methods
207-
#endregion
291+
#region Private Methods
292+
#endregion
208293
}
209294
}
210295
#endif

Libraries/Opc.Ua.Security.Certificates/PEM/PEMWriter.cs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,65 @@ public static byte[] ExportPrivateKeyAsPEM(
150150
return EncodeAsPEM(exportedPkcs8PrivateKey,
151151
String.IsNullOrEmpty(password) ? "PRIVATE KEY" : "ENCRYPTED PRIVATE KEY");
152152
}
153+
154+
/// <summary>
155+
/// Returns a byte array containing the private key in PEM format.
156+
/// </summary>
157+
public static bool TryRemovePublicKeyFromPEM(
158+
string thumbprint,
159+
ReadOnlySpan<byte> pemDataBlob,
160+
out byte[] modifiedPemDataBlob
161+
)
162+
{
163+
modifiedPemDataBlob = null;
164+
string label = "CERTIFICATE";
165+
string beginlabel = $"-----BEGIN {label}-----";
166+
string endlabel = $"-----END {label}-----";
167+
try
168+
{
169+
string pemText = Encoding.UTF8.GetString(pemDataBlob);
170+
int searchPosition = 0;
171+
int count = 0;
172+
int endIndex = 0;
173+
while (endIndex > -1 && count < 99)
174+
{
175+
count++;
176+
int beginIndex = pemText.IndexOf(beginlabel, searchPosition, StringComparison.Ordinal);
177+
if (beginIndex < 0)
178+
{
179+
return false;
180+
}
181+
endIndex = pemText.IndexOf(endlabel, searchPosition, StringComparison.Ordinal);
182+
beginIndex += beginlabel.Length;
183+
if (endIndex < 0 || endIndex <= beginIndex)
184+
{
185+
return false;
186+
}
187+
var pemCertificateContent = pemText.Substring(beginIndex, endIndex - beginIndex);
188+
Span<byte> pemCertificateDecoded = new Span<byte>(new byte[pemCertificateContent.Length]);
189+
if (Convert.TryFromBase64Chars(pemCertificateContent, pemCertificateDecoded, out var bytesWritten))
190+
{
191+
#if NET6_0_OR_GREATER
192+
var certificate = X509CertificateLoader.LoadCertificate(pemCertificateDecoded);
193+
#else
194+
var certificate = X509CertificateLoader.LoadCertificate(pemCertificateDecoded.ToArray());
195+
#endif
196+
if (thumbprint.Equals(certificate.Thumbprint, StringComparison.OrdinalIgnoreCase))
197+
{
198+
modifiedPemDataBlob = Encoding.ASCII.GetBytes(pemText.Replace(pemText.Substring(beginIndex -= beginlabel.Length, endIndex + endlabel.Length), string.Empty));
199+
return true;
200+
}
201+
}
202+
203+
searchPosition = endIndex + endlabel.Length;
204+
}
205+
}
206+
catch (Exception)
207+
{
208+
return false;
209+
}
210+
return false;
211+
}
153212
#endif
154213
#endregion
155214

0 commit comments

Comments
 (0)