// Certificate is used by clients to request a copy of their current certificate, or to // request a reissuance of the certificate. func (wfe *WebFrontEndImpl) Certificate(ctx context.Context, logEvent *requestEvent, response http.ResponseWriter, request *http.Request) { serial := request.URL.Path // Certificate paths consist of the CertBase path, plus exactly sixteen hex // digits. if !core.ValidSerial(serial) { logEvent.AddError("certificate serial provided was not valid: %s", serial) wfe.sendError(response, logEvent, probs.NotFound("Certificate not found"), nil) return } logEvent.Extra["RequestedSerial"] = serial cert, err := wfe.SA.GetCertificate(ctx, serial) // TODO(#991): handle db errors if err != nil { logEvent.AddError("unable to get certificate by serial id %#v: %s", serial, err) if strings.HasPrefix(err.Error(), "gorp: multiple rows returned") { wfe.sendError(response, logEvent, probs.Conflict("Multiple certificates with same short serial"), err) } else { wfe.sendError(response, logEvent, probs.NotFound("Certificate not found"), err) } return } // TODO Content negotiation response.Header().Set("Content-Type", "application/pkix-cert") response.Header().Add("Link", link(issuerPath, "up")) response.WriteHeader(http.StatusOK) if _, err = response.Write(cert.DER); err != nil { logEvent.AddError(err.Error()) wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err)) } return }
// Certificate is used by clients to request a copy of their current certificate, or to // request a reissuance of the certificate. func (wfe *WebFrontEndImpl) Certificate(ctx context.Context, logEvent *requestEvent, response http.ResponseWriter, request *http.Request) { path := request.URL.Path // Certificate paths consist of the CertBase path, plus exactly sixteen hex // digits. if !strings.HasPrefix(path, CertPath) { logEvent.AddError("this request path should not have gotten to Certificate: %#v is not a prefix of %#v", path, CertPath) wfe.sendError(response, logEvent, probs.NotFound("Certificate not found"), nil) addNoCacheHeader(response) return } serial := path[len(CertPath):] if !core.ValidSerial(serial) { logEvent.AddError("certificate serial provided was not valid: %s", serial) wfe.sendError(response, logEvent, probs.NotFound("Certificate not found"), nil) addNoCacheHeader(response) return } logEvent.Extra["RequestedSerial"] = serial cert, err := wfe.SA.GetCertificate(ctx, serial) // TODO(#991): handle db errors if err != nil { logEvent.AddError("unable to get certificate by serial id %#v: %s", serial, err) if strings.HasPrefix(err.Error(), "gorp: multiple rows returned") { wfe.sendError(response, logEvent, probs.Conflict("Multiple certificates with same short serial"), err) } else { addNoCacheHeader(response) wfe.sendError(response, logEvent, probs.NotFound("Certificate not found"), err) } return } parsedCertificate, err := x509.ParseCertificate([]byte(cert.DER)) if err != nil { logEvent.AddError("unable to parse certificate: %s", err) wfe.sendError(response, logEvent, probs.Malformed("Unable to parse certificate"), err) return } if err = wfe.addIssuerCertificateURLs(response, parsedCertificate); err != nil { logEvent.AddError("unable to parse IssuingCertificateURL: %s", err) wfe.sendError(response, logEvent, probs.Malformed("unable to parse IssuingCertificateURL"), err) return } addCacheHeader(response, wfe.CertCacheDuration.Seconds()) // TODO Content negotiation response.Header().Set("Content-Type", "application/pkix-cert") response.WriteHeader(http.StatusOK) if _, err = response.Write(cert.DER); err != nil { logEvent.AddError(err.Error()) wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err)) } return }
// ProblemDetailsForError turns an error into a ProblemDetails with the special // case of returning the same error back if its already a ProblemDetails. If the // error is of an type unknown to ProblemDetailsForError, it will return a // ServerInternal ProblemDetails. func ProblemDetailsForError(err error, msg string) *probs.ProblemDetails { switch e := err.(type) { case *probs.ProblemDetails: return e case MalformedRequestError: return probs.Malformed(fmt.Sprintf("%s :: %s", msg, err)) case NotSupportedError: return &probs.ProblemDetails{ Type: probs.ServerInternalProblem, Detail: fmt.Sprintf("%s :: %s", msg, err), HTTPStatus: http.StatusNotImplemented, } case UnauthorizedError: return probs.Unauthorized(fmt.Sprintf("%s :: %s", msg, err)) case NotFoundError: return probs.NotFound(fmt.Sprintf("%s :: %s", msg, err)) case LengthRequiredError: prob := probs.Malformed("missing Content-Length header") prob.HTTPStatus = http.StatusLengthRequired return prob case SignatureValidationError: return probs.Malformed(fmt.Sprintf("%s :: %s", msg, err)) case RateLimitedError: return probs.RateLimited(fmt.Sprintf("%s :: %s", msg, err)) case BadNonceError: return probs.BadNonce(fmt.Sprintf("%s :: %s", msg, err)) default: // Internal server error messages may include sensitive data, so we do // not include it. return probs.ServerInternal(msg) } }
// Authorization is used by clients to submit an update to one of their // authorizations. func (wfe *WebFrontEndImpl) Authorization(logEvent *requestEvent, 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 { logEvent.AddError("No such authorization at id %s", id) // TODO(#1199): handle db errors wfe.sendError(response, logEvent, probs.NotFound("Unable to find authorization"), err) return } logEvent.Extra["AuthorizationID"] = authz.ID logEvent.Extra["AuthorizationRegistrationID"] = authz.RegistrationID logEvent.Extra["AuthorizationIdentifier"] = authz.Identifier logEvent.Extra["AuthorizationStatus"] = authz.Status logEvent.Extra["AuthorizationExpires"] = authz.Expires // After expiring, authorizations are inaccessible if authz.Expires == nil || authz.Expires.Before(wfe.clk.Now()) { msg := fmt.Sprintf("Authorization %v expired in the past (%v)", authz.ID, *authz.Expires) logEvent.AddError(msg) wfe.sendError(response, logEvent, probs.NotFound("Expired authorization"), nil) return } wfe.prepAuthorizationForDisplay(&authz) jsonReply, err := json.Marshal(authz) if err != nil { // InternalServerError because this is a failure to decode from our DB. logEvent.AddError("Failed to JSON marshal authz: %s", err) wfe.sendError(response, logEvent, probs.ServerInternal("Failed to JSON marshal authz"), err) return } response.Header().Add("Link", link(wfe.NewCert, "next")) response.Header().Set("Content-Type", "application/json") response.WriteHeader(http.StatusOK) if _, err = response.Write(jsonReply); err != nil { logEvent.AddError(err.Error()) wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err)) } }
// Challenge handles POST requests to challenge URLs. Such requests are clients' // responses to the server's challenges. func (wfe *WebFrontEndImpl) Challenge( logEvent *requestEvent, response http.ResponseWriter, request *http.Request) { notFound := func() { wfe.sendError(response, logEvent, probs.NotFound("No such challenge"), nil) } // Challenge URIs are of the form /acme/challenge/<auth id>/<challenge id>. // Here we parse out the id components. TODO: Use a better tool to parse out // URL structure: https://github.com/letsencrypt/boulder/issues/437 slug := strings.Split(request.URL.Path[len(ChallengePath):], "/") if len(slug) != 2 { notFound() return } authorizationID := slug[0] challengeID, err := strconv.ParseInt(slug[1], 10, 64) if err != nil { notFound() return } logEvent.Extra["AuthorizationID"] = authorizationID logEvent.Extra["ChallengeID"] = challengeID authz, err := wfe.SA.GetAuthorization(authorizationID) if err != nil { // TODO(#1198): handle db errors etc notFound() return } // After expiring, challenges are inaccessible if authz.Expires == nil || authz.Expires.Before(wfe.clk.Now()) { logEvent.AddError("Authorization %v expired in the past (%v)", authz.ID, *authz.Expires) wfe.sendError(response, logEvent, probs.NotFound("Expired authorization"), nil) return } // Check that the requested challenge exists within the authorization challengeIndex := authz.FindChallenge(challengeID) if challengeIndex == -1 { notFound() return } challenge := authz.Challenges[challengeIndex] logEvent.Extra["ChallengeType"] = challenge.Type logEvent.Extra["AuthorizationRegistrationID"] = authz.RegistrationID logEvent.Extra["AuthorizationIdentifier"] = authz.Identifier logEvent.Extra["AuthorizationStatus"] = authz.Status logEvent.Extra["AuthorizationExpires"] = authz.Expires switch request.Method { case "GET", "HEAD": wfe.getChallenge(response, request, authz, &challenge, logEvent) case "POST": wfe.postChallenge(response, request, authz, challengeIndex, logEvent) } }
// RevokeCertificate is used by clients to request the revocation of a cert. func (wfe *WebFrontEndImpl) RevokeCertificate(logEvent *requestEvent, response http.ResponseWriter, request *http.Request) { // We don't ask verifyPOST to verify there is a corresponding registration, // because anyone with the right private key can revoke a certificate. body, requestKey, registration, prob := wfe.verifyPOST(logEvent, request, false, core.ResourceRevokeCert) if prob != nil { // verifyPOST handles its own setting of logEvent.Errors wfe.sendError(response, logEvent, prob, nil) return } type RevokeRequest struct { CertificateDER core.JSONBuffer `json:"certificate"` } var revokeRequest RevokeRequest if err := json.Unmarshal(body, &revokeRequest); err != nil { logEvent.AddError(fmt.Sprintf("Couldn't unmarshal in revoke request %s", string(body))) wfe.sendError(response, logEvent, probs.Malformed("Unable to JSON parse revoke request"), err) return } providedCert, err := x509.ParseCertificate(revokeRequest.CertificateDER) if err != nil { logEvent.AddError("unable to parse revoke certificate DER: %s", err) wfe.sendError(response, logEvent, probs.Malformed("Unable to parse certificate DER"), err) return } serial := core.SerialToString(providedCert.SerialNumber) logEvent.Extra["ProvidedCertificateSerial"] = serial cert, err := wfe.SA.GetCertificate(serial) // TODO(#991): handle db errors better if err != nil || !bytes.Equal(cert.DER, revokeRequest.CertificateDER) { wfe.sendError(response, logEvent, probs.NotFound("No such certificate"), err) return } parsedCertificate, err := x509.ParseCertificate(cert.DER) if err != nil { // InternalServerError because this is a failure to decode from our DB. wfe.sendError(response, logEvent, probs.ServerInternal("invalid parse of stored certificate"), err) return } logEvent.Extra["RetrievedCertificateSerial"] = core.SerialToString(parsedCertificate.SerialNumber) logEvent.Extra["RetrievedCertificateDNSNames"] = parsedCertificate.DNSNames logEvent.Extra["RetrievedCertificateEmailAddresses"] = parsedCertificate.EmailAddresses logEvent.Extra["RetrievedCertificateIPAddresses"] = parsedCertificate.IPAddresses certStatus, err := wfe.SA.GetCertificateStatus(serial) if err != nil { logEvent.AddError("unable to get certificate status: %s", err) // TODO(#991): handle db errors wfe.sendError(response, logEvent, probs.NotFound("Certificate status not yet available"), err) return } logEvent.Extra["CertificateStatus"] = certStatus.Status if certStatus.Status == core.OCSPStatusRevoked { logEvent.AddError("Certificate already revoked: %#v", serial) wfe.sendError(response, logEvent, probs.Conflict("Certificate already revoked"), nil) return } // TODO: Implement method of revocation by authorizations on account. if !(core.KeyDigestEquals(requestKey, parsedCertificate.PublicKey) || registration.ID == cert.RegistrationID) { wfe.sendError(response, logEvent, probs.Unauthorized("Revocation request must be signed by private key of cert to be revoked, or by the account key of the account that issued it."), nil) return } // Use revocation code 0, meaning "unspecified" err = wfe.RA.RevokeCertificateWithReg(*parsedCertificate, 0, registration.ID) if err != nil { logEvent.AddError("failed to revoke certificate: %s", err) wfe.sendError(response, logEvent, core.ProblemDetailsForError(err, "Failed to revoke certificate"), err) } else { wfe.log.Debug(fmt.Sprintf("Revoked %v", serial)) response.WriteHeader(http.StatusOK) } }