From d938deb3fda476cb8f508c39ba47078ab85b71c9 Mon Sep 17 00:00:00 2001 From: Richard Barnes Date: Sat, 14 Mar 2015 19:07:16 -0400 Subject: [PATCH 1/8] Separate resources for challenges [initial] --- core/challenges.go | 2 ++ core/core_test.go | 3 ++ core/interfaces.go | 2 +- core/objects.go | 10 ++++-- core/util.go | 8 +++++ ra/registration-authority.go | 40 ++++++++--------------- ra/registration-authority_test.go | 35 ++++++-------------- rpc/rpc-wrappers.go | 21 +++++++++--- va/validation-authority.go | 34 +++++++------------ wfe/web-front-end.go | 54 +++++++++++++++++++++++++------ 10 files changed, 116 insertions(+), 93 deletions(-) diff --git a/core/challenges.go b/core/challenges.go index 5131cab91..1c9a1a15b 100644 --- a/core/challenges.go +++ b/core/challenges.go @@ -12,6 +12,7 @@ import ( func SimpleHTTPSChallenge() Challenge { return Challenge{ + Type: ChallengeTypeSimpleHTTPS, Status: StatusPending, Token: NewToken(), } @@ -21,6 +22,7 @@ func DvsniChallenge() Challenge { nonce := make([]byte, 16) rand.Read(nonce) return Challenge{ + Type: ChallengeTypeDVSNI, Status: StatusPending, R: RandomString(32), Nonce: hex.EncodeToString(nonce), diff --git a/core/core_test.go b/core/core_test.go index 3b8e09966..e1c70f4e3 100644 --- a/core/core_test.go +++ b/core/core_test.go @@ -246,6 +246,9 @@ func TestURL(t *testing.T) { url.URL.Path != path || url.URL.RawQuery != query { t.Errorf("Improper URL contents: %v", url.URL) } + if s := url.URL.PathSegments(); len(s) != 2 { + t.Errorf("Path segments failed to parse properly: %v", s) + } err = json.Unmarshal([]byte(badJSON), &url) if err == nil { diff --git a/core/interfaces.go b/core/interfaces.go index 27515bf93..43220ebb3 100644 --- a/core/interfaces.go +++ b/core/interfaces.go @@ -50,7 +50,7 @@ type RegistrationAuthority interface { NewCertificate(CertificateRequest, jose.JsonWebKey) (Certificate, error) // [WebFrontEnd] - UpdateAuthorization(Authorization) (Authorization, error) + UpdateAuthorization(Authorization, int, Challenge) (Authorization, error) // [WebFrontEnd] RevokeCertificate(x509.Certificate) error diff --git a/core/objects.go b/core/objects.go index 52a53f717..58ec29c88 100644 --- a/core/objects.go +++ b/core/objects.go @@ -92,6 +92,9 @@ func (cr CertificateRequest) MarshalJSON() ([]byte, error) { // challenge, we just throw all the elements into one bucket, // together with the common metadata elements. type Challenge struct { + // The type of challenge + Type string `json:"type"` + // The status of this challenge Status AcmeStatus `json:"status,omitempty"` @@ -99,6 +102,9 @@ type Challenge struct { // was completed by the server. Completed time.Time `json:"completed,omitempty"` + // A URI to which a response can be POSTed + URI AcmeURL `json:"uri"` + // Used by simpleHttps, recoveryToken, and dns challenges Token string `json:"token,omitempty"` @@ -153,11 +159,11 @@ type Authorization struct { // in process, these are challenges to be fulfilled; for // final authorizations, they describe the evidence that // the server used in support of granting the authorization. - Challenges map[string]Challenge `json:"challenges,omitempty"` + Challenges []Challenge `json:"challenges,omitempty"` // The server may suggest combinations of challenges if it // requires more than one challenge to be completed. - Combinations [][]string `json:"combinations,omitempty"` + Combinations [][]int `json:"combinations,omitempty"` // The client may provide contact URIs to allow the server // to push information to it. diff --git a/core/util.go b/core/util.go index 42c784f88..c45a036aa 100644 --- a/core/util.go +++ b/core/util.go @@ -89,6 +89,14 @@ func Fingerprint256(data []byte) string { // URLs that automatically marshal/unmarshal to JSON strings type AcmeURL url.URL +func (u AcmeURL) PathSegments() (segments []string) { + segments = strings.Split(u.Path, "/") + if len(segments) > 0 && len(segments[0]) == 0 { + segments = segments[1:] + } + return +} + func (u AcmeURL) MarshalJSON() ([]byte, error) { uu := url.URL(u) return json.Marshal(uu.String()) diff --git a/ra/registration-authority.go b/ra/registration-authority.go index 511a6f5cd..d0942664a 100644 --- a/ra/registration-authority.go +++ b/ra/registration-authority.go @@ -99,6 +99,7 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(request core.Authorization } // Create validations + // TODO: Assign URLs simpleHttps := core.SimpleHTTPSChallenge() dvsni := core.DvsniChallenge() authID, err := ra.SA.NewPendingAuthorization() @@ -109,9 +110,9 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(request core.Authorization Identifier: identifier, Key: key, Status: core.StatusPending, - Challenges: map[string]core.Challenge{ - core.ChallengeTypeSimpleHTTPS: simpleHttps, - core.ChallengeTypeDVSNI: dvsni, + Challenges: []core.Challenge{ + simpleHttps, + dvsni, }, } @@ -167,37 +168,22 @@ func (ra *RegistrationAuthorityImpl) NewCertificate(req core.CertificateRequest, return } -func (ra *RegistrationAuthorityImpl) UpdateAuthorization(delta core.Authorization) (authz core.Authorization, err error) { - // Fetch the copy of this authorization we have on file - authz, err = ra.SA.GetAuthorization(delta.ID) - if err != nil { +func (ra *RegistrationAuthorityImpl) UpdateAuthorization(base core.Authorization, challengeIndex int, response core.Challenge) (authz core.Authorization, err error) { + // Copy information over that the client is allowed to supply + authz = base + if challengeIndex >= len(authz.Challenges) { + err = core.MalformedRequestError("Invalid challenge index") return } - - // Copy information over that the client is allowed to supply - if len(delta.Contact) > 0 { - authz.Contact = delta.Contact - } - newResponse := false - for t, challenge := range authz.Challenges { - response, present := delta.Challenges[t] - if !present { - continue - } - - newResponse = true - authz.Challenges[t] = challenge.MergeResponse(response) - } + authz.Challenges[challengeIndex] = authz.Challenges[challengeIndex].MergeResponse(response) // Store the updated version ra.SA.UpdatePendingAuthorization(authz) - // If any challenges were updated, dispatch to the VA for service - if newResponse { - ra.VA.UpdateValidations(authz) - } + // Dispatch to the VA for service + ra.VA.UpdateValidations(authz) - return authz, nil + return } func (ra *RegistrationAuthorityImpl) RevokeCertificate(cert x509.Certificate) error { diff --git a/ra/registration-authority_test.go b/ra/registration-authority_test.go index 295006455..1afdfa768 100644 --- a/ra/registration-authority_test.go +++ b/ra/registration-authority_test.go @@ -120,15 +120,9 @@ var ( }, } - AuthzDelta = core.Authorization{ - Challenges: map[string]core.Challenge{ - core.ChallengeTypeSimpleHTTPS: core.Challenge{ - Path: "Hf5GrX4Q7EBax9hc2jJnfw", - }, - core.ChallengeTypeDVSNI: core.Challenge{ - S: "23029d88d9e123e", - }, - }, + ResponseIndex = 0 + Response = core.Challenge{ + Path: "Hf5GrX4Q7EBax9hc2jJnfw", } ExampleCSR = &x509.CertificateRequest{} @@ -193,10 +187,7 @@ func TestNewAuthorization(t *testing.T) { test.Assert(t, authz.Identifier == AuthzRequest.Identifier, "Initial authz had wrong identifier") test.Assert(t, authz.Status == core.StatusPending, "Initial authz not pending") - _, ok := authz.Challenges[core.ChallengeTypeDVSNI] - test.Assert(t, ok, "Initial authz does not include DVSNI challenge") - _, ok = authz.Challenges[core.ChallengeTypeSimpleHTTPS] - test.Assert(t, ok, "Initial authz does not include SimpleHTTPS challenge") + // TODO Verify challenges // If we get to here, we'll use this authorization for the next test AuthzInitial = authz @@ -209,9 +200,8 @@ func TestUpdateAuthorization(t *testing.T) { _, va, sa, ra := initAuthorities(t) AuthzInitial.ID, _ = sa.NewPendingAuthorization() sa.UpdatePendingAuthorization(AuthzInitial) - AuthzDelta.ID = AuthzInitial.ID - authz, err := ra.UpdateAuthorization(AuthzDelta) + authz, err := ra.UpdateAuthorization(AuthzInitial, ResponseIndex, Response) test.AssertNotError(t, err, "UpdateAuthorization failed") // Verify that returned authz same as DB @@ -224,14 +214,9 @@ func TestUpdateAuthorization(t *testing.T) { assertAuthzEqual(t, authz, va.Argument) // Verify that the responses are reflected - simpleHttps, ok := va.Argument.Challenges[core.ChallengeTypeSimpleHTTPS] - simpleHttpsOrig, _ := AuthzDelta.Challenges[core.ChallengeTypeSimpleHTTPS] - test.Assert(t, ok, "Authz passed to VA has no simpleHttps challenge") - test.Assert(t, simpleHttps.Path == simpleHttpsOrig.Path, "simpleHttps changed") - dvsni, ok := va.Argument.Challenges[core.ChallengeTypeDVSNI] - dvsniOrig, _ := AuthzDelta.Challenges[core.ChallengeTypeDVSNI] - test.Assert(t, ok, "Authz passed to VA has no dvsni challenge") - test.Assert(t, dvsni.Token == dvsniOrig.Token, "dvsni changed") + test.Assert(t, len(va.Argument.Challenges) > 0, "Authz passed to VA has no challenges") + simpleHttps := va.Argument.Challenges[0] + test.Assert(t, simpleHttps.Path == Response.Path, "simpleHttps changed") // If we get to here, we'll use this authorization for the next test AuthzUpdated = authz @@ -247,9 +232,7 @@ func TestOnValidationUpdate(t *testing.T) { // Simulate a successful simpleHttps challenge AuthzFromVA = AuthzUpdated - challenge := AuthzFromVA.Challenges[core.ChallengeTypeSimpleHTTPS] - challenge.Status = core.StatusValid - AuthzFromVA.Challenges[core.ChallengeTypeSimpleHTTPS] = challenge + AuthzFromVA.Challenges[0].Status = core.StatusValid ra.OnValidationUpdate(AuthzFromVA) diff --git a/rpc/rpc-wrappers.go b/rpc/rpc-wrappers.go index c9a435c55..980919661 100644 --- a/rpc/rpc-wrappers.go +++ b/rpc/rpc-wrappers.go @@ -112,13 +112,17 @@ func NewRegistrationAuthorityServer(serverQueue string, channel *amqp.Channel, i }) rpc.Handle(MethodUpdateAuthorization, func(req []byte) (response []byte) { - var authz core.Authorization + var authz struct { + Authz core.Authorization + Index int + Response core.Challenge + } err := json.Unmarshal(req, &authz) if err != nil { return } - newAuthz, err := impl.UpdateAuthorization(authz) + newAuthz, err := impl.UpdateAuthorization(authz.Authz, authz.Index, authz.Response) if err != nil { return } @@ -204,8 +208,17 @@ func (rac RegistrationAuthorityClient) NewCertificate(cr core.CertificateRequest return } -func (rac RegistrationAuthorityClient) UpdateAuthorization(authz core.Authorization) (newAuthz core.Authorization, err error) { - data, err := json.Marshal(authz) +func (rac RegistrationAuthorityClient) UpdateAuthorization(authz core.Authorization, index int, response core.Challenge) (newAuthz core.Authorization, err error) { + var toSend struct { + Authz core.Authorization + Index int + Response core.Challenge + } + toSend.Authz = authz + toSend.Index = index + toSend.Response = response + + data, err := json.Marshal(toSend) if err != nil { return } diff --git a/va/validation-authority.go b/va/validation-authority.go index 2c3bf53db..e3fded49b 100644 --- a/va/validation-authority.go +++ b/va/validation-authority.go @@ -28,14 +28,8 @@ func NewValidationAuthorityImpl() ValidationAuthorityImpl { // Validation methods -func (va ValidationAuthorityImpl) validateSimpleHTTPS(authz core.Authorization) (challenge core.Challenge) { - identifier := authz.Identifier.Value - - challenge, ok := authz.Challenges[core.ChallengeTypeSimpleHTTPS] - if !ok { - challenge.Status = core.StatusInvalid - return - } +func (va ValidationAuthorityImpl) validateSimpleHTTPS(identifier core.AcmeIdentifier, input core.Challenge) (challenge core.Challenge) { + challenge = input if len(challenge.Path) == 0 { challenge.Status = core.StatusInvalid @@ -52,7 +46,7 @@ func (va ValidationAuthorityImpl) validateSimpleHTTPS(authz core.Authorization) return } - httpRequest.Host = identifier + httpRequest.Host = identifier.Value client := http.Client{Timeout: 5 * time.Second} httpResponse, err := client.Do(httpRequest) @@ -74,14 +68,8 @@ func (va ValidationAuthorityImpl) validateSimpleHTTPS(authz core.Authorization) return } -func (va ValidationAuthorityImpl) validateDvsni(authz core.Authorization) (challenge core.Challenge) { - // identifier := authz.Identifier.Value // XXX: Local version; uncomment for real version - challenge, ok := authz.Challenges[core.ChallengeTypeDVSNI] - - if !ok { - challenge.Status = core.StatusInvalid - return - } +func (va ValidationAuthorityImpl) validateDvsni(identifier core.AcmeIdentifier, input core.Challenge) (challenge core.Challenge) { + challenge = input const DVSNI_SUFFIX = ".acme.invalid" nonceName := challenge.Nonce + DVSNI_SUFFIX @@ -139,13 +127,13 @@ func (va ValidationAuthorityImpl) validateDvsni(authz core.Authorization) (chall func (va ValidationAuthorityImpl) validate(authz core.Authorization) { // Select the first supported validation method // XXX: Remove the "break" lines to process all supported validations - for i := range authz.Challenges { - switch i { - case "simpleHttps": - authz.Challenges[i] = va.validateSimpleHTTPS(authz) + for i, challenge := range authz.Challenges { + switch challenge.Type { + case core.ChallengeTypeSimpleHTTPS: + authz.Challenges[i] = va.validateSimpleHTTPS(authz.Identifier, challenge) break - case "dvsni": - authz.Challenges[i] = va.validateDvsni(authz) + case core.ChallengeTypeDVSNI: + authz.Challenges[i] = va.validateDvsni(authz.Identifier, challenge) break } } diff --git a/wfe/web-front-end.go b/wfe/web-front-end.go index 5491a5da3..004c7c3b3 100644 --- a/wfe/web-front-end.go +++ b/wfe/web-front-end.go @@ -10,6 +10,7 @@ import ( "fmt" "io/ioutil" "net/http" + "net/url" "regexp" "github.com/letsencrypt/boulder/core" @@ -175,13 +176,22 @@ func (wfe *WebFrontEndImpl) NewCert(response http.ResponseWriter, request *http. response.Write(cert.DER) } -func (wfe *WebFrontEndImpl) Authz(response http.ResponseWriter, request *http.Request) { - // Requests to this handler should have a path that leads to a known authz - id := parseIDFromPath(request.URL.Path) - authz, err := wfe.SA.GetAuthorization(id) - if err != nil { +func (wfe *WebFrontEndImpl) Challenge(authz core.Authorization, response http.ResponseWriter, request *http.Request) { + // Check that the requested challenge exists within the authorization + found := false + var challengeIndex int + for i, challenge := range authz.Challenges { + tempURL := url.URL(challenge.URI) + if tempURL.String() == request.URL.String() { + found = true + challengeIndex = i + break + } + } + + if !found { sendError(response, - fmt.Sprintf("Unable to find authorization: %+v", err), + fmt.Sprintf("Unable to find challenge"), http.StatusNotFound) return } @@ -198,8 +208,8 @@ func (wfe *WebFrontEndImpl) Authz(response http.ResponseWriter, request *http.Re return } - var initialAuthz core.Authorization - err = json.Unmarshal(body, &initialAuthz) + var challengeResponse core.Challenge + err = json.Unmarshal(body, &challengeResponse) if err != nil { sendError(response, "Error unmarshaling authorization", http.StatusBadRequest) return @@ -214,8 +224,7 @@ func (wfe *WebFrontEndImpl) Authz(response http.ResponseWriter, request *http.Re } // Ask the RA to update this authorization - initialAuthz.ID = authz.ID - updatedAuthz, err := wfe.RA.UpdateAuthorization(initialAuthz) + updatedAuthz, err := wfe.RA.UpdateAuthorization(authz, challengeIndex, challengeResponse) if err != nil { sendError(response, "Unable to update authorization", http.StatusInternalServerError) return @@ -229,6 +238,31 @@ func (wfe *WebFrontEndImpl) Authz(response http.ResponseWriter, request *http.Re response.WriteHeader(http.StatusAccepted) response.Write(jsonReply) + } +} + +func (wfe *WebFrontEndImpl) Authz(response http.ResponseWriter, request *http.Request) { + // Requests to this handler should have a path that leads to a known authz + id := parseIDFromPath(request.URL.Path) + authz, err := wfe.SA.GetAuthorization(id) + if err != nil { + sendError(response, + fmt.Sprintf("Unable to find authorization: %+v", err), + http.StatusNotFound) + return + } + + // If there is a fragment, then this is actually a request to a challenge URI + if len(request.URL.Fragment) != 0 { + wfe.Challenge(authz, response, request) + return + } + + switch request.Method { + default: + sendError(response, "Method not allowed", http.StatusMethodNotAllowed) + return + case "GET": jsonReply, err := json.Marshal(authz) if err != nil { From 96bd7e215a14b3be65f5632997f395b67cebba61 Mon Sep 17 00:00:00 2001 From: Richard Barnes Date: Sun, 15 Mar 2015 15:33:05 -0400 Subject: [PATCH 2/8] Further plumbing of registrations --- ca/certificate-authority.go | 5 +- cmd/boulder-start/main.go | 11 ++- core/interfaces.go | 21 ++++- core/objects.go | 31 +++++++ ra/registration-authority.go | 28 ++++++ rpc/rpc-wrappers.go | 170 ++++++++++++++++++++++++++++++----- sa/storage-authority.go | 143 ++++++++++++++++++++++++----- wfe/web-front-end.go | 60 ++++++++++++- 8 files changed, 412 insertions(+), 57 deletions(-) diff --git a/ca/certificate-authority.go b/ca/certificate-authority.go index 4370fe56b..2dc7a6556 100644 --- a/ca/certificate-authority.go +++ b/ca/certificate-authority.go @@ -34,8 +34,9 @@ type CertificateAuthorityImpl struct { func NewCertificateAuthorityImpl(hostport string, authKey string, profile string) (ca *CertificateAuthorityImpl, err error) { // Create the remote signer localProfile := config.SigningProfile{ - Expiry: 60 * time.Minute, // BOGUS: Required by CFSSL, but not used - RemoteName: hostport, + Expiry: time.Hour, // BOGUS: Required by CFSSL, but not used + RemoteName: hostport, // BOGUS: Only used as a flag by CFSSL + RemoteServer: hostport, } localProfile.Provider, err = auth.New(authKey, nil) diff --git a/cmd/boulder-start/main.go b/cmd/boulder-start/main.go index 448a8f7bf..8233ff1da 100644 --- a/cmd/boulder-start/main.go +++ b/cmd/boulder-start/main.go @@ -97,12 +97,6 @@ func main() { Usage: "Start the CA in monolithic mode, without using AMQP", Action: func(c *cli.Context) { - // XXX Print the config - fmt.Println(c.GlobalString("amqp")) - fmt.Println(c.GlobalString("cfssl")) - fmt.Println(c.GlobalString("cfsslAuthKey")) - fmt.Println(c.GlobalString("cfsslProfile")) - // Grab parameters cfsslServer := c.GlobalString("cfssl") authKey := c.GlobalString("cfsslAuthKey") @@ -112,6 +106,8 @@ func main() { wfe := wfe.NewWebFrontEndImpl() sa, err := sa.NewSQLStorageAuthority("sqlite3", ":memory:") failOnError(err, "Unable to create SA") + err = sa.InitTables() + failOnError(err, "Unable to initialize SA") ra := ra.NewRegistrationAuthorityImpl() va := va.NewValidationAuthorityImpl() ca, err := ca.NewCertificateAuthorityImpl(cfsslServer, authKey, profile) @@ -127,10 +123,13 @@ func main() { // Go! authority := "0.0.0.0:4000" + regPath := "/acme/reg/" authzPath := "/acme/authz/" certPath := "/acme/cert/" + wfe.SetRegBase("http://" + authority + regPath) wfe.SetAuthzBase("http://" + authority + authzPath) wfe.SetCertBase("http://" + authority + certPath) + http.HandleFunc("/acme/new-reg", wfe.NewReg) http.HandleFunc("/acme/new-authz", wfe.NewAuthz) http.HandleFunc("/acme/new-cert", wfe.NewCert) http.HandleFunc("/acme/authz/", wfe.Authz) diff --git a/core/interfaces.go b/core/interfaces.go index 43220ebb3..4c63a573b 100644 --- a/core/interfaces.go +++ b/core/interfaces.go @@ -29,12 +29,18 @@ type WebFrontEnd interface { // Set the base URL for certificates SetCertBase(path string) + // This method represents the ACME new-registration resource + NewRegistration(response http.ResponseWriter, request *http.Request) + // This method represents the ACME new-authorization resource NewAuthz(response http.ResponseWriter, request *http.Request) // This method represents the ACME new-certificate resource NewCert(response http.ResponseWriter, request *http.Request) + // Provide access to requests for registration resources + Registration(response http.ResponseWriter, request *http.Request) + // Provide access to requests for authorization resources Authz(response http.ResponseWriter, request *http.Request) @@ -43,12 +49,18 @@ type WebFrontEnd interface { } type RegistrationAuthority interface { + // [WebFrontEnd] + NewRegistration(Registration, jose.JsonWebKey) (Registration, error) + // [WebFrontEnd] NewAuthorization(Authorization, jose.JsonWebKey) (Authorization, error) // [WebFrontEnd] NewCertificate(CertificateRequest, jose.JsonWebKey) (Certificate, error) + // [WebFrontEnd] + UpdateRegistration(Registration, Registration) (Registration, error) + // [WebFrontEnd] UpdateAuthorization(Authorization, int, Challenge) (Authorization, error) @@ -70,15 +82,20 @@ type CertificateAuthority interface { } type StorageGetter interface { - GetCertificate(string) ([]byte, error) + GetRegistration(string) (Registration, error) GetAuthorization(string) (Authorization, error) + GetCertificate(string) ([]byte, error) } type StorageAdder interface { - AddCertificate([]byte) (string, error) + NewRegistration() (string, error) + UpdateRegistration(Registration) error + NewPendingAuthorization() (string, error) UpdatePendingAuthorization(Authorization) error FinalizeAuthorization(Authorization) error + + AddCertificate([]byte) (string, error) } // The StorageAuthority interface represnts a simple key/value diff --git a/core/objects.go b/core/objects.go index 58ec29c88..dce792349 100644 --- a/core/objects.go +++ b/core/objects.go @@ -88,6 +88,36 @@ func (cr CertificateRequest) MarshalJSON() ([]byte, error) { }) } +// Registration objects represent non-public metadata attached +// to account keys. +type Registration struct { + // Unique identifier + ID string `json:"-"` + + // Account key to which the details are attached + Key jose.JsonWebKey `json:"key"` + + // Recovery Token is used to prove connection to an earlier transaction + RecoveryToken string `json:"recoveryToken"` + + // Contact URIs + Contact []AcmeURL `json:"contact,omitempty"` + + // Agreement with terms of service + Agreement string `json:"agreement,omitempty"` +} + +func (r *Registration) MergeUpdate(input Registration) { + if len(input.Contact) > 0 { + r.Contact = input.Contact + } + + // TODO: Test to make sure this has the proper value + if len(input.Agreement) > 0 { + r.Agreement = input.Agreement + } +} + // Rather than define individual types for different types of // challenge, we just throw all the elements into one bucket, // together with the common metadata elements. @@ -118,6 +148,7 @@ type Challenge struct { } // Merge a client-provide response to a challenge with the issued challenge +// TODO: Remove return type from this method func (ch Challenge) MergeResponse(resp Challenge) Challenge { // Only override fields that are supposed to be client-provided if len(ch.Path) == 0 { diff --git a/ra/registration-authority.go b/ra/registration-authority.go index d0942664a..415d104b4 100644 --- a/ra/registration-authority.go +++ b/ra/registration-authority.go @@ -83,6 +83,24 @@ func lastPathSegment(url core.AcmeURL) string { return allButLastPathSegment.ReplaceAllString(url.Path, "") } +func (ra *RegistrationAuthorityImpl) NewRegistration(init core.Registration, key jose.JsonWebKey) (reg core.Registration, err error) { + regID, err := ra.SA.NewRegistration() + if err != nil { + return + } + + reg = core.Registration{ + ID: regID, + Key: key, + RecoveryToken: core.NewToken(), + } + reg.MergeUpdate(init) + + // Store the authorization object, then return it + err = ra.SA.UpdateRegistration(reg) + return +} + func (ra *RegistrationAuthorityImpl) NewAuthorization(request core.Authorization, key jose.JsonWebKey) (authz core.Authorization, err error) { identifier := request.Identifier @@ -103,6 +121,9 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(request core.Authorization simpleHttps := core.SimpleHTTPSChallenge() dvsni := core.DvsniChallenge() authID, err := ra.SA.NewPendingAuthorization() + if err != nil { + return + } // Create a new authorization object authz = core.Authorization{ @@ -168,6 +189,13 @@ func (ra *RegistrationAuthorityImpl) NewCertificate(req core.CertificateRequest, return } +func (ra *RegistrationAuthorityImpl) UpdateRegistration(base core.Registration, update core.Registration) (reg core.Registration, err error) { + base.MergeUpdate(update) + reg = base + err = ra.SA.UpdateRegistration(base) + return +} + func (ra *RegistrationAuthorityImpl) UpdateAuthorization(base core.Authorization, challengeIndex int, response core.Challenge) (authz core.Authorization, err error) { // Copy information over that the client is allowed to supply authz = base diff --git a/rpc/rpc-wrappers.go b/rpc/rpc-wrappers.go index 980919661..af5c98b97 100644 --- a/rpc/rpc-wrappers.go +++ b/rpc/rpc-wrappers.go @@ -33,19 +33,22 @@ import ( // so it doesn't need wrappers. const ( + MethodNewRegistration = "NewRegistration" // RA, SA MethodNewAuthorization = "NewAuthorization" // RA MethodNewCertificate = "NewCertificate" // RA + MethodUpdateRegistration = "UpdateRegistration" // RA, SA MethodUpdateAuthorization = "UpdateAuthorization" // RA MethodRevokeCertificate = "RevokeCertificate" // RA MethodOnValidationUpdate = "OnValidationUpdate" // RA MethodUpdateValidations = "UpdateValidations" // VA MethodIssueCertificate = "IssueCertificate" // CA - MethodGetCertificate = "GetCertificate" // SA + MethodGetRegistration = "GetRegistration" // SA MethodGetAuthorization = "GetAuthorization" // SA - MethodAddCertificate = "AddCertificate" // SA + MethodGetCertificate = "GetCertificate" // SA MethodNewPendingAuthorization = "NewPendingAuthorization" // SA MethodUpdatePendingAuthorization = "UpdatePendingAuthorization" // SA MethodFinalizeAuthorization = "FinalizeAuthorization" // SA + MethodAddCertificate = "AddCertificate" // SA ) // RegistrationAuthorityClient / Server @@ -54,6 +57,11 @@ const ( // -> UpdateAuthorization // -> RevokeCertificate // -> OnValidationUpdate +type registrationRequest struct { + Reg core.Registration + Key jose.JsonWebKey +} + type authorizationRequest struct { Authz core.Authorization Key jose.JsonWebKey @@ -67,6 +75,25 @@ type certificateRequest struct { func NewRegistrationAuthorityServer(serverQueue string, channel *amqp.Channel, impl core.RegistrationAuthority) (rpc *AmqpRpcServer, err error) { rpc = NewAmqpRpcServer(serverQueue, channel) + rpc.Handle(MethodNewRegistration, func(req []byte) (response []byte) { + var rr registrationRequest + err := json.Unmarshal(req, &rr) + if err != nil { + return + } + + reg, err := impl.NewRegistration(rr.Reg, rr.Key) + if err != nil { + return + } + + response, err = json.Marshal(reg) + if err != nil { + response = []byte{} + } + return + }) + rpc.Handle(MethodNewAuthorization, func(req []byte) (response []byte) { var ar authorizationRequest err := json.Unmarshal(req, &ar) @@ -111,6 +138,27 @@ func NewRegistrationAuthorityServer(serverQueue string, channel *amqp.Channel, i return }) + rpc.Handle(MethodUpdateRegistration, func(req []byte) (response []byte) { + var request struct { + Base, Update core.Registration + } + err := json.Unmarshal(req, &request) + if err != nil { + return + } + + reg, err := impl.UpdateRegistration(request.Base, request.Update) + if err != nil { + return + } + + response, err = json.Marshal(reg) + if err != nil { + response = []byte{} + } + return + }) + rpc.Handle(MethodUpdateAuthorization, func(req []byte) (response []byte) { var authz struct { Authz core.Authorization @@ -178,6 +226,21 @@ func NewRegistrationAuthorityClient(clientQueue, serverQueue string, channel *am return } +func (rac RegistrationAuthorityClient) NewRegistration(reg core.Registration, key jose.JsonWebKey) (newReg core.Registration, err error) { + data, err := json.Marshal(registrationRequest{reg, key}) + if err != nil { + return + } + + newRegData, err := rac.rpc.DispatchSync(MethodNewRegistration, data) + if err != nil || len(newRegData) == 0 { + return + } + + err = json.Unmarshal(newRegData, &newReg) + return +} + func (rac RegistrationAuthorityClient) NewAuthorization(authz core.Authorization, key jose.JsonWebKey) (newAuthz core.Authorization, err error) { data, err := json.Marshal(authorizationRequest{authz, key}) if err != nil { @@ -208,6 +271,25 @@ func (rac RegistrationAuthorityClient) NewCertificate(cr core.CertificateRequest return } +func (rac RegistrationAuthorityClient) UpdateRegistration(base core.Registration, update core.Registration) (newReg core.Registration, err error) { + var toSend struct{ Base, Update core.Registration } + toSend.Base = base + toSend.Update = update + + data, err := json.Marshal(toSend) + if err != nil { + return + } + + newRegData, err := rac.rpc.DispatchSync(MethodUpdateRegistration, data) + if err != nil || len(newRegData) == 0 { + return + } + + err = json.Unmarshal(newRegData, &newReg) + return +} + func (rac RegistrationAuthorityClient) UpdateAuthorization(authz core.Authorization, index int, response core.Challenge) (newAuthz core.Authorization, err error) { var toSend struct { Authz core.Authorization @@ -350,11 +432,17 @@ func (cac CertificateAuthorityClient) IssueCertificate(csr x509.CertificateReque func NewStorageAuthorityServer(serverQueue string, channel *amqp.Channel, impl core.StorageAuthority) (rpc *AmqpRpcServer) { rpc = NewAmqpRpcServer(serverQueue, channel) - rpc.Handle(MethodGetCertificate, func(req []byte) (response []byte) { - cert, err := impl.GetCertificate(string(req)) - if err == nil { - response = []byte(cert) + rpc.Handle(MethodGetRegistration, func(req []byte) (response []byte) { + reg, err := impl.GetCertificate(string(req)) + if err != nil { + return } + + jsonReg, err := json.Marshal(reg) + if err != nil { + return + } + response = jsonReg return }) @@ -379,6 +467,14 @@ func NewStorageAuthorityServer(serverQueue string, channel *amqp.Channel, impl c return }) + rpc.Handle(MethodNewRegistration, func(req []byte) (response []byte) { + id, err := impl.NewRegistration() + if err == nil { + response = []byte(id) + } + return + }) + rpc.Handle(MethodNewPendingAuthorization, func(req []byte) (response []byte) { id, err := impl.NewPendingAuthorization() if err == nil { @@ -398,17 +494,6 @@ func NewStorageAuthorityServer(serverQueue string, channel *amqp.Channel, impl c return }) - rpc.Handle(MethodUpdatePendingAuthorization, func(req []byte) (response []byte) { - var authz core.Authorization - err := json.Unmarshal(req, authz) - if err != nil { - return - } - - impl.UpdatePendingAuthorization(authz) - return - }) - rpc.Handle(MethodFinalizeAuthorization, func(req []byte) (response []byte) { var authz core.Authorization err := json.Unmarshal(req, authz) @@ -420,6 +505,14 @@ func NewStorageAuthorityServer(serverQueue string, channel *amqp.Channel, impl c return }) + rpc.Handle(MethodGetCertificate, func(req []byte) (response []byte) { + cert, err := impl.GetCertificate(string(req)) + if err == nil { + response = []byte(cert) + } + return + }) + return } @@ -437,8 +530,13 @@ func NewStorageAuthorityClient(clientQueue, serverQueue string, channel *amqp.Ch return } -func (cac StorageAuthorityClient) GetCertificate(id string) (cert []byte, err error) { - cert, err = cac.rpc.DispatchSync(MethodGetCertificate, []byte(id)) +func (cac StorageAuthorityClient) GetRegistration(id string) (reg core.Registration, err error) { + jsonReg, err := cac.rpc.DispatchSync(MethodGetRegistration, []byte(id)) + if err != nil { + return + } + + err = json.Unmarshal(jsonReg, ®) return } @@ -452,10 +550,26 @@ func (cac StorageAuthorityClient) GetAuthorization(id string) (authz core.Author return } -func (cac StorageAuthorityClient) AddCertificate(cert []byte) (id string, err error) { - response, err := cac.rpc.DispatchSync(MethodAddCertificate, cert) +func (cac StorageAuthorityClient) GetCertificate(id string) (cert []byte, err error) { + cert, err = cac.rpc.DispatchSync(MethodGetCertificate, []byte(id)) + return +} + +func (cac StorageAuthorityClient) UpdateRegistration(reg core.Registration) (err error) { + jsonReg, err := json.Marshal(reg) + if err != nil { + return + } + + // XXX: Is this catching all the errors? + _, err = cac.rpc.DispatchSync(MethodUpdatePendingAuthorization, jsonReg) + return +} + +func (cac StorageAuthorityClient) NewRegistration() (id string, err error) { + response, err := cac.rpc.DispatchSync(MethodNewPendingAuthorization, []byte{}) if err != nil || len(response) == 0 { - err = errors.New("AddCertificate RPC failed") // XXX + err = errors.New("NewRegistration RPC failed") // XXX return } id = string(response) @@ -465,7 +579,7 @@ func (cac StorageAuthorityClient) AddCertificate(cert []byte) (id string, err er func (cac StorageAuthorityClient) NewPendingAuthorization() (id string, err error) { response, err := cac.rpc.DispatchSync(MethodNewPendingAuthorization, []byte{}) if err != nil || len(response) == 0 { - err = errors.New("AddCertificate RPC failed") // XXX + err = errors.New("NewPendingAuthorization RPC failed") // XXX return } id = string(response) @@ -493,3 +607,13 @@ func (cac StorageAuthorityClient) FinalizeAuthorization(authz core.Authorization _, err = cac.rpc.DispatchSync(MethodFinalizeAuthorization, jsonAuthz) return } + +func (cac StorageAuthorityClient) AddCertificate(cert []byte) (id string, err error) { + response, err := cac.rpc.DispatchSync(MethodAddCertificate, cert) + if err != nil || len(response) == 0 { + err = errors.New("AddCertificate RPC failed") // XXX + return + } + id = string(response) + return +} diff --git a/sa/storage-authority.go b/sa/storage-authority.go index db0ccbacf..956ca42f6 100644 --- a/sa/storage-authority.go +++ b/sa/storage-authority.go @@ -8,8 +8,10 @@ package sa import ( "crypto/sha256" "database/sql" + "encoding/hex" "encoding/json" "errors" + "fmt" "github.com/letsencrypt/boulder/core" ) @@ -47,8 +49,8 @@ func (ssa *SQLStorageAuthority) InitTables() (err error) { return } - // Create certificates table - _, err = tx.Exec("CREATE TABLE certificates (sequence INTEGER, digest TEXT, value BLOB);") + // Create registrations table + _, err = tx.Exec("CREATE TABLE registrations (id TEXT, thumbprint TEXT, value TEXT);") if err != nil { tx.Rollback() return @@ -68,13 +70,38 @@ func (ssa *SQLStorageAuthority) InitTables() (err error) { return } + // Create certificates table + _, err = tx.Exec("CREATE TABLE certificates (sequence INTEGER, digest TEXT, value BLOB);") + if err != nil { + tx.Rollback() + return + } + err = tx.Commit() return } -func (ssa *SQLStorageAuthority) GetCertificate(id string) (cert []byte, err error) { - err = ssa.db.QueryRow("SELECT value FROM certificates WHERE digest = ?;", id).Scan(&cert) - return +func (ssa *SQLStorageAuthority) dumpTables(tx *sql.Tx) { + fmt.Printf("===== TABLE DUMP =====\n") + fmt.Printf("\n----- registrations -----\n") + rows, err := tx.Query("SELECT id, thumbprint, value FROM registrations") + if err != nil { + fmt.Printf("ERROR: %v\n", err) + } else { + defer rows.Close() + for rows.Next() { + var id, key, value []byte + if err := rows.Scan(&id, &key, &value); err == nil { + fmt.Printf("%s | %s | %s\n", string(id), string(key), hex.EncodeToString(value)) + } else { + fmt.Printf("ERROR: %v\n", err) + } + } + } + + fmt.Printf("\n----- pending_authz -----\n") // TODO + fmt.Printf("\n----- authz -----\n") // TODO + fmt.Printf("\n----- certificates -----\n") // TODO } func statusIsPending(status core.AcmeStatus) bool { @@ -91,6 +118,22 @@ func existingFinal(tx *sql.Tx, id string) (count int64) { return } +func existingRegistration(tx *sql.Tx, id string) (count int64) { + tx.QueryRow("SELECT count(*) FROM registrations WHERE id = ?;", id).Scan(&count) + return +} + +func (ssa *SQLStorageAuthority) GetRegistration(id string) (reg core.Registration, err error) { + var jsonReg []byte + err = ssa.db.QueryRow("SELECT value FROM registrations WHERE id = ?;", id).Scan(&jsonReg) + if err != nil { + return + } + + err = json.Unmarshal(jsonReg, ®) + return +} + func (ssa *SQLStorageAuthority) GetAuthorization(id string) (authz core.Authorization, err error) { tx, err := ssa.db.Begin() if err != nil { @@ -116,28 +159,57 @@ func (ssa *SQLStorageAuthority) GetAuthorization(id string) (authz core.Authoriz return } -func (ssa *SQLStorageAuthority) AddCertificate(cert []byte) (id string, err error) { +func (ssa *SQLStorageAuthority) GetCertificate(id string) (cert []byte, err error) { + err = ssa.db.QueryRow("SELECT value FROM certificates WHERE digest = ?;", id).Scan(&cert) + return +} + +func (ssa *SQLStorageAuthority) NewRegistration() (id string, err error) { tx, err := ssa.db.Begin() if err != nil { return } - // Manually set the index, to avoid AUTOINCREMENT issues - var sequence int64 - var scanTarget sql.NullInt64 - err = tx.QueryRow("SELECT max(sequence) FROM certificates;").Scan(&scanTarget) - switch { - case !scanTarget.Valid: - sequence = 0 - case err != nil: - tx.Rollback() - return - default: - sequence += scanTarget.Int64 + 1 + // Check that it doesn't exist already + candidate := core.NewToken() + for existingRegistration(tx, candidate) > 0 { + candidate = core.NewToken() } - id = core.Fingerprint256(cert) - _, err = tx.Exec("INSERT INTO certificates (sequence, digest, value) VALUES (?,?,?);", sequence, id, cert) + // Insert a stub row in pending + _, err = tx.Exec("INSERT INTO registrations (id) VALUES (?);", candidate) + if err != nil { + tx.Rollback() + return + } + + if err = tx.Commit(); err != nil { + return + } + + id = candidate + return +} + +func (ssa *SQLStorageAuthority) UpdateRegistration(reg core.Registration) (err error) { + tx, err := ssa.db.Begin() + if err != nil { + return + } + + if existingRegistration(tx, reg.ID) != 1 { + err = errors.New("Requested registration not found " + reg.ID) + tx.Rollback() + return + } + + jsonReg, err := json.Marshal(reg) + if err != nil { + tx.Rollback() + return + } + + _, err = tx.Exec("UPDATE registrations SET thumbprint=?, value=? WHERE id = ?;", reg.Key.Thumbprint, string(jsonReg), reg.ID) if err != nil { tx.Rollback() return @@ -271,3 +343,34 @@ func (ssa *SQLStorageAuthority) FinalizeAuthorization(authz core.Authorization) err = tx.Commit() return } + +func (ssa *SQLStorageAuthority) AddCertificate(cert []byte) (id string, err error) { + tx, err := ssa.db.Begin() + if err != nil { + return + } + + // Manually set the index, to avoid AUTOINCREMENT issues + var sequence int64 + var scanTarget sql.NullInt64 + err = tx.QueryRow("SELECT max(sequence) FROM certificates;").Scan(&scanTarget) + switch { + case !scanTarget.Valid: + sequence = 0 + case err != nil: + tx.Rollback() + return + default: + sequence += scanTarget.Int64 + 1 + } + + id = core.Fingerprint256(cert) + _, err = tx.Exec("INSERT INTO certificates (sequence, digest, value) VALUES (?,?,?);", sequence, id, cert) + if err != nil { + tx.Rollback() + return + } + + err = tx.Commit() + return +} diff --git a/wfe/web-front-end.go b/wfe/web-front-end.go index 004c7c3b3..d5283f891 100644 --- a/wfe/web-front-end.go +++ b/wfe/web-front-end.go @@ -23,7 +23,11 @@ type WebFrontEndImpl struct { // URL configuration parameters baseURL string + newReg string + regBase string + newAuthz string authzBase string + newCert string certBase string } @@ -89,6 +93,14 @@ func sendError(response http.ResponseWriter, message string, code int) { http.Error(response, string(problemDoc), code) } +func link(url, relation string) string { + return fmt.Sprintf("<%s>;rel=\"%s\"", url, relation) +} + +func (wfe *WebFrontEndImpl) SetRegBase(base string) { + wfe.regBase = base +} + func (wfe *WebFrontEndImpl) SetAuthzBase(base string) { wfe.authzBase = base } @@ -97,6 +109,46 @@ func (wfe *WebFrontEndImpl) SetCertBase(base string) { wfe.certBase = base } +func (wfe *WebFrontEndImpl) NewReg(response http.ResponseWriter, request *http.Request) { + if request.Method != "POST" { + sendError(response, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + body, key, err := verifyPOST(request) + if err != nil { + sendError(response, fmt.Sprintf("Unable to read/verify body: %v", err), http.StatusBadRequest) + return + } + + var init core.Registration + err = json.Unmarshal(body, &init) + if err != nil { + sendError(response, "Error unmarshaling JSON", http.StatusBadRequest) + return + } + + reg, err := wfe.RA.NewRegistration(init, key) + if err != nil { + sendError(response, + fmt.Sprintf("Error creating new registration: %+v", err), + http.StatusInternalServerError) + } + + regURL := wfe.regBase + string(reg.ID) + reg.ID = "" + responseBody, err := json.Marshal(reg) + if err != nil { + sendError(response, "Error marshaling authz", http.StatusInternalServerError) + return + } + + response.Header().Add("Location", regURL) + response.Header().Add("Link", link(wfe.newAuthz, "next")) + response.WriteHeader(http.StatusCreated) + response.Write(responseBody) +} + func (wfe *WebFrontEndImpl) NewAuthz(response http.ResponseWriter, request *http.Request) { if request.Method != "POST" { sendError(response, "Method not allowed", http.StatusMethodNotAllowed) @@ -105,7 +157,7 @@ func (wfe *WebFrontEndImpl) NewAuthz(response http.ResponseWriter, request *http body, key, err := verifyPOST(request) if err != nil { - sendError(response, "Unable to read body", http.StatusBadRequest) + sendError(response, "Unable to read/verify body", http.StatusBadRequest) return } @@ -116,7 +168,7 @@ func (wfe *WebFrontEndImpl) NewAuthz(response http.ResponseWriter, request *http return } - // TODO: Create new authz and return + // Create new authz and return authz, err := wfe.RA.NewAuthorization(init, key) if err != nil { sendError(response, @@ -147,7 +199,7 @@ func (wfe *WebFrontEndImpl) NewCert(response http.ResponseWriter, request *http. body, key, err := verifyPOST(request) if err != nil { - sendError(response, "Unable to read body", http.StatusBadRequest) + sendError(response, "Unable to read/verify body", http.StatusBadRequest) return } @@ -204,7 +256,7 @@ func (wfe *WebFrontEndImpl) Challenge(authz core.Authorization, response http.Re case "POST": body, key, err := verifyPOST(request) if err != nil { - sendError(response, "Unable to read body", http.StatusBadRequest) + sendError(response, "Unable to read/verify body", http.StatusBadRequest) return } From f5546ad407c2a5e483e8239b9e07493c65d47e70 Mon Sep 17 00:00:00 2001 From: Richard Barnes Date: Sun, 15 Mar 2015 22:42:35 -0400 Subject: [PATCH 3/8] Miscellaneous fixes to get e2e working --- ca/certificate-authority.go | 13 +++--- cmd/boulder-start/main.go | 76 ++++++++++++++++++++++++++---------- ra/registration-authority.go | 8 ++++ wfe/web-front-end.go | 53 ++++++++++--------------- 4 files changed, 92 insertions(+), 58 deletions(-) diff --git a/ca/certificate-authority.go b/ca/certificate-authority.go index 2dc7a6556..0f727d448 100644 --- a/ca/certificate-authority.go +++ b/ca/certificate-authority.go @@ -57,15 +57,18 @@ func (ca *CertificateAuthorityImpl) IssueCertificate(csr x509.CertificateRequest // XXX Take in authorizations and verify that union covers CSR? // Pull hostnames from CSR hostNames := csr.DNSNames // DNSNames + CN from CSR - if len(hostNames) < 1 { - err = errors.New("Cannot issue a certificate without a hostname.") - return - } var commonName string if len(csr.Subject.CommonName) > 0 { commonName = csr.Subject.CommonName - } else { + } else if len(hostNames) > 0 { commonName = hostNames[0] + } else { + err = errors.New("Cannot issue a certificate without a hostname.") + return + } + + if len(hostNames) == 0 { + hostNames = []string{commonName} } // Convert the CSR to PEM diff --git a/cmd/boulder-start/main.go b/cmd/boulder-start/main.go index 8233ff1da..4ec129338 100644 --- a/cmd/boulder-start/main.go +++ b/cmd/boulder-start/main.go @@ -55,7 +55,6 @@ func main() { app.Usage = "Command-line utility to start Boulder's servers in stand-alone mode" app.Version = "0.0.0" - // Specify AMQP Server app.Flags = []cli.Flag{ cli.StringFlag{ Name: "amqp", @@ -120,20 +119,33 @@ func main() { ra.SA = sa ra.VA = &va va.RA = &ra + ca.SA = sa // Go! authority := "0.0.0.0:4000" + urlBase := "http://" + authority + newRegPath := "/acme/new-reg" regPath := "/acme/reg/" + newAuthzPath := "/acme/new-authz" authzPath := "/acme/authz/" + newCertPath := "/acme/new-cert" certPath := "/acme/cert/" - wfe.SetRegBase("http://" + authority + regPath) - wfe.SetAuthzBase("http://" + authority + authzPath) - wfe.SetCertBase("http://" + authority + certPath) - http.HandleFunc("/acme/new-reg", wfe.NewReg) - http.HandleFunc("/acme/new-authz", wfe.NewAuthz) - http.HandleFunc("/acme/new-cert", wfe.NewCert) - http.HandleFunc("/acme/authz/", wfe.Authz) - http.HandleFunc("/acme/cert/", wfe.Cert) + wfe.NewReg = urlBase + newRegPath + wfe.RegBase = urlBase + regPath + wfe.NewAuthz = urlBase + newAuthzPath + wfe.AuthzBase = urlBase + authzPath + wfe.NewCert = urlBase + newCertPath + wfe.CertBase = urlBase + certPath + http.HandleFunc(newRegPath, wfe.NewRegistration) + http.HandleFunc(newAuthzPath, wfe.NewAuthorization) + http.HandleFunc(newCertPath, wfe.NewCertificate) + // TODO wire up regPath handler + http.HandleFunc(authzPath, wfe.Authorization) + http.HandleFunc(certPath, wfe.Certificate) + + // We need to tell the RA how to make challenge URIs + // XXX: Better way to do this? Part of improved configuration + ra.AuthzBase = wfe.AuthzBase fmt.Fprintf(os.Stderr, "Server running...\n") err = http.ListenAndServe(authority, nil) @@ -200,14 +212,25 @@ func main() { // Go! authority := "0.0.0.0:4000" + urlBase := "http://" + authority + newRegPath := "/acme/new-reg" + regPath := "/acme/reg/" + newAuthzPath := "/acme/new-authz" authzPath := "/acme/authz/" + newCertPath := "/acme/new-cert" certPath := "/acme/cert/" - wfe.SetAuthzBase("http://" + authority + authzPath) - wfe.SetCertBase("http://" + authority + certPath) - http.HandleFunc("/acme/new-authz", wfe.NewAuthz) - http.HandleFunc("/acme/new-cert", wfe.NewCert) - http.HandleFunc("/acme/authz/", wfe.Authz) - http.HandleFunc("/acme/cert/", wfe.Cert) + wfe.NewReg = urlBase + newRegPath + wfe.RegBase = urlBase + regPath + wfe.NewAuthz = urlBase + newAuthzPath + wfe.AuthzBase = urlBase + authzPath + wfe.NewCert = urlBase + newCertPath + wfe.CertBase = urlBase + certPath + http.HandleFunc(newRegPath, wfe.NewRegistration) + http.HandleFunc(newAuthzPath, wfe.NewAuthorization) + http.HandleFunc(newCertPath, wfe.NewCertificate) + // TODO wire up regPath handler + http.HandleFunc(authzPath, wfe.Authorization) + http.HandleFunc(certPath, wfe.Certificate) fmt.Fprintf(os.Stderr, "Server running...\n") err = http.ListenAndServe(authority, nil) @@ -234,14 +257,25 @@ func main() { // Connect the front end to HTTP authority := "0.0.0.0:4000" + urlBase := "http://" + authority + newRegPath := "/acme/new-reg" + regPath := "/acme/reg/" + newAuthzPath := "/acme/new-authz" authzPath := "/acme/authz/" + newCertPath := "/acme/new-cert" certPath := "/acme/cert/" - wfe.SetAuthzBase("http://" + authority + authzPath) - wfe.SetCertBase("http://" + authority + certPath) - http.HandleFunc("/acme/new-authz", wfe.NewAuthz) - http.HandleFunc("/acme/new-cert", wfe.NewCert) - http.HandleFunc("/acme/authz/", wfe.Authz) - http.HandleFunc("/acme/cert/", wfe.Cert) + wfe.NewReg = urlBase + newRegPath + wfe.RegBase = urlBase + regPath + wfe.NewAuthz = urlBase + newAuthzPath + wfe.AuthzBase = urlBase + authzPath + wfe.NewCert = urlBase + newCertPath + wfe.CertBase = urlBase + certPath + http.HandleFunc(newRegPath, wfe.NewRegistration) + http.HandleFunc(newAuthzPath, wfe.NewAuthorization) + http.HandleFunc(newCertPath, wfe.NewCertificate) + // TODO wire up regPath handler + http.HandleFunc(authzPath, wfe.Authorization) + http.HandleFunc(certPath, wfe.Certificate) fmt.Fprintf(os.Stderr, "Server running...\n") http.ListenAndServe(authority, nil) diff --git a/ra/registration-authority.go b/ra/registration-authority.go index 415d104b4..db9a5d219 100644 --- a/ra/registration-authority.go +++ b/ra/registration-authority.go @@ -8,6 +8,7 @@ package ra import ( "crypto/x509" "fmt" + "net/url" "regexp" "strings" "time" @@ -22,6 +23,8 @@ type RegistrationAuthorityImpl struct { CA core.CertificateAuthority VA core.ValidationAuthority SA core.StorageAuthority + + AuthzBase string } func NewRegistrationAuthorityImpl() RegistrationAuthorityImpl { @@ -124,6 +127,11 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(request core.Authorization if err != nil { return } + // Ignoring these errors because we construct the URLs to be correct + simpleHTTPSURI, _ := url.Parse(ra.AuthzBase + authID + "?" + core.RandomString(4)) + dvsniURI, _ := url.Parse(ra.AuthzBase + authID + "?" + core.RandomString(4)) + simpleHttps.URI = core.AcmeURL(*simpleHTTPSURI) + dvsni.URI = core.AcmeURL(*dvsniURI) // Create a new authorization object authz = core.Authorization{ diff --git a/wfe/web-front-end.go b/wfe/web-front-end.go index d5283f891..77e4aea27 100644 --- a/wfe/web-front-end.go +++ b/wfe/web-front-end.go @@ -6,6 +6,7 @@ package wfe import ( + "encoding/hex" "encoding/json" "fmt" "io/ioutil" @@ -22,13 +23,12 @@ type WebFrontEndImpl struct { SA core.StorageGetter // URL configuration parameters - baseURL string - newReg string - regBase string - newAuthz string - authzBase string - newCert string - certBase string + NewReg string + RegBase string + NewAuthz string + AuthzBase string + NewCert string + CertBase string } func NewWebFrontEndImpl() WebFrontEndImpl { @@ -97,19 +97,7 @@ func link(url, relation string) string { return fmt.Sprintf("<%s>;rel=\"%s\"", url, relation) } -func (wfe *WebFrontEndImpl) SetRegBase(base string) { - wfe.regBase = base -} - -func (wfe *WebFrontEndImpl) SetAuthzBase(base string) { - wfe.authzBase = base -} - -func (wfe *WebFrontEndImpl) SetCertBase(base string) { - wfe.certBase = base -} - -func (wfe *WebFrontEndImpl) NewReg(response http.ResponseWriter, request *http.Request) { +func (wfe *WebFrontEndImpl) NewRegistration(response http.ResponseWriter, request *http.Request) { if request.Method != "POST" { sendError(response, "Method not allowed", http.StatusMethodNotAllowed) return @@ -135,7 +123,7 @@ func (wfe *WebFrontEndImpl) NewReg(response http.ResponseWriter, request *http.R http.StatusInternalServerError) } - regURL := wfe.regBase + string(reg.ID) + regURL := wfe.RegBase + string(reg.ID) reg.ID = "" responseBody, err := json.Marshal(reg) if err != nil { @@ -144,12 +132,12 @@ func (wfe *WebFrontEndImpl) NewReg(response http.ResponseWriter, request *http.R } response.Header().Add("Location", regURL) - response.Header().Add("Link", link(wfe.newAuthz, "next")) + response.Header().Add("Link", link(wfe.NewAuthz, "next")) response.WriteHeader(http.StatusCreated) response.Write(responseBody) } -func (wfe *WebFrontEndImpl) NewAuthz(response http.ResponseWriter, request *http.Request) { +func (wfe *WebFrontEndImpl) NewAuthorization(response http.ResponseWriter, request *http.Request) { if request.Method != "POST" { sendError(response, "Method not allowed", http.StatusMethodNotAllowed) return @@ -178,7 +166,7 @@ func (wfe *WebFrontEndImpl) NewAuthz(response http.ResponseWriter, request *http } // Make a URL for this authz, then blow away the ID before serializing - authzURL := wfe.authzBase + string(authz.ID) + authzURL := wfe.AuthzBase + string(authz.ID) authz.ID = "" responseBody, err := json.Marshal(authz) if err != nil { @@ -187,11 +175,12 @@ func (wfe *WebFrontEndImpl) NewAuthz(response http.ResponseWriter, request *http } response.Header().Add("Location", authzURL) + response.Header().Add("Link", link(wfe.NewCert, "next")) response.WriteHeader(http.StatusCreated) response.Write(responseBody) } -func (wfe *WebFrontEndImpl) NewCert(response http.ResponseWriter, request *http.Request) { +func (wfe *WebFrontEndImpl) NewCertificate(response http.ResponseWriter, request *http.Request) { if request.Method != "POST" { sendError(response, "Method not allowed", http.StatusMethodNotAllowed) return @@ -220,7 +209,9 @@ func (wfe *WebFrontEndImpl) NewCert(response http.ResponseWriter, request *http. } // Make a URL for this authz - certURL := wfe.certBase + string(cert.ID) + certURL := wfe.CertBase + string(cert.ID) + + fmt.Printf("Returning certificate: %+v", hex.EncodeToString(cert.DER)) // TODO: Content negotiation for cert format response.Header().Add("Location", certURL) @@ -234,7 +225,7 @@ func (wfe *WebFrontEndImpl) Challenge(authz core.Authorization, response http.Re var challengeIndex int for i, challenge := range authz.Challenges { tempURL := url.URL(challenge.URI) - if tempURL.String() == request.URL.String() { + if tempURL.Path == request.URL.Path && tempURL.RawQuery == request.URL.RawQuery { found = true challengeIndex = i break @@ -269,8 +260,6 @@ func (wfe *WebFrontEndImpl) Challenge(authz core.Authorization, response http.Re // Check that the signing key is the right key if !key.Equals(authz.Key) { - fmt.Printf("req: %+v\n", key) - fmt.Printf("authz: %+v\n", authz.Key) sendError(response, "Signing key does not match key in authorization", http.StatusForbidden) return } @@ -293,7 +282,7 @@ func (wfe *WebFrontEndImpl) Challenge(authz core.Authorization, response http.Re } } -func (wfe *WebFrontEndImpl) Authz(response http.ResponseWriter, request *http.Request) { +func (wfe *WebFrontEndImpl) Authorization(response http.ResponseWriter, request *http.Request) { // Requests to this handler should have a path that leads to a known authz id := parseIDFromPath(request.URL.Path) authz, err := wfe.SA.GetAuthorization(id) @@ -305,7 +294,7 @@ func (wfe *WebFrontEndImpl) Authz(response http.ResponseWriter, request *http.Re } // If there is a fragment, then this is actually a request to a challenge URI - if len(request.URL.Fragment) != 0 { + if len(request.URL.RawQuery) != 0 { wfe.Challenge(authz, response, request) return } @@ -326,7 +315,7 @@ func (wfe *WebFrontEndImpl) Authz(response http.ResponseWriter, request *http.Re } } -func (wfe *WebFrontEndImpl) Cert(response http.ResponseWriter, request *http.Request) { +func (wfe *WebFrontEndImpl) Certificate(response http.ResponseWriter, request *http.Request) { switch request.Method { default: sendError(response, "Method not allowed", http.StatusMethodNotAllowed) From bc583a0df37464ed2ce78781e35f9b198d047c87 Mon Sep 17 00:00:00 2001 From: Richard Barnes Date: Sun, 15 Mar 2015 22:56:46 -0400 Subject: [PATCH 4/8] Simple walkthrough node.js script --- test/js/README.md | 33 ++++ test/js/acme-util.js | 68 ++++++++ test/js/crypto-util.js | 341 ++++++++++++++++++++++++++++++++++++ test/js/test.js | 384 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 826 insertions(+) create mode 100644 test/js/README.md create mode 100644 test/js/acme-util.js create mode 100644 test/js/crypto-util.js create mode 100644 test/js/test.js diff --git a/test/js/README.md b/test/js/README.md new file mode 100644 index 000000000..adf9c6551 --- /dev/null +++ b/test/js/README.md @@ -0,0 +1,33 @@ +# A JS tester for boulder + +The node.js scripts in this directory provide a simple end-to-end test of Boulder. (Using some pieces from [node-acme](https://github.com/letsencrypt/node-acme/)) To run: + +``` +# Install dependencies +> npm install inquirer cli node-forge + +# Start cfssl with signing parameters +# (These are the default parameters to use a Yubikey.) +# (You'll need to make your own key, cert, and policy.) +> go install -tags pkcs11 github.com/cloudflare/cfssl/cmd/cfssl +> cfssl serve -port 8888 -ca ca.cert.pem \ + -pkcs11-module "/Library/OpenSC/lib/opensc-pkcs11.so" \ + -pkcs11-token "Yubico Yubik NEO CCID" \ + -pkcs11-pin 123456 \ + -pkcs11-label "PIV AUTH key" \ + -config policy.json + +# Start boulder +# (Change CFSSL parameters to match your setup.) +> go install github.com/letsencrypt/boulder +> boulder-start --cfssl localhost:8888 + --cfsslProfile ee \ + --cfsslAuthKey 79999d86250c367a2b517a1ae7d409c1 \ + monolithic + +# Client side +> mkdir -p .well-known/acme-challenge/ +> node demo.js +> mv -- *.txt .well-known/acme-challenge/ # In a different window +> python -m SimpleHTTPServer 5001 # In yet another window +``` diff --git a/test/js/acme-util.js b/test/js/acme-util.js new file mode 100644 index 000000000..151e37c76 --- /dev/null +++ b/test/js/acme-util.js @@ -0,0 +1,68 @@ +module.exports = { + + fromStandardB64: function(x) { + return x.replace(/[+]/g, "-").replace(/\//g, "_").replace(/=/g,""); + }, + + toStandardB64: function(x) { + var b64 = x.replace(/-/g, "+").replace(/_/g, "/").replace(/=/g, ""); + + switch (b64.length % 4) { + case 2: b64 += "=="; break; + case 3: b64 += "="; break; + } + + return b64; + }, + + b64enc: function(buffer) { + return this.fromStandardB64(buffer.toString("base64")); + }, + + b64dec: function(str) { + return new Buffer(this.toStandardB64(str), "base64"); + }, + + isB64String: function(x) { + return (typeof(x) == "string") && !x.match(/[^a-zA-Z0-9_-]/); + }, + + fieldsPresent: function(fields, object) { + for (var i in fields) { + if (!(fields[i] in object)) { + return false; + } + } + return true; + }, + + validSignature: function(sig) { + return ((typeof(sig) == "object") && + ("alg" in sig) && (typeof(sig.alg) == "string") && + ("nonce" in sig) && this.isB64String(sig.nonce) && + ("sig" in sig) && this.isB64String(sig.sig) && + ("jwk" in sig) && this.validJWK(sig.jwk)); + }, + + validJWK: function(jwk) { + return ((typeof(jwk) == "object") && ("kty" in jwk) && ( + ((jwk.kty == "RSA") + && ("n" in jwk) && this.isB64String(jwk.n) + && ("e" in jwk) && this.isB64String(jwk.e)) || + ((jwk.kty == "EC") + && ("crv" in jwk) + && ("x" in jwk) && this.isB64String(jwk.x) + && ("y" in jwk) && this.isB64String(jwk.y)) + ) && !("d" in jwk)); + }, + + // A simple, non-standard fingerprint for a JWK, + // just so that we don't have to store objects + keyFingerprint: function(jwk) { + switch (jwk.kty) { + case "RSA": return jwk.n; + case "EC": return jwk.crv + jwk.x + jwk.y; + } + throw "Unrecognized key type"; + } +}; diff --git a/test/js/crypto-util.js b/test/js/crypto-util.js new file mode 100644 index 000000000..dcc9854ca --- /dev/null +++ b/test/js/crypto-util.js @@ -0,0 +1,341 @@ +var crypto = require("crypto"); +var forge = require("node-forge"); +var util = require("./acme-util.js"); + +var TOKEN_SIZE = 16; +var NONCE_SIZE = 16; + +function bytesToBuffer(bytes) { + return new Buffer(forge.util.bytesToHex(bytes), "hex"); +} + +function bufferToBytes(buf) { + return forge.util.hexToBytes(buf.toString("hex")); +} + +function bytesToBase64(bytes) { + return util.b64enc(bytesToBuffer(bytes)); +} + +function base64ToBytes(base64) { + return bufferToBytes(util.b64dec(base64)); +} + +function bnToBase64(bn) { + var hex = bn.toString(16); + if (hex.length % 2 == 1) { hex = "0" + hex; } + return util.b64enc(new Buffer(hex, "hex")); +} + +function base64ToBn(base64) { + return new forge.jsbn.BigInteger(util.b64dec(base64).toString("hex"), 16); +} + +function importPrivateKey(privateKey) { + return forge.pki.rsa.setPrivateKey( + base64ToBn(privateKey.n), + base64ToBn(privateKey.e), base64ToBn(privateKey.d), + base64ToBn(privateKey.p), base64ToBn(privateKey.q), + base64ToBn(privateKey.dp),base64ToBn(privateKey.dq), + base64ToBn(privateKey.qi)); +} + +function importPublicKey(publicKey) { + return forge.pki.rsa.setPublicKey( + base64ToBn(publicKey.n), + base64ToBn(publicKey.e)); +} + +function exportPrivateKey(privateKey) { + return { + "kty": "RSA", + "n": bnToBase64(privateKey.n), + "e": bnToBase64(privateKey.e), + "d": bnToBase64(privateKey.d), + "p": bnToBase64(privateKey.p), + "q": bnToBase64(privateKey.q), + "dp": bnToBase64(privateKey.dP), + "dq": bnToBase64(privateKey.dQ), + "qi": bnToBase64(privateKey.qInv) + }; +} + +function exportPublicKey(publicKey) { + return { + "kty": "RSA", + "n": bnToBase64(publicKey.n), + "e": bnToBase64(publicKey.e) + }; +} + +// A note on formats: +// * Keys are always represented as JWKs +// * Signature objects are in ACME format +// * Certs and CSRs are base64-encoded +module.exports = { + ///// RANDOM STRINGS + + randomString: function(nBytes) { + return bytesToBase64(forge.random.getBytesSync(nBytes)); + }, + + randomSerialNumber: function() { + return forge.util.bytesToHex(forge.random.getBytesSync(4)); + }, + + newToken: function() { + return this.randomString(TOKEN_SIZE); + }, + + ///// SHA-256 + + sha256: function(buf) { + return crypto.createHash('sha256').update(buf).digest('hex'); + }, + + ///// KEY PAIR MANAGEMENT + + generateKeyPair: function(bits) { + var keyPair = forge.pki.rsa.generateKeyPair({bits: bits, e: 0x10001}); + return { + privateKey: exportPrivateKey(keyPair.privateKey), + publicKey: exportPublicKey(keyPair.publicKey) + }; + }, + + importPemPrivateKey: function(pem) { + var key = forge.pki.privateKeyFromPem(pem); + return { + privateKey: exportPrivateKey(key), + publicKey: exportPublicKey(key) + }; + }, + + importPemCertificate: function(pem) { + return forge.pki.certificateFromPem(pem); + }, + + privateKeyToPem: function(privateKey) { + var priv = importPrivateKey(privateKey); + return forge.pki.privateKeyToPem(priv); + }, + + certificateToPem: function(certificate) { + var derCert = base64ToBytes(certificate); + var cert = forge.pki.certificateFromAsn1(forge.asn1.fromDer(derCert)); + return forge.pki.certificateToPem(cert); + }, + + certificateRequestToPem: function(csr) { + var derReq = base64ToBytes(csr); + var c = forge.pki.certificateFromAsn1(forge.asn1.fromDer(derReq)); + return forge.pki.certificateRequestToPem(c); + }, + + ///// SIGNATURE GENERATION / VERIFICATION + + generateSignature: function(keyPair, payload) { + var nonce = bytesToBuffer(forge.random.getBytesSync(NONCE_SIZE)); + var privateKey = importPrivateKey(keyPair.privateKey); + + // Compute JWS signature + var protectedHeader = JSON.stringify({ + nonce: util.b64enc(nonce) + }); + var protected64 = util.b64enc(new Buffer(protectedHeader)); + var payload64 = util.b64enc(payload); + var signatureInputBuf = new Buffer(protected64 + "." + payload64); + var signatureInput = bufferToBytes(signatureInputBuf); + var md = forge.md.sha256.create(); + md.update(signatureInput); + var sig = privateKey.sign(md); + + return { + header: { + alg: "RS256", + jwk: keyPair.publicKey, + }, + protected: protected64, + payload: payload64, + signature: util.b64enc(bytesToBuffer(sig)), + } + }, + + verifySignature: function(jws) { + if (jws.protected) { + if (!jws.header) { + jws.header = {}; + } + + try { + console.log(jws.protected); + var protectedJSON = util.b64dec(jws.protected).toString(); + console.log(protectedJSON); + var protectedObj = JSON.parse(protectedJSON); + for (key in protectedObj) { + jws.header[key] = protectedObj[key]; + } + } catch (e) { + console.log("error unmarshaling json: "+e) + return false; + } + } + + // Assumes validSignature(sig) + if (!jws.header.jwk || (jws.header.jwk.kty != "RSA")) { + // Unsupported key type + console.log("Unsupported key type"); + return false; + } else if (!jws.header.alg || !jws.header.alg.match(/^RS/)) { + // Unsupported algorithm + console.log("Unsupported alg: "+jws.header.alg); + return false; + } + + // Compute signature input + var protected64 = (jws.protected)? jws.protected : ""; + var payload64 = (jws.payload)? jws.payload : ""; + var signatureInputBuf = new Buffer(protected64 + "." + payload64); + var signatureInput = bufferToBytes(signatureInputBuf); + + // Compute message digest + var md; + switch (jws.header.alg) { + case "RS1": md = forge.md.sha1.create(); break; + case "RS256": md = forge.md.sha256.create(); break; + case "RS384": md = forge.md.sha384.create(); break; + case "RS512": md = forge.md.sha512.create(); break; + default: return false; // Unsupported algorithm + } + md.update(signatureInput); + + // Import the key and signature + var publicKey = importPublicKey(jws.header.jwk); + var sig = bufferToBytes(util.b64dec(jws.signature)); + + return publicKey.verify(md.digest().bytes(), sig); + }, + + ///// CSR GENERATION / VERIFICATION + + generateCSR: function(keyPair, identifier) { + var privateKey = importPrivateKey(keyPair.privateKey); + var publicKey = importPublicKey(keyPair.publicKey); + + // Create and sign the CSR + var csr = forge.pki.createCertificationRequest(); + csr.publicKey = publicKey; + csr.setSubject([{ name: 'commonName', value: identifier }]); + csr.sign(privateKey); + + // Convert CSR -> DER -> Base64 + var der = forge.asn1.toDer(forge.pki.certificationRequestToAsn1(csr)); + return util.b64enc(bytesToBuffer(der)); + }, + + verifiedCommonName: function(csr_b64) { + var der = bufferToBytes(util.b64dec(csr_b64)); + var csr = forge.pki.certificationRequestFromAsn1(forge.asn1.fromDer(der)); + + if (!csr.verify()) { + return false; + } + + for (var i=0; i]/g, ""); + var info = parts.reduce(function(acc, p) { + var m = p.trim().match(/(.+) *= *"(.+)"/); + if (m) acc[m[1]] = m[2]; + return acc + }, {}); + info["url"] = url; + return info; + }).reduce(function(acc, link) { + if ("rel" in link) { + acc[link["rel"]] = link["url"] + } + return acc; + }, {}); + return links; + } catch (e) { + return null; + } +} + +/* + +The asynchronous nature of node.js libraries makes the control flow a +little hard to follow here, but it pretty much goes straight down the +page, with detours through the `inquirer` and `http` libraries. + +main + | +register + | +getTerms + | \ + | getAgreement (TODO) + | / +getDomain + | +getChallenges + | +getReadyToValidate + | +sendResponse + | +ensureValidation + | +getCertificate + | +downloadCertificate + | +saveFiles + + +*/ + +function main() { + console.log("Generating key pair..."); + state.keyPair = crypto.generateKeyPair(state.keyPairBits); + console.log(); + inquirer.prompt(questions.email, register) +} + +function register(answers) { + var email = answers.email; + + // Register public key + var registerMessage = JSON.stringify({ + contact: [ "mailto:" + email ] + }); + var jws = crypto.generateSignature(state.keyPair, new Buffer(registerMessage)); + var payload = JSON.stringify(jws); + + var options = url.parse(state.newRegistrationURL); + options.method = "POST"; + var req = http.request(options, getTerms); + req.write(payload) + req.end(); +} + +function getTerms(resp) { + if (Math.floor(resp.statusCode / 100) != 2) { + // Non-2XX response + console.log("Registration request failed with code " + resp.statusCode); + return; + } + + var links = parseLink(resp.headers["link"]); + if (!links || !("next" in links)) { + console.log("The server did not provide information to proceed"); + return + } + + state.registrationURL = resp.headers["location"]; + state.newAuthorizationURL = links["next"]; + state.termsRequired = ("terms-of-service" in links); + + if (state.termsRequired) { + // TODO getAgreement + // inquirer.prompt(questions.terms, getAgreement); + console.log("The CA requires your agreement to terms (not supported)."); + return + } else { + inquirer.prompt(questions.domain, getChallenges); + } +} + +function getChallenges(answers) { + state.domain = answers.domain; + + // Register public key + var authzMessage = JSON.stringify({ + identifier: { + type: "dns", + value: state.domain + } + }); + var jws = crypto.generateSignature(state.keyPair, new Buffer(authzMessage)); + var payload = JSON.stringify(jws); + + var options = url.parse(state.newAuthorizationURL); + options.method = "POST"; + var req = http.request(options, getReadyToValidate); + req.write(payload) + req.end(); +} + +function getReadyToValidate(resp) { + if (Math.floor(resp.statusCode / 100) != 2) { + // Non-2XX response + console.log("Authorization request failed with code " + resp.statusCode) + return; + } + + var links = parseLink(resp.headers["link"]); + if (!links || !("next" in links)) { + console.log("The server did not provide information to proceed"); + return + } + + state.authorizationURL = resp.headers["location"]; + state.newCertificateURL = links["next"]; + + var body = "" + resp.on('data', function(chunk) { + body += chunk; + }); + resp.on('end', function(chunk) { + if (chunk) { body += chunk; } + + var authz = JSON.parse(body); + + var simpleHttps = authz.challenges.filter(function(x) { return x.type == "simpleHttps"; }); + if (simpleHttps.length == 0) { + console.log("The server didn't offer any challenges we can handle."); + return; + } + + var challenge = simpleHttps[0]; + var path = crypto.randomString(8) + ".txt"; + fs.writeFileSync(path, challenge.token); + state.responseURL = challenge["uri"]; + state.path = path; + + console.log(); + console.log("To validate that you own "+ state.domain +", the CA has\n" + + "asked you to provision a file on your server. I've saved\n" + + "the file here for you.\n"); + console.log(" File: " + path); + console.log(" URL: http://"+ state.domain +"/.well-known/acme-challenge/"+ path); + console.log(); + + // To do this locally (boulder connects to port 5001) + // > mkdir -p .well-known/acme-challenge/ + // > mv $CHALLENGE_FILE ./well-known/acme-challenge/ + // > python -m SimpleHTTPServer 5001 + + inquirer.prompt(questions.readyToValidate, sendResponse); + }); +} + +function sendResponse() { + var responseMessage = JSON.stringify({ + path: state.path + }); + var jws = crypto.generateSignature(state.keyPair, new Buffer(responseMessage)); + var payload = JSON.stringify(jws); + + cli.spinner("Validating domain"); + + var options = url.parse(state.responseURL); + options.method = "POST"; + var req = http.request(options, ensureValidation); + req.write(payload) + req.end(); +} + +function ensureValidation(resp) { + if (Math.floor(resp.statusCode / 100) != 2) { + // Non-2XX response + console.log("Authorization status request failed with code " + resp.statusCode) + return; + } + + var body = ""; + resp.on('data', function(chunk) { + body += chunk; + }); + resp.on('end', function(chunk) { + if (chunk) { body += chunk; } + + var authz = JSON.parse(body); + + if (authz.status == "pending") { + setTimeout(function() { + http.get(state.authorizationURL, ensureValidation); + }, state.retryDelay); + } else if (authz.status == "valid") { + cli.spinner("Validating domain ... done", true); + console.log(); + getCertificate(); + } else if (authz.status == "invalid") { + console.log("The CA was unable to validate the file you provisioned."); + return; + } else { + console.log("The CA returned an authorization in an unexpected state"); + console.log(JSON.stringify(authz, null, " ")); + return; + } + }); +} + +function getCertificate() { + var csr = crypto.generateCSR(state.keyPair, state.domain); + + var certificateMessage = JSON.stringify({ + csr: csr, + authorizations: [ state.authorizationURL ] + }); + var jws = crypto.generateSignature(state.keyPair, new Buffer(certificateMessage)); + var payload = JSON.stringify(jws); + + cli.spinner("Requesting certificate"); + + var options = url.parse(state.newCertificateURL); + options.method = "POST"; + var req = http.request(options, downloadCertificate); + req.write(payload) + req.end(); +} + +function downloadCertificate(resp) { + var chunks = []; + resp.on('data', function(chunk) { + chunks.push(chunk); + }); + resp.on('end', function(chunk) { + if (chunk) { chunks.push(chunk); } + var body = Buffer.concat(chunks); + + if (Math.floor(resp.statusCode / 100) != 2) { + // Non-2XX response + console.log("Certificate request failed with code " + resp.statusCode); + console.log(body.toString()); + return; + } + + cli.spinner("Requesting certificate ... done", true); + console.log(); + var certB64 = util.b64enc(body); + + state.certificate = certB64; + inquirer.prompt(questions.files, saveFiles); + }); +} + +function saveFiles(answers) { + var keyPEM = crypto.privateKeyToPem(state.keyPair.privateKey); + fs.writeFileSync(answers.keyFile, keyPEM); + + var certPEM = crypto.certificateToPem(state.certificate); + fs.writeFileSync(answers.certFile, certPEM); + + console.log("Done!") + console.log("To try it out:"); + console.log("openssl s_server -accept 8080 -www -key "+ + answers.keyFile +" -cert "+ answers.certFile); + + // XXX: Explicitly exit, since something's tenacious here + process.exit(0); +} + + +// BEGIN +main(); + From 19fada5b27a17656d3ab88b9f998ee37c86392b1 Mon Sep 17 00:00:00 2001 From: Richard Barnes Date: Sun, 15 Mar 2015 23:47:55 -0400 Subject: [PATCH 5/8] Support for subscriber agreement --- cmd/boulder-start/main.go | 13 +++++-- test/js/README.md | 4 +- test/js/test.js | 77 +++++++++++++++++++++++++++++++++++---- wfe/web-front-end.go | 74 +++++++++++++++++++++++++++++++++++-- 4 files changed, 152 insertions(+), 16 deletions(-) diff --git a/cmd/boulder-start/main.go b/cmd/boulder-start/main.go index 4ec129338..023afbfe8 100644 --- a/cmd/boulder-start/main.go +++ b/cmd/boulder-start/main.go @@ -139,10 +139,17 @@ func main() { http.HandleFunc(newRegPath, wfe.NewRegistration) http.HandleFunc(newAuthzPath, wfe.NewAuthorization) http.HandleFunc(newCertPath, wfe.NewCertificate) - // TODO wire up regPath handler + http.HandleFunc(regPath, wfe.Registration) http.HandleFunc(authzPath, wfe.Authorization) http.HandleFunc(certPath, wfe.Certificate) + // Add a simple ToS + termsPath := "/terms" + http.HandleFunc(termsPath, func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "You agree to do the right thing") + }) + wfe.SubscriberAgreementURL = urlBase + termsPath + // We need to tell the RA how to make challenge URIs // XXX: Better way to do this? Part of improved configuration ra.AuthzBase = wfe.AuthzBase @@ -228,7 +235,7 @@ func main() { http.HandleFunc(newRegPath, wfe.NewRegistration) http.HandleFunc(newAuthzPath, wfe.NewAuthorization) http.HandleFunc(newCertPath, wfe.NewCertificate) - // TODO wire up regPath handler + http.HandleFunc(regPath, wfe.Registration) http.HandleFunc(authzPath, wfe.Authorization) http.HandleFunc(certPath, wfe.Certificate) @@ -273,7 +280,7 @@ func main() { http.HandleFunc(newRegPath, wfe.NewRegistration) http.HandleFunc(newAuthzPath, wfe.NewAuthorization) http.HandleFunc(newCertPath, wfe.NewCertificate) - // TODO wire up regPath handler + http.HandleFunc(regPath, wfe.Registration) http.HandleFunc(authzPath, wfe.Authorization) http.HandleFunc(certPath, wfe.Certificate) diff --git a/test/js/README.md b/test/js/README.md index adf9c6551..cde2cea35 100644 --- a/test/js/README.md +++ b/test/js/README.md @@ -10,7 +10,7 @@ The node.js scripts in this directory provide a simple end-to-end test of Boulde # (These are the default parameters to use a Yubikey.) # (You'll need to make your own key, cert, and policy.) > go install -tags pkcs11 github.com/cloudflare/cfssl/cmd/cfssl -> cfssl serve -port 8888 -ca ca.cert.pem \ +> cfssl serve -port 9000 -ca ca.cert.pem \ -pkcs11-module "/Library/OpenSC/lib/opensc-pkcs11.so" \ -pkcs11-token "Yubico Yubik NEO CCID" \ -pkcs11-pin 123456 \ @@ -20,7 +20,7 @@ The node.js scripts in this directory provide a simple end-to-end test of Boulde # Start boulder # (Change CFSSL parameters to match your setup.) > go install github.com/letsencrypt/boulder -> boulder-start --cfssl localhost:8888 +> boulder-start --cfssl localhost:9000 \ --cfsslProfile ee \ --cfsslAuthKey 79999d86250c367a2b517a1ae7d409c1 \ monolithic diff --git a/test/js/test.js b/test/js/test.js index 077a1a73c..b97fa3995 100644 --- a/test/js/test.js +++ b/test/js/test.js @@ -1,3 +1,8 @@ +// Copyright 2014 ISRG. All rights reserved +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + "use strict"; var inquirer = require("inquirer"); @@ -27,7 +32,7 @@ var questions = { type: "confirm", name: "terms", message: "Do you agree to these terms?", - default: false + default: false, }], domain: [{ @@ -71,6 +76,8 @@ var state = { registrationURL: "", termsRequired: false, + termsAgreed: false, + termsURL: null, domain: null, @@ -121,7 +128,9 @@ register | getTerms | \ - | getAgreement (TODO) + | getAgreement + | | + | sendAgreement | / getDomain | @@ -153,9 +162,10 @@ function register(answers) { var email = answers.email; // Register public key - var registerMessage = JSON.stringify({ + state.registration = { contact: [ "mailto:" + email ] - }); + } + var registerMessage = JSON.stringify(state.registration); var jws = crypto.generateSignature(state.keyPair, new Buffer(registerMessage)); var payload = JSON.stringify(jws); @@ -184,15 +194,66 @@ function getTerms(resp) { state.termsRequired = ("terms-of-service" in links); if (state.termsRequired) { - // TODO getAgreement - // inquirer.prompt(questions.terms, getAgreement); - console.log("The CA requires your agreement to terms (not supported)."); - return + state.termsURL = links["terms-of-service"]; + http.get(state.termsURL, getAgreement) } else { inquirer.prompt(questions.domain, getChallenges); } } +function getAgreement(resp) { + var body = ""; + resp.on("data", function(chunk) { + body += chunk; + }); + resp.on("end", function(chunk) { + if (chunk) { body += chunk; } + + // TODO: Check content-type + console.log("The CA requires your agreement to terms (not supported)."); + console.log(); + console.log(body); + console.log(); + + inquirer.prompt(questions.terms, sendAgreement); + }); +} + +function sendAgreement(answers) { + state.termsAgreed = answers.terms; + + if (state.termsRequired && !state.termsAgreed) { + console.log("Sorry, can't proceed if you don't agree."); + process.exit(1); + } + + state.registration.agreement = state.termsURL; + var registerMessage = JSON.stringify(state.registration); + var jws = crypto.generateSignature(state.keyPair, new Buffer(registerMessage)); + var payload = JSON.stringify(jws); + + console.log("Posting agreement to: " + state.registrationURL) + var options = url.parse(state.registrationURL); + options.method = "POST"; + var req = http.request(options, function(resp) { + var body = ""; + resp.on("data", function(chunk) { body += chunk; }); + resp.on("end", function() { + if (Math.floor(resp.statusCode / 100) != 2) { + // Non-2XX response + console.log("Couldn't POST agreement back to server, aborting."); + console.log("Code: "+ resp.statusCode); + console.log(body); + process.exit(1); + } + }); + + inquirer.prompt(questions.domain, getChallenges); + }); + req.write(payload) + req.end(); +} + function getChallenges(answers) { state.domain = answers.domain; diff --git a/wfe/web-front-end.go b/wfe/web-front-end.go index 77e4aea27..924900579 100644 --- a/wfe/web-front-end.go +++ b/wfe/web-front-end.go @@ -6,7 +6,6 @@ package wfe import ( - "encoding/hex" "encoding/json" "fmt" "io/ioutil" @@ -29,6 +28,8 @@ type WebFrontEndImpl struct { AuthzBase string NewCert string CertBase string + + SubscriberAgreementURL string } func NewWebFrontEndImpl() WebFrontEndImpl { @@ -133,6 +134,10 @@ func (wfe *WebFrontEndImpl) NewRegistration(response http.ResponseWriter, reques response.Header().Add("Location", regURL) response.Header().Add("Link", link(wfe.NewAuthz, "next")) + if len(wfe.SubscriberAgreementURL) > 0 { + response.Header().Add("Link", link(wfe.SubscriberAgreementURL, "terms-of-service")) + } + response.WriteHeader(http.StatusCreated) response.Write(responseBody) } @@ -211,8 +216,6 @@ func (wfe *WebFrontEndImpl) NewCertificate(response http.ResponseWriter, request // Make a URL for this authz certURL := wfe.CertBase + string(cert.ID) - fmt.Printf("Returning certificate: %+v", hex.EncodeToString(cert.DER)) - // TODO: Content negotiation for cert format response.Header().Add("Location", certURL) response.WriteHeader(http.StatusCreated) @@ -282,6 +285,71 @@ func (wfe *WebFrontEndImpl) Challenge(authz core.Authorization, response http.Re } } +func (wfe *WebFrontEndImpl) Registration(response http.ResponseWriter, request *http.Request) { + // Requests to this handler should have a path that leads to a known authz + id := parseIDFromPath(request.URL.Path) + reg, err := wfe.SA.GetRegistration(id) + if err != nil { + sendError(response, + fmt.Sprintf("Unable to find registration: %+v", err), + http.StatusNotFound) + return + } + reg.ID = id + + switch request.Method { + default: + sendError(response, "Method not allowed", http.StatusMethodNotAllowed) + return + + case "GET": + jsonReply, err := json.Marshal(reg) + if err != nil { + sendError(response, "Failed to marshal authz", http.StatusInternalServerError) + return + } + response.WriteHeader(http.StatusOK) + response.Write(jsonReply) + + case "POST": + body, key, err := verifyPOST(request) + if err != nil { + sendError(response, "Unable to read/verify body", http.StatusBadRequest) + return + } + + var update core.Registration + err = json.Unmarshal(body, &update) + if err != nil { + sendError(response, "Error unmarshaling registration", http.StatusBadRequest) + return + } + + // Check that the signing key is the right key + if !key.Equals(reg.Key) { + sendError(response, "Signing key does not match key in registration", http.StatusForbidden) + return + } + + // Ask the RA to update this authorization + updatedReg, err := wfe.RA.UpdateRegistration(reg, update) + if err != nil { + fmt.Println(err) + sendError(response, "Unable to update registration", http.StatusInternalServerError) + return + } + + jsonReply, err := json.Marshal(updatedReg) + if err != nil { + sendError(response, "Failed to marshal authz", http.StatusInternalServerError) + return + } + response.WriteHeader(http.StatusAccepted) + response.Write(jsonReply) + + } +} + func (wfe *WebFrontEndImpl) Authorization(response http.ResponseWriter, request *http.Request) { // Requests to this handler should have a path that leads to a known authz id := parseIDFromPath(request.URL.Path) From b5d67c733a52858115aad3a683552be0e146ceaf Mon Sep 17 00:00:00 2001 From: Richard Barnes Date: Tue, 17 Mar 2015 10:17:21 -0400 Subject: [PATCH 6/8] Addressing JCJ comments --- test/js/acme-util.js | 5 +++++ test/js/crypto-util.js | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/test/js/acme-util.js b/test/js/acme-util.js index 151e37c76..d76369893 100644 --- a/test/js/acme-util.js +++ b/test/js/acme-util.js @@ -1,3 +1,8 @@ +// Copyright 2014 ISRG. All rights reserved +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + module.exports = { fromStandardB64: function(x) { diff --git a/test/js/crypto-util.js b/test/js/crypto-util.js index dcc9854ca..a80c5de9e 100644 --- a/test/js/crypto-util.js +++ b/test/js/crypto-util.js @@ -1,3 +1,8 @@ +// Copyright 2014 ISRG. All rights reserved +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + var crypto = require("crypto"); var forge = require("node-forge"); var util = require("./acme-util.js"); From 34da176328d3249539311c4d2f97db4977749c77 Mon Sep 17 00:00:00 2001 From: Richard Barnes Date: Tue, 17 Mar 2015 11:29:01 -0400 Subject: [PATCH 7/8] Use remote signer in CA testing --- ca/certificate-authority_test.go | 233 ++++++++++++++++++++++++------- 1 file changed, 186 insertions(+), 47 deletions(-) diff --git a/ca/certificate-authority_test.go b/ca/certificate-authority_test.go index 1a7d7c713..7575ea484 100644 --- a/ca/certificate-authority_test.go +++ b/ca/certificate-authority_test.go @@ -7,10 +7,16 @@ package ca import ( "crypto/x509" + "encoding/asn1" "encoding/hex" "encoding/pem" + "net/http" "testing" + "time" + apisign "github.com/cloudflare/cfssl/api/sign" + "github.com/cloudflare/cfssl/auth" + "github.com/cloudflare/cfssl/config" "github.com/cloudflare/cfssl/signer/local" _ "github.com/mattn/go-sqlite3" @@ -108,24 +114,109 @@ var CA_CERT_PEM = "-----BEGIN CERTIFICATE-----\n" + // * Random public key // * CN = example.com // * DNSNames = example.com, www.example.com -var CSR_HEX = "308202953082017d0201003016311430120603" + - "550403130b6578616d706c652e636f6d30820122300d06092a864886f70d0101010500038201" + - "0f003082010a0282010100baaf16e891828470cad87b849a73356f65e20ad3699fd5583a7200" + - "e924512d9eeb1dbe16441ad7bd804fa2e5726a06f0af5279012fe6354a5677259f5591984aa9" + - "99b8ea3ea10fbd5ecfa30e5f563b41c419374decfc98ea62c611046ad011c326470a2426f46d" + - "be6cc44fae3b247e19710810585f9f3ad7f64b2f305aebb72e2829866f89b20b03a300b7ff5f" + - "4e6204f41420d9fa731252692cee8e616636723abe8a7053fd86e2673190fa8b618ada5bc735" + - "ba57a145af86904a8f55a288d4d6ba9e501530f23f197f5b623443193fc92b7f87d6abbf740d" + - "9fc92800c7e0e1484d5eec6ffae1007c139c1ec19d67e749743fe8d8396fe190cfbcf2f90e05" + - "230203010001a03a303806092a864886f70d01090e312b302930270603551d110420301e820b" + - "6578616d706c652e636f6d820f7777772e6578616d706c652e636f6d300d06092a864886f70d" + - "01010b05000382010100514c622dc866b31c86777a26e9b2911618ce5b188591f08007b42772" + - "3497b733676a7d493c434fc819b8089869442fd299aa99ff7f7b9df881843727f0c8b89ca62a" + - "f8a12b38c963e9210148c4e1c0fc964aef2605f88ed777e6497d2e43e9a9713835d1ae96260c" + - "ca826c34c7ae52c77f5d8468643ee1936eadf04e1ebf8bbbb68b0ec7d0ef694432451292e4a3" + - "1989fd8339c07e01e04b6dd3834872b828d3f5b2b4dadda0596396e15fbdf446998066a74873" + - "2baf53f3f7ebb849e83cf734753c35ab454d1b62e1741a6514c5c575c0c805b4d668fcf71746" + - "ef32017613a52d6b807e2977f4fbc0a88b2e263347c4d9e35435333cf4f8288be53df41228ec" +var CN_AND_SAN_CSR_HEX = "308202a130820189020100301a311830160603550403130f6e6f742d6578" + + "616d706c652e636f6d30820122300d06092a864886f70d01010105000382" + + "010f003082010a0282010100e56ccbe37003c150202e6f543f9eb1d0e590" + + "76ac7f1f62654fa82fe131a23c66bd53a2f62ff7852015c84a394e36836d" + + "2018eba278e0740c85c4c6102787400c2ef069b4a72e6eb8ad8d1da5d76b" + + "f3e70dafc126578ed28cf40030e7fe5b5307ef630254726c639561b5445d" + + "372847bdb02576aa3622a688158c6af09d3938dbeba4d670cec4325be73a" + + "fa52a0a04dcba2f335f1e85020704db94ca125dce70b3209294c6c46ed4b" + + "48b95d8d51ae2d2fd227116023a48ca7381e35fd302ad2999df625a4b5ee" + + "82a0d0fefa88ac6a62b01674de75637ef83328202cda9930947d932000b0" + + "e53b82e099ab60fec9c8b6d4eccdee508b6ebca7e6ca3f752046c8350203" + + "010001a042304006092a864886f70d01090e31333031302f0603551d1104" + + "283026820f6e6f742d6578616d706c652e636f6d82137777772e6e6f742d" + + "6578616d706c652e636f6d300d06092a864886f70d01010b050003820101" + + "008c4bf2ab4dfd28d768697eecc5be889a6275287c7dd24f9232ffad5675" + + "de708c9cc911545d0e84f61b6584c5e237915bbf231d6518e7e228be2e65" + + "b4d50bd9729ce9e6aee00482e014de4edd4b9a4f9a7777b8943ef3512dbf" + + "940ac561c25b34ded9db1074136b978a65943ab1259608fb8109e008eac6" + + "23d7b29b2f1fad3a8e358aa070ead688016d9efed6da43412b136903de07" + + "137462d3f9203a344d84d7eb336999004e7e9972d5176001e2792f206e6c" + + "7c70b86d312459f21751d29ea53b41f9d02a229f9d7615b2a7ac83e849d0" + + "d0d9f8a08f8d7ba23295e77c95bc060c9227bfec0afb8c898e33c89903d7" + + "bbde4cf059dcc3e6c4ae4eef207c499d62" + +// CSR generated by Go: +// * Random public key +// * CN = not-example.com +// * DNSNames = [none] +var NO_SAN_CSR_HEX = "3082025f30820147020100301a311830160603550403130f6e6f742d6578" + + "616d706c652e636f6d30820122300d06092a864886f70d01010105000382" + + "010f003082010a0282010100aa6e56ff24906f93b855e7871dc8411a3cf7" + + "678d9563627e8ca37ab17dfe814ef7f828d6aa92b717f0da9df56990b953" + + "989d5afc3f2dddacd2b504b89782b49e55a04a64a4370d8ab1b2688f2596" + + "98132e5ce536f812ef5eb13824a922bbb89e30d6f2cace77462b9e65264a" + + "32320a7b348f9903b16640bc8c1c5f1208c6b456fd85bfa96ee9b7642c68" + + "3ab05b142d249525a730b230b39f2ba8d6f253263b5c3948b1a3d8a3467f" + + "7cfcdd1fdd6bff7828fda12784fd277be8c680fcdf2cc4676acff5df759f" + + "f4bc712ee1a560157233cbf6bb4bcb91dd1c5d2824b42f4913e4715c1ba4" + + "001fde0d90c274bfa81a79e4a0d00a7ddcbfdd8de4183b497487a20d0203" + + "010001a000300d06092a864886f70d01010b050003820101000ead204cfd" + + "45d307dd49de6937d7e2d8abf17490a49a8cee5250ef7799ef53229f8cfc" + + "735b9f65d789f898945f3d5536a09932e241050bd5473c47a4ac2493707f" + + "1142bf9a06d047384ad463463acb3744d435b4cff8c8b0f9673e8700e13b" + + "6bc99a486823fa85f7707e1bb8430e62541715ab6cb3fae3efb8356042a5" + + "c9f493dd08eff690570cce65cffc4fe354aa40957dc16a37a833aa968f62" + + "693d5059d53f6a96a159195d3fb7b558d462de63d945d4e3680d2b1f2c98" + + "33c3bfd92a9235de3d345a431ee5a675e0e18308bd2729413acd84432da4" + + "2410e1b87ae70227dd9a98e49ee6aeea9eaff67f968691918201e94697f2" + + "da010d6f939cea40c26038" + +// CSR generated by Go: +// * Random public key +// * C = US +// * CN = [none] +// * DNSNames = not-example.com +var NO_CN_CSR_HEX = "3082027f30820167020100300d310b300906035504061302555330820122" + + "300d06092a864886f70d01010105000382010f003082010a0282010100d8" + + "b3c11610ce17614f6d78de3f079db430e479c38978da8cd625b7c70dd445" + + "57fd99b9831693e6b9b09fb7c74a82058a1f1a4e1e087f04f93aa73bc35a" + + "688440205a6f5fd56ff478c5554b14c3b2a1a0b5eed1aef7189ad848e117" + + "04b1eb6c29b47ada40a5719a38ce2f2869896bf5405c2bafd4c7dfb99c0e" + + "9f26f80145e16b73bbacf67aedcd3b7ce57bb5b67cf692aec7956d23c236" + + "2336c2408b65469630dccca3ca006f28e36ca8c95dda84b6586f29c8de63" + + "661c09b58253e386a74707394cbba4de165f2745a65b717b9fd4b8b84c09" + + "85583b5c17d3e88bbf71c88eeeccb5d552d61cde7835ec83d6ec9b41114a" + + "0583f8eeae8a536cb3ca5786c22ab30203010001a02d302b06092a864886" + + "f70d01090e311e301c301a0603551d1104133011820f6e6f742d6578616d" + + "706c652e636f6d300d06092a864886f70d01010b05000382010100430239" + + "8db6b64b94d93399db32335232967ca6a748048483db8cb2b387871f758c" + + "6f7bf1593624b142127847cd2a511897bbadd8ad038468fb309fa2161031" + + "949b9ba24931b0d363ad2f8dae56a4c908ba748d41c664aa129dcb1a6f88" + + "0b90502cd244d9abd8dd5e78f763730660655a350f1c25af95cf1f89dda9" + + "076f4e6b84b6da9a98ed87f538624e4338fa0ff1a404e763dd6800694a21" + + "d28595927606308aefa1ac7e8f5600b05e33c0a7b25d3a9f5032c7c25264" + + "026c039733b179315254af4f25e90a1d00facd69313b36fdc66a5818fb49" + + "a0d90e0745d66a82d337289c9968b3ec4a4826c530c758cacecc18e06366" + + "dd8962c451c3ce22c2aed33726" + +// CSR generated by Go: +// * Random public key +// * C = US +// * CN = [none] +// * DNSNames = [none] +var NO_NAME_CSR_HEX = "308202523082013a020100300d310b300906035504061302555330820122" + + "300d06092a864886f70d01010105000382010f003082010a0282010100bc" + + "fae49f68f02c42500b2faf251628ee19e8ef048a35fef311c9c419c80606" + + "ab37340ad6e25cf4cc63c0283994b4ba705d86950ad5298094e0b9684647" + + "8d67abc695741317b4ff8da9fd33120342559cfdaf9109ac403f0d0bf9ff" + + "54dd79fa2256b218a9bdb17c608167c7fcad4cf839733c7eab9589fe6137" + + "e99bb24c24b7eb74e19f51ffee4ea62c4ab756f099ff5197c5032f60edff" + + "36022b8a99d35aeb706854fa9a31ea8a362a2251f08b93023b32e1df771a" + + "970f08a30ced656950b8ef71600d65d6995a0b92903b179c05a76f702a08" + + "0b41402c308d8ab57f14b5516b89fe317e38e13d7adad7f7025743610881" + + "9fb60268f0773b08b62ac8c8c84f2d0203010001a000300d06092a864886" + + "f70d01010b050003820101001eda9ce8253e8b933348851acd38ab63cd64" + + "f833d7ffc711f1b6e6a37a7deb7ad44b5589d90533ed61dfd48cab2775e2" + + "a19c41f5cb69faa9dde856606822a3bf798381836214154c17bc037f23ad" + + "67c84d876855c0aea871dc55bd14b2cd267e49b734bc7a38c29c334611bf" + + "ec7efdc56a1512e25fd12ca99a5809b1b6a808caf6a8baefff7fb2bda454" + + "5c226849674900ce7a1f90287ab31be80a4e2b6d64765b9d973628e60299" + + "6423edd74e7a58005bd520d4173f0c30d935de530477480d7725d9758f9a" + + "58c004d9e1e55af59ea517dfbd2bccca58216d8130b9f77c90328b2aa54b" + + "1778a629b584f2bc059489a236131de9b444adca90218c31a499a485" func TestIssueCertificate(t *testing.T) { // Decode pre-generated values @@ -135,44 +226,92 @@ func TestIssueCertificate(t *testing.T) { caCertPEM, _ := pem.Decode([]byte(CA_CERT_PEM)) caCert, _ := x509.ParseCertificate(caCertPEM.Bytes) - csrDER, _ := hex.DecodeString(CSR_HEX) - csr, _ := x509.ParseCertificateRequest(csrDER) + // Uncomment to create a CFSSL local signer - // Create a CFSSL local signer - signer, _ := local.NewSigner(caKey, caCert, x509.SHA256WithRSA, nil) + // CFSSL config + hostPort := "localhost:9000" + authKey := "79999d86250c367a2b517a1ae7d409c1" + profileName := "ee" // Create an SA sa, err := sa.NewSQLStorageAuthority("sqlite3", ":memory:") test.AssertNotError(t, err, "Failed to create SA") sa.InitTables() + // Create an online CFSSL instance + // This is designed to mimic what LE plans to do + authHandler, err := auth.New(authKey, nil) + test.AssertNotError(t, err, "Failed to create authentication handler") + policy := &config.Signing{ + Profiles: map[string]*config.SigningProfile{ + profileName: &config.SigningProfile{ + Usage: []string{"server auth"}, + CA: false, + IssuerURL: []string{"http://not-example.com/issuer-url"}, + OCSP: "http://not-example.com/ocsp", + CRL: "http://not-example.com/crl", + + Policies: []asn1.ObjectIdentifier{ + asn1.ObjectIdentifier{2, 23, 140, 1, 2, 1}, + }, + Expiry: 8760 * time.Hour, + Backdate: time.Hour, + Provider: authHandler, + }, + }, + Default: &config.SigningProfile{ + Expiry: time.Hour, + }, + } + signer, err := local.NewSigner(caKey, caCert, x509.SHA256WithRSA, policy) + test.AssertNotError(t, err, "Failed to create signer") + signHandler, err := apisign.NewAuthHandlerFromSigner(signer) + test.AssertNotError(t, err, "Failed to create signing API endpoint") + http.Handle("/api/v1/cfssl/authsign", signHandler) + // This goroutine should get killed when main() return + go (func() { http.ListenAndServe(hostPort, nil) })() + // Create a CA + // Uncomment to test with a remote signer + ca, err := NewCertificateAuthorityImpl(hostPort, authKey, profileName) + test.AssertNotError(t, err, "Failed to create CA") + ca.SA = sa + /* - // Uncomment to test with a remote signer - ca, err := NewCertificateAuthorityImpl("localhost:9000", "79999d86250c367a2b517a1ae7d409c1", "ee") - test.AssertNotError(t, err, "Failed to create CA") - ca.SA = sa + // Uncomment to test with a local signer + signer, _ := local.NewSigner(caKey, caCert, x509.SHA256WithRSA, nil) + ca := CertificateAuthorityImpl{ + Signer: signer, + SA: sa, + } */ - ca := CertificateAuthorityImpl{ - Signer: signer, - SA: sa, + + csrs := []string{CN_AND_SAN_CSR_HEX, NO_SAN_CSR_HEX, NO_CN_CSR_HEX} + for _, csrHEX := range csrs { + csrDER, _ := hex.DecodeString(csrHEX) + csr, _ := x509.ParseCertificateRequest(csrDER) + + // Sign CSR + certObj, err := ca.IssueCertificate(*csr) + test.AssertNotError(t, err, "Failed to sign certificate") + + // Verify cert contents + cert, err := x509.ParseCertificate(certObj.DER) + test.AssertNotError(t, err, "Certificate failed to parse") + + test.AssertEquals(t, cert.Subject.CommonName, "not-example.com") + + if len(cert.DNSNames) == 0 || cert.DNSNames[0] != "not-example.com" { + // NB: This does not check for www.not-example.com in the 'both' case + t.Errorf("Improper list of domain names %v", cert.DNSNames) + } + + if len(cert.Subject.Country) > 0 { + t.Errorf("Subject contained unauthorized values") + } + + // Verify that the cert got stored in the DB + _, err = sa.GetCertificate(certObj.ID) + test.AssertNotError(t, err, "Certificate not found in database") } - - // Sign CSR - certObj, err := ca.IssueCertificate(*csr) - test.AssertNotError(t, err, "Failed to sign certificate") - - // Verify cert contents - cert, err := x509.ParseCertificate(certObj.DER) - test.AssertNotError(t, err, "Certificate failed to parse") - - test.AssertEquals(t, cert.Subject.CommonName, "example.com") - - if len(cert.DNSNames) != 2 || cert.DNSNames[0] != "example.com" || cert.DNSNames[1] != "www.example.com" { - t.Errorf("Improper list of domain names %v", cert.DNSNames) - } - - // Verify that the cert got stored in the DB - _, err = sa.GetCertificate(certObj.ID) - test.AssertNotError(t, err, "Certificate not found in database") } From 74bbea6920147853ad471d08606f01979b99cb35 Mon Sep 17 00:00:00 2001 From: Richard Barnes Date: Tue, 17 Mar 2015 11:31:54 -0400 Subject: [PATCH 8/8] Test coverage for the no-name case --- ca/certificate-authority_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ca/certificate-authority_test.go b/ca/certificate-authority_test.go index 7575ea484..9212e010a 100644 --- a/ca/certificate-authority_test.go +++ b/ca/certificate-authority_test.go @@ -314,4 +314,12 @@ func TestIssueCertificate(t *testing.T) { _, err = sa.GetCertificate(certObj.ID) test.AssertNotError(t, err, "Certificate not found in database") } + + // Test that the CA rejects CSRs with no names + csrDER, _ := hex.DecodeString(NO_NAME_CSR_HEX) + csr, _ := x509.ParseCertificateRequest(csrDER) + _, err = ca.IssueCertificate(*csr) + if err == nil { + t.Errorf("CA improperly agreed to create a certificate with no name") + } }