.net - 如何在 .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 发送到需要创建证书的服务器 - 问题是如何做到这一点。
解决方案
你真的想这样做吗?
解析认证请求(通俗地称为证书签名请求或 CSR)并盲目地对其进行签名是一种非常非常糟糕的操作实践。
如果您想成为证书颁发机构,甚至是私人证书颁发机构,您应该阅读并理解 CA/浏览器论坛当前(在您阅读此内容时)基线要求文档中的所有内容,网址为https://cabforum.org/baseline-requirements -文件/。也许你故意决定某件事不适用于你,但至少它是故意的。
至少您应该检查请求:
- 不授予自己 CA 权限(提示,使用 0 的 pathLenConstraint 颁发您的签名证书以帮助阻止此),除非您当然打算创建从属 CA(但可能不是)。
- 仅使用批准的密钥用法和扩展的密钥用法值。
- 仅使用已批准的主题名称和主题备用名称扩展值(如果请求没有 EKU 扩展,或包含 TLS 服务器使用)。
- 不定义干扰 CA 操作的扩展(授权密钥标识符、授权信息访问、颁发者备用名称、CRL 分发点,...)
- 不定义任何您不理解的扩展(例如证书透明度“毒药”扩展)/授权请求。
你确定你真的想这样做吗?
- .NET 没有对读取主题备用名称的内置支持,您应该对其进行验证。(不要使用字符串解析,使用 System.Formats.Asn1.AsnReader 之类的东西)
- 您可能还想为您发出的请求添加授权信息访问扩展、授权密钥标识符扩展和可能的 CRL 分发点扩展,也没有内置支持。
- .NET 没有对编写 CRL 或读取 OCSP 请求或生成 OCSP 响应的内置支持,因此您需要自行撤销。
- 您需要处理大量操作流程(请参阅 CA/浏览器论坛基线要求)
如果你坚持...
此代码使用新的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;
}
}
推荐阅读
- c++ - 是否可以使用 boost windows_shared_memory 共享向量?
- python - 如何获得数据帧之间的总时间重叠?
- python-3.x - 在 Linux 服务器上向 3D 散点图添加颜色条失败,而在 Windows 上成功
- elasticsearch - 带排序的弹性搜索分页
- java - 如何在 Mapper 中删除警告未映射的目标属性?
- ios - 使用正文参数进行 API 调用
- python - Python - 附加到对象列表中的对象列表
- ios - 无法引用类中的下拉菜单值
- r - IBM WSL 中的 keras - 如何让 keras 的 R 实现在 IBM Watson Studio Local 中工作?
- java - 为什么 ConcurrentHashMap 中的 keySet() 返回 KeySetView 而不仅仅是 Set 像其他 Map 实现?