Generate and Sign Certificate Request Using Pure .Net Framework

Generate and Sign Certificate Request using pure .net Framework

Short answer: You can starting in .NET Framework 4.7.2.

This functionality was originally added to .NET Core 2.0 in the form of the CertificateRequest class, which can build a PKCS#10 certification signing request or an X.509 (self-signed or chained) public key certificate.

The classes for that feature were made available in .NET Framework 4.7.2.

using (RSA parent = RSA.Create(4096))
using (RSA rsa = RSA.Create(2048))
{
CertificateRequest parentReq = new CertificateRequest(
"CN=Experimental Issuing Authority",
parent,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);

parentReq.CertificateExtensions.Add(
new X509BasicConstraintsExtension(true, false, 0, true));

parentReq.CertificateExtensions.Add(
new X509SubjectKeyIdentifierExtension(parentReq.PublicKey, false));

using (X509Certificate2 parentCert = parentReq.CreateSelfSigned(
DateTimeOffset.UtcNow.AddDays(-45),
DateTimeOffset.UtcNow.AddDays(365)))
{
CertificateRequest req = new CertificateRequest(
"CN=Valid-Looking Timestamp Authority",
rsa,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);

req.CertificateExtensions.Add(
new X509BasicConstraintsExtension(false, false, 0, false));

req.CertificateExtensions.Add(
new X509KeyUsageExtension(
X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.NonRepudiation,
false));

req.CertificateExtensions.Add(
new X509EnhancedKeyUsageExtension(
new OidCollection
{
new Oid("1.3.6.1.5.5.7.3.8")
},
true));

req.CertificateExtensions.Add(
new X509SubjectKeyIdentifierExtension(req.PublicKey, false));

using (X509Certificate2 cert = req.Create(
parentCert,
DateTimeOffset.UtcNow.AddDays(-1),
DateTimeOffset.UtcNow.AddDays(90),
new byte[] { 1, 2, 3, 4 }))
{
// Do something with these certs, like export them to PFX,
// or add them to an X509Store, or whatever.
}
}
}

Longer answer if you're stuck on older versions: To accomplish your goal without adding any new P/Invokes, you would need to read and understand the following documents:

  • ITU-T X.680-201508, the ASN.1 language
  • IETF RFC 5280 or ITU-T X.509, the documents that explain the fields in X.509 certificates.
  • IETF RFC 2986, explains the PKCS#10 certification signing request
  • ITU-T X.690, explains the BER encoding family for ASN.1 (including DER) which tells you how to read and write bytes to achieve the semantic meaning from X.509 / PKCS#10.

And then you could write a DER writer/reader, and just emit the bytes for what you want.

Generate a certificate request and submit to a CA using only .Net

Multipart questions are hard, since they require multipart answers. Here are the parts I can answer:

How do I specify the CA and the template to use in the CertificateRequest object?

You can't, but that's OK, because you don't in the CertEnroll code, either. The CertificateRequest object is equivalent to your objPkcs10, the CA and template are for what you do with the CreateSigningRequest output.

I have a public key that is a RSAParameters object. How can I get that into an RSA object to use with the CertificateRequst constructor?

using (RSA key = RSA.Create())
{
key.ImportParameters(rsaParameters);
...
}

Once I have the DER encoded CSR, how do I submit that to the CA? I can't find any classes or methods in the System.Security.Cryptography.X509Certificates namespace that accomplishes that.

There's nothing directly in the box for this. Based on the CX509CertificateRequest class name, it seems to be using Certificate Management over CMS (CMC), but then there's still the authentication and request submission parts to solve.

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


Do You Really Want To Do This?

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:

  • Doesn't grant itself CA authority (hint, issue your signing certificate with a pathLenConstraint of 0 to help block this), unless of course you intend to create a subordinate CA (but, probably not).
  • Uses only approved key usage and extended key usage values.
  • Uses only approved subject name and Subject Alternative Names extension values (if the request has no EKU extension, or contains the TLS Server usage).
  • Doesn't define extensions that interfere with the operation of your CA (Authority Key Identifier, Authority Information Access, Issuer Alternative Name, CRL Distribution Points, ...)
  • Doesn't define any extensions you don't understand (e.g. the Certificate Transparency "poison" extension) / authorize for the request.

