如何在 .NET 中加载证书请求并从中创建证书

How to load a certificate request and create a certificate from it in .NET

我想从其序列化形式加载证书请求 (CSR) 并对其进行签名。 这在纯 .NET 中可能吗?

CSR 如下所示:

-----BEGIN CERTIFICATE REQUEST-----
MIIDejCCAmICAQAwZTE0MDIGCgmSJom....
-----END CERTIFICATE REQUEST-----

它是使用 .NET 4.7.2 CertificateRequest 生成的,类似于这个问题中的答案:

然后将序列化的 CSR 发送到需要创建证书的服务器 - 问题是如何创建证书。

你真的想这样做吗?

解析认证请求(俗称证书签名请求或 CSR)并盲目签名是一种非常非常糟糕的操作实践。

如果您想成为证书颁发机构,即使是私人证书颁发机构,您应该阅读并理解 CA/Browser 论坛当前(无论您何时阅读本文)基本要求文档中的所有内容,网址为 https://cabforum.org/baseline-requirements-documents/.也许你有意地决定某些东西不适用于你,但至少它是有意的。

至少您应该检查请求:

  • 不授予自己 CA 权限(提示,颁发 pathLenConstraint 为 0 的签名证书以帮助阻止这种情况),当然除非您打算创建从属 CA(但可能不会)。
  • 仅使用批准的密钥用法和扩展的密钥用法值。
  • 仅使用批准的主题名称和主题备用名称扩展值(如果请求没有 EKU 扩展,或包含 TLS 服务器用法)。
  • 未定义干扰 CA 操作的扩展(授权密钥标识符、授权信息访问、颁发者备用名称、CRL 分发点,...)
  • 不定义您不理解的任何扩展(例如 Certificate Transparency“毒药”扩展)/授权请求。

确定真的想要这样做吗?

如果你坚持...

此代码使用新的 System.Formats.Asn1 包(具体来说,它是在 .NET Framework 上使用版本 5.0.0-preview.8.20407.11 [应该是 2020 年 11 月的稳定版本 5.0.0] 进行测试的4.8 来自针对 .NET Framework 4.7.2 构建的可执行文件)。

它确实验证 proof-of-private-key-possession 签名是否有效,并且在这样做时将自己限制为 RSA-SSA-PKCS1_v1.5 签名(没有 ECDSA,没有 RSA-SSA-附言)。添加其他算法(当然)是可能的。

此代码不提供任何类型的操作策略。由调用者来验证是否只使用了适当的扩展(包括“关键”位是适当的),名称是否都是适当的,以及除了“它可以被解码和主题 public 密钥验证请求签名。

有一个 API 奇怪之处在于您需要告诉解码例程您最终打算在签署请求时使用哪种哈希算法,因为 CertificateRequest 需要在构造函数中使用它以使后续签名调用更容易。

好的,我认为免责声明就足够了,代码中还有一些免责声明。所以,这里的代码足以成为一个“糟糕的”CA。

internal static class CertificationRequestDecoder
{
    private const string BadPemRequest = "Input is not a PEM-encoded Certification Request.";

