diff --git a/cmd/boulder-ra/main.go b/cmd/boulder-ra/main.go index c0d6cd535..ffa9e28af 100644 --- a/cmd/boulder-ra/main.go +++ b/cmd/boulder-ra/main.go @@ -18,7 +18,6 @@ import ( blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/ra" "github.com/letsencrypt/boulder/rpc" - "github.com/letsencrypt/boulder/wfe" ) func main() { @@ -44,7 +43,6 @@ func main() { cmd.FailOnError(err, "Couldn't create PA") rai := ra.NewRegistrationAuthorityImpl(clock.Default(), auditlogger) - rai.AuthzBase = c.Common.BaseURL + wfe.AuthzPath rai.PA = pa raDNSTimeout, err := time.ParseDuration(c.Common.DNSTimeout) cmd.FailOnError(err, "Couldn't parse RA DNS timeout") diff --git a/core/objects.go b/core/objects.go index 1d6456c7d..b3d617e10 100644 --- a/core/objects.go +++ b/core/objects.go @@ -241,6 +241,8 @@ type ValidationRecord struct { // challenge, we just throw all the elements into one bucket, // together with the common metadata elements. type Challenge struct { + ID int64 `json:"id,omitempty"` + // The type of challenge Type string `json:"type"` @@ -255,7 +257,7 @@ type Challenge struct { Validated *time.Time `json:"validated,omitempty"` // A URI to which a response can be POSTed - URI *AcmeURL `json:"uri"` + URI string `json:"uri"` // Used by simpleHttp, dvsni, and dns challenges Token string `json:"token,omitempty"` @@ -431,6 +433,18 @@ type Authorization struct { Combinations [][]int `json:"combinations,omitempty" db:"combinations"` } +// FindChallenge will look for the given challenge inside this authorization. If +// found, it will return the index of that challenge within the Authorization's +// Challenges array. Otherwise it will return -1. +func (authz *Authorization) FindChallenge(challengeID int64) int { + for i, c := range authz.Challenges { + if c.ID == challengeID { + return i + } + } + return -1 +} + // JSONBuffer fields get encoded and decoded JOSE-style, in base64url encoding // with stripped padding. type JSONBuffer []byte diff --git a/ra/registration-authority.go b/ra/registration-authority.go index c618b66d0..83ec6bd1c 100644 --- a/ra/registration-authority.go +++ b/ra/registration-authority.go @@ -10,7 +10,6 @@ import ( "errors" "fmt" "net/mail" - "strconv" "strings" "time" @@ -31,8 +30,6 @@ type RegistrationAuthorityImpl struct { DNSResolver core.DNSResolver clk clock.Clock log *blog.AuditLogger - - AuthzBase string } // NewRegistrationAuthorityImpl constructs a new RA object. @@ -150,6 +147,11 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(request core.Authorization // Create validations, but we have to update them with URIs later challenges, combinations := ra.PA.ChallengesFor(identifier) + for i, _ := range challenges { + // Add the account key used to generate the challenge + challenges[i].AccountKey = ®.Key + } + // Partially-filled object authz = core.Authorization{ Identifier: identifier, @@ -165,33 +167,19 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(request core.Authorization // InternalServerError since the user-data was validated before being // passed to the SA. err = core.InternalServerError(fmt.Sprintf("Invalid authorization request: %s", err)) - return authz, err + return core.Authorization{}, err } - // Construct all the challenge URIs - for i := range authz.Challenges { - // Ignoring these errors because we construct the URLs to be correct - challengeURI, _ := core.ParseAcmeURL(ra.AuthzBase + authz.ID + "?challenge=" + strconv.Itoa(i)) - authz.Challenges[i].URI = challengeURI - - // Add the account key used to generate the challenge - authz.Challenges[i].AccountKey = ®.Key - - if !authz.Challenges[i].IsSane(false) { + // Check each challenge for sanity. + for _, challenge := range authz.Challenges { + if !challenge.IsSane(false) { // InternalServerError because we generated these challenges, they should // be OK. - err = core.InternalServerError(fmt.Sprintf("Challenge didn't pass sanity check: %+v", authz.Challenges[i])) - return authz, err + err = core.InternalServerError(fmt.Sprintf("Challenge didn't pass sanity check: %+v", challenge)) + return core.Authorization{}, err } } - // Store the authorization object, then return it - err = ra.SA.UpdatePendingAuthorization(authz) - if err != nil { - // InternalServerError because we created the authorization just above, - // and adding Sane challenges should not break it. - err = core.InternalServerError(err.Error()) - } return authz, err } diff --git a/ra/registration-authority_test.go b/ra/registration-authority_test.go index bfca00aad..5cbb69aa7 100644 --- a/ra/registration-authority_test.go +++ b/ra/registration-authority_test.go @@ -214,7 +214,6 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, *sa.SQLStorageAut ra.VA = va ra.CA = &ca ra.PA = pa - ra.AuthzBase = "http://acme.invalid/authz/" ra.DNSResolver = &mocks.MockDNS{} AuthzInitial.RegistrationID = Registration.ID diff --git a/rpc/amqp-rpc.go b/rpc/amqp-rpc.go index ba926fa98..538a2c9d2 100644 --- a/rpc/amqp-rpc.go +++ b/rpc/amqp-rpc.go @@ -366,6 +366,7 @@ func (rpc *AmqpRPCServer) processMessage(msg amqp.Delivery) { CorrelationId: msg.CorrelationId, Type: msg.Type, Body: jsonResponse, // XXX-JWS: jws.Sign(privKey, body) + Expiration: "30000", }) } @@ -486,7 +487,12 @@ func NewAmqpRPCClient(clientQueuePrefix, serverQueue string, channel *amqp.Chann return nil, err } - clientQueue := fmt.Sprintf("%s.%s", clientQueuePrefix, hostname) + randID := make([]byte, 3) + _, err = rand.Read(randID) + if err != nil { + return nil, err + } + clientQueue := fmt.Sprintf("%s.%s.%x", clientQueuePrefix, hostname, randID) rpc = &AmqpRPCCLient{ serverQueue: serverQueue, @@ -558,6 +564,7 @@ func (rpc *AmqpRPCCLient) Dispatch(method string, body []byte) chan []byte { ReplyTo: rpc.clientQueue, Type: method, Body: body, // XXX-JWS: jws.Sign(privKey, body) + Expiration: "30000", }) return responseChan diff --git a/sa/_db/migrations/20150828163255_RemoveChallengeURI.sql b/sa/_db/migrations/20150828163255_RemoveChallengeURI.sql new file mode 100644 index 000000000..ed640adf9 --- /dev/null +++ b/sa/_db/migrations/20150828163255_RemoveChallengeURI.sql @@ -0,0 +1,13 @@ + +-- +goose Up +-- SQL in section 'Up' is executed when this migration is applied + +ALTER TABLE `challenges` DROP COLUMN `uri`; + + +-- +goose Down +-- SQL section 'Down' is executed when this migration is rolled back + +ALTER TABLE `challenges` ADD COLUMN ( + `uri` varchar(255) +); diff --git a/sa/model.go b/sa/model.go index e206331ee..0d0216414 100644 --- a/sa/model.go +++ b/sa/model.go @@ -36,7 +36,6 @@ type challModel struct { Status core.AcmeStatus `db:"status"` Error []byte `db:"error"` Validated *time.Time `db:"validated"` - URI string `db:"uri"` Token string `db:"token"` TLS *bool `db:"tls"` Validation []byte `db:"validation"` @@ -85,6 +84,7 @@ func modelToRegistration(rm *regModel) (core.Registration, error) { func challengeToModel(c *core.Challenge, authID string) (*challModel, error) { cm := challModel{ + ID: c.ID, AuthorizationID: authID, Type: c.Type, Status: c.Status, @@ -108,12 +108,6 @@ func challengeToModel(c *core.Challenge, authID string) (*challModel, error) { } cm.Error = errJSON } - if c.URI != nil { - if len(c.URI.String()) > 255 { - return nil, fmt.Errorf("URI is too long to store in the database") - } - cm.URI = c.URI.String() - } if len(c.ValidationRecord) > 0 { vrJSON, err := json.Marshal(c.ValidationRecord) if err != nil { @@ -139,19 +133,13 @@ func challengeToModel(c *core.Challenge, authID string) (*challModel, error) { func modelToChallenge(cm *challModel) (core.Challenge, error) { c := core.Challenge{ + ID: cm.ID, Type: cm.Type, Status: cm.Status, Validated: cm.Validated, Token: cm.Token, TLS: cm.TLS, } - if len(cm.URI) > 0 { - uri, err := core.ParseAcmeURL(cm.URI) - if err != nil { - return core.Challenge{}, err - } - c.URI = uri - } if len(cm.Validation) > 0 { val, err := jose.ParseSigned(string(cm.Validation)) if err != nil { diff --git a/sa/storage-authority.go b/sa/storage-authority.go index 55022e28c..6b3d3a8f2 100644 --- a/sa/storage-authority.go +++ b/sa/storage-authority.go @@ -440,17 +440,28 @@ func (ssa *SQLStorageAuthority) NewPendingAuthorization(authz core.Authorization return } - for _, c := range authz.Challenges { - chall, err := challengeToModel(&c, pendingAuthz.ID) + for i, c := range authz.Challenges { + challModel, err := challengeToModel(&c, pendingAuthz.ID) if err != nil { tx.Rollback() return core.Authorization{}, err } - err = tx.Insert(chall) + // Magic happens here: Gorp will modify challModel, setting challModel.ID + // to the auto-increment primary key. This is important because we want + // the challenge objects inside the Authorization we return to know their + // IDs, so they can have proper URLs. + // See https://godoc.org/github.com/coopernurse/gorp#DbMap.Insert + err = tx.Insert(challModel) if err != nil { tx.Rollback() return core.Authorization{}, err } + challenge, err := modelToChallenge(challModel) + if err != nil { + tx.Rollback() + return core.Authorization{}, err + } + authz.Challenges[i] = challenge } err = tx.Commit() diff --git a/sa/storage-authority_test.go b/sa/storage-authority_test.go index c6b50bfd8..206c73e6b 100644 --- a/sa/storage-authority_test.go +++ b/sa/storage-authority_test.go @@ -177,9 +177,7 @@ func CreateDomainAuthWithRegId(t *testing.T, domainName string, sa *SQLStorageAu test.Assert(t, authz.ID != "", "ID shouldn't be blank") // prepare challenge for auth - u, err := core.ParseAcmeURL(domainName) - test.AssertNotError(t, err, "Couldn't parse domainName "+domainName) - chall := core.Challenge{Type: "simpleHttp", Status: core.StatusValid, URI: u, Token: "THISWOULDNTBEAGOODTOKEN"} + chall := core.Challenge{Type: "simpleHttp", Status: core.StatusValid, URI: domainName, Token: "THISWOULDNTBEAGOODTOKEN"} combos := make([][]int, 1) combos[0] = []int{0, 1} exp := time.Now().AddDate(0, 0, 1) // expire in 1 day diff --git a/wfe/web-front-end.go b/wfe/web-front-end.go index c502de3ae..2e6dbe033 100644 --- a/wfe/web-front-end.go +++ b/wfe/web-front-end.go @@ -11,7 +11,6 @@ import ( "database/sql" "encoding/json" "fmt" - "html/template" "io/ioutil" "net/http" "regexp" @@ -32,6 +31,7 @@ const ( RegPath = "/acme/reg/" NewAuthzPath = "/acme/new-authz" AuthzPath = "/acme/authz/" + ChallengePath = "/acme/challenge/" NewCertPath = "/acme/new-cert" CertPath = "/acme/cert/" RevokeCertPath = "/acme/revoke-cert" @@ -47,13 +47,14 @@ type WebFrontEndImpl struct { log *blog.AuditLogger // URL configuration parameters - BaseURL string - NewReg string - RegBase string - NewAuthz string - AuthzBase string - NewCert string - CertBase string + BaseURL string + NewReg string + RegBase string + NewAuthz string + AuthzBase string + ChallengeBase string + NewCert string + CertBase string // JSON encoded endpoint directory DirectoryJSON []byte @@ -195,6 +196,7 @@ func (wfe *WebFrontEndImpl) Handler() (http.Handler, error) { wfe.RegBase = wfe.BaseURL + RegPath wfe.NewAuthz = wfe.BaseURL + NewAuthzPath wfe.AuthzBase = wfe.BaseURL + AuthzPath + wfe.ChallengeBase = wfe.BaseURL + ChallengePath wfe.NewCert = wfe.BaseURL + NewCertPath wfe.CertBase = wfe.BaseURL + CertPath @@ -212,18 +214,22 @@ func (wfe *WebFrontEndImpl) Handler() (http.Handler, error) { wfe.DirectoryJSON = directoryJSON m := http.NewServeMux() - wfe.HandleFunc(m, "/", wfe.Index, "GET") wfe.HandleFunc(m, DirectoryPath, wfe.Directory, "GET") wfe.HandleFunc(m, NewRegPath, wfe.NewRegistration, "POST") wfe.HandleFunc(m, NewAuthzPath, wfe.NewAuthorization, "POST") wfe.HandleFunc(m, NewCertPath, wfe.NewCertificate, "POST") wfe.HandleFunc(m, RegPath, wfe.Registration, "POST") - wfe.HandleFunc(m, AuthzPath, wfe.Authorization, "GET", "POST") + wfe.HandleFunc(m, AuthzPath, wfe.Authorization, "GET") + wfe.HandleFunc(m, ChallengePath, wfe.Challenge, "GET", "POST") wfe.HandleFunc(m, CertPath, wfe.Certificate, "GET") wfe.HandleFunc(m, RevokeCertPath, wfe.RevokeCertificate, "POST") wfe.HandleFunc(m, TermsPath, wfe.Terms, "GET") wfe.HandleFunc(m, IssuerPath, wfe.Issuer, "GET") wfe.HandleFunc(m, BuildIDPath, wfe.BuildID, "GET") + // We don't use our special HandleFunc for "/" because it matches everything, + // meaning we can wind up returning 405 when we mean to return 404. See + // https://github.com/letsencrypt/boulder/issues/717 + m.HandleFunc("/", wfe.Index) return m, nil } @@ -243,16 +249,22 @@ func (wfe *WebFrontEndImpl) Index(response http.ResponseWriter, request *http.Re return } - tmpl := template.Must(template.New("body").Parse(` -
- This is an ACME - Certificate Authority running Boulder, - New registration is available at {{.NewReg}}. - - -`)) - tmpl.Execute(response, wfe) + if request.Method != "GET" { + logEvent.Error = "Bad method" + response.Header().Set("Allow", "GET") + response.WriteHeader(http.StatusMethodNotAllowed) + return + } + response.Header().Set("Content-Type", "text/html") + response.Write([]byte(fmt.Sprintf(` + + This is an ACME + Certificate Authority running Boulder. + JSON directory is available at %s. + + + `, DirectoryPath, DirectoryPath))) addCacheHeader(response, wfe.IndexCacheDuration.Seconds()) } @@ -265,6 +277,7 @@ func addCacheHeader(w http.ResponseWriter, age float64) { } func (wfe *WebFrontEndImpl) Directory(response http.ResponseWriter, request *http.Request) { + response.Header().Set("Content-Type", "application/json") response.Write(wfe.DirectoryJSON) } @@ -554,8 +567,7 @@ func (wfe *WebFrontEndImpl) NewAuthorization(response http.ResponseWriter, reque // Make a URL for this authz, then blow away the ID and RegID before serializing authzURL := wfe.AuthzBase + string(authz.ID) - authz.ID = "" - authz.RegistrationID = 0 + wfe.prepAuthorizationForDisplay(&authz) responseBody, err := json.Marshal(authz) if err != nil { logEvent.Error = err.Error() @@ -773,46 +785,93 @@ func (wfe *WebFrontEndImpl) NewCertificate(response http.ResponseWriter, request wfe.Stats.Inc("Certificates", 1, 1.0) } -func (wfe *WebFrontEndImpl) challenge( +func (wfe *WebFrontEndImpl) Challenge( response http.ResponseWriter, - request *http.Request, - authz core.Authorization, - logEvent *requestEvent) { + request *http.Request) { + logEvent := wfe.populateRequestEvent(request) + defer wfe.logRequestDetails(&logEvent) - // Check that the requested challenge exists within the authorization - found := false - var challengeIndex int - var challenge core.Challenge - for i, challenge := range authz.Challenges { - tempURL := challenge.URI - if tempURL.Path == request.URL.Path && tempURL.RawQuery == request.URL.RawQuery { - found = true - challengeIndex = i - break - } + notFound := func() { + wfe.sendError(response, "No such registration", request.URL.Path, http.StatusNotFound) } - if !found { - logEvent.Error = "Unable to find challenge" - wfe.sendError(response, logEvent.Error, request.URL.RawQuery, http.StatusNotFound) + // Challenge URIs are of the form /acme/challenge/