Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for ACME Profiles #473

Merged
merged 6 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading