Files
shankar0123 21aeed4f4e legal: addlicense headers + normalize legacy variants (Phase 0 RED-4)
Phase 0 closure (Path B2, post-rewrite):

addlicense sweep — adds the canonical certctl LLC copyright + BUSL-1.1
SPDX header to every production Go file. Template:

  // Copyright 2026 certctl LLC. All rights reserved.
  // SPDX-License-Identifier: BUSL-1.1

Coverage: 338 / 338 production Go files (cmd/ + internal/, excluding
*_test.go and **/testdata/**). Pre-sweep coverage was 22 / 338 (6.5%);
post-sweep is 338 / 338 (100%).

Normalized 22 pre-existing legacy headers (`// Copyright (c) certctl`
+ `// SPDX-License-Identifier: BSL-1.1`) and 1 file using a
`Certctl Contributors` attribution. The legacy SPDX ID `BSL-1.1`
is non-standard; the official SPDX identifier for Business Source
License 1.1 is `BUSL-1.1` (capital U). All 338 files now share the
canonical form.

Generated via:
  addlicense -c "certctl LLC" -y 2026 \
    -f cowork/legal/copyright-header.tpl \
    -ignore '**/testdata/**' -ignore '**/*_test.go' \
    cmd/ internal/

Verification:
  find cmd internal -name '*.go' -not -name '*_test.go' \
    -not -path '*/testdata/*' \
    -exec grep -L '^// Copyright 2026 certctl LLC' {} \; | wc -l

  Returns: 0

gofmt clean. Header additions are comments only, no compile impact.

Closes: cowork/certctl-architecture-diligence-audit.html#fix-RED-4
2026-05-13 21:23:35 +00:00

86 lines
3.6 KiB
Go

// Copyright 2026 certctl LLC. All rights reserved.
// SPDX-License-Identifier: BUSL-1.1
// Package acme implements the ACME server-side protocol surface (RFC 8555
// + RFC 9773 ARI). It is deliberately separate from
// internal/connector/issuer/acme/ which is the consumer surface (certctl
// talks UP to Let's Encrypt / ZeroSSL / pebble). The two surfaces share
// no types — the consumer's data model is client-shaped; the server's
// is request-handler-shaped.
//
// Phase 1a: directory + nonce + JSON-Problem (RFC 7807) error envelopes
// only. JWS verification, account resource, orders, challenges, key
// rollover, revocation, ARI all land in subsequent phases (1b → 4).
package acme
import "fmt"
// Directory is the JSON document RFC 8555 §7.1.1 mandates the server
// publish at /acme/profile/<id>/directory (and at /acme/directory when
// CERTCTL_ACME_SERVER_DEFAULT_PROFILE_ID is set).
//
// Each URL is the per-profile path the ACME client POSTs against. Even
// though Phase 1a only wires up new-nonce, the directory advertises
// the full surface — RFC 8555 doesn't permit a partial directory and
// clients use the directory's URL fields exclusively (they don't
// hand-construct paths from a base URL).
type Directory struct {
NewNonce string `json:"newNonce"`
NewAccount string `json:"newAccount"`
NewOrder string `json:"newOrder"`
RevokeCert string `json:"revokeCert"`
KeyChange string `json:"keyChange"`
// RenewalInfo (RFC 9773 ARI) lands in Phase 4. Omitted now via the
// `,omitempty` tag so the JSON output stays clean for clients that
// don't yet support ARI.
RenewalInfo string `json:"renewalInfo,omitempty"`
Meta *Meta `json:"meta,omitempty"`
}
// Meta is the optional metadata block per RFC 8555 §7.1.1. Every field
// is operator-supplied via CERTCTL_ACME_SERVER_* env vars; an empty
// Meta is omitted from the marshaled directory.
type Meta struct {
TermsOfService string `json:"termsOfService,omitempty"`
Website string `json:"website,omitempty"`
CAAIdentities []string `json:"caaIdentities,omitempty"`
ExternalAccountRequired bool `json:"externalAccountRequired,omitempty"`
}
// BuildDirectory constructs the per-profile directory document.
//
// baseURL is the per-profile base path (no trailing slash, e.g.
// "https://certctl.example.com/acme/profile/prof-corp"). The default-
// profile shorthand path passes the same baseURL — clients writing
// their config against the shorthand naturally re-derive the per-
// profile URLs from the directory.
//
// All five canonical RFC 8555 endpoints are populated; renewalInfo is
// populated only when ARIEnabled=true so Phase 1a (where ARI is
// non-functional) doesn't advertise a 404-returning URL. ARI flips on
// in Phase 4 along with the actual handler.
func BuildDirectory(baseURL, tos, website string, caa []string, eabRequired, ariEnabled bool) *Directory {
dir := &Directory{
NewNonce: fmt.Sprintf("%s/new-nonce", baseURL),
NewAccount: fmt.Sprintf("%s/new-account", baseURL),
NewOrder: fmt.Sprintf("%s/new-order", baseURL),
RevokeCert: fmt.Sprintf("%s/revoke-cert", baseURL),
KeyChange: fmt.Sprintf("%s/key-change", baseURL),
}
if ariEnabled {
// RFC 9773 §4.1 publishes ARI as `renewalInfo`. Phase 4 wires
// the actual handler; until then, BuildDirectory callers pass
// ariEnabled=false.
dir.RenewalInfo = fmt.Sprintf("%s/renewal-info", baseURL)
}
if tos != "" || website != "" || len(caa) > 0 || eabRequired {
dir.Meta = &Meta{
TermsOfService: tos,
Website: website,
CAAIdentities: caa,
ExternalAccountRequired: eabRequired,
}
}
return dir
}