diff --git a/Libraries/Opc.Ua.Gds.Server.Common/CertificateGroup.cs b/Libraries/Opc.Ua.Gds.Server.Common/CertificateGroup.cs index 08f5bc5cad..d23cd51d2c 100644 --- a/Libraries/Opc.Ua.Gds.Server.Common/CertificateGroup.cs +++ b/Libraries/Opc.Ua.Gds.Server.Common/CertificateGroup.cs @@ -344,9 +344,7 @@ public virtual Task VerifySigningRequestAsync( { try { - var pkcs10CertificationRequest - = new Org.BouncyCastle.Pkcs.Pkcs10CertificationRequest( - certificateRequest); + var pkcs10CertificationRequest = new Pkcs10CertificationRequest(certificateRequest); if (!pkcs10CertificationRequest.Verify()) { @@ -355,9 +353,8 @@ var pkcs10CertificationRequest "CSR signature invalid."); } - Org.BouncyCastle.Asn1.Pkcs.CertificationRequestInfo info = - pkcs10CertificationRequest.GetCertificationRequestInfo(); - X509SubjectAltNameExtension altNameExtension = GetAltNameExtensionFromCSRInfo(info); + X509SubjectAltNameExtension altNameExtension = + Pkcs10Utils.GetSubjectAltNameExtension(pkcs10CertificationRequest.Attributes); if (altNameExtension != null && altNameExtension.Uris.Count > 0 && !altNameExtension.Uris.Contains(application.ApplicationUri)) @@ -383,9 +380,7 @@ public virtual async Task SigningRequestAsync( { try { - var pkcs10CertificationRequest - = new Org.BouncyCastle.Pkcs.Pkcs10CertificationRequest( - certificateRequest); + var pkcs10CertificationRequest = new Pkcs10CertificationRequest(certificateRequest); if (!pkcs10CertificationRequest.Verify()) { @@ -394,9 +389,8 @@ var pkcs10CertificationRequest "CSR signature invalid."); } - Org.BouncyCastle.Asn1.Pkcs.CertificationRequestInfo info = - pkcs10CertificationRequest.GetCertificationRequestInfo(); - X509SubjectAltNameExtension altNameExtension = GetAltNameExtensionFromCSRInfo(info); + X509SubjectAltNameExtension altNameExtension = + Pkcs10Utils.GetSubjectAltNameExtension(pkcs10CertificationRequest.Attributes); if (altNameExtension != null) { if (altNameExtension.Uris.Count > 0 && @@ -433,7 +427,7 @@ var pkcs10CertificationRequest m_telemetry, ct) .ConfigureAwait(false); - var subjectName = new X500DistinguishedName(info.Subject.GetEncoded()); + var subjectName = pkcs10CertificationRequest.Subject; ICertificateBuilder builder = CertificateBuilder .Create(subjectName) @@ -445,13 +439,13 @@ var pkcs10CertificationRequest return TryGetECCCurve(certificateType, out ECCurve curve) ? builder .SetIssuer(signingKey) - .SetECDsaPublicKey(info.SubjectPublicKeyInfo.GetEncoded()) + .SetECDsaPublicKey(pkcs10CertificationRequest.SubjectPublicKeyInfo) .CreateForECDsa() : builder .SetHashAlgorithm(X509Utils.GetRSAHashAlgorithmName( Configuration.DefaultCertificateHashSize)) .SetIssuer(signingKey) - .SetRSAPublicKey(info.SubjectPublicKeyInfo.GetEncoded()) + .SetRSAPublicKey(pkcs10CertificationRequest.SubjectPublicKeyInfo) .CreateForRSA(); } catch (Exception ex) when (ex is not ServiceResultException) @@ -766,47 +760,6 @@ X509CRL crl in await authorityStore } } - protected X509SubjectAltNameExtension GetAltNameExtensionFromCSRInfo( - Org.BouncyCastle.Asn1.Pkcs.CertificationRequestInfo info) - { - try - { - for (int i = 0; i < info.Attributes.Count; i++) - { - var sequence = Org.BouncyCastle.Asn1.Asn1Sequence - .GetInstance(info.Attributes[i].ToAsn1Object()); - var oid = Org.BouncyCastle.Asn1.DerObjectIdentifier - .GetInstance(sequence[0].ToAsn1Object()); - if (oid.Equals( - Org.BouncyCastle.Asn1.Pkcs.PkcsObjectIdentifiers.Pkcs9AtExtensionRequest)) - { - var extensionInstance = Org.BouncyCastle.Asn1.Asn1Set - .GetInstance(sequence[1]); - var extensionSequence = Org.BouncyCastle.Asn1.Asn1Sequence - .GetInstance(extensionInstance[0]); - var extensions = Org.BouncyCastle.Asn1.X509.X509Extensions - .GetInstance(extensionSequence); - Org.BouncyCastle.Asn1.X509.X509Extension extension = extensions - .GetExtension( - Org.BouncyCastle.Asn1.X509.X509Extensions.SubjectAlternativeName); - var asnEncodedAltNameExtension = new AsnEncodedData( - Org.BouncyCastle.Asn1.X509.X509Extensions.SubjectAlternativeName - .ToString(), - extension.Value.GetOctets()); - return new X509SubjectAltNameExtension( - asnEncodedAltNameExtension, - extension.IsCritical); - } - } - } - catch - { - throw new ServiceResultException( - StatusCodes.BadInvalidArgument, - "CSR altNameExtension invalid."); - } - return null; - } private readonly ITelemetryContext m_telemetry; private readonly ILogger m_logger; diff --git a/Libraries/Opc.Ua.Gds.Server.Common/Opc.Ua.Gds.Server.Common.csproj b/Libraries/Opc.Ua.Gds.Server.Common/Opc.Ua.Gds.Server.Common.csproj index 83fd11aaad..cb4acae2cc 100644 --- a/Libraries/Opc.Ua.Gds.Server.Common/Opc.Ua.Gds.Server.Common.csproj +++ b/Libraries/Opc.Ua.Gds.Server.Common/Opc.Ua.Gds.Server.Common.csproj @@ -19,11 +19,9 @@ - - - + diff --git a/Libraries/Opc.Ua.Security.Certificates/PKCS10/Pkcs10CertificationRequest.cs b/Libraries/Opc.Ua.Security.Certificates/PKCS10/Pkcs10CertificationRequest.cs new file mode 100644 index 0000000000..8e277a3017 --- /dev/null +++ b/Libraries/Opc.Ua.Security.Certificates/PKCS10/Pkcs10CertificationRequest.cs @@ -0,0 +1,357 @@ +/* ======================================================================== + * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System; +using System.Formats.Asn1; +using System.Linq; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +namespace Opc.Ua.Security.Certificates +{ + /// + /// Represents a PKCS#10 Certificate Signing Request (CSR). + /// + /// + /// This class provides functionality to parse and verify PKCS#10 CSRs + /// using .NET Framework APIs, eliminating the need for BouncyCastle. + /// + /// Based on RFC 2986: PKCS #10: Certification Request Syntax Specification + /// https://tools.ietf.org/html/rfc2986 + /// + public sealed class Pkcs10CertificationRequest + { + private readonly byte[] m_certificationRequestInfo; + private readonly byte[] m_signature; + private readonly string m_signatureAlgorithm; + private readonly byte[] m_subjectPublicKeyInfo; + private readonly X500DistinguishedName m_subject; + private readonly byte[] m_attributes; + + /// + /// Initializes a new instance of the Pkcs10CertificationRequest class from DER-encoded data. + /// + /// The DER-encoded PKCS#10 certificate request. + /// Thrown when encodedRequest is null. + /// Thrown when the request cannot be parsed. + public Pkcs10CertificationRequest(byte[] encodedRequest) + { + if (encodedRequest == null) + { + throw new ArgumentNullException(nameof(encodedRequest)); + } + + try + { + // Parse the outer SEQUENCE + var reader = new AsnReader(encodedRequest, AsnEncodingRules.DER); + AsnReader sequenceReader = reader.ReadSequence(); + + // Read CertificationRequestInfo + m_certificationRequestInfo = sequenceReader.ReadEncodedValue().ToArray(); + + // Parse CertificationRequestInfo to extract components + (m_subject, m_subjectPublicKeyInfo, m_attributes) = + ParseCertificationRequestInfo(m_certificationRequestInfo); + + // Read SignatureAlgorithm + AsnReader algReader = sequenceReader.ReadSequence(); + m_signatureAlgorithm = algReader.ReadObjectIdentifier(); + + // Read Signature (BIT STRING) + m_signature = sequenceReader.ReadBitString(out int unusedBitCount); + if (unusedBitCount != 0) + { + throw new CryptographicException("Invalid signature bit string padding."); + } + + sequenceReader.ThrowIfNotEmpty(); + } + catch (AsnContentException ex) + { + throw new CryptographicException("Failed to parse PKCS#10 certificate request.", ex); + } + } + + /// + /// Gets the subject distinguished name from the CSR. + /// + public X500DistinguishedName Subject => m_subject; + + /// + /// Gets the subject public key info as DER-encoded bytes. + /// + public byte[] SubjectPublicKeyInfo => m_subjectPublicKeyInfo; + + /// + /// Gets the attributes from the CSR. + /// + public byte[] Attributes => m_attributes; + + /// + /// Verifies the signature of the certificate request. + /// + /// True if the signature is valid; otherwise, false. + public bool Verify() + { + try + { + // Get the hash algorithm from the signature algorithm OID + HashAlgorithmName hashAlgorithm = Oids.GetHashAlgorithmName(m_signatureAlgorithm); + + // Parse the public key to get the key for verification + var publicKeyReader = new AsnReader(m_subjectPublicKeyInfo, AsnEncodingRules.DER); + AsnReader pkSequence = publicKeyReader.ReadSequence(); + + // Read algorithm identifier + AsnReader algIdReader = pkSequence.ReadSequence(); + string keyAlgorithmOid = algIdReader.ReadObjectIdentifier(); + + // Read public key (BIT STRING) + byte[] publicKeyBytes = pkSequence.ReadBitString(out int unusedBitCount); + if (unusedBitCount != 0) + { + throw new CryptographicException("Invalid public key bit string padding."); + } + + // Verify based on key type + if (keyAlgorithmOid == Oids.Rsa) + { + return VerifyRsaSignature(publicKeyBytes, hashAlgorithm); + } + else if (keyAlgorithmOid == Oids.ECPublicKey) + { + return VerifyEcdsaSignature(publicKeyBytes, hashAlgorithm); + } + else + { + throw new NotSupportedException($"Unsupported key algorithm: {keyAlgorithmOid}"); + } + } + catch (CryptographicException) + { + // Invalid CSR format or signature + return false; + } + catch (AsnContentException) + { + // Invalid ASN.1 structure + return false; + } + catch (NotSupportedException) + { + // Unsupported algorithm - rethrow to inform caller + throw; + } + catch (Exception) + { + // Any other unexpected error + return false; + } + } + + /// + /// Gets the certification request info as DER-encoded bytes. + /// + /// The certification request info bytes. + public byte[] GetCertificationRequestInfo() + { + return m_certificationRequestInfo; + } + + private static (X500DistinguishedName subject, byte[] subjectPublicKeyInfo, byte[] attributes) + ParseCertificationRequestInfo(byte[] certificationRequestInfo) + { + var infoReader = new AsnReader(certificationRequestInfo, AsnEncodingRules.DER); + AsnReader infoSequence = infoReader.ReadSequence(); + + // Read version (INTEGER) + infoSequence.ReadInteger(); + + // Read subject (Name - SEQUENCE) + byte[] subjectBytes = infoSequence.ReadEncodedValue().ToArray(); + var subject = new X500DistinguishedName(subjectBytes); + + // Read SubjectPublicKeyInfo (SEQUENCE) + byte[] subjectPublicKeyInfo = infoSequence.ReadEncodedValue().ToArray(); + + // Read attributes [0] IMPLICIT + byte[] attributes = null; + if (infoSequence.HasData) + { + // Attributes are context-specific tag [0] + attributes = infoSequence.ReadEncodedValue().ToArray(); + } + + return (subject, subjectPublicKeyInfo, attributes); + } + + private bool VerifyRsaSignature(byte[] publicKeyBytes, HashAlgorithmName hashAlgorithm) + { + // Parse RSA public key from PKCS#1 format + var keyReader = new AsnReader(publicKeyBytes, AsnEncodingRules.DER); + AsnReader keySequence = keyReader.ReadSequence(); + + // Read modulus and exponent + ReadOnlyMemory modulus = keySequence.ReadIntegerBytes(); + if (modulus.Length > 1 && modulus.Span[0] == 0) + { + modulus = modulus[1..]; + } + + // Validate key sizes to prevent issues with malformed CSRs + // Modulus should be at least 1024 bits (128 bytes), commonly 2048+ bits + if (modulus.Length < 128) + { + throw new CryptographicException( + $"RSA modulus is too small: {modulus.Length * 8} bits. Minimum is 1024 bits."); + } + + // Exponent should be small (commonly 3) + ReadOnlyMemory exponent = keySequence.ReadIntegerBytes(); + if (exponent.Length > 8) + { + throw new CryptographicException( + $"RSA exponent is unexpectedly large: {exponent.Length} bytes."); + } + + // Create RSA parameters + var rsaParameters = new RSAParameters + { + Modulus = modulus.ToArray(), + Exponent = exponent.ToArray() + }; + + using var rsa = RSA.Create(); + rsa.ImportParameters(rsaParameters); + + // Verify signature using PKCS#1 v1.5 padding + return rsa.VerifyData( + m_certificationRequestInfo, + m_signature, + hashAlgorithm, + RSASignaturePadding.Pkcs1); + } + + private bool VerifyEcdsaSignature(byte[] publicKeyBytes, HashAlgorithmName hashAlgorithm) + { +#if NET6_0_OR_GREATER + // .NET 6+ has ImportSubjectPublicKeyInfo + using var ecdsa = ECDsa.Create(); + try + { + ecdsa.ImportSubjectPublicKeyInfo(m_subjectPublicKeyInfo, out _); + + // PKCS#10 CSRs store ECDSA signatures in DER format (ASN.1 SEQUENCE) + // but .NET's VerifyData expects IEEE P1363 format (r || s) + // We need to convert the signature format + byte[] ieee1363Signature = ConvertEcdsaSignatureDerToIeee1363(m_signature); + + // Verify signature + return ecdsa.VerifyData( + m_certificationRequestInfo, + ieee1363Signature, + hashAlgorithm); + } + catch + { + return false; + } +#else + // For .NET Framework 4.8 and .NET Standard 2.x, ECDSA CSR verification is not supported + // This is acceptable as the GDS Server primarily uses RSA certificates + throw new NotSupportedException( + "ECDSA certificate signing request verification is not supported on this platform. " + + "Please use .NET 6.0 or later."); +#endif + } + + /// + /// Converts ECDSA signature from DER format to IEEE P1363 format. + /// + /// DER-encoded signature (SEQUENCE { r INTEGER, s INTEGER }) + /// IEEE P1363 format signature (r || s) + private static byte[] ConvertEcdsaSignatureDerToIeee1363(byte[] derSignature) + { + // Parse DER SEQUENCE + var reader = new AsnReader(derSignature, AsnEncodingRules.DER); + AsnReader sequenceReader = reader.ReadSequence(); + + // Read r and s as integers + byte[] r = sequenceReader.ReadIntegerBytes().ToArray(); + byte[] s = sequenceReader.ReadIntegerBytes().ToArray(); + + // Remove leading zero bytes that may be present for sign bit in ASN.1 INTEGER encoding + r = TrimLeadingZero(r); + s = TrimLeadingZero(s); + + // Determine the key size - both r and s should have the same size for ECDSA + // The size is determined by the larger of the two after trimming + // Common sizes: P-256: 32 bytes, P-384: 48 bytes, P-521: 66 bytes + int keySize = Math.Max(r.Length, s.Length); + + // For standard curves, round up to the nearest standard size if needed + if (keySize <= 32) + { + keySize = 32; // P-256 + } + else if (keySize <= 48) + { + keySize = 48; // P-384 + } + else if (keySize <= 66) + { + keySize = 66; // P-521 + } + + // Pad to the correct size + byte[] ieee1363Signature = new byte[keySize * 2]; + Array.Copy(r, 0, ieee1363Signature, keySize - r.Length, r.Length); + Array.Copy(s, 0, ieee1363Signature, keySize * 2 - s.Length, s.Length); + + return ieee1363Signature; + } + + /// + /// Removes leading zero byte if present (used for sign bit in ASN.1 INTEGER encoding). + /// ASN.1 INTEGER adds a leading zero byte when the most significant bit is 1 + /// to prevent the value from being interpreted as negative. + /// + private static byte[] TrimLeadingZero(byte[] data) + { + if (data.Length > 1 && data[0] == 0 && (data[1] & 0x80) == 0x80) + { + byte[] trimmed = new byte[data.Length - 1]; + Array.Copy(data, 1, trimmed, 0, trimmed.Length); + return trimmed; + } + return data; + } + } +} diff --git a/Libraries/Opc.Ua.Security.Certificates/PKCS10/Pkcs10Utils.cs b/Libraries/Opc.Ua.Security.Certificates/PKCS10/Pkcs10Utils.cs new file mode 100644 index 0000000000..a9100ea759 --- /dev/null +++ b/Libraries/Opc.Ua.Security.Certificates/PKCS10/Pkcs10Utils.cs @@ -0,0 +1,126 @@ +/* ======================================================================== + * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System; +using System.Formats.Asn1; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +namespace Opc.Ua.Security.Certificates +{ + /// + /// Utility class for parsing PKCS#10 CSR attributes and extensions. + /// + public static class Pkcs10Utils + { + // OID for PKCS#9 Extension Request attribute + private const string Pkcs9AtExtensionRequest = "1.2.840.113549.1.9.14"; + + // OID for Subject Alternative Name extension + private const string SubjectAlternativeNameOid = "2.5.29.17"; + + /// + /// Extracts the Subject Alternative Name extension from CSR attributes. + /// + /// The CSR attributes encoded as DER bytes. + /// The X509SubjectAltNameExtension if found; otherwise, null. + public static X509SubjectAltNameExtension GetSubjectAltNameExtension(byte[] attributes) + { + if (attributes == null || attributes.Length == 0) + { + return null; + } + + try + { + // Attributes are encoded as [0] IMPLICIT Attributes + // which is a SET OF Attribute + var reader = new AsnReader(attributes, AsnEncodingRules.DER); + + // Read the context-specific tag [0] + AsnReader attributesReader = reader.ReadSetOf(new Asn1Tag(TagClass.ContextSpecific, 0)); + + while (attributesReader.HasData) + { + // Each attribute is a SEQUENCE + AsnReader attributeReader = attributesReader.ReadSequence(); + + // Read the attribute type (OID) + string attributeOid = attributeReader.ReadObjectIdentifier(); + + // Read the attribute values (SET) + AsnReader valuesReader = attributeReader.ReadSetOf(); + + // Check if this is an Extension Request attribute + if (attributeOid == Pkcs9AtExtensionRequest) + { + // The extension request contains a SEQUENCE of extensions + AsnReader extensionsSequenceReader = valuesReader.ReadSequence(); + + while (extensionsSequenceReader.HasData) + { + // Each extension is a SEQUENCE + AsnReader extensionReader = extensionsSequenceReader.ReadSequence(); + + // Read extension OID + string extensionOid = extensionReader.ReadObjectIdentifier(); + + // Check for critical flag (optional BOOLEAN, default FALSE) + bool critical = false; + if (extensionReader.PeekTag().HasSameClassAndValue(Asn1Tag.Boolean)) + { + critical = extensionReader.ReadBoolean(); + } + + // Read extension value (OCTET STRING) + byte[] extensionValue = extensionReader.ReadOctetString(); + + // Check if this is the Subject Alternative Name extension + if (extensionOid == SubjectAlternativeNameOid) + { + var asnEncodedData = new AsnEncodedData( + SubjectAlternativeNameOid, + extensionValue); + return new X509SubjectAltNameExtension(asnEncodedData, critical); + } + } + } + } + } + catch (Exception ex) + { + throw new CryptographicException( + "Failed to parse CSR attributes for Subject Alternative Name extension.", + ex); + } + + return null; + } + } +} diff --git a/Libraries/Opc.Ua.Security.Certificates/X509Crl/X509Signature.cs b/Libraries/Opc.Ua.Security.Certificates/X509Crl/X509Signature.cs index 260b476e59..33839adf82 100644 --- a/Libraries/Opc.Ua.Security.Certificates/X509Crl/X509Signature.cs +++ b/Libraries/Opc.Ua.Security.Certificates/X509Crl/X509Signature.cs @@ -211,6 +211,10 @@ private bool VerifyForECDsa(X509Certificate2 certificate) return false; } byte[] decodedSignature = DecodeECDsa(Signature, key.KeySize); + if (decodedSignature == null) + { + return false; + } return key.VerifyData(Tbs, decodedSignature, Name); } @@ -237,6 +241,7 @@ private static string DecodeAlgorithm(byte[] oid) /// /// The signature to decode from ASN.1 /// The keySize in bits. + /// private static byte[] DecodeECDsa(ReadOnlyMemory signature, int keySize) { var reader = new AsnReader(signature, AsnEncodingRules.DER); @@ -245,16 +250,21 @@ private static byte[] DecodeECDsa(ReadOnlyMemory signature, int keySize) ReadOnlyMemory r = seqReader.ReadIntegerBytes(); ReadOnlyMemory s = seqReader.ReadIntegerBytes(); seqReader.ThrowIfNotEmpty(); - keySize >>= 3; - if (r.Span[0] == 0 && r.Length > keySize) + if (r.Span[0] == 0 && r.Length > 1) { r = r[1..]; } - if (s.Span[0] == 0 && s.Length > keySize) + if (s.Span[0] == 0 && s.Length > 1) { s = s[1..]; } + keySize >>= 3; byte[] result = new byte[2 * keySize]; + if (r.Length > keySize || s.Length > keySize) + { + // Size does not match our key size. + return null; + } int offset = keySize - r.Length; r.CopyTo(new Memory(result, offset, r.Length)); offset = (2 * keySize) - s.Length; diff --git a/Tests/Opc.Ua.Gds.Tests/ClientTest.cs b/Tests/Opc.Ua.Gds.Tests/ClientTest.cs index b9d6611610..76f0c90600 100644 --- a/Tests/Opc.Ua.Gds.Tests/ClientTest.cs +++ b/Tests/Opc.Ua.Gds.Tests/ClientTest.cs @@ -131,6 +131,12 @@ .. m_server .SelectMany(cg => cg.CertificateTypes) .Select(s => typeof(Ua.ObjectTypeIds).GetField(s).GetValue(null) as NodeId) .Where(n => n != null && Utils.IsSupportedCertificateType(n)) +#if NETFRAMEWORK + // Only rsa gds issuance supported in net framework + .Where(n => + n == Ua.ObjectTypeIds.RsaSha256ApplicationCertificateType || + n == Ua.ObjectTypeIds.RsaMinApplicationCertificateType) +#endif ]; } diff --git a/Tests/Opc.Ua.Gds.Tests/PushTest.cs b/Tests/Opc.Ua.Gds.Tests/PushTest.cs index dd208295ba..00db3c87e1 100644 --- a/Tests/Opc.Ua.Gds.Tests/PushTest.cs +++ b/Tests/Opc.Ua.Gds.Tests/PushTest.cs @@ -664,6 +664,13 @@ public async Task UpdateCertificateCASignedAsync() public async Task UpdateCertificateCASignedAsync(bool regeneratePrivateKey) { +#if NETFRAMEWORK + if (m_certificateType != OpcUa.ObjectTypeIds.RsaMinApplicationCertificateType && + m_certificateType != OpcUa.ObjectTypeIds.RsaSha256ApplicationCertificateType) + { + NUnit.Framework.Assert.Ignore("ECC signing requests not yet supported on .NET Framework"); + } +#endif await ConnectPushClientAsync(true).ConfigureAwait(false); await ConnectGDSClientAsync(true).ConfigureAwait(false); TestContext.Out.WriteLine("Create Signing Request"); diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/Assets/test_rsa.csr b/Tests/Opc.Ua.Security.Certificates.Tests/Assets/test_rsa.csr new file mode 100644 index 0000000000..7d98d96ffd Binary files /dev/null and b/Tests/Opc.Ua.Security.Certificates.Tests/Assets/test_rsa.csr differ diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/Pkcs10CertificationRequestTests.cs b/Tests/Opc.Ua.Security.Certificates.Tests/Pkcs10CertificationRequestTests.cs new file mode 100644 index 0000000000..18568358a2 --- /dev/null +++ b/Tests/Opc.Ua.Security.Certificates.Tests/Pkcs10CertificationRequestTests.cs @@ -0,0 +1,349 @@ +/* ======================================================================== + * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using NUnit.Framework; +using Opc.Ua.Tests; +using Assert = NUnit.Framework.Legacy.ClassicAssert; + +namespace Opc.Ua.Security.Certificates.Tests +{ + /// + /// Tests for the Pkcs10CertificationRequest class. + /// + [TestFixture] + [Category("Certificate")] + [Category("PKCS10")] + [Parallelizable] + [SetCulture("en-us")] + public class Pkcs10CertificationRequestTests + { + #region Test Methods + /// + /// Test parsing a valid RSA CSR from file. + /// + [Test] + public void ParseValidRsaCsrFromFile() + { + // Load the test CSR file + string csrPath = Path.Combine(Utils.GetAbsoluteDirectoryPath("Assets", true, false, false), "test_rsa.csr"); + byte[] csrData = File.ReadAllBytes(csrPath); + + // Parse the CSR + var csr = new Pkcs10CertificationRequest(csrData); + + // Verify subject + Assert.NotNull(csr.Subject); + Assert.IsNotEmpty(csr.Subject.Name); + + // Verify public key info + Assert.NotNull(csr.SubjectPublicKeyInfo); + Assert.Greater(csr.SubjectPublicKeyInfo.Length, 0); + + // Verify signature + bool isValid = csr.Verify(); + Assert.True(isValid, "CSR signature should be valid"); + } + + /// + /// Test creating and parsing an RSA CSR. + /// + [Test] + public void CreateAndParseRsaCsr() + { + const string subject = "CN=Test RSA CSR, O=OPC Foundation"; + string applicationUri = "urn:localhost:opcfoundation.org:TestRsaCsr"; + string[] domainNames = new[] { "localhost", "127.0.0.1" }; + + // Create a certificate to generate CSR from + using X509Certificate2 certificate = CertificateBuilder.Create(subject) + .SetNotBefore(DateTime.UtcNow.AddDays(-1)) + .SetLifeTime(TimeSpan.FromDays(30)) + .AddExtension(new X509SubjectAltNameExtension(applicationUri, domainNames)) + .CreateForRSA(); + + // Create CSR + byte[] csrData = CertificateFactory.CreateSigningRequest(certificate, domainNames); + Assert.NotNull(csrData); + Assert.Greater(csrData.Length, 0); + + // Parse the CSR + var csr = new Pkcs10CertificationRequest(csrData); + + // Verify subject + Assert.NotNull(csr.Subject); + Assert.That(csr.Subject.Name, Does.Contain("CN=Test RSA CSR")); + + // Verify signature + bool isValid = csr.Verify(); + Assert.True(isValid, "CSR signature should be valid"); + + // Verify SubjectPublicKeyInfo + Assert.NotNull(csr.SubjectPublicKeyInfo); + Assert.Greater(csr.SubjectPublicKeyInfo.Length, 0); + } + + /// + /// Test creating and parsing an ECDSA CSR (P-256). + /// + [Test] + public void CreateAndParseEcdsaCsrP256() + { + const string subject = "CN=Test ECDSA P256 CSR, O=OPC Foundation"; + string applicationUri = "urn:localhost:opcfoundation.org:TestEcdsaCsr"; + string[] domainNames = new[] { "localhost", "127.0.0.1" }; + + // Create a certificate to generate CSR from + using X509Certificate2 certificate = CertificateBuilder.Create(subject) + .SetNotBefore(DateTime.UtcNow.AddDays(-1)) + .SetLifeTime(TimeSpan.FromDays(30)) + .AddExtension(new X509SubjectAltNameExtension(applicationUri, domainNames)) + .SetECCurve(ECCurve.NamedCurves.nistP256) + .CreateForECDsa(); + + // Create CSR + byte[] csrData = CertificateFactory.CreateSigningRequest(certificate, domainNames); + Assert.NotNull(csrData); + Assert.Greater(csrData.Length, 0); + + // Parse the CSR + var csr = new Pkcs10CertificationRequest(csrData); + + // Verify subject + Assert.NotNull(csr.Subject); + Assert.That(csr.Subject.Name, Does.Contain("CN=Test ECDSA P256 CSR")); + + // Verify SubjectPublicKeyInfo + Assert.NotNull(csr.SubjectPublicKeyInfo); + Assert.Greater(csr.SubjectPublicKeyInfo.Length, 0); + + // Verify signature +#if NET6_0_OR_GREATER + bool isValid = csr.Verify(); + Assert.True(isValid, "ECDSA CSR signature should be valid"); +#else + // ECDSA verification not supported on older frameworks + Assert.Throws(() => csr.Verify()); +#endif + } + + /// + /// Test parsing CSR with null data throws ArgumentNullException. + /// + [Test] + public void ParseNullCsrThrowsArgumentNullException() + { + Assert.Throws(() => new Pkcs10CertificationRequest(null)); + } + + /// + /// Test parsing invalid CSR data throws CryptographicException. + /// + [Test] + public void ParseInvalidCsrThrowsCryptographicException() + { + byte[] invalidData = new byte[] { 0x01, 0x02, 0x03, 0x04 }; + Assert.Throws(() => new Pkcs10CertificationRequest(invalidData)); + } + + /// + /// Test parsing CSR with tampered signature fails verification. + /// + [Test] + public void ParseCsrWithTamperedSignatureFails() + { + const string subject = "CN=Test Tampered CSR, O=OPC Foundation"; + string applicationUri = "urn:localhost:opcfoundation.org:TestTamperedCsr"; + string[] domainNames = new[] { "localhost" }; + + // Create a certificate to generate CSR from + using X509Certificate2 certificate = CertificateBuilder.Create(subject) + .SetNotBefore(DateTime.UtcNow.AddDays(-1)) + .SetLifeTime(TimeSpan.FromDays(30)) + .AddExtension(new X509SubjectAltNameExtension(applicationUri, domainNames)) + .CreateForRSA(); + + // Create CSR + byte[] csrData = CertificateFactory.CreateSigningRequest(certificate, domainNames); + + // Tamper with the signature (last 10 bytes) + for (int i = csrData.Length - 10; i < csrData.Length; i++) + { + csrData[i] ^= 0xFF; + } + + // Parse should succeed but verification should fail + var csr = new Pkcs10CertificationRequest(csrData); + bool isValid = csr.Verify(); + Assert.False(isValid, "Tampered CSR signature should be invalid"); + } + + /// + /// Test parsing CSR and extracting Subject Alternative Name. + /// + [Test] + public void ParseCsrAndExtractSubjectAltName() + { + const string subject = "CN=Test SAN CSR, O=OPC Foundation"; + string applicationUri = "urn:localhost:opcfoundation.org:TestSanCsr"; + string[] domainNames = new[] { "localhost", "testhost.local", "192.168.1.1" }; + + // Create a certificate to generate CSR from + using X509Certificate2 certificate = CertificateBuilder.Create(subject) + .SetNotBefore(DateTime.UtcNow.AddDays(-1)) + .SetLifeTime(TimeSpan.FromDays(30)) + .AddExtension(new X509SubjectAltNameExtension(applicationUri, domainNames)) + .CreateForRSA(); + + // Create CSR + byte[] csrData = CertificateFactory.CreateSigningRequest(certificate, domainNames); + + // Parse the CSR + var csr = new Pkcs10CertificationRequest(csrData); + + // Extract Subject Alternative Name + X509SubjectAltNameExtension sanExtension = Pkcs10Utils.GetSubjectAltNameExtension(csr.Attributes); + + Assert.NotNull(sanExtension); + Assert.That(sanExtension.Uris, Has.Count.EqualTo(1)); + Assert.That(sanExtension.Uris[0], Is.EqualTo(applicationUri)); + + // Verify domain names (may include URIs and domain names) + int totalNames = sanExtension.DomainNames.Count + sanExtension.IPAddresses.Count; + Assert.That(totalNames, Is.EqualTo(domainNames.Length)); + } + + /// + /// Test parsing CSR with minimal attributes. + /// + [Test] + public void ParseCsrWithMinimalAttributes() + { + const string subject = "CN=Test Minimal Attributes CSR, O=OPC Foundation"; + + // Create a simple certificate without explicit SAN + using X509Certificate2 certificate = CertificateBuilder.Create(subject) + .SetNotBefore(DateTime.UtcNow.AddDays(-1)) + .SetLifeTime(TimeSpan.FromDays(30)) + .CreateForRSA(); + + // Create CSR + // Note: CertificateFactory.CreateSigningRequest always adds a SAN extension + byte[] csrData = CertificateFactory.CreateSigningRequest(certificate); + + // Parse the CSR + var csr = new Pkcs10CertificationRequest(csrData); + + // Extract Subject Alternative Name + // CertificateFactory always creates a SAN extension, even if empty + X509SubjectAltNameExtension sanExtension = Pkcs10Utils.GetSubjectAltNameExtension(csr.Attributes); + + // SAN extension should exist (created by CertificateFactory) + Assert.NotNull(sanExtension); + } + + /// + /// Test GetCertificationRequestInfo returns valid data. + /// + [Test] + public void GetCertificationRequestInfoReturnsValidData() + { + const string subject = "CN=Test Info CSR, O=OPC Foundation"; + + using X509Certificate2 certificate = CertificateBuilder.Create(subject) + .SetNotBefore(DateTime.UtcNow.AddDays(-1)) + .SetLifeTime(TimeSpan.FromDays(30)) + .CreateForRSA(); + + byte[] csrData = CertificateFactory.CreateSigningRequest(certificate); + var csr = new Pkcs10CertificationRequest(csrData); + + byte[] requestInfo = csr.GetCertificationRequestInfo(); + Assert.NotNull(requestInfo); + Assert.Greater(requestInfo.Length, 0); + } + + /// + /// Test parsing multiple CSRs in sequence. + /// + [Test] + public void ParseMultipleCsrsInSequence() + { + const int count = 5; + var csrList = new System.Collections.Generic.List(); + + for (int i = 0; i < count; i++) + { + string subject = $"CN=Test CSR {i}, O=OPC Foundation"; + string applicationUri = $"urn:localhost:opcfoundation.org:TestCsr{i}"; + + using X509Certificate2 certificate = CertificateBuilder.Create(subject) + .SetNotBefore(DateTime.UtcNow.AddDays(-1)) + .SetLifeTime(TimeSpan.FromDays(30)) + .AddExtension(new X509SubjectAltNameExtension(applicationUri, new[] { "localhost" })) + .CreateForRSA(); + + byte[] csrData = CertificateFactory.CreateSigningRequest(certificate); + var csr = new Pkcs10CertificationRequest(csrData); + + Assert.NotNull(csr); + Assert.True(csr.Verify()); + csrList.Add(csr); + } + + Assert.That(csrList, Has.Count.EqualTo(count)); + } + + /// + /// Test that Subject property contains expected DN components. + /// + [Test] + public void SubjectContainsExpectedDNComponents() + { + const string subject = "CN=TestSubject, O=TestOrg, C=US, ST=TestState, L=TestCity"; + + using X509Certificate2 certificate = CertificateBuilder.Create(subject) + .SetNotBefore(DateTime.UtcNow.AddDays(-1)) + .SetLifeTime(TimeSpan.FromDays(30)) + .CreateForRSA(); + + byte[] csrData = CertificateFactory.CreateSigningRequest(certificate); + var csr = new Pkcs10CertificationRequest(csrData); + + string subjectName = csr.Subject.Name; + Assert.That(subjectName, Does.Contain("CN=TestSubject")); + Assert.That(subjectName, Does.Contain("O=TestOrg")); + } + #endregion + } +}