| File: System\Security\Cryptography\X509Certificates\CertificateRevocationListBuilder.cs | Web Access |
| Project: src\runtime\src\libraries\System.Security.Cryptography\src\System.Security.Cryptography.csproj (System.Security.Cryptography) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; using System.Collections.Generic; using System.Diagnostics; using System.Formats.Asn1; using System.Numerics; using System.Security.Cryptography.Asn1; namespace System.Security.Cryptography.X509Certificates { /// <summary> /// Facilitates building a Certificate Revocation List (CRL). /// </summary> public sealed partial class CertificateRevocationListBuilder { private readonly List<RevokedCertificate> _revoked; private AsnWriter? _writer; /// <summary> /// Initializes a new instance of the <see cref="CertificateRevocationListBuilder" /> class. /// </summary> public CertificateRevocationListBuilder() { _revoked = new List<RevokedCertificate>(); } private CertificateRevocationListBuilder(List<RevokedCertificate> revoked) { Debug.Assert(revoked != null); _revoked = revoked; } /// <summary> /// Adds the specified certificate to the revocation list with an optional revocation time /// and an optional revocation reason. /// </summary> /// <param name="certificate"> /// The certificate to revoke. /// </param> /// <param name="revocationTime"> /// The time the certificate was revoked, /// or <see langword="null" /> to use the current system time. /// The default is <see langword="null" />. /// </param> /// <param name="reason"> /// The reason why the certificate was revoked, /// or <see langword="null" /> to not include a reason. /// The default is <see langword="null" />. /// </param> /// <remarks> /// This method does not check that the certificate issuer is appropriate for the /// CRL that is being built, the certificate is just used for extracting the serial /// number. /// </remarks> /// <exception cref="ArgumentNullException"> /// <paramref name="certificate"/> is <see langword="null" />. /// </exception> /// <exception cref="ArgumentOutOfRangeException"> /// <paramref name="reason" /> is not a supported revocation reason. /// </exception> public void AddEntry( X509Certificate2 certificate, DateTimeOffset? revocationTime = null, X509RevocationReason? reason = null) { ArgumentNullException.ThrowIfNull(certificate); AddEntry(certificate.SerialNumberBytes.Span, revocationTime, reason); } /// <summary> /// Adds the specified serial number to the revocation list with an optional revocation time /// and an optional revocation reason. /// </summary> /// <param name="serialNumber"> /// The serial number of the certificate to revoke. /// </param> /// <param name="revocationTime"> /// The time the certificate was revoked, /// or <see langword="null" /> to use the current system time. /// The default is <see langword="null" />. /// </param> /// <param name="reason"> /// The reason why the certificate was revoked, /// or <see langword="null" /> to not include a reason. /// The default is <see langword="null" />. /// </param> /// <exception cref="ArgumentNullException"> /// <paramref name="serialNumber"/> is <see langword="null" />. /// </exception> /// <exception cref="ArgumentOutOfRangeException"> /// <paramref name="reason" /> is not a supported revocation reason. /// </exception> /// <exception cref="ArgumentException"> /// <paramref name="serialNumber"/> is empty. /// </exception> public void AddEntry( byte[] serialNumber, DateTimeOffset? revocationTime = null, X509RevocationReason? reason = null) { ArgumentNullException.ThrowIfNull(serialNumber); AddEntry(new ReadOnlySpan<byte>(serialNumber), revocationTime, reason); } /// <summary> /// Adds the specified serial number to the revocation list with an optional revocation time /// and an optional revocation reason. /// </summary> /// <param name="serialNumber"> /// The serial number of the certificate to revoke. /// </param> /// <param name="revocationTime"> /// The time the certificate was revoked, /// or <see langword="null" /> to use the current system time. /// The default is <see langword="null" />. /// </param> /// <param name="reason"> /// The reason why the certificate was revoked, /// or <see langword="null" /> to not include a reason. /// The default is <see langword="null" />. /// </param> /// <exception cref="ArgumentOutOfRangeException"> /// <paramref name="reason" /> is not a supported revocation reason. /// </exception> /// <exception cref="ArgumentException"> /// <paramref name="serialNumber"/> is empty. /// </exception> public void AddEntry( ReadOnlySpan<byte> serialNumber, DateTimeOffset? revocationTime = null, X509RevocationReason? reason = null) { if (serialNumber.IsEmpty) throw new ArgumentException(SR.Arg_EmptyOrNullArray, nameof(serialNumber)); if (serialNumber.Length > 1) { if ((serialNumber[0] == 0x00 && serialNumber[1] < 0x80) || (serialNumber[0] == 0xFF && serialNumber[1] > 0x7F)) { throw new ArgumentException( SR.Argument_InvalidSerialNumberBytes, nameof(serialNumber)); } } byte[]? extensions = null; if (reason.HasValue) { X509RevocationReason reasonValue = reason.GetValueOrDefault(); switch (reasonValue) { case X509RevocationReason.Unspecified: case X509RevocationReason.KeyCompromise: case X509RevocationReason.CACompromise: case X509RevocationReason.AffiliationChanged: case X509RevocationReason.Superseded: case X509RevocationReason.CessationOfOperation: case X509RevocationReason.CertificateHold: case X509RevocationReason.PrivilegeWithdrawn: case X509RevocationReason.WeakAlgorithmOrKey: break; default: // Includes RemoveFromCrl (no delta CRL support) // Includes AaCompromise (no support for attribute certificates) throw new ArgumentOutOfRangeException( nameof(reason), reasonValue, SR.Cryptography_CRLBuilder_ReasonNotSupported); } AsnWriter writer = (_writer ??= new AsnWriter(AsnEncodingRules.DER)); writer.Reset(); // SEQUENCE OF Extension using (writer.PushSequence()) { // Extension using (writer.PushSequence()) { writer.WriteObjectIdentifier(Oids.CrlReasons); using (writer.PushOctetString()) { writer.WriteEnumeratedValue(reasonValue); } } } extensions = writer.Encode(); } _revoked.Add( new RevokedCertificate { Serial = serialNumber.ToArray(), RevocationTime = (revocationTime ?? DateTimeOffset.UtcNow).ToUniversalTime(), Extensions = extensions, }); } /// <summary> /// Removes the specified serial number from the revocation list. /// </summary> /// <param name="serialNumber"> /// The serial number to remove. /// </param> /// <returns> /// <see langword="true" /> if the serial number was found in the list and was removed; /// otherwise, <see langword="false" />. /// </returns> /// <remarks> /// This method assumes that the same serial number is not present on the list more than once, /// and thus stops at the first match. /// </remarks> /// <exception cref="ArgumentNullException"> /// <paramref name="serialNumber"/> is <see langword="null" />. /// </exception> public bool RemoveEntry(byte[] serialNumber) { ArgumentNullException.ThrowIfNull(serialNumber); return RemoveEntry(new ReadOnlySpan<byte>(serialNumber)); } /// <summary> /// Removes the specified serial number from the revocation list. /// </summary> /// <param name="serialNumber"> /// The serial number to remove. /// </param> /// <returns> /// <see langword="true" /> if the serial number was found in the list and was removed; /// otherwise, <see langword="false" />. /// </returns> /// <remarks> /// This method assumes that the same serial number is not present on the list more than once, /// and thus stops at the first match. /// </remarks> public bool RemoveEntry(ReadOnlySpan<byte> serialNumber) { for (int i = _revoked.Count - 1; i >= 0; i--) { if (serialNumber.SequenceEqual(_revoked[i].Serial)) { _revoked.RemoveAt(i); return true; } } return false; } private static DateTimeOffset ReadX509Time(ref ValueAsnReader reader) { if (reader.PeekTag().HasSameClassAndValue(Asn1Tag.UtcTime)) { return reader.ReadUtcTime(); } return reader.ReadGeneralizedTime(); } private static DateTimeOffset? ReadX509TimeOpt(ref ValueAsnReader reader) { if (reader.PeekTag().HasSameClassAndValue(Asn1Tag.UtcTime)) { return reader.ReadUtcTime(); } if (reader.PeekTag().HasSameClassAndValue(Asn1Tag.GeneralizedTime)) { return reader.ReadGeneralizedTime(); } return null; } private static void WriteX509Time(AsnWriter writer, DateTimeOffset time) { DateTimeOffset timeUtc = time.ToUniversalTime(); int year = timeUtc.Year; if (year >= 1950 && year < 2050) { writer.WriteUtcTime(timeUtc); } else { writer.WriteGeneralizedTime(time, omitFractionalSeconds: true); } } private struct RevokedCertificate { internal byte[] Serial; internal DateTimeOffset RevocationTime; internal byte[]? Extensions; internal RevokedCertificate(ref ValueAsnReader reader, int version) { ValueAsnReader revokedCertificate = reader.ReadSequence(); Serial = revokedCertificate.ReadIntegerBytes().ToArray(); RevocationTime = ReadX509Time(ref revokedCertificate); Extensions = null; if (version > 0 && revokedCertificate.HasData) { if (!revokedCertificate.PeekTag().HasSameClassAndValue(Asn1Tag.Sequence)) { throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } Extensions = revokedCertificate.ReadEncodedValue().ToArray(); } revokedCertificate.ThrowIfNotEmpty(); } } } }