Skip to content

feat(acme, zerossl): ACME (RFC 8555) and ZeroSSL SDKs - #542

Open
sam-goodwin wants to merge 3 commits into
mainfrom
acme
Open

feat(acme, zerossl): ACME (RFC 8555) and ZeroSSL SDKs#542
sam-goodwin wants to merge 3 commits into
mainfrom
acme

Conversation

@sam-goodwin

@sam-goodwin sam-goodwin commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Two SDKs for issuing TLS certificates: ACME (RFC 8555) and ZeroSSL's REST API. Both models are hand-written Smithy — there is no vendor spec for either, and an OpenAPI intermediary would only add a conversion step and a patch chain to the same content.

@distilled.cloud/acme

specs/acme.json transcribes RFC 8555: twelve operations (getDirectory, newNonce, newAccount, updateAccount, newOrder, getOrder, finalizeOrder, getAuthorization, deactivateAuthorization, respondChallenge, downloadCertificate, revokeCertificate), the RFC's shapes with their wire member names, and the 24 urn:ietf:params:acme:error:* types (§6.7) as error structures matched on a problem document's type, attached to the operations that raise them (AcmeBadNonce and AcmeServerInternal retryable, AcmeRateLimited carries retryAfter, AcmeAccountDoesNotExist on every kid-authenticated op).

src/protocol.ts is the one hand-written piece of runtime — ACME is JWS-signed JSON the generic REST protocol can't express:

// every POST body is a flattened JWS signed by the account key from Credentials
{ protected: base64url({ alg, nonce, url, kid | jwk }), payload, signature }
  • Addressing: the endpoint is the CA's directory URL; newNonce/newAccount/newOrder/revokeCert resolve from the directory, everything else names its resource by the absolute url label the CA returned. The model's http uris are placeholders that identify the operation.
  • newAccount embeds the JWK and attaches the External Account Binding from Credentials; later requests use kid; reads are POST-as-GET. Nonces are cached per CA from Replay-Nonce.
  • Jose: WebCrypto-only account keys, thumbprints, key authorizations and DNS-01 TXT values.

Directories presets: Let's Encrypt (+ staging), ZeroSSL, Google Trust Services (+ staging).

@distilled.cloud/zerossl

generateEabCredentials, authenticated by the access_key query parameter, with success: false bodies decoded as typed errors (InvalidAccessKey, RateLimitExceeded).

Verified live against ZeroSSL (EAB → account → wildcard order → DNS-01 authorization), Let's Encrypt staging and Pebble, and consumed by alchemy-run/alchemy#1464.

🤖 Generated with Claude Code

sam-goodwin and others added 3 commits September 2, 2026 18:27
`@distilled.cloud/acme`: a hand-authored OpenAPI document for RFC 8555 (directory,
nonce, account, order, authorization, challenge, certificate, revocation) converted to
Smithy and compiled by the shared generator. The protocol is hand-written: every POST is
a flattened JWS signed by the account key from `Credentials` (kid vs embedded JWK,
POST-as-GET reads, External Account Binding on newAccount), requests are addressed via
the CA's directory or the resource `url` label, nonces are cached per CA, and RFC 7807
problem documents are matched on their `type` URN against 24 typed `Acme*` errors
patched onto the model (`patches/acme/errors.json`). `Jose` exports WebCrypto-only
helpers: account key generation, thumbprints, key authorizations, DNS-01 values.

`@distilled.cloud/zerossl`: the REST API's `generateEabCredentials`, authenticated by
the `access_key` query parameter, with `success: false` bodies decoded as typed errors.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A deactivated or purged account answers `urn:ietf:params:acme:error:accountDoesNotExist`
on any request signed with its `kid`, not only `newAccount`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There is no vendor spec for either API, so the OpenAPI document was only an
intermediary: the converter turned it into Smithy, and a 1000-line JSON patch then
added the typed errors. The models are now written directly as Smithy
(`specs/acme.json`, `specs/zerossl.json`) with the error shapes and traits inline —
one source of truth, no `convert.ts`, no `.generated-specs`, no patch chain. The
generated SDKs are unchanged apart from the converter's `RespondChallengeRequest2`
name becoming `RespondChallengeRequest`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant