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
This commit is contained in:
Daniel McCarney 2017-02-24 13:54:16 -05:00 committed by Jacob Hoffman-Andrews
parent 34529377db
commit be500adb10
6 changed files with 350 additions and 90 deletions

View File

@ -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"`
}

View File

@ -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 {

29
core/types.go Normal file
View File

@ -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
}

View File

@ -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
}

24
wfe/token.go Normal file
View File

@ -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)
}

View File

@ -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 {