Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@

#if !NETSTANDARD2_1 && !NET5_0_OR_GREATER
using System;
using System.Security.Cryptography;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Opc.Ua.Security.Certificates.BouncyCastle;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Crypto.Parameters;
using Opc.Ua.Security.Certificates.BouncyCastle;

namespace Opc.Ua.Security.Certificates
{
Expand All @@ -45,6 +46,88 @@ namespace Opc.Ua.Security.Certificates
public static class PEMReader
{
#region Public Methods
/// <summary>
/// Checks if the PEM data contains a private key.
/// </summary>
/// <param name="pemDataBlob">The PEM data as a byte span.</param>
/// <returns>True if a private key is found.</returns>
public static bool ContainsPrivateKey(byte[] pemDataBlob)
{
using (var ms = new MemoryStream(pemDataBlob))
using (var reader = new StreamReader(ms, Encoding.UTF8, true))
{
var pemReader = new Org.BouncyCastle.OpenSsl.PemReader(reader);
try
{
object pemObject = pemReader.ReadObject();
while (pemObject != null)
{
// Check for AsymmetricCipherKeyPair (private key)
if (pemObject is Org.BouncyCastle.Crypto.AsymmetricCipherKeyPair)
{
return true;
}
// Check for direct private key parameters
if (pemObject is Org.BouncyCastle.Crypto.Parameters.RsaPrivateCrtKeyParameters)
{
return true;
}
#if NET472_OR_GREATER
if (pemObject is Org.BouncyCastle.Crypto.Parameters.ECPrivateKeyParameters)
{
return true;
}
#endif
pemObject = pemReader.ReadObject();
}
}
finally
{
pemReader.Reader.Dispose();
}
}
return false;
}


/// <summary>
/// Import multiple X509 certificates from PEM data.
/// Supports a maximum of 99 certificates in the PEM data.
/// </summary>
/// <param name="pemDataBlob">The PEM datablob as byte array.</param>
/// <returns>The certificates.</returns>
public static X509Certificate2Collection ImportPublicKeysFromPEM(
byte[] pemDataBlob)
{
var certificates = new X509Certificate2Collection();
using (var ms = new MemoryStream(pemDataBlob))
using (var reader = new StreamReader(ms, Encoding.UTF8, true))
{
var pemReader = new Org.BouncyCastle.OpenSsl.PemReader(reader);
int certCount = 0;
try
{
object pemObject = pemReader.ReadObject();
while (pemObject != null && certCount < 99)
{
if (pemObject is Org.BouncyCastle.X509.X509Certificate bcCert)
{
var rawData = bcCert.GetEncoded();
var cert = new X509Certificate2(rawData);
certificates.Add(cert);
certCount++;
}
pemObject = pemReader.ReadObject();
}
}
finally
{
pemReader.Reader.Dispose();
}
}
return certificates;
}

/// <summary>
/// Import an RSA private key from PEM.
/// </summary>
Expand Down Expand Up @@ -92,7 +175,7 @@ private static AsymmetricAlgorithm ImportPrivateKey(
byte[] pemDataBlob,
string password = null)
{

Org.BouncyCastle.OpenSsl.PemReader pemReader;
using (var pemStreamReader = new StreamReader(new MemoryStream(pemDataBlob), Encoding.UTF8, true))
{
Expand Down Expand Up @@ -191,7 +274,7 @@ private static ECDsa CreateECDsaFromECPrivateKey(ECPrivateKeyParameters eCPrivat
}
#endif

#endregion
#endregion

#region Internal class
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@
#if !NETSTANDARD2_1 && !NET5_0_OR_GREATER

using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Opc.Ua.Security.Certificates.BouncyCastle;
using Org.BouncyCastle.Asn1.Pkcs;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Asn1.Pkcs;

namespace Opc.Ua.Security.Certificates
{
Expand Down Expand Up @@ -77,7 +79,61 @@ public static byte[] ExportPrivateKeyAsPEM(
throw new ArgumentException("ExportPrivateKeyAsPEM not supported on this platform."); // Only on NETSTANDARD2_0
#endif
}
#endregion

/// <summary>
/// Returns a byte array containing the private key in PEM format.
/// </summary>
public static bool TryRemovePublicKeyFromPEM(
string thumbprint,
byte[] pemDataBlob,
out byte[] modifiedPemDataBlob
)
{
modifiedPemDataBlob = null;
string label = "CERTIFICATE";
string beginlabel = $"-----BEGIN {label}-----";
string endlabel = $"-----END {label}-----";
try
{
string pemText = Encoding.UTF8.GetString(pemDataBlob);
int searchPosition = 0;
int count = 0;
int endIndex = 0;
while (endIndex > -1 && count < 99)
{
count++;
int beginIndex = pemText.IndexOf(beginlabel, searchPosition, StringComparison.Ordinal);
if (beginIndex < 0)
{
return false;
}
endIndex = pemText.IndexOf(endlabel, searchPosition, StringComparison.Ordinal);
beginIndex += beginlabel.Length;
if (endIndex < 0 || endIndex <= beginIndex)
{
return false;
}
var pemCertificateContent = pemText.Substring(beginIndex, endIndex - beginIndex);
var pemCertificateDecoded = Convert.FromBase64CharArray(pemCertificateContent.ToCharArray(), 0, pemCertificateContent.Length);

var certificate = X509CertificateLoader.LoadCertificate(pemCertificateDecoded);
if (thumbprint.Equals(certificate.Thumbprint, StringComparison.OrdinalIgnoreCase))
{
modifiedPemDataBlob = Encoding.ASCII.GetBytes(pemText.Replace(pemText.Substring(beginIndex -= beginlabel.Length, endIndex + endlabel.Length), string.Empty));
return true;
}


searchPosition = endIndex + endlabel.Length;
}
}
catch (Exception)
{
return false;
}
return false;
}
#endregion
}
}
#endif
97 changes: 91 additions & 6 deletions Libraries/Opc.Ua.Security.Certificates/PEM/PEMReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@
#if NETSTANDARD2_1 || NET5_0_OR_GREATER

using System;
using System.Security.Cryptography;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;

namespace Opc.Ua.Security.Certificates
Expand All @@ -41,7 +43,90 @@ namespace Opc.Ua.Security.Certificates
/// </summary>
public static class PEMReader
{
#region Public Methods
#region Public Methods
/// <summary>
/// Checks if the PEM data contains a private key.
/// </summary>
/// <param name="pemDataBlob">The PEM data as a byte span.</param>
/// <returns>True if a private key is found.</returns>
public static bool ContainsPrivateKey(ReadOnlySpan<byte> pemDataBlob)
{
try
{
string pemText = Encoding.UTF8.GetString(pemDataBlob);


string[] valuesToCheck = {
"-----BEGIN PRIVATE KEY-----",
"-----BEGIN RSA PRIVATE KEY-----",
"-----BEGIN ENCRYPTED PRIVATE KEY-----",
"-----BEGIN EC PRIVATE KEY-----"
};

return valuesToCheck.Any(value => pemText.Contains(value, StringComparison.Ordinal));
}
catch
{
return false;
}
}
/// <summary>
/// Import multiple X509 certificates from PEM data.
/// Supports a maximum of 99 certificates in the PEM data.
/// </summary>
/// <param name="pemDataBlob">The PEM datablob as byte array.</param>
/// <returns>The certificates.</returns>
public static X509Certificate2Collection ImportPublicKeysFromPEM(
ReadOnlySpan<byte> pemDataBlob)
{
var certificates = new X509Certificate2Collection();
string label = "CERTIFICATE";
string beginlabel = $"-----BEGIN {label}-----";
string endlabel = $"-----END {label}-----";
try
{
ReadOnlySpan<char> pemText = Encoding.UTF8.GetString(pemDataBlob).AsSpan();
int count = 0;
int endIndex = 0;
while (endIndex > -1 && count < 99)
{
count++;
int beginIndex = pemText.IndexOf(beginlabel, StringComparison.Ordinal);
if (beginIndex < 0)
{
return certificates;
}
endIndex = pemText.IndexOf(endlabel, StringComparison.Ordinal);
beginIndex += beginlabel.Length;
if (endIndex < 0 || endIndex <= beginIndex)
{
return certificates;
}
var pemCertificateContent = pemText.Slice(beginIndex, endIndex - beginIndex);
Span<byte> pemCertificateDecoded = new Span<byte>(new byte[pemCertificateContent.Length]);
if (Convert.TryFromBase64Chars(pemCertificateContent, pemCertificateDecoded, out var bytesWritten))
{
#if NET6_0_OR_GREATER
certificates.Add(X509CertificateLoader.LoadCertificate(pemCertificateDecoded));
#else
certificates.Add(X509CertificateLoader.LoadCertificate(pemCertificateDecoded.ToArray()));
#endif
}

pemText = pemText.Slice(endIndex + endlabel.Length);
}
}
catch (CryptographicException)
{
throw;
}
catch (Exception ex)
{
throw new CryptographicException("Failed to decode the PEM encoded Certificates.", ex);
}
return certificates;
}

/// <summary>
/// Import a PKCS#8 private key or RSA private key from PEM.
/// The PKCS#8 private key may be encrypted using a password.
Expand Down Expand Up @@ -156,7 +241,7 @@ public static ECDsa ImportECDsaPrivateKeyFromPEM(
// Extract the base64-encoded section
string pemData = pemText.Substring(beginIndex, endIndex - beginIndex).Trim();
byte[] decodedBytes = new byte[pemData.Length];
if(Convert.TryFromBase64Chars(pemData, decodedBytes, out int bytesDecoded))
if (Convert.TryFromBase64Chars(pemData, decodedBytes, out int bytesDecoded))
{
// Resize array to actual decoded length
Array.Resize(ref decodedBytes, bytesDecoded);
Expand Down Expand Up @@ -201,10 +286,10 @@ public static ECDsa ImportECDsaPrivateKeyFromPEM(
// If no recognized PEM label was found
throw new ArgumentException("No ECDSA private PEM key found.");
}
#endregion
#endregion

#region Private Methods
#endregion
#region Private Methods
#endregion
}
}
#endif
59 changes: 59 additions & 0 deletions Libraries/Opc.Ua.Security.Certificates/PEM/PEMWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,65 @@ public static byte[] ExportPrivateKeyAsPEM(
return EncodeAsPEM(exportedPkcs8PrivateKey,
String.IsNullOrEmpty(password) ? "PRIVATE KEY" : "ENCRYPTED PRIVATE KEY");
}

/// <summary>
/// Returns a byte array containing the private key in PEM format.
/// </summary>
public static bool TryRemovePublicKeyFromPEM(
string thumbprint,
ReadOnlySpan<byte> pemDataBlob,
out byte[] modifiedPemDataBlob
)
{
modifiedPemDataBlob = null;
string label = "CERTIFICATE";
string beginlabel = $"-----BEGIN {label}-----";
string endlabel = $"-----END {label}-----";
try
{
string pemText = Encoding.UTF8.GetString(pemDataBlob);
int searchPosition = 0;
int count = 0;
int endIndex = 0;
while (endIndex > -1 && count < 99)
{
count++;
int beginIndex = pemText.IndexOf(beginlabel, searchPosition, StringComparison.Ordinal);
if (beginIndex < 0)
{
return false;
}
endIndex = pemText.IndexOf(endlabel, searchPosition, StringComparison.Ordinal);
beginIndex += beginlabel.Length;
if (endIndex < 0 || endIndex <= beginIndex)
{
return false;
}
var pemCertificateContent = pemText.Substring(beginIndex, endIndex - beginIndex);
Span<byte> pemCertificateDecoded = new Span<byte>(new byte[pemCertificateContent.Length]);
if (Convert.TryFromBase64Chars(pemCertificateContent, pemCertificateDecoded, out var bytesWritten))
{
#if NET6_0_OR_GREATER
var certificate = X509CertificateLoader.LoadCertificate(pemCertificateDecoded);
#else
var certificate = X509CertificateLoader.LoadCertificate(pemCertificateDecoded.ToArray());
#endif
if (thumbprint.Equals(certificate.Thumbprint, StringComparison.OrdinalIgnoreCase))
{
modifiedPemDataBlob = Encoding.ASCII.GetBytes(pemText.Replace(pemText.Substring(beginIndex -= beginlabel.Length, endIndex + endlabel.Length), string.Empty));
return true;
}
}

searchPosition = endIndex + endlabel.Length;
}
}
catch (Exception)
{
return false;
}
return false;
}
#endif
#endregion

Expand Down
Loading