    /// <summary>
    ///   Load a CertificateRequest from a PEM-encoded Certification Request
    ///   (a.k.a. Certificate Signing Request, CSR)
    /// </summary>
    /// <param name="pem">The PEM-encoded Certification Request</param>
    /// <param name="signatureHashAlgorithm">
    ///   The hash algorithm to be used with the CA signature.
    /// </param>
    /// <returns>
    ///   A certificate request object containing the same data as the signing request.
    /// </returns>
    /// <exception cref="ArgumentNullException"><paramref name="pem"/> is <c>null</c>.</exception>
    /// <exception cref="ArgumentException">
    ///   <paramref name="pem"/> is not a well-formed PEM encoding for a Certification Request.
    /// </exception>
    /// <exception cref="AsnContentException">
    ///   <paramref name="pem"/> does not contain a well-formed Certification Request.
    /// </exception>
    /// <exception cref="InvalidOperationException">
    ///   The request contains unsupported elements.
    /// </exception>
    /// <exception cref="CryptographicException">
    ///   The Certification Request signature is invalid.
    /// </exception>
    /// <seealso cref="DecodeDer(ReadOnlyMemory{byte},HashAlgorithmName"/>
    internal static CertificateRequest DecodePem(
        string pem,
        HashAlgorithmName signatureHashAlgorithm)
    {
        if (pem == null)
            throw new ArgumentNullException(nameof(pem));

        // This PEM reader is overly lax. It should check for a newline at the end of preEB
        // and another at the beginning of postEB, but it skips it for Unix/Windows newline
        // reasons.
        //
        // After all, this is just a sample, right?
        const string PreEB = "-----BEGIN CERTIFICATE REQUEST-----";
        const string PostEB = "-----END CERTIFICATE REQUEST-----";

        int startIdx = pem.IndexOf(PreEB, StringComparison.Ordinal);
        int endIdx = pem.IndexOf(PostEB, StringComparison.Ordinal);

        if (startIdx < 0 || endIdx < 0)
            throw new ArgumentException(BadPemRequest, nameof(pem));

        if (startIdx != 0 && !string.IsNullOrWhiteSpace(pem.Substring(0, startIdx)))
            throw new ArgumentException(BadPemRequest, nameof(pem));

        if (endIdx < startIdx || !string.IsNullOrWhiteSpace(pem.Substring(endIdx + PostEB.Length)))
            throw new ArgumentException(BadPemRequest, nameof(pem));

        byte[] der;

        try
        {
            int base64Start = startIdx + PreEB.Length;
            string base64 = pem.Substring(base64Start, endIdx - base64Start);

            der = Convert.FromBase64String(base64);
        }
        catch (FormatException e)
        {
            throw new ArgumentException(BadPemRequest, nameof(pem), e);
        }

        return DecodeDer(der, signatureHashAlgorithm);
    }

    internal static CertificateRequest DecodeDer(
        byte[] der,
        HashAlgorithmName signatureHashAlgorithm)
    {
        if (der == null)
            throw new ArgumentNullException(nameof(der));

        return DecodeDer(der.AsMemory(), signatureHashAlgorithm);
    }

    /// <summary>
    ///   Load a CertificateRequest from a DER-encoded Certification Request
    ///   (a.k.a. Certificate Signing Request, CSR)
    /// </summary>
    /// <param name="der">The DER-encoded Certification Request.</param>
    /// <param name="signatureHashAlgorithm">
    ///   The hash algorithm to be used with the CA signature.
    /// </param>
    /// <returns>
    ///   A certificate request object containing the same data as the signing request.
    /// </returns>
    /// <exception cref="FormatException">
    ///   <paramref name="der"/> is not well-formed.
    /// </exception>
    /// <exception cref="InvalidOperationException">
    ///   The request contains unsupported elements.
    /// </exception>
    /// <exception cref="CryptographicException">
    ///   The Certification Request signature is invalid.
    /// </exception>
    /// <remarks>
    ///   This routine does not perform any sort of operational policy.
    ///   The caller is responsible for verifying that only valid extensions
    ///   are used, that the subject name is appropriate, and any other operational
    ///   concerns.
    /// </remarks>
    internal static CertificateRequest DecodeDer(
        ReadOnlyMemory<byte> der,
        HashAlgorithmName signatureHashAlgorithm)
    {
        AsnReader reader = new AsnReader(der, AsnEncodingRules.DER);
        AsnReader certificationRequest = reader.ReadSequence();
        reader.ThrowIfNotEmpty();

        byte[] encodedRequestInfo = certificationRequest.PeekEncodedValue().ToArray();
        AsnReader certificationRequestInfo = certificationRequest.ReadSequence();
        AsnReader algorithm = certificationRequest.ReadSequence();
        byte[] signature = certificationRequest.ReadBitString(out int unused);
        
        if (unused != 0)
        {
            throw new InvalidOperationException("The signature was not complete bytes.");
        }

        certificationRequest.ThrowIfNotEmpty();

        string algorithmOid = algorithm.ReadObjectIdentifier();
        HashAlgorithmName hashAlg;
        RSASignaturePadding signaturePadding = RSASignaturePadding.Pkcs1;

        // This only supports RSA.
        // Other algorithms could be added.
        switch (algorithmOid)
        {
            case "1.2.840.113549.1.1.5":
                hashAlg = HashAlgorithmName.SHA1;
                break;
            case "1.2.840.113549.1.1.11":
                hashAlg = HashAlgorithmName.SHA256;
                break;
            case "1.2.840.113549.1.1.12":
                hashAlg = HashAlgorithmName.SHA384;
                break;
            case "1.2.840.113549.1.1.13":
                hashAlg = HashAlgorithmName.SHA512;
                break;
            default:
                throw new InvalidOperationException(
                    $"No support for signature algorithm '{algorithmOid}'");
        }

        // Since only RSA-SSA-PKCS1 made it here, we know the parameters are missing, or NULL.
        if (algorithm.HasData)
        {
            algorithm.ReadNull();
        }

        algorithm.ThrowIfNotEmpty();

        CertificateRequest certReq =
            DecodeCertificationRequestInfo(certificationRequestInfo, signatureHashAlgorithm);

        RSA pubKey = GetRSA(certReq.PublicKey);

        if (pubKey == null)
        {
            throw new InvalidOperationException("Requested public key was not an RSA key.");
        }

        if (!pubKey.VerifyData(encodedRequestInfo, signature, hashAlg, signaturePadding))
        {
            throw new CryptographicException();
        }

        return certReq;
    }

