A certificate signing request (CSR) asks a CA to certify a public key. This example issues a private TLS certificate.
dotnet add package RecursiveAI.OneCrypto --prereleaseRequires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.
test-ca.pem and request.pem are test-only inputs in the zip; the full program regenerates both. Never reuse its demonstration CA key.
using RecursiveAI.OneCrypto; using RecursiveAI.OneCrypto.Issuance; // Test-only CA and CSR from the zip. using var bundle = Crypto.Load("test-ca.pem"); var ca = new CertificateAuthority( bundle.Certificates[0], X509Signer.FromKey( bundle.PrivateKeys[0].Signing!)); var request = CertificateSigningRequest.FromPem( File.ReadAllText("request.pem")); var leaf = ca.Issue(request, new CertificateIssueOptions { Profile = CertificateProfile.TlsServer, NotBefore = DateTimeOffset.UtcNow, Validity = TimeSpan.FromDays(30) }); File.WriteAllText("leaf.pem", leaf.ToPem()); Console.WriteLine("Wrote leaf.pem");
Wrote leaf.pemUnzip the example, then dotnet run.
The full program
This program creates an isolated RSA CA and applicant key, makes a CSR, then issues a TLS server certificate. The CSR asks for DNS name www.example.test and CA:TRUE; the TLS profile deliberately issues a non-CA certificate. These are the request’s input fields:
Subject: CN=www.example.test Requested subject alternative name: DNS:www.example.test Requested basic constraints: CA:TRUE
using System.Security.Cryptography; using RecursiveAI.OneCrypto; using RecursiveAI.OneCrypto.Issuance; using RecursiveAI.OneCrypto.X509; // Generate an isolated demonstration CA and applicant key in memory. var now = DateTimeOffset.UtcNow; using var caKey = RSA.Create(2048); var ca = CertificateAuthority.CreateRoot( DistinguishedName.Create( (KnownOids.CommonName, "Example Root")), X509Signer.FromRsa(caKey, HashAlgorithmName.SHA256), now.AddDays(-1), TimeSpan.FromDays(365)); using var applicantKey = RSA.Create(2048); var request = CertificateSigningRequestBuilder.Create( new CertificateSigningRequestOptions { Subject = DistinguishedName.Create((KnownOids.CommonName, "www.example.test")), Extensions = [ Extension.Create(new SubjectAlternativeName([GeneralName.Dns("www.example.test")]), false), Extension.Create(new BasicConstraints(true), true) ] }, X509Signer.FromRsa(applicantKey, HashAlgorithmName.SHA256)); File.WriteAllText("request.pem", request.ToPem()); // Test-only unencrypted key: never install or reuse this demonstration CA. File.WriteAllText("test-ca.pem", ca.Certificate.ToPem() + caKey.ExportPkcs8PrivateKeyPem()); var options = new CertificateIssueOptions { Profile = CertificateProfile.TlsServer, NotBefore = now, Validity = TimeSpan.FromDays(30), }; var leaf = ca.Issue(request, options); File.WriteAllText("leaf.pem", leaf.ToPem()); Console.WriteLine("Wrote request.pem and leaf.pem"); Console.WriteLine($"CA: {leaf.BasicConstraints!.IsCA}"); Console.WriteLine($"Issuer signature: {leaf.VerifySignature(ca.Certificate).IsValid}"); // Damage only the signature, preserving the request's ASN.1 structure. byte[] damaged = request.Encoded.ToArray(); damaged[^1] ^= 1; try { ca.Issue(CertificateSigningRequest.Parse(damaged), options); } catch (IssuanceException error) { Console.WriteLine($"Changed CSR: {error.Rule}"); }
dotnet run prints:
Wrote request.pem and leaf.pem CA: False Issuer signature: True Changed CSR: csr-signature
A CSR with a bad signature raises IssuanceException with rule csr-signature; this example writes the issued certificate only after Issue succeeds.
Variations
Override the requested DNS names. Add this property to CertificateIssueOptions in the snippet at the top; keep its other properties:
SubjectAlternativeNames = [GeneralName.Dns("approved.example.test")]After Issue returns, also print the issued SAN:
Console.WriteLine(leaf.SubjectAlternativeName!.Names[0].Value);The modified snippet prints:
Wrote leaf.pem approved.example.test
Check a request’s signature without issuing. Uses request.pem from the download:
using RecursiveAI.OneCrypto; var request = CertificateSigningRequest.FromPem( File.ReadAllText("request.pem")); Console.WriteLine(request.VerifySignature().IsValid);
TrueRelated recipes
The short answer
Load your issuing CA certificate and its signing key, construct CertificateAuthority, parse the CSR with CertificateSigningRequest.FromPem, then call ca.Issue(request, options). Set the certificate profile and issuance time explicitly. Write the returned certificate with ToPem().
The runnable example uses RecursiveAI.OneCrypto 0.4.0-preview.2, a preview package. It creates a demonstration CA locally; it does not submit the CSR to an external authority. Replace the demonstration issuer with your authorized CA and its key for a private issuance workflow. The issuance documentation defines what the library checks and what it takes from a request.
How to generate a certificate from CSR?
A CSR is an input to issuance, not a certificate and not the applicant’s private key. OneCrypto takes the subject and public key from it, checks its signature, and then applies the selected profile. The CA’s private signing key signs the resulting certificate; the applicant keeps its own private key. The PKCS #10 model is described in RFC 2986 section 3.
The constructor checks that the issuer is actually a CA, permits certificate signing and matches the signer’s public key. A server certificate plus an unrelated key is not a substitute for an issuer. In the example the root and applicant are separate RSA keys, even though both are created in one process for convenience.
Choose CertificateProfile.TlsServer for a server certificate. That profile requires a DNS or IP subject alternative name (SAN), the names clients use to identify the service. If SubjectAlternativeNames is null, the issuer takes a SAN request from the CSR. The variation supplies an explicit allowlisted name instead. Do not assume that checking the CSR’s signature authorizes its requested DNS names: your issuance workflow must decide which names it will certify.
The profile controls other extensions. A requested CA:TRUE or extended key usage is ignored rather than blindly copied. That is why the example’s output says CA: False despite its CSR asking for CA status. The profile writes the certificate’s basic constraints, key usage and extended key usage. A caller may supply additional extensions, but repeating an extension the CA itself writes is refused.
Validity also belongs to the issuer’s decision. The sample chooses 30 days and supplies the current time. An explicitly requested validity beyond the profile limit or the issuer’s end date is refused, not silently shortened. When validity is omitted, the documented behavior is to use the profile’s longest period cut back to the issuer’s end date. The issuer must itself be valid at NotBefore.
How to verify a CSR certificate?
For a CSR, check request.VerifySignature().IsValid, as in the variation. For issuance, Issue performs that check first and rejects a failed proof of possession with rule csr-signature. The full program flips one bit in the signature bytes, parses the still-structured request and catches exactly that rejection.
This checks possession of the corresponding signing key, not a person’s identity, control of a domain or authorization to receive a certificate. Those decisions must happen in the surrounding CA workflow before calling Issue.
For the issued certificate, leaf.VerifySignature(ca.Certificate).IsValid checks its signature against the issuer. That is the Issuer signature: True line in the output. It is not a trust-store lookup or a full certificate-path validation result. RFC 5280 section 6.1.1 treats the trust anchor and validation time as explicit inputs to path validation. A certificate issued by this demonstration CA does not become publicly trusted because its signature is correct.
The demonstration writes an unencrypted CA key solely to make the top snippet runnable after downloading. Treat test-ca.pem as public test material, not as a deployable issuer. Running the full program replaces it with another disposable key; no system trust store is modified.