Skip to content

Commit

Permalink
Add support for ACME Profiles (#473)
Browse files Browse the repository at this point in the history
Fixes #471
  • Loading branch information
aarongable authored Aug 22, 2024
1 parent ac77969 commit e08dd94
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 22 deletions.
2 changes: 1 addition & 1 deletion .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ linters:
- bidichk
- bodyclose
- containedctx
- copyloopvar
- decorder
- dogsled
- dupword
- durationcheck
- errcheck
- errchkjson
- errorlint
- exportloopref
- forcetypeassert
- ginkgolinter
- gocheckcompilerdirectives
Expand Down
1 change: 1 addition & 0 deletions acme/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ type Order struct {
Error *ProblemDetails `json:"error,omitempty"`
Expires string `json:"expires"`
Identifiers []Identifier `json:"identifiers,omitempty"`
Profile string `json:"profile,omitempty"`
Finalize string `json:"finalize"`
NotBefore string `json:"notBefore,omitempty"`
NotAfter string `json:"notAfter,omitempty"`
Expand Down
49 changes: 34 additions & 15 deletions ca/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,28 @@ import (
const (
rootCAPrefix = "Pebble Root CA "
intermediateCAPrefix = "Pebble Intermediate CA "
defaultValidityPeriod = 157766400
defaultValidityPeriod = 7776000
)

type CAImpl struct {
log *log.Logger
db *db.MemoryStore
ocspResponderURL string

chains []*chain

certValidityPeriod uint64
chains []*chain
profiles map[string]*Profile
}

type chain struct {
root *issuer
intermediates []*issuer
}

type Profile struct {
Description string
ValidityPeriod uint64
}

func (c *chain) String() string {
fullchain := append(c.intermediates, c.root)
n := len(fullchain)
Expand Down Expand Up @@ -253,7 +257,7 @@ func (ca *CAImpl) newChain(intermediateKey crypto.Signer, intermediateSubject pk
return c
}

func (ca *CAImpl) newCertificate(domains []string, ips []net.IP, key crypto.PublicKey, accountID, notBefore, notAfter string, extensions []pkix.Extension) (*core.Certificate, error) {
func (ca *CAImpl) newCertificate(domains []string, ips []net.IP, key crypto.PublicKey, accountID, notBefore, notAfter, profileName string, extensions []pkix.Extension) (*core.Certificate, error) {
if len(domains) == 0 && len(ips) == 0 {
return nil, errors.New("must specify at least one domain name or IP address")
}
Expand All @@ -264,6 +268,11 @@ func (ca *CAImpl) newCertificate(domains []string, ips []net.IP, key crypto.Publ
}
issuer := defaultChain[0]

prof, ok := ca.profiles[profileName]
if !ok {
return nil, fmt.Errorf("unrecgonized profile name %q", profileName)
}

certNotBefore := time.Now()
var err error
if notBefore != "" {
Expand All @@ -273,7 +282,7 @@ func (ca *CAImpl) newCertificate(domains []string, ips []net.IP, key crypto.Publ
}
}

certNotAfter := certNotBefore.Add(time.Duration(ca.certValidityPeriod-1) * time.Second)
certNotAfter := certNotBefore.Add(time.Duration(prof.ValidityPeriod-1) * time.Second)
maxNotAfter := time.Date(9999, 12, 31, 0, 0, 0, 0, time.UTC)
if certNotAfter.After(maxNotAfter) {
certNotAfter = maxNotAfter
Expand Down Expand Up @@ -337,11 +346,11 @@ func (ca *CAImpl) newCertificate(domains []string, ips []net.IP, key crypto.Publ
return newCert, nil
}

func New(log *log.Logger, db *db.MemoryStore, ocspResponderURL string, alternateRoots int, chainLength int, certificateValidityPeriod uint64) *CAImpl {
func New(log *log.Logger, db *db.MemoryStore, ocspResponderURL string, alternateRoots int, chainLength int, profiles map[string]Profile) *CAImpl {
ca := &CAImpl{
log: log,
db: db,
certValidityPeriod: defaultValidityPeriod,
log: log,
db: db,
profiles: make(map[string]*Profile, len(profiles)),
}

if ocspResponderURL != "" {
Expand All @@ -361,12 +370,14 @@ func New(log *log.Logger, db *db.MemoryStore, ocspResponderURL string, alternate
ca.chains[i] = ca.newChain(intermediateKey, intermediateSubject, subjectKeyID, chainLength)
}

if certificateValidityPeriod != 0 && certificateValidityPeriod < 9223372038 {
ca.certValidityPeriod = certificateValidityPeriod
for name, prof := range profiles {
if prof.ValidityPeriod <= 0 || prof.ValidityPeriod >= 9223372038 {
prof.ValidityPeriod = defaultValidityPeriod
}
ca.profiles[name] = &prof
ca.log.Printf("Loaded profile %q with certificate validity period of %d seconds", name, prof.ValidityPeriod)
}

ca.log.Printf("Using certificate validity period of %d seconds", ca.certValidityPeriod)

return ca
}

Expand Down Expand Up @@ -420,7 +431,7 @@ func (ca *CAImpl) CompleteOrder(order *core.Order) {

// issue a certificate for the csr
csr := order.ParsedCSR
cert, err := ca.newCertificate(csr.DNSNames, csr.IPAddresses, csr.PublicKey, order.AccountID, order.NotBefore, order.NotAfter, extensions)
cert, err := ca.newCertificate(csr.DNSNames, csr.IPAddresses, csr.PublicKey, order.AccountID, order.NotBefore, order.NotAfter, order.Profile, extensions)
if err != nil {
ca.log.Printf("Error: unable to issue order: %s", err.Error())
return
Expand Down Expand Up @@ -506,3 +517,11 @@ func (ca *CAImpl) GetIntermediateKey(no int) *rsa.PrivateKey {
}
return nil
}

func (ca *CAImpl) GetProfiles() map[string]string {
res := make(map[string]string, len(ca.profiles))
for name, prof := range ca.profiles {
res[name] = prof.Description
}
return res
}
3 changes: 2 additions & 1 deletion ca/ca_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ var (
func makeCa() *CAImpl {
logger := log.New(os.Stdout, "Pebble ", log.LstdFlags)
db := db.NewMemoryStore()
return New(logger, db, "", 0, 1, 0)
return New(logger, db, "", 0, 1, map[string]Profile{"default": {}})
}

func makeCertOrderWithExtensions(extensions []pkix.Extension) core.Order {
Expand All @@ -50,6 +50,7 @@ func makeCertOrderWithExtensions(extensions []pkix.Extension) core.Order {
Status: acme.StatusPending,
Expires: time.Now().AddDate(0, 0, 1).UTC().Format(time.RFC3339),
Identifiers: []acme.Identifier{},
Profile: "default",
NotBefore: time.Now().UTC().Format(time.RFC3339),
NotAfter: time.Now().AddDate(30, 0, 0).UTC().Format(time.RFC3339),
},
Expand Down
19 changes: 16 additions & 3 deletions cmd/pebble/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,15 @@ type config struct {
ExternalAccountMACKeys map[string]string
// Configure policies to deny certain domains
DomainBlocklist []string
Profiles map[string]ca.Profile

CertificateValidityPeriod uint64
RetryAfter struct {
RetryAfter struct {
Authz int
Order int
}

// Deprecated: use Profiles.ValidityPeriod instead
CertificateValidityPeriod uint64
}
}

Expand Down Expand Up @@ -100,8 +103,18 @@ func main() {
chainLength = int(val)
}

profiles := c.Pebble.Profiles
if len(profiles) == 0 {
profiles = map[string]ca.Profile{
"default": {
Description: "The default profile",
ValidityPeriod: 0, // Will be overridden by the CA's default
},
}
}

db := db.NewMemoryStore()
ca := ca.New(logger, db, c.Pebble.OCSPResponderURL, alternateRoots, chainLength, c.Pebble.CertificateValidityPeriod)
ca := ca.New(logger, db, c.Pebble.OCSPResponderURL, alternateRoots, chainLength, profiles)
va := va.New(logger, c.Pebble.HTTPPort, c.Pebble.TLSPort, *strictMode, *resolverAddress, db)

for keyID, key := range c.Pebble.ExternalAccountMACKeys {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/letsencrypt/pebble/v2

go 1.21
go 1.22

require (
github.com/go-jose/go-jose/v4 v4.0.1
Expand Down
11 changes: 10 additions & 1 deletion test/config/pebble-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@
"authz": 3,
"order": 5
},
"certificateValidityPeriod": 157766400
"profiles": {
"default": {
"description": "The profile you know and love",
"validityPeriod": 7776000
},
"shortlived": {
"description": "A short-lived cert profile, without actual enforcement",
"validityPeriod": 518400
}
}
}
}
20 changes: 20 additions & 0 deletions wfe/wfe.go
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,7 @@ func (wfe *WebFrontEndImpl) relativeDirectory(request *http.Request, directory m
relativeDir["meta"] = map[string]interface{}{
"termsOfService": ToSURL,
"externalAccountRequired": wfe.requireEAB,
"profiles": wfe.ca.GetProfiles(),
}

directoryJSON, err := marshalIndent(relativeDir)
Expand Down Expand Up @@ -1724,6 +1725,24 @@ func (wfe *WebFrontEndImpl) NewOrder(
return
}

profiles := wfe.ca.GetProfiles()
profileName := newOrder.Profile
if profileName == "" {
// In true pebble chaos fashion, pick a random profile for orders that
// don't specify one.
profNames := make([]string, 0, len(profiles))
for name := range profiles {
profNames = append(profNames, name)
}
profileName = profNames[rand.Intn(len(profiles))]
}
_, ok := profiles[profileName]
if !ok {
wfe.sendError(
acme.MalformedProblem(fmt.Sprintf("Order includes unrecognized profile name %q", profileName)), response)
return
}

var orderDNSs []string
var orderIPs []net.IP
for _, ident := range newOrder.Identifiers {
Expand Down Expand Up @@ -1754,6 +1773,7 @@ func (wfe *WebFrontEndImpl) NewOrder(
Order: acme.Order{
Status: acme.StatusPending,
Expires: expires.UTC().Format(time.RFC3339),
Profile: profileName,
// Only the Identifiers, NotBefore and NotAfter from the submitted order
// are carried forward
Identifiers: uniquenames,
Expand Down

0 comments on commit e08dd94

Please sign in to comment.