    private static CertificateRequest DecodeCertificationRequestInfo(
        AsnReader certReqInfo,
        HashAlgorithmName signatureHashAlgorithm)
    {
        //https://tools.ietf.org/html/rfc2986#section-4.1
        // CertificationRequestInfo::= SEQUENCE {
        //     version INTEGER { v1(0) } (v1, ...),
        //     subject Name,
        //     subjectPKInfo SubjectPublicKeyInfo{ { PKInfoAlgorithms } },
        //     attributes[0] Attributes{ { CRIAttributes } }
        // }

        // As of Sept 2020, there's not a V2 request format.
        if (!certReqInfo.TryReadInt32(out int version) || version != 0)
        {
            throw new InvalidOperationException("Only V1 requests are supported.");
        }

        byte[] encodedSubject = certReqInfo.ReadEncodedValue().ToArray();
        X500DistinguishedName subject = new X500DistinguishedName(encodedSubject);

        AsnReader spki = certReqInfo.ReadSequence();
        AsnReader reqAttrs =certReqInfo.ReadSetOf(new Asn1Tag(TagClass.ContextSpecific, 0));
        certReqInfo.ThrowIfNotEmpty();

        // https://tools.ietf.org/html/rfc3280#section-4.1
        // SubjectPublicKeyInfo::= SEQUENCE {
        //     algorithm AlgorithmIdentifier,
        //     subjectPublicKey     BIT STRING
        // }

        AsnReader pubKeyAlg = spki.ReadSequence();
        string algOid = pubKeyAlg.ReadObjectIdentifier();
        byte[] algParams;

        if (pubKeyAlg.HasData)
        {
            algParams = pubKeyAlg.ReadEncodedValue().ToArray();
            pubKeyAlg.ThrowIfNotEmpty();
        }
        else
        {
            algParams = new byte[] { 0x05, 0x00 };
        }

        byte[] keyBytes = spki.ReadBitString(out int unusedBitCount);

        if (unusedBitCount != 0)
        {
            throw new InvalidOperationException(
                "The subjectPublicKey field was not made of full bytes.");
        }

        PublicKey publicKey = new PublicKey(
            new Oid(algOid, null),
            new AsnEncodedData(algParams),
            new AsnEncodedData(keyBytes));

        CertificateRequest request = new CertificateRequest(
            subject,
            publicKey,
            signatureHashAlgorithm);

        if (reqAttrs.HasData)
        {
            // This decode routine only supports one extension: the PKCS#9 extensionRequest

            // https://tools.ietf.org/html/rfc2985
            // extensionRequest ATTRIBUTE ::= {
            //     WITH SYNTAX ExtensionRequest
            //     SINGLE VALUE TRUE
            //     ID pkcs-9-at-extensionRequest
            // }
            //
            // ExtensionRequest::= Extensions

            // https://www.itu.int/ITU-T/formal-language/itu-t/x/x501/2012/InformationFramework.html
            // Attribute{ATTRIBUTE: SupportedAttributes} ::= SEQUENCE {
            //    type ATTRIBUTE.&id({SupportedAttributes}),
            //    values SET SIZE(0..MAX) OF ATTRIBUTE.&Type({SupportedAttributes}{@type}),
            //    valuesWithContext SIZE(1..MAX) OF
            //      SEQUENCE {
            //        value ATTRIBUTE.&Type({SupportedAttributes}{@type}),
            //        contextList SET SIZE(1..MAX) OF Context,
            //        ...
            //      } OPTIONAL,
            //    ...
            // }

            // https://tools.ietf.org/html/rfc5280#section-4.1
            // Extensions::= SEQUENCE SIZE(1..MAX) OF Extension
            //
            // Extension::= SEQUENCE  {
            //     extnID OBJECT IDENTIFIER,
            //     critical BOOLEAN DEFAULT FALSE,
            //     extnValue OCTET STRING
            //       --contains the DER encoding of an ASN.1 value
            //       --corresponding to the extension type identified
            //       --by extnID
            // }

            AsnReader attribute = reqAttrs.ReadSequence();
            string attrType = attribute.ReadObjectIdentifier();
            AsnReader attrValues = attribute.ReadSetOf();

            if (attrType != "1.2.840.113549.1.9.14")
            {
                throw new InvalidOperationException(
                    $"Certification Request attribute '{attrType}' is not supported.");
            }

            // No contexts are defined for the extensionRequest attribute,
            // so valuesWithContext can't exist.
            attribute.ThrowIfNotEmpty();

            // The attribute is single-value, so it must be present
            // and there mustn't be a second one.
            AsnReader extensions = attrValues.ReadSequence();
            attrValues.ThrowIfNotEmpty();

            while (extensions.HasData)
            {
                AsnReader extension = extensions.ReadSequence();
                string extnId = extension.ReadObjectIdentifier();
                bool critical = false;
                byte[] extnValue;

                if (extension.PeekTag().HasSameClassAndValue(Asn1Tag.Boolean))
                {
                    critical = extension.ReadBoolean();
                }

                extnValue = extension.ReadOctetString();
                extension.ThrowIfNotEmpty();

                X509Extension ext = new X509Extension(
                    extnId,
                    extnValue,
                    critical);

                if (CryptoConfig.CreateFromName(extnId) is X509Extension typedExtn)
                {
                    typedExtn.CopyFrom(ext);
                    ext = typedExtn;
                }

                request.CertificateExtensions.Add(ext);
            }
        }

        return request;
    }

