OpenCertServer is a modular certificate authority platform supporting the following open standards:
- EST – Enrollment over Secure Transport (RFC 7030)
- ACME – Automatic Certificate Management Environment (RFC 8555)
- OCSP – Online Certificate Status Protocol (RFC 6960)
- CRL – Certificate Revocation Lists (RFC 5280)
The ACME implementation is derived from the PKISharp ACME Server and FluffySpoon EncryptWeMust projects, both MIT licensed.
The project is licensed under the MIT license.
Run the appropriate build script from the repository root to compile and package all components:
# macOS / Linux
./build.sh
# Windows
./build.ps1This produces NuGet packages and a self-contained server publish under artifacts/.
The certserver application is configured entirely through command-line arguments, with optional fall-through to environment variables and appsettings.json. There are no required environment variables – all runtime configuration is passed directly on the command line.
Pass a Distinguished Name and let the server generate its own RSA and ECDSA root CA certificates at startup:
dotnet opencertserver.certserver.dll \
--dn "CN=My Internal CA" \
--port 5001 \
--ocsp http://localhost:5001/ca/ocsp \
--ca-issuer http://localhost:5001/ca/certificate| Argument | Description |
|---|---|
--dn <name> |
Distinguished Name for the self-signed CA root. A CN= prefix is added automatically if omitted. |
--port <n> |
HTTPS port to listen on (default: 5001). |
--ocsp <url> |
Repeatable. OCSP responder URL embedded in issued certificates. |
--ca-issuer <url> |
Repeatable. CA Issuers URL embedded in issued certificates' AIA extension. |
--authority <url> |
JWT token authority for bearer-token authentication (default: https://identity.reimers.dk). |
Supply PEM-encoded certificate and private key files when you already have a root CA:
dotnet opencertserver.certserver.dll \
--rsa /path/to/rsa-ca.pem \
--rsa-key /path/to/rsa-ca-key.pem \
--ec /path/to/ec-ca.pem \
--ec-key /path/to/ec-ca-key.pem \
--port 5001 \
--ocsp http://pki.example.com/ocsp \
--ca-issuer http://pki.example.com/ca/certificate| Argument | Description |
|---|---|
--rsa <path> |
Path to the RSA CA certificate PEM file. |
--rsa-key <path> |
Path to the RSA CA private key PEM file (optional if key is embedded in the cert file). |
--ec <path> |
Path to the ECDSA CA certificate PEM file. |
--ec-key <path> |
Path to the ECDSA CA private key PEM file (optional if key is embedded in the cert file). |
At least one of --dn or --rsa/--ec must be supplied; the server will throw on startup otherwise.
ACME server behaviour is driven by the AcmeServer section in appsettings.json:
{
"AcmeServer": {
"WebsiteUrl": "https://pki.example.com",
"TOS": {
"RequireAgreement": false,
"Url": "https://pki.example.com/tos",
"LastUpdate": "2024-01-01T00:00:00Z"
},
"HostedWorkers": {
"EnableValidationService": true,
"EnableIssuanceService": false,
"ValidationCheckInterval": 1,
"IssuanceCheckInterval": 1
}
},
"Cors": {
"TrustedOrigins": [
"https://app.example.com"
]
}
}The server components are available as NuGet packages and can be embedded in any ASP.NET Core host.
// 1. Certificate store (in-memory; swap for a persistent implementation in production)
services.AddInMemoryCertificateStore();
// 2a. Self-signed CA (generates RSA + ECDSA roots at startup)
services.AddSelfSignedCertificateAuthority(
new X500DistinguishedName("CN=My Internal CA"),
ocspUrls: ["https://pki.example.com/ca/ocsp"],
crlUrls: [],
caIssuersUrls: ["https://pki.example.com/ca/certificate"],
certificateValidity: TimeSpan.FromDays(90));
// 2b. — OR — bring your own CA certificates
services.AddCertificateAuthority(
new CaConfiguration(
new CaProfileSet("default", rsaProfile, ecdsaProfile),
ocspUrls: ["https://pki.example.com/ca/ocsp"],
crlUrls: [],
caIssuersUrls: ["https://pki.example.com/ca/certificate"]));
// 3. EST server (supply a CSR template loader implementation)
services.AddEstServer<MyCsrTemplateLoader>();
// 4. ACME server
services.AddAcmeServer(configuration)
.AddAcmeInMemoryStore(); // or .AddAcmeFileStore(configuration)
// 5. Authentication – both certificate and JWT bearer are supported
services.AddAuthentication()
.AddJwtBearer()
.AddCertificate()
.AddCertificateCache(options =>
{
options.CacheSize = 1024;
options.CacheEntryExpiration = TimeSpan.FromMinutes(5);
});app.UseHttpsRedirection()
.UseForwardedHeaders()
.UseAcmeServer() // maps ACME endpoints
.UseEstServer() // maps EST endpoints + authentication/authorization middleware
.UseCertificateAuthorityServer(); // maps /ca/* endpoints (CSR, OCSP, CRL, revocation)| Protocol | Path | Method | Auth required |
|---|---|---|---|
| EST | /.well-known/est/cacerts |
GET | No |
| EST | /.well-known/est/csrattrs |
GET | Yes |
| EST | /.well-known/est/simpleenroll |
POST | Yes |
| EST | /.well-known/est/simplereenroll |
POST | Yes |
| EST | /.well-known/est/serverkeygen |
POST | Yes |
| EST | /.well-known/est/{profile}/* |
— | As above (per-profile) |
| ACME | /directory |
GET | No |
| ACME | /new-nonce |
HEAD/GET | No |
| ACME | /new-account |
POST | JWS |
| ACME | /new-order |
POST | JWS |
| ACME | /order/{id}/finalize |
POST | JWS |
| ACME | /order/{id}/certificate |
POST | JWS |
| CA | /ca/csr |
POST | Yes |
| CA | /ca/inventory |
GET | No |
| CA | /ca/revoke |
DELETE | Yes |
| CA | /ca/crl |
GET | No |
| CA | /ca/{profile}/crl |
GET | No |
| CA | /ca/ocsp |
POST | No |
| CA | /ca/certificate |
GET | No |
The EST implementation conforms to RFC 7030:
/cacerts(Section 4.1): Returns the current CA certificate chain in PKCS#7application/pkcs-mimeformat. Responses are cached for 30 days./simpleenroll(Section 4.2): Accepts a PKCS#10 CSR (PEM or DER) in the request body and returns the signed certificate. Bothapplication/pkix-cert(DER/PKCS#7) andapplication/pem-certificate-chainresponses are supported; the client selects via theAcceptheader./simplereenroll(Section 4.2.3): Re-enrolls an existing certificate. The client authenticates using its current certificate (mTLS) or a JWT bearer token, and the server issues a new certificate preserving the original subject./csrattrs(Section 4.5): Returns server-recommended CSR attributes as a DER-encodedCsrAttrsstructure so clients can build conformant signing requests./serverkeygen(Section 4.4): The server generates a new ECDSA key pair on behalf of the client, signs the corresponding certificate, and returns both the private key (PKCS#8) and the certificate as amultipart/mixedresponse.- Per-profile paths (Section 3.2.2): All operations are available with an optional
/{profile}/path segment, allowing a single server to act as multiple logical CAs. - Authentication: Both TLS client certificate authentication and JWT bearer tokens are accepted, matching the dual-scheme requirement of the RFC.
The ACME implementation conforms to RFC 8555:
- Directory (
/directory): AdvertisesnewNonce,newAccount,newOrder,keyChange, and optionalmeta(Terms of Service, website URL). All URLs are generated as absolute HTTPS URIs via ASP.NET CoreLinkGenerator. - Replay-nonce protection: Every mutating request must carry a fresh nonce obtained from
/new-nonce; nonces are validated and discarded after use. - Account management (
/new-account, key rollover): Accounts are created and retrieved by public key. Key rollover is supported via thekey-changeendpoint. - Order lifecycle: Clients create orders (
/new-order), fulfil authorizations (http-01 and dns-01 challenges), finalize orders (/order/{id}/finalize), and download the issued certificate chain (/order/{id}/certificate). - Challenge validation: http-01 challenges are validated over HTTP; dns-01 challenges are resolved via
DnsClient. A backgroundHostedValidationServiceprocesses pending validations asynchronously. - JWS request format: All client requests use the compact JWS serialization with
alg,nonce,url, and eitherjwk(new accounts) orkid(existing accounts) header parameters. - Certificate issuance: After a successful finalize, the server issues a certificate chain signed by the configured CA. The certificate is returned as
application/pem-certificate-chain. - Profile support: Orders can carry an optional
profilefield that maps to a named CA profile, enabling multiple certificate types from a single ACME server. - Storage: The server ships with an in-memory store (default) and a file-backed store (
AddAcmeFileStore). Custom persistence can be provided by implementingIStoreAccounts,IStoreOrders, andINonceStore.
The OCSP responder at /ca/ocsp conforms to RFC 6960:
- Request parsing: Incoming POST requests contain a DER-encoded
OCSPRequest. The request is decoded withAsnReaderagainst the RFC 6960 ASN.1 schema. - Response signing: Responses are DER-encoded
OCSPResponsestructures. TheBasicOCSPResponseincludesResponseDatawith aproducedAttimestamp, the responder ID, and oneSingleResponseper certificate in the request. - Certificate status: Each
SingleResponsereports the certificate's current status (good,revoked, orunknown) by querying the certificate store. - Pluggable validation: Zero or more
IValidateOcspRequestservices are resolved from DI and run before status lookup; a malformed request returnsOCSPResponseStatus.MalformedRequest. - Content type: Responses are returned with
Content-Type: application/ocsp-response. - URL embedding: OCSP responder URLs are embedded in the Authority Information Access (AIA) extension of every issued certificate when
--ocsparguments are supplied at startup.
- Certificate Revocation Lists are available at
/ca/crland/ca/{profile}/crl, returned withContent-Type: application/pkix-crl. - CRL responses are cached for 12 hours.
- Revocation is performed via the authenticated
DELETE /ca/revokeendpoint. The caller must present a valid client certificate and sign the serial number and reason code with the corresponding private key to prove possession. - CRL Distribution Point URLs are embedded in issued certificates when
--crlarguments are supplied.
The opencert tool provides a command-line interface for key generation, CSR management, and EST enrollment. All commands follow the opencert <command> [options] pattern.
opencert generate-keys \
--algorithm rsa \ # rsa (default) or ecdsa
--rsa-key-size 3072 \ # RSA key size in bits (minimum 2048, default 3072)
--out keys/my-key # writes my-key-private.pem and my-key-public.pemAlternatively, specify paths explicitly:
opencert generate-keys \
--algorithm ecdsa \
--ecdsa-curve nistP256 \ # nistP256 (default), nistP384, or nistP521
--private-key-out private.pem \
--public-key-out public.pemopencert print-cert --cert path/to/cert.pemAccepts PEM or DER-encoded X.509 certificates and prints the subject, issuer, validity dates, serial number, key usage, and extensions in a human-readable format.
opencert create-csr \
--private-key private.pem \
--common-name "server.example.com" \
--organization "Example Corp" \
--country "US" \
--san "server.example.com,alt.example.com" \
--out server.csr.pem| Option | Description |
|---|---|
--private-key |
PEM private key to sign the CSR (RSA or ECDSA) |
--common-name |
Subject common name |
--organization |
Subject organization |
--country |
Two-letter country code |
--state |
State or province |
--locality |
Locality/city |
--organizational-unit |
Organizational unit |
--email |
Email address |
--san |
Comma-separated Subject Alternative Names (DNS names) |
--key-usage |
Key usage flags |
--enhanced-key-usage |
Enhanced key usage OIDs |
--subject |
Full subject DN string (overrides individual fields) |
--out |
Output path (default: csr.pem) |
opencert create-csr-from-keys \
--private-key private.pem \
--public-key public.pem \
--common-name "device.example.com" \
--out device.csr.pemThe private and public keys are validated to be a matching pair before the CSR is created.
opencert sign-csr \
--csr request.csr.pem \
--ca-cert ca.crt \
--ca-key ca.key \
--out issued.pemIssues a certificate valid for one year, signed directly by the supplied CA key/certificate pair. Useful for offline signing workflows and testing.
opencert est-enroll \
--url https://pki.example.com \
--private-key private.pem \
--common-name "client.example.com" \
--san "client.example.com" \
--auth "Bearer <token>" \
--out enrolled.pemThe command generates a CSR from the supplied private key and CSR fields, then submits it to the EST /simpleenroll endpoint. On success the issued certificate is written to --out.
For mTLS authentication, supply a PKCS#12 file that includes the private key:
opencert est-enroll \
--url https://pki.example.com \
--private-key private.pem \
--client-cert client-auth.pfx \
--common-name "client.example.com" \
--out enrolled.pem| Option | Description |
|---|---|
--url |
HTTPS base URL of the EST server (required) |
--private-key |
PEM private key used to sign the CSR |
--profile |
Optional EST profile name |
--client-cert |
PEM or PKCS#12 client certificate for mTLS authentication |
--auth |
Authorization header value, e.g. Bearer <token> |
--out |
Output path for the enrolled certificate (default: est-cert.pem) |
opencert est-reenroll \
--url https://pki.example.com \
--private-key private.pem \
--cert current-cert.pem \
--out renewed.pemThe private key is validated against the current certificate's public key before the request is submitted. The server authenticates the client using the existing certificate (mTLS).
| Option | Description |
|---|---|
--url |
HTTPS base URL of the EST server (required) |
--private-key |
PEM private key matching the current certificate |
--cert |
Current certificate to re-enroll (PEM or DER) |
--profile |
Optional EST profile name |
--out |
Output path for the renewed certificate (default: reenrolled.pem) |
opencert est-server-certificates \
--url https://pki.example.comRetrieves the CA certificates from the EST /cacerts endpoint and prints them in PEM format. Useful for bootstrapping trust in a new environment.
When reporting issues and bugs, please provide a clear set of steps to reproduce the issue. The best way is to provide a failing test case as a pull request.
If that is not possible, please provide a set of steps which allow the bug to be reliably reproduced. These steps must also reproduce the issue on a computer that is not your own.
All contributions are appreciated. Please provide them as an issue with an accompanying pull request.
This is an open source project. Please respect the license terms and the fact that issues and contributions may not be handled as fast as you may wish. The best way to get your contribution adopted is to make it easy to pull into the code base.