首页 > 解决方案 > 如何在 .NET 中加载证书请求并从中创建证书

问题描述

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

CSR 如下所示:

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

它是使用 .NET 4.7.2 生成CertificateRequest的,类似于这个问题的答案: Generate and Sign Certificate Request using pure .net Framework

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

标签: .netcertificatex509certificate2

解决方案


你真的想这样做吗?

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

如果您想成为证书颁发机构,甚至是私人证书颁发机构,您应该阅读并理解 CA/浏览器论坛当前(在您阅读此内容时)基线要求文档中的所有内容,网址为https://cabforum.org/baseline-requirements -文件/。也许你故意决定某件事不适用于你,但至少它是故意的。

至少您应该检查请求:

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

确定真的想这样做吗?

如果你坚持...

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

它确实验证了私钥持有证明签名是否有效,并且这样做将自己限制为 RSA-SSA-PKCS1_v1.5 签名(无 ECDSA,无 RSA-SSA-PSS)。添加其他算法(当然)是可能的。

此代码不提供任何类型的操作策略。由调用者来验证仅使用了适当的扩展名(包括“关键”位是否适当),名称是否适当,以及除“它可以被解码并且主题公钥验证”之外的任何其他内容请求签名”。

有一个 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;
    }
}

推荐阅读