    private static RSA GetRSA(PublicKey certReqPublicKey)
    {
        try
        {
            return certReqPublicKey.Key as RSA;
        }
        catch (CryptographicException)
        {
        }
        catch (PlatformNotSupportedException)
        {
        }

        // The try will fail on .NET Framework with any RSA key whose public exponent
        // is bigger than uint.MaxValue, because RSACryptoServiceProvider (Windows CAPI)
        // doesn't support them.

        if (certReqPublicKey.Oid.Value != "1.2.840.113549.1.1.1")
        {
            throw new InvalidOperationException(
                $"The public key algorithm '{certReqPublicKey.Oid.Value}' is not supported.");
        }

        byte[] encodedParams = certReqPublicKey.EncodedParameters.RawData;

        if (encodedParams != null && encodedParams.Length != 0)
        {
            if (encodedParams.Length != 2 ||
                encodedParams[0] != 0x05 ||
                encodedParams[1] != 0x00)
            {
                throw new InvalidOperationException(
                    "Invalid algorithm parameters for an RSA key.");
            }
        }

        AsnReader encodedKey = new AsnReader(
            certReqPublicKey.EncodedKeyValue.RawData,
            AsnEncodingRules.DER);

        // https://tools.ietf.org/html/rfc3447#appendix-A.1.1
        // RSAPublicKey::= SEQUENCE {
        //     modulus INTEGER,  --n
        //     publicExponent INTEGER   --e
        // }

        AsnReader rsaPublicKey = encodedKey.ReadSequence();
        BigInteger modulus = rsaPublicKey.ReadInteger();
        BigInteger publicExponent = rsaPublicKey.ReadInteger();
        rsaPublicKey.ThrowIfNotEmpty();

        byte[] n = modulus.ToByteArray();
        byte[] e = publicExponent.ToByteArray();

        if (n[n.Length - 1] == 0)
        {
            Array.Resize(ref n, n.Length - 1);
        }

        if (e[e.Length - 1] == 0)
        {
            Array.Resize(ref e, e.Length - 1);
        }

        Array.Reverse(n);
        Array.Reverse(e);
        
        RSAParameters rsaParameters = new RSAParameters
        {
            Modulus = n,
            Exponent = e,
        };

        RSACng rsaCng = new RSACng();
        rsaCng.ImportParameters(rsaParameters);
        return rsaCng;
    }
}