Reputation: 11040
I want to load a Certificate Request (CSR) from its serialized form and sign it. Is this possible in pure .NET?
The CSR looks like this:
-----BEGIN CERTIFICATE REQUEST-----
MIIDejCCAmICAQAwZTE0MDIGCgmSJom....
-----END CERTIFICATE REQUEST-----
It was generated using .NET 4.7.2 CertificateRequest
, similar to the answer in this question:
Generate and Sign Certificate Request using pure .net Framework
The serialized CSR is then sent to a server, which needs to create the certificate - the question is how to do that.
Upvotes: 1
Views: 2972
Reputation: 33108
Parsing a Certification Request (colloquially known as a Certificate Signing Request or CSR) and signing it blindly is a very, very bad operational practice.
If you want to be a Certificate Authority, even a private one, you should read and understand everything in the CA/Browser Forum's current (as of whenever you read this) Baseline Requirements document at https://cabforum.org/baseline-requirements-documents/. Maybe you intentionally decide something doesn't apply to you, but then at least it's intentional.
At minimum you should be checking that the request:
This code uses the new System.Formats.Asn1 package (specifically, it was tested with version 5.0.0-preview.8.20407.11 [which should be stable version 5.0.0 in November 2020] on .NET Framework 4.8 from an executable built targeting .NET Framework 4.7.2).
It does verify that the proof-of-private-key-possession signature is valid, and in doing so limits itself to RSA-SSA-PKCS1_v1.5 signatures (no ECDSA, no RSA-SSA-PSS). Adding other algorithms is (of course) possible.
This code DOES NOT provide any sort of operational policy. It's up to the caller to verify that only appropriate extensions are used (including that "critical" bits are appropriate), that names are all appropriate, and, well, anything else aside from "it can be decoded and the subject public key verifies the request signature".
There's an API oddity in that you need to tell the decode routine what hash algorithm you eventually intend to use when signing the request, because CertificateRequest requires it in the constructor to make subsequent signing calls easier.
OK, I think that's enough disclaimer, along with some more disclaimers in the code. So, here's enough code to be a "terrible" 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;
}
}
.NET 7 has added the ability to load CSRs, via CertificateRequest.LoadSigningRequest and a PEM-input variant (LoadSigningRequestPem
).
Upvotes: 8