From be500adb10d78c44758c9dc602e28f7c011ce78e Mon Sep 17 00:00:00 2001 From: Daniel McCarney Date: Fri, 24 Feb 2017 13:54:16 -0500 Subject: [PATCH] Adds authorizations to orders. (#7) Refactors the `acme` package to clearly separate out `core` objects that are used by Pebble internally vs those that are read to/from protocol messages. Few misc fixes: * fixed returning the parsedCSR for the order endpoint * fixed the pebble-client shell to not bail on the "meta" directory * updated the in-memory DB's "add" functions to return a count to avoid needing funcs like `countRegistration` Added the authorization types, challenge types, identifier types, and a "pending" status type. New orders now have a pending authorization created for each of the identifiers present in the CSR's SAN fields. Each authorization has a http-01 challenge created for it. Authorizations, orders and challenges are persisted in memory and have GET methods. TODO: Create TLS-SNI-02 and DNS-01 challenges --- acme/common.go | 59 +++++----- cmd/pebble-client/main.go | 10 +- core/types.go | 29 +++++ wfe/memorystore.go | 94 ++++++++++++---- wfe/token.go | 24 ++++ wfe/wfe.go | 224 ++++++++++++++++++++++++++++++++------ 6 files changed, 350 insertions(+), 90 deletions(-) create mode 100644 core/types.go create mode 100644 wfe/token.go diff --git a/acme/common.go b/acme/common.go index c6dbd25..15084ab 100644 --- a/acme/common.go +++ b/acme/common.go @@ -1,12 +1,6 @@ package acme import ( - "crypto/rand" - "crypto/x509" - "encoding/base64" - "fmt" - "io" - "gopkg.in/square/go-jose.v1" ) @@ -19,48 +13,49 @@ const ( ResourceNewOrder = Resource("new-order") ) +const ( + StatusPending = "pending" + + IdentifierDNS = "dns" + + ChallengeHTTP01 = "http-01" +) + +type Identifier struct { + Type string `json:"type"` + Value string `json:"value"` +} + // TODO(@cpu) - Rename Registration to Account, update refs type Registration struct { - ID string `json:"id"` + Status string `json:"status"` Key *jose.JsonWebKey `json:"key"` Contact []string `json:"contact"` ToSAgreed bool `json:"terms-of-service-agreed"` Orders string `json:"orders"` - Status string } -// OrderRequest is used for new-order requests -type OrderRequest struct { - ID string `json:"id"` +// An Order is created to request issuance for a CSR +type Order struct { Status string `json:"status"` Expires string `json:"expires"` CSR string `json:"csr"` NotBefore string `json:"notBefore"` NotAfter string `json:"notAfter"` Authorizations []string `json:"authorizations"` - Certificate string `json:"certificate"` + Certificate string `json:"certificate,omitempty"` } -// Order is constructed out of an OrderRequest and is an internal type -type Order struct { - OrderRequest - ParsedCSR *x509.CertificateRequest +// An Authorization is created for each identifier in an order +type Authorization struct { + Status string `json:"status"` + Identifier Identifier `json:"identifier"` + Challenges []string `json:"challenges"` } -// TODO(@cpu): Create an "Authorizations" type - -// RandomString and NewToken come from Boulder core/util.go -// RandomString returns a randomly generated string of the requested length. -func RandomString(byteLength int) string { - b := make([]byte, byteLength) - _, err := io.ReadFull(rand.Reader, b) - if err != nil { - panic(fmt.Sprintf("Error reading random bytes: %s", err)) - } - return base64.RawURLEncoding.EncodeToString(b) -} - -// NewToken produces a random string for Challenges, etc. -func NewToken() string { - return RandomString(32) +// A Challenge is used to validate an Authorization +type Challenge struct { + Type string `json:"type"` + URL string `json:"url"` + Token string `json:"token"` } diff --git a/cmd/pebble-client/main.go b/cmd/pebble-client/main.go index 113669f..3662224 100644 --- a/cmd/pebble-client/main.go +++ b/cmd/pebble-client/main.go @@ -33,7 +33,7 @@ func userAgent() string { type client struct { server *url.URL - directory map[string]string + directory map[string]interface{} email string acctID string http *http.Client @@ -98,7 +98,7 @@ func (c *client) updateDirectory() error { return err } - var directory map[string]string + var directory map[string]interface{} err = json.Unmarshal(respBody, &directory) if err != nil { return err @@ -109,7 +109,7 @@ func (c *client) updateDirectory() error { } func (c *client) updateNonce() error { - nonceURL := c.directory["new-nonce"] + nonceURL := c.directory["new-nonce"].(string) if nonceURL == "" { return fmt.Errorf("Missing \"new-nonce\" entry in server directory") } @@ -129,7 +129,7 @@ func (c *client) updateNonce() error { } func (c *client) register() error { - regURL := c.directory["new-reg"] + regURL := c.directory["new-reg"].(string) if regURL == "" { return fmt.Errorf("Missing \"new-reg\" entry in server directory") } @@ -243,7 +243,7 @@ func (c *client) readEndpoint() (string, error) { fmt.Printf("$> Enter a directory endpoint to POST: ") continue } - endpoint = c.directory[line] + endpoint = c.directory[line].(string) break } if err := scanner.Err(); err != nil { diff --git a/core/types.go b/core/types.go new file mode 100644 index 0000000..c56ed96 --- /dev/null +++ b/core/types.go @@ -0,0 +1,29 @@ +package core + +import ( + "crypto/x509" + + "github.com/letsencrypt/pebble/acme" +) + +type Order struct { + acme.Order + ID string + ParsedCSR *x509.CertificateRequest +} + +type Registration struct { + acme.Registration + ID string +} + +type Authorization struct { + acme.Authorization + ID string + URL string +} + +type Challenge struct { + acme.Challenge + ID string +} diff --git a/wfe/memorystore.go b/wfe/memorystore.go index 7a493ff..8d3c2a6 100644 --- a/wfe/memorystore.go +++ b/wfe/memorystore.go @@ -4,7 +4,7 @@ import ( "fmt" "sync" - "github.com/letsencrypt/pebble/acme" + "github.com/letsencrypt/pebble/core" ) // Pebble keeps all of its various objects (registrations, orders, etc) @@ -15,19 +15,25 @@ type memoryStore struct { // Each Registration's ID is the hex encoding of a SHA256 sum over its public // key bytes. - registrationsByID map[string]*acme.Registration + registrationsByID map[string]*core.Registration - ordersByID map[string]*acme.Order + ordersByID map[string]*core.Order + + authorizationsByID map[string]*core.Authorization + + challengesByID map[string]*core.Challenge } func newMemoryStore() *memoryStore { return &memoryStore{ - registrationsByID: make(map[string]*acme.Registration), - ordersByID: make(map[string]*acme.Order), + registrationsByID: make(map[string]*core.Registration), + ordersByID: make(map[string]*core.Order), + authorizationsByID: make(map[string]*core.Authorization), + challengesByID: make(map[string]*core.Challenge), } } -func (m *memoryStore) getRegistrationByID(id string) *acme.Registration { +func (m *memoryStore) getRegistrationByID(id string) *core.Registration { m.RLock() defer m.RUnlock() if reg, present := m.registrationsByID[id]; present { @@ -36,47 +42,41 @@ func (m *memoryStore) getRegistrationByID(id string) *acme.Registration { return nil } -func (m *memoryStore) countRegistrations() int { - m.RLock() - defer m.RUnlock() - return len(m.registrationsByID) -} - -func (m *memoryStore) addRegistration(reg *acme.Registration) (*acme.Registration, error) { +func (m *memoryStore) addRegistration(reg *core.Registration) (int, error) { m.Lock() defer m.Unlock() regID := reg.ID if len(regID) == 0 { - return nil, fmt.Errorf("registration must have a non-empty ID to add to memoryStore") + return 0, fmt.Errorf("registration must have a non-empty ID to add to memoryStore") } if _, present := m.registrationsByID[regID]; present { - return nil, fmt.Errorf("registration %q already exists", regID) + return 0, fmt.Errorf("registration %q already exists", regID) } m.registrationsByID[regID] = reg - return reg, nil + return len(m.registrationsByID), nil } -func (m *memoryStore) addOrder(order *acme.Order) (*acme.Order, error) { +func (m *memoryStore) addOrder(order *core.Order) (int, error) { m.Lock() defer m.Unlock() orderID := order.ID if len(orderID) == 0 { - return nil, fmt.Errorf("order must have a non-empty ID to add to memoryStore") + return 0, fmt.Errorf("order must have a non-empty ID to add to memoryStore") } if _, present := m.ordersByID[orderID]; present { - return nil, fmt.Errorf("order %q already exists", orderID) + return 0, fmt.Errorf("order %q already exists", orderID) } m.ordersByID[orderID] = order - return order, nil + return len(m.ordersByID), nil } -func (m *memoryStore) getOrderByID(id string) *acme.Order { +func (m *memoryStore) getOrderByID(id string) *core.Order { m.RLock() defer m.RUnlock() if order, present := m.ordersByID[id]; present { @@ -84,3 +84,55 @@ func (m *memoryStore) getOrderByID(id string) *acme.Order { } return nil } + +func (m *memoryStore) addAuthorization(authz *core.Authorization) (int, error) { + m.Lock() + defer m.Unlock() + + authzID := authz.ID + if len(authzID) == 0 { + return 0, fmt.Errorf("authz must have a non-empty ID to add to memoryStore") + } + + if _, present := m.authorizationsByID[authzID]; present { + return 0, fmt.Errorf("authz %q already exists", authzID) + } + + m.authorizationsByID[authzID] = authz + return len(m.authorizationsByID), nil +} + +func (m *memoryStore) getAuthorizationByID(id string) *core.Authorization { + m.RLock() + defer m.RUnlock() + if authz, present := m.authorizationsByID[id]; present { + return authz + } + return nil +} + +func (m *memoryStore) addChallenge(chal *core.Challenge) (int, error) { + m.Lock() + defer m.Unlock() + + chalID := chal.ID + if len(chalID) == 0 { + return 0, fmt.Errorf("challenge must have a non-empty ID to add to memoryStore") + } + + if _, present := m.challengesByID[chalID]; present { + return 0, fmt.Errorf("challenge %q already exists", chalID) + } + + m.challengesByID[chalID] = chal + return len(m.challengesByID), nil +} + +func (m *memoryStore) getChallengeByID(id string) *core.Challenge { + m.RLock() + defer m.RUnlock() + if chal, present := m.challengesByID[id]; present { + return chal + } + return nil +} diff --git a/wfe/token.go b/wfe/token.go new file mode 100644 index 0000000..c45f393 --- /dev/null +++ b/wfe/token.go @@ -0,0 +1,24 @@ +package wfe + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + "io" +) + +// randomString and newToken come from Boulder core/util.go +// randomString returns a randomly generated string of the requested length. +func randomString(byteLength int) string { + b := make([]byte, byteLength) + _, err := io.ReadFull(rand.Reader, b) + if err != nil { + panic(fmt.Sprintf("Error reading random bytes: %s", err)) + } + return base64.RawURLEncoding.EncodeToString(b) +} + +// newToken produces a random string for Challenges, etc. +func newToken() string { + return randomString(32) +} diff --git a/wfe/wfe.go b/wfe/wfe.go index fe49b4f..c406280 100644 --- a/wfe/wfe.go +++ b/wfe/wfe.go @@ -19,6 +19,7 @@ import ( "time" "github.com/letsencrypt/pebble/acme" + "github.com/letsencrypt/pebble/core" "gopkg.in/square/go-jose.v1" ) @@ -31,6 +32,8 @@ const ( regPath = "/my-reg/" newOrderPath = "/order-plz" orderPath = "/my-order/" + authzPath = "/authZ/" + challengePath = "/chalZ/" ) type requestEvent struct { @@ -149,6 +152,8 @@ func (wfe *WebFrontEndImpl) Handler() http.Handler { wfe.HandleFunc(m, newRegPath, wfe.NewRegistration, "POST") wfe.HandleFunc(m, newOrderPath, wfe.NewOrder, "POST") wfe.HandleFunc(m, orderPath, wfe.Order, "GET") + wfe.HandleFunc(m, authzPath, wfe.Authz, "GET") + wfe.HandleFunc(m, challengePath, wfe.Challenge, "GET") // TODO(@cpu): Handle regPath for existing reg updates return m @@ -160,7 +165,6 @@ func (wfe *WebFrontEndImpl) Directory( response http.ResponseWriter, request *http.Request) { - // TODO(@cpu): Add directory metadata (e.g. TOS url) directoryEndpoints := map[string]string{ "new-nonce": noncePath, "new-reg": newRegPath, @@ -358,6 +362,7 @@ func (wfe *WebFrontEndImpl) NewRegistration( return } + // newReg is the ACME registration information submitted by the client var newReg acme.Registration err := json.Unmarshal(body, &newReg) if err != nil { @@ -366,15 +371,19 @@ func (wfe *WebFrontEndImpl) NewRegistration( return } - newReg.Key = key - regID, err := keyToID(newReg.Key) + // createdReg is the internal Pebble account object + createdReg := core.Registration{ + Registration: newReg, + } + + regID, err := keyToID(key) if err != nil { wfe.sendError(acme.MalformedProblem(err.Error()), response) return } - newReg.ID = regID + createdReg.ID = regID - if existingReg := wfe.db.getRegistrationByID(newReg.ID); existingReg != nil { + if existingReg := wfe.db.getRegistrationByID(regID); existingReg != nil { regURL := wfe.relativeEndpoint(request, fmt.Sprintf("%s%s", regPath, existingReg.ID)) response.Header().Set("Location", regURL) wfe.sendError(acme.Conflict("Registration key is already in use"), response) @@ -390,23 +399,116 @@ func (wfe *WebFrontEndImpl) NewRegistration( return } - wfe.db.addRegistration(&newReg) - wfe.log.Printf("There are now %d registrations in memory\n", wfe.db.countRegistrations()) + count, err := wfe.db.addRegistration(&createdReg) + if err != nil { + wfe.sendError(acme.InternalErrorProblem("Error saving registration"), response) + return + } + wfe.log.Printf("There are now %d registrations in memory\n", count) - regURL := wfe.relativeEndpoint(request, fmt.Sprintf("%s%s", regPath, newReg.ID)) + regURL := wfe.relativeEndpoint(request, fmt.Sprintf("%s%s", regPath, regID)) response.Header().Add("Location", regURL) err = wfe.writeJsonResponse(response, http.StatusCreated, newReg) if err != nil { wfe.sendError(acme.InternalErrorProblem("Error marshalling registration"), response) + return } } -func (wfe *WebFrontEndImpl) validateOrder(order *acme.Order) error { - // TODO(@cpu) - Apply some more advanced policy decisions on the CSR +func (wfe *WebFrontEndImpl) verifyOrder(order *core.Order, reg *core.Registration) *acme.ProblemDetails { + // Shouldn't happen - defensive check + if order == nil { + return acme.InternalErrorProblem("Order is nil") + } + if reg == nil { + return acme.InternalErrorProblem("Registration is nil") + } + csr := order.ParsedCSR + if csr == nil { + return acme.InternalErrorProblem("Parsed CSR is nil") + } + if len(csr.DNSNames) == 0 { + return acme.MalformedProblem("CSR has no names in it") + } + orderKeyID, err := keyToID(csr.PublicKey) + if err != nil { + return acme.MalformedProblem("CSR has an invalid PublicKey") + } + if orderKeyID == reg.ID { + return acme.MalformedProblem("Certificate public key must be different than account key") + } return nil } +// makeAuthorizations populates an order with new authz's. The request parameter +// is required to make the authz URL's absolute based on the request host +func (wfe *WebFrontEndImpl) makeAuthorizations(order *core.Order, request *http.Request) error { + var auths []string + + names := make([]string, len(order.ParsedCSR.DNSNames)) + copy(names, order.ParsedCSR.DNSNames) + + // Create one authz for each name in the CSR + for _, name := range names { + ident := acme.Identifier{ + Type: acme.IdentifierDNS, + Value: name, + } + authz := &core.Authorization{ + ID: newToken(), + Authorization: acme.Authorization{ + Status: acme.StatusPending, + Identifier: ident, + }, + } + authz.URL = wfe.relativeEndpoint(request, fmt.Sprintf("%s%s", authzPath, authz.ID)) + // Create the challenges for this authz + err := wfe.makeChallenges(authz, request) + if err != nil { + return err + } + // Save the authorization in memory + count, err := wfe.db.addAuthorization(authz) + if err != nil { + return err + } + fmt.Printf("There are now %d authorizations in the db\n", count) + authzURL := wfe.relativeEndpoint(request, fmt.Sprintf("%s%s", authzPath, authz.ID)) + auths = append(auths, authzURL) + } + + order.Authorizations = auths + return nil +} + +// makeChallenges populates an authz with new challenges. The request parameter +// is required to make the challenge URL's absolute based on the request host +func (wfe *WebFrontEndImpl) makeChallenges(authz *core.Authorization, request *http.Request) error { + var chals []string + + // TODO(@cpu): construct challenges for DNS-01 and TLS-SNI-02 + chal := &core.Challenge{ + ID: newToken(), + Challenge: acme.Challenge{ + Type: acme.ChallengeHTTP01, + Token: newToken(), + URL: authz.URL, + }, + } + count, err := wfe.db.addChallenge(chal) + if err != nil { + return err + } + fmt.Printf("There are now %d challenges in the db\n", count) + chalURL := wfe.relativeEndpoint(request, fmt.Sprintf("%s%s", challengePath, chal.ID)) + chals = append(chals, chalURL) + + authz.Challenges = chals + return nil +} + +// NewOrder creates a new Order request and populates its authorizations func (wfe *WebFrontEndImpl) NewOrder( ctx context.Context, logEvent *requestEvent, @@ -419,6 +521,7 @@ func (wfe *WebFrontEndImpl) NewOrder( return } + // Compute the registration ID for the signer's key regID, err := keyToID(key) if err != nil { wfe.log.Printf("keyToID err: %s\n", err.Error()) @@ -427,16 +530,17 @@ func (wfe *WebFrontEndImpl) NewOrder( } wfe.log.Printf("received new-order req from reg ID %s\n", regID) - var existingReg *acme.Registration + // Find the existing registration object for that key ID + var existingReg *core.Registration if existingReg = wfe.db.getRegistrationByID(regID); existingReg == nil { wfe.sendError( - acme.MalformedProblem( - fmt.Sprintf("No existing registration with ID %q", regID)), + acme.MalformedProblem("No existing registration for signer's public key"), response) return } - var newOrder acme.OrderRequest + // Unpack the order request body + var newOrder acme.Order err = json.Unmarshal(body, &newOrder) if err != nil { wfe.sendError( @@ -444,13 +548,13 @@ func (wfe *WebFrontEndImpl) NewOrder( return } + // Decode and parse the CSR bytes from the order csrBytes, err := base64.RawURLEncoding.DecodeString(newOrder.CSR) if err != nil { wfe.sendError( acme.MalformedProblem("Error decoding Base64url-encoded CSR: "+err.Error()), response) return } - parsedCSR, err := x509.ParseCertificateRequest(csrBytes) if err != nil { wfe.sendError( @@ -458,30 +562,43 @@ func (wfe *WebFrontEndImpl) NewOrder( return } - newOrder.Expires = time.Now().AddDate(0, 0, 1).Format(time.RFC3339) - - order := &acme.Order{ - OrderRequest: newOrder, - ParsedCSR: parsedCSR, + order := &core.Order{ + ID: newToken(), + Order: acme.Order{ + Status: acme.StatusPending, + Expires: time.Now().AddDate(0, 0, 1).Format(time.RFC3339), + // Only the CSR, NotBefore and NotAfter fields of the client request are + // copied as-is + CSR: newOrder.CSR, + NotBefore: newOrder.NotBefore, + NotAfter: newOrder.NotAfter, + }, + ParsedCSR: parsedCSR, } - order.ID = acme.NewToken() - fmt.Printf("Order: %#v\n", order) - if err := wfe.validateOrder(order); err != nil { - wfe.sendError( - // TODO(@cpu) validateOrder should return a problem (e.g. - // rejectedIdentifier, unsupportedIdentifier, etc) as appropriate - acme.MalformedProblem( - fmt.Sprintf("Error validating order: %s", err.Error())), response) + // Verify the details of the order before creating authorizations + if err := wfe.verifyOrder(order, existingReg); err != nil { + wfe.sendError(err, response) return } - _, err = wfe.db.addOrder(order) + // Create the authorizations for the order + err = wfe.makeAuthorizations(order, request) + if err != nil { + wfe.sendError( + acme.InternalErrorProblem("Error creating authorizations for order"), response) + return + } + + // Add the order to the in-memory DB + count, err := wfe.db.addOrder(order) if err != nil { wfe.sendError( acme.InternalErrorProblem("Error saving order"), response) return } + fmt.Printf("Added order %q to the db\n", order.ID) + fmt.Printf("There are now %d orders in the db\n", count) orderURL := wfe.relativeEndpoint(request, fmt.Sprintf("%s%s", orderPath, order.ID)) response.Header().Add("Location", orderURL) @@ -492,6 +609,7 @@ func (wfe *WebFrontEndImpl) NewOrder( } } +// Order retrieves the details of an existing order func (wfe *WebFrontEndImpl) Order( ctx context.Context, logEvent *requestEvent, @@ -499,21 +617,63 @@ func (wfe *WebFrontEndImpl) Order( request *http.Request) { orderID := strings.TrimPrefix(request.URL.Path, orderPath) - fmt.Printf("Order ID: %#v\n", orderID) - order := wfe.db.getOrderByID(orderID) if order == nil { response.WriteHeader(http.StatusNotFound) return } - err := wfe.writeJsonResponse(response, http.StatusOK, order) + // Return only the initial OrderRequest not the internal object with the + // parsedCSR + orderReq := order.Order + + err := wfe.writeJsonResponse(response, http.StatusOK, orderReq) if err != nil { wfe.sendError(acme.InternalErrorProblem("Error marshalling order"), response) return } } +func (wfe *WebFrontEndImpl) Authz( + ctx context.Context, + logEvent *requestEvent, + response http.ResponseWriter, + request *http.Request) { + + authzID := strings.TrimPrefix(request.URL.Path, authzPath) + authz := wfe.db.getAuthorizationByID(authzID) + if authz == nil { + response.WriteHeader(http.StatusNotFound) + return + } + + err := wfe.writeJsonResponse(response, http.StatusOK, authz.Authorization) + if err != nil { + wfe.sendError(acme.InternalErrorProblem("Error marshalling authz"), response) + return + } +} + +func (wfe *WebFrontEndImpl) Challenge( + ctx context.Context, + logEvent *requestEvent, + response http.ResponseWriter, + request *http.Request) { + + chalID := strings.TrimPrefix(request.URL.Path, challengePath) + chal := wfe.db.getChallengeByID(chalID) + if chal == nil { + response.WriteHeader(http.StatusNotFound) + return + } + + err := wfe.writeJsonResponse(response, http.StatusOK, chal.Challenge) + if err != nil { + wfe.sendError(acme.InternalErrorProblem("Error marshalling challenge"), response) + return + } +} + func (wfe *WebFrontEndImpl) writeJsonResponse(response http.ResponseWriter, status int, v interface{}) error { jsonReply, err := marshalIndent(v) if err != nil {