Are You Sure You Really Want To Do This?

  • .NET doesn't have built-in support for reading Subject Alternative Names, which you're supposed to verify. (Don't use string parsing, use something like System.Formats.Asn1.AsnReader)
  • You probably also want to add an Authority Information Access extension, Authority Key Identifier extension, and probably CRL Distribution Point extension to the requests you issue, there's no built-in support for that either.
    • https://github.com/dotnet/runtime/blob/8b8c390755189d45efc0c407992cb7c006b802b5/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs does have examples of all of this (for the tests of X509Chain).
  • .NET doesn't have built-in support for writing CRLs or reading OCSP requests or producing OCSP responses, so you're on your own for revocation.
    • https://github.com/dotnet/runtime/blob/8b8c390755189d45efc0c407992cb7c006b802b5/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/RevocationResponder.cs (again, from the tests for X509Chain).
  • There's a whole lot of operational processes you need to deal with (see the CA/Browser Forum Baseline Requirements)

If you insist...

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;
}
}

Is there a way to generate a DSA certificate using pure .NET Framework and if not, why?


Are there any problems with the algorithm itself (maybe I shouldn't use it at all)?

Non-EC DSA is dying.

I'll speculate that the thing that really did it in is the original specification (FIPS 186-1) limited the keys to 1024-bit and the algorithm to SHA-1. In 2009 the algorithm got updated in FIPS 186-3 to support slightly larger keys and the SHA-2 hashes. FIPS 186-1 (and FIPS 186-2) DSA signatures only required data and a private key (verification only required data, signature, and a public key), FIPS 186-3 signatures also require the hash algorithm as an input... so the API isn't exactly compatible.

Windows CAPI (the older of the two Windows cryptography platforms) ignored the FIPS 186-3 update, as did Apple's Security.framework. Windows CNG and OpenSSL both support "new DSA". Apple can't process certificates signed with "new DSA" (and maybe not even with "DSA classic", I forget), and Windows doesn't support "new DSA" in cert chains, only "DSA classic".

So DSA certificates are generally limited to FIPS 186-1/186-2 restrictions, which means SHA-1 (not on anyone's good side these days) and 1024-bit keys (which are too small by today's reckoning). If you know you're being validated by OpenSSL you can use better DSA keys.

DSA is also generally much slower at signature verification than RSA.

At the 80 bits of security level, using OpenSSL's speed tool on a random VM of mine (output slightly modified for presentation purposes, sorted by verify/s descending):

                                sign    verify    sign/s verify/s
rsa 1024 bits 0.000301s 0.000018s 3326.3 56419.7
dsa 1024 bits 0.000309s 0.000236s 3236.2 4240.5
ecdsa 160 bits (secp160r1) 0.0005s 0.0004s 1984.6 2385.7

112 bits of security

                                sign    verify    sign/s verify/s
rsa 2048 bits 0.002030s 0.000062s 492.6 16062.4
ecdsa 224 bits (nistp224) 0.0001s 0.0002s 9020.6 4252.2
dsa 2048 bits 0.000885s 0.000802s 1129.4 1247.3

128 bits of security

                                      sign    verify    sign/s verify/s
rsa 3072 bits 0.006935s 0.000135s 144.2 7401.6
ecdsa 256 bits (nistp256) 0.0001s 0.0002s 16901.5 5344.7
ecdsa 256 bits (brainpoolP256t1) 0.0010s 0.0008s 980.1 1262.5
ecdsa 256 bits (brainpoolP256r1) 0.0010s 0.0008s 1012.9 1209.5
dsa 3072 bits (not in test suite)

192 bits of security

                                      sign    verify    sign/s verify/s
rsa 7680 bits 0.122805s 0.000820s 8.1 1220.2
ecdsa 384 bits (nistp384) 0.0024s 0.0018s 416.1 571.2
ecdsa 384 bits (brainpoolP384t1) 0.0024s 0.0018s 410.0 545.1
ecdsa 384 bits (brainpoolP384r1) 0.0025s 0.0019s 407.4 540.1
dsa 7680 bits (beyond FIPS 186-3 DSA maximum of 3072 bits)

256 bits of security

                                      sign    verify    sign/s verify/s
ecdsa 521 bits (nistp521) 0.0006s 0.0012s 1563.1 841.3
ecdsa 512 bits (brainpoolP512t1) 0.0038s 0.0027s 265.2 369.1
ecdsa 512 bits (brainpoolP512r1) 0.0038s 0.0028s 262.4 360.5
rsa 15360 bits 0.783846s 0.003190s 1.3 313.5
dsa 15360 bits (beyond FIPS 186-3 DSA maximum of 3072 bits)

I'm able to use only RSA and ECDsa in CertificateRequest calls. Are there any reasons why DSA is not included?

From the thread with the original feature proposal:

Based on new data from Windows (and their lack of support for FIPS 186-3 DSA certificates) I'm going to pull the DSA typed constructor and leave DSA as a "power user" scenario (custom X509SignatureGenerator class, etc)

So, it was removed mainly because DSA is dying.

Or am I missing something in the API?

The API allows for custom signature generators to be provided. In the tests for CertificateRequest it proves this out with a DSAX509SignatureGenerator

X509SignatureGenerator dsaGen = new DSAX509SignatureGenerator(dsaCsp);

// Use SHA-1 because that's all DSACryptoServiceProvider understands.
HashAlgorithmName hashAlgorithm = HashAlgorithmName.SHA1;

CertificateRequest request = new CertificateRequest(
new X500DistinguishedName($"CN={KeyName}-{provType}"),
dsaGen.PublicKey,
hashAlgorithm);

DateTimeOffset now = DateTimeOffset.UtcNow;

using (X509Certificate2 cert = request.Create(request.SubjectName, dsaGen, now, now.AddDays(1), new byte[1]))
using (X509Certificate2 certWithPrivateKey = cert.CopyWithPrivateKey(dsaCsp))
using (DSA dsa = certWithPrivateKey.GetDSAPrivateKey())
{
byte[] signature = dsa.SignData(Array.Empty<byte>(), hashAlgorithm);

Assert.True(dsaCsp.VerifyData(Array.Empty<byte>(), signature, hashAlgorithm));
}

(snippet from https://github.com/dotnet/runtime/blob/4f9ae42d861fcb4be2fcd5d3d55d5f227d30e723/src/libraries/System.Security.Cryptography.X509Certificates/tests/CertificateCreation/PrivateKeyAssociationTests.cs#L276-L295)

Generate and Sign Certificate using .NET, verifiable with OpenSSL

The problem is you've created your files as DER, but that OpenSSL command only reads PEM.

You can either convert them with something like openssl x509 -in parentCert.cer -inform der -out parentCert.pem, or just change your export code to something like

File.WriteAllText(
Path.Combine(path, "parentCert.crt"),
new string(PemEncoding.Write("CERTIFICATE", parentCert.RawData)));
File.WriteAllText(
Path.Combine(path, "cert.crt"),
new string(PemEncoding.Write("CERTIFICATE", cert.RawData)));

Or, in .NET 7 it's a little cleaner:

File.WriteAllText(
Path.Combine(path, "parentCert.crt"),
PemEncoding.WriteString("CERTIFICATE", parentCert.RawData));
File.WriteAllText(
Path.Combine(path, "cert.crt"),
PemEncoding.WriteString("CERTIFICATE", cert.RawData));

How can I create a self-signed certificate using C#?

This implementation uses the CX509CertificateRequestCertificate COM object (and friends - MSDN doc) from certenroll.dll to create a self signed certificate request and sign it.

The example below is pretty straight forward (if you ignore the bits of COM stuff that goes on here) and there are a few parts of the code that are really optional (such as EKU) which are none-the-less useful and easy to adapt to your use.



Related Topics



Leave a reply



Submit