/* ========================================================================
* 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);
}
}
}