Skip to content

Latest commit

 

History

History
402 lines (301 loc) · 11.6 KB

APIv2.md

File metadata and controls

402 lines (301 loc) · 11.6 KB

API for ACME v2

This document details the API exposed for handling ACME flows, as of draft-12.

Initialization

Create the context with specific ACME server by providing the directory URI.

var context = new AcmeContext(WellKnownServers.LetsEncryptStagingV2);

Use specific key for existing account or creating new account.

var context = new AcmeContext(
    WellKnownServers.LetsEncryptStagingV2,
    KeyFactory.FromPem("account-key.pem"));

Export the account key for later use.

var pem = context.AccountKey.ToPem();
var der = context.AccountKey.ToDer();

Accounts

Get the url to Terms of Service for user to review.

var tos = context.TermsOfService();

Create new account.

var account = await context.NewAccount(
    new [] { "mailto:admin@example.com", "mailto:it@example.com" }, true);
var account = await context.NewAccount("admin@example.com", true);

// external account binding
var account = await context.NewAccount("admin@example.com", true, "(EAB Key Identifier)","(EAB Key)");
var account = await context.NewAccount("admin@example.com", true, "(EAB Key Identifier)","(EAB Key)","(EAB Key Algorithm e.g.HS256)");

Fetch existing account from server.

var account = await context.Account();

Fetch the account info from server.

var accountInfo = await account.Resource();

Update contacts, or accept Terms of Service again if it's updated.

await account.UpdateUpdate(
    contact: new[] { $"mailto:support@example.com" },
    agreeTermsOfService: true);

Update the account key.

var newKey = KeyFactory.NewKey(KeyAlgorithm.ES256);
await account.ChangeKey(newKey);
 
File.WriteAllText("new-key.pem", newKey.ToPem());

Deactivate account.

await account.Deactivate();

Orders

Apply for certificate issuance.

var order = await context.NewOrder(new [] { "*.example.com" });
var orderUri = order.Location;

Retrieve order by URI.

var order = context.Order(orderUri);

Finalize the order.

var certKey = KeyFactory.NewKey(KeyAlgorithm.RS256);
await orderCtx.Finalize(
    new CsrInfo
    {
        CountryName = "CA",
        State = "State",
        Locality = "City",
        Organization = "Dept",
    }, certKey);

Send customized CSR to finalize the order.

var csr = new CertificationRequestBuilder();
csr.AddName($"C=CA, ST=State, L=City, O=Dept, CN=*.example.com");

await orderCtx.Finalize(csr.Generate());

Download the certificate PEM.

var certChain = await order.Download();

Download the certificate PEM signed with a specific root certificate

var certChain = await order.Download("ISRG X1 Root");

Finalize and download the certificate.

var certKey = KeyFactory.NewKey(KeyAlgorithm.RS256);
var cert = await order.Generate(
    new CsrInfo
    {
        CountryName = "CA",
        State = "State",
        Locality = "City",
        Organization = "Dept",
    }, certKey);

Finalize and download the certificate signed with a specific root certificate.

var certKey = KeyFactory.NewKey(KeyAlgorithm.RS256);
var cert = await order.Generate(
    new CsrInfo
    {
        CountryName = "CA",
        State = "State",
        Locality = "City",
        Organization = "Dept",
    }, certKey, "ISRG X1 Root");

Certificate profiles

Servers supporting the ACME profiles extension (Internet-Draft) advertise available profiles in directory metadata. Profile descriptions are strings: they can contain prose or a documentation URL.

var directory = await context.GetDirectory();
var profiles = directory.Meta?.Profiles;
if (profiles != null && profiles.ContainsKey("tlsserver"))
{
    var order = await context.NewOrderWithProfile(new[] { "your.domain.name" }, "tlsserver");
    // Validate authorizations and generate the certificate as usual.
    var selectedProfile = (await order.Resource()).Profile;
}

Names are case-sensitive and server-specific; Certes does not hard-code any CA's profiles. NewOrderWithProfile throws NotSupportedException when profiles are not advertised and ArgumentException for empty or unadvertised names, rather than silently falling back. A server may still reject an advertised profile for an account or requested identifier; its ACME problem details are preserved in AcmeRequestException. The directory is cached by the context; use a new context when refreshed profile discovery is needed.

To send a name the directory does not list, such as a private profile agreed with the CA, pass allowUnadvertisedProfile: true. The server must still advertise profile support, and it may reject the name with invalidProfile.

Use the optional replacedCertificateId parameter to combine profile selection with ARI replacement orders:

var order = await context.NewOrderWithProfile(new[] { "your.domain.name" }, "tlsserver",
    replacedCertificateId: previousChain.GetRenewalInfoCertificateId());

Existing NewOrder and NewReplacementOrder calls omit profile, leaving the choice to the CA. A profile can affect certificate lifetime and permitted CSR fields; consult the CA's description before selecting one.

IP address identifiers

Servers supporting RFC 8738 can issue certificates for IP addresses. The string overloads of NewOrder, NewOrderWithProfile, and NewReplacementOrder detect IP addresses: values that strictly parse as an IP address are sent as ip identifiers, and everything else as dns. To set the type explicitly, pass Identifier values instead.

// Let's Encrypt issues IP address certificates only under its shortlived profile.
var order = await context.NewOrderWithProfile(new[] { "203.0.113.10", "your.domain.name" }, "shortlived");

// Equivalent, with explicit types:
var identifiers = new[]
{
    new Identifier { Type = IdentifierType.Ip, Value = "203.0.113.10" },
    new Identifier { Type = IdentifierType.Dns, Value = "your.domain.name" },
};

order = await context.NewOrderWithProfile(identifiers, "shortlived");
var authz = await order.Authorization("203.0.113.10", IdentifierType.Ip);
  • IPv4 addresses must be dotted-quad without leading zeros; IPv6 addresses must not include a zone ID. Values are sent in canonical form (RFC 5952 for IPv6), and invalid typed IP values throw ArgumentException before any request is made. With the string overloads, values that are not valid IP addresses (such as 01.2.3.4 or fe80::1%eth0) are sent as dns identifiers.
  • Authorization(value, IdentifierType.Ip) matches by address, so any valid spelling of the address finds the authorization.
  • Use http-01 or tls-alpn-01; dns-01 cannot validate IP addresses. For http-01, serve the key authorization on port 80 of the address. For tls-alpn-01, the server connects to the address using a reverse-DNS SNI name (for example 10.113.0.203.in-addr.arpa), and TlsAlpnCertificate should be passed the IP address so the certificate carries an IP SAN.
  • CSRs from CreateCsr, Finalize, and Generate encode IP addresses as IP SANs. When CsrInfo.CommonName is not set, the common name is the first DNS identifier of at most 64 characters; IP addresses are never used, and an IP-only CSR has no common name.

Authorizations

Retrieve authorizations of the order.

var authorizations = await order.Authorizations();

Search authorization by domain name.

var authz = await order.Authorization("*.example.com");
var authzUri = authz.Location;

Retrieve authorization by URI.

var authz = await context.Authorization(authzUri);

Challenges

Retrieve challenges of the authorzation.

var challenges = await authz.Challenges();
var dnsChallenge = await authz.Dns();
var httpChallenge = await authz.Http();
var tlsAlpnChallenge = await authz.TlsAlpn();

Create the respone file for provisioning to /.well-know/acme-challenge/.

var keyAuth = httpChallenge.KeyAuthz;
File.WriteAllText(httpChallenge.Token, keyAuth);

Compute the value for DNS TXT record.

var dnsTxt = context.AccountKey.DnsTxt(challenge.Token);

Generate certificate with X509 ACME validation extension.

var alpnCertKey = KeyFactory.NewKey(KeyAlgorithm.ES256);
var alpnCert = context.AccountKey.TlsAlpnCertificate(challenge.Token, "www.my-domain.com", alpnCertKey);

Let the ACME server to validate the challenge once it is ready.

await challenge.Validate();

Certificates

Download certificate for a pending order.

var certKey = KeyFactory.NewKey(KeyAlgorithm.ES256);
var certChain = await order.Generate(
    new CsrInfo
    {
        CountryName = "CA",
        State = "State",
        Locality = "City",
        Organization = "Dept",
    },
    certKey);

Download the certifcate for a finalized order.

var certChain = await order.Download();

Export the certificate to PEM, DER, or PFX.

var pem = certChain.ToPem();                // certificate and issuers
var pemWithKey = certChain.ToPem(certKey);  // private key first, then certificates
var der = certChain.Certificate.ToDer();    // leaf certificate only
var pfx = certChain.ToPfx(certKey).Build("cert-name", "abcd1234");

var keyPem = certKey.ToPem();

Revoke certificate with account key.

await context.RevokeCertificate(certChain.Certificate.ToDer(), RevocationReason.KeyCompromise);

Revoke certificate with certificate private key.

await context.RevokeCertificate(certChain.Certificate.ToDer(), RevocationReason.KeyCompromise, certKey);

Renewal Information (ARI)

When the server supports RFC 9773 (its directory lists renewalInfo), ask it when to renew a certificate. The certificate identifier is derived from the certificate's authority key identifier and serial number.

var certChain = new CertificateChain(savedPem); // or the chain from order.Generate()
var certId = certChain.GetRenewalInfoCertificateId();

var info = await context.GetRenewalInfo(certId);
// Renew at a random time within info.SuggestedWindow.Start..End.
// Bound info.RetryAfter before scheduling the next poll (see below).

The application schedules renewal-info polling. RetryAfter exposes the positive delay reported by the HTTP client without clamping it. Per RFC 9773 section 4.3.2, callers must set reasonable limits on the checking interval, for example one minute to one day. When no delay is available (null), use a locally configured fallback, such as six hours. Error backoff takes priority over this interval. Stop polling after the certificate expires or is considered replaced.

When renewing, create the order with NewReplacementOrder so the server knows which certificate it replaces, then proceed as usual.

var order = await context.NewReplacementOrder(new[] { "your.domain.name" }, certId);

GetRenewalInfo throws NotSupportedException when the directory has no renewalInfo endpoint. The request is an unauthenticated GET and does not require an account.

A missing suggested window, or one whose end is at or before its start, causes GetRenewalInfo to throw AcmeException. Treat that as a failed renewal-info request: use a fallback renewal schedule and retry after the locally configured default interval (RFC 9773 section 4.3.3). A valid window entirely in the past is accepted; if the chosen renewal time is already past, attempt renewal immediately, subject to error backoff.