/* ======================================================================== * 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.Collections.Generic; using System.Formats.Asn1; using System.Linq; using System.Numerics; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; namespace Opc.Ua.Security.Certificates { /// /// Supporting functions for X509 extensions. /// public static class X509Extensions { /// /// Find a typed extension in a certificate. /// /// The type of the extension. /// The certificate with extensions. public static T? FindExtension(this Certificate certificate) where T : X509Extension { return FindExtension(certificate.Extensions); } /// /// Find a typed extension in a extension collection. /// /// The type of the extension. /// The extensions to search. /// is null. public static T? FindExtension(this X509ExtensionCollection extensions) where T : X509Extension { if (extensions == null) { throw new ArgumentNullException(nameof(extensions)); } lock (extensions.SyncRoot) { // search known custom extensions if (typeof(T) == typeof(X509AuthorityKeyIdentifierExtension)) { X509Extension? extension = extensions .Cast() .FirstOrDefault(e => e.Oid?.Value is X509AuthorityKeyIdentifierExtension.AuthorityKeyIdentifierOid or X509AuthorityKeyIdentifierExtension .AuthorityKeyIdentifier2Oid); if (extension != null) { return new X509AuthorityKeyIdentifierExtension( extension, extension.Critical) as T; } } if (typeof(T) == typeof(X509SubjectAltNameExtension)) { X509Extension? extension = extensions .Cast() .FirstOrDefault(e => e.Oid?.Value is X509SubjectAltNameExtension.SubjectAltNameOid or X509SubjectAltNameExtension.SubjectAltName2Oid); if (extension != null) { return new X509SubjectAltNameExtension(extension, extension.Critical) as T; } } if (typeof(T) == typeof(X509CrlNumberExtension)) { X509Extension? extension = extensions .Cast() .FirstOrDefault(e => e.Oid?.Value == X509CrlNumberExtension.CrlNumberOid); if (extension != null) { return new X509CrlNumberExtension(extension, extension.Critical) as T; } } // search builtin extension return extensions.OfType().FirstOrDefault(); } } /// /// Build the Authority information Access extension. /// /// Array of CA Issuer Urls /// optional, the OCSP responder /// public static X509Extension BuildX509AuthorityInformationAccess( this string[] caIssuerUrls, string? ocspResponder = null) { if (string.IsNullOrEmpty(ocspResponder) && (caIssuerUrls == null || caIssuerUrls.Length == 0)) { throw new ArgumentNullException( nameof(caIssuerUrls), "One CA Issuer Url or OCSP responder is required for the extension."); } var generalNameUriChoice = new Asn1Tag(TagClass.ContextSpecific, 6); var writer = new AsnWriter(AsnEncodingRules.DER); writer.PushSequence(); if (caIssuerUrls != null) { foreach (string caIssuerUrl in caIssuerUrls) { writer.PushSequence(); writer.WriteObjectIdentifier(Oids.CertificateAuthorityIssuers); writer.WriteCharacterString( UniversalTagNumber.IA5String, caIssuerUrl, generalNameUriChoice); writer.PopSequence(); } } if (!string.IsNullOrEmpty(ocspResponder)) { writer.PushSequence(); writer.WriteObjectIdentifier(Oids.OnlineCertificateStatusProtocol); writer.WriteCharacterString( UniversalTagNumber.IA5String, ocspResponder!, generalNameUriChoice); writer.PopSequence(); } writer.PopSequence(); return new X509Extension(Oids.AuthorityInfoAccess, writer.Encode(), false); } /// /// Build the CRL Distribution Point extension. /// /// The CRL distribution point public static X509Extension BuildX509CRLDistributionPoints(this string distributionPoint) { return BuildX509CRLDistributionPoints([distributionPoint]); } /// /// Build the CRL Distribution Point extension with multiple distribution points. /// /// The CRL distribution points public static X509Extension BuildX509CRLDistributionPoints( this IEnumerable distributionPoints) { var context0 = new Asn1Tag(TagClass.ContextSpecific, 0, true); Asn1Tag distributionPointChoice = context0; Asn1Tag fullNameChoice = context0; var generalNameUriChoice = new Asn1Tag(TagClass.ContextSpecific, 6); var writer = new AsnWriter(AsnEncodingRules.DER); writer.PushSequence(); writer.PushSequence(); writer.PushSequence(distributionPointChoice); writer.PushSequence(fullNameChoice); foreach (string distributionPoint in distributionPoints) { writer.WriteCharacterString( UniversalTagNumber.IA5String, distributionPoint, generalNameUriChoice); } writer.PopSequence(fullNameChoice); writer.PopSequence(distributionPointChoice); writer.PopSequence(); writer.PopSequence(); return new X509Extension(Oids.CRLDistributionPoint, writer.Encode(), false); } /// /// Read an ASN.1 extension sequence as X509Extension object. /// /// The ASN reader. public static X509Extension? ReadExtension(this AsnReader reader) { if (reader.HasData) { var boolTag = new Asn1Tag(UniversalTagNumber.Boolean); AsnReader extReader = reader.ReadSequence(); string extOid = extReader.ReadObjectIdentifier(); bool critical = false; Asn1Tag peekTag = extReader.PeekTag(); if (peekTag == boolTag) { critical = extReader.ReadBoolean(); } byte[] data = extReader.ReadOctetString(); extReader.ThrowIfNotEmpty(); return new X509Extension(new Oid(extOid), data, critical); } return null; } /// /// Write an extension object as ASN.1. /// /// public static void WriteExtension(this AsnWriter writer, X509Extension extension) { Asn1Tag etag = Asn1Tag.Sequence; writer.PushSequence(etag); writer.WriteObjectIdentifier(extension.Oid?.Value ?? throw new CryptographicException("Extension OID value is null.")); if (extension.Critical) { writer.WriteBoolean(extension.Critical); } writer.WriteOctetString(extension.RawData); writer.PopSequence(etag); } /// /// Build the CRL Reason extension. /// public static X509Extension BuildX509CRLReason(this CRLReason reason) { var writer = new AsnWriter(AsnEncodingRules.DER); writer.WriteEnumeratedValue(reason); return new X509Extension(Oids.CrlReasonCode, writer.Encode(), false); } /// /// Build the Authority Key Identifier from an Issuer CA certificate. /// /// The issuer CA certificate public static X509Extension BuildAuthorityKeyIdentifier( this Certificate issuerCaCertificate) { // force exception if SKI is not present X509SubjectKeyIdentifierExtension ski = issuerCaCertificate .Extensions .OfType() .Single(); return new X509AuthorityKeyIdentifierExtension( ski.SubjectKeyIdentifier.FromHexString() ?? [], issuerCaCertificate.IssuerName, issuerCaCertificate.GetSerialNumber()); } /// /// Build the CRL number. /// public static X509Extension BuildCRLNumber(this BigInteger crlNumber) { var writer = new AsnWriter(AsnEncodingRules.DER); writer.WriteInteger(crlNumber); return new X509Extension(Oids.CrlNumber, writer.Encode(), false); } /// /// Patch serial number in a Url. byte version. /// public static string PatchExtensionUrl(this string extensionUrl, byte[] serialNumber) { return PatchExtensionUrl(extensionUrl, serialNumber.ToHexString()); } /// /// Patch serial number in a Url. string version. /// public static string PatchExtensionUrl(this string extensionUrl, string serial) { return extensionUrl.Replace("%serial%", serial.ToLowerInvariant(), StringComparison.Ordinal); } } }