// 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/. package core import ( "crypto/x509" "encoding/base64" "encoding/hex" "encoding/json" "fmt" jose "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/square/go-jose" "path/filepath" "sort" "strings" "time" ) type IdentifierType string type AcmeStatus string type OCSPStatus string type Buffer []byte const ( StatusUnknown = AcmeStatus("unknown") // Unknown status; the default StatusPending = AcmeStatus("pending") // In process; client has next action StatusProcessing = AcmeStatus("processing") // In process; server has next action StatusValid = AcmeStatus("valid") // Validation succeeded StatusInvalid = AcmeStatus("invalid") // Validation failed StatusRevoked = AcmeStatus("revoked") // Object no longer valid ) const ( OCSPStatusGood = OCSPStatus("good") OCSPStatusRevoked = OCSPStatus("revoked") ) const ( ChallengeTypeSimpleHTTP = "simpleHttp" ChallengeTypeDVSNI = "dvsni" ChallengeTypeDNS = "dns" ChallengeTypeRecoveryToken = "recoveryToken" ) const ( IdentifierDNS = IdentifierType("dns") ) func cmpStrSlice(a, b []string) bool { if len(a) != len(b) { return false } sort.Strings(a) sort.Strings(b) for i := range a { if a[i] != b[i] { return false } } return true } func cmpExtKeyUsageSlice(a, b []x509.ExtKeyUsage) bool { if len(a) != len(b) { return false } testMap := make(map[int]bool, len(a)) for i := range a { testMap[int(a[i])] = true } for i := range b { if !testMap[int(b[i])] { return false } } return true } // An AcmeIdentifier encodes an identifier that can // be validated by ACME. The protocol allows for different // types of identifier to be supported (DNS names, IP // addresses, etc.), but currently we only support // domain names. type AcmeIdentifier struct { Type IdentifierType `json:"type"` // The type of identifier being encoded Value string `json:"value"` // The identifier itself } // An ACME certificate request is just a CSR together with // URIs pointing to authorizations that should collectively // authorize the certificate being requsted. // // This type is never marshaled, since we only ever receive // it from the client. So it carries some additional information // that is useful internally. (We rely on Go's case-insensitive // JSON unmarshal to properly unmarshal client requests.) type CertificateRequest struct { CSR *x509.CertificateRequest // The CSR Authorizations []AcmeURL // Links to Authorization over the account key } type rawCertificateRequest struct { CSR JsonBuffer `json:"csr"` // The encoded CSR Authorizations []AcmeURL `json:"authorizations"` // Authorizations } func (cr *CertificateRequest) UnmarshalJSON(data []byte) error { var raw rawCertificateRequest if err := json.Unmarshal(data, &raw); err != nil { return err } csr, err := x509.ParseCertificateRequest(raw.CSR) if err != nil { return err } cr.CSR = csr cr.Authorizations = raw.Authorizations return nil } func (cr CertificateRequest) MarshalJSON() ([]byte, error) { return json.Marshal(rawCertificateRequest{ CSR: cr.CSR.Raw, Authorizations: cr.Authorizations, }) } // Registration objects represent non-public metadata attached // to account keys. type Registration struct { // Unique identifier ID int64 `json:"id" db:"id"` // Account key to which the details are attached Key jose.JsonWebKey `json:"key" db:"jwk"` // Recovery Token is used to prove connection to an earlier transaction RecoveryToken string `json:"recoveryToken" db:"recoveryToken"` // Contact URIs Contact []AcmeURL `json:"contact,omitempty" db:"contact"` // Agreement with terms of service Agreement string `json:"agreement,omitempty" db:"agreement"` LockCol int64 `json:"-"` } func (r *Registration) MergeUpdate(input Registration) { if len(input.Contact) > 0 { r.Contact = input.Contact } 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. type Challenge struct { // The type of challenge Type string `json:"type"` // The status of this challenge Status AcmeStatus `json:"status,omitempty"` // If successful, the time at which this challenge // was completed by the server. Validated *time.Time `json:"validated,omitempty"` // A URI to which a response can be POSTed URI AcmeURL `json:"uri"` // Used by simpleHTTP, recoveryToken, and dns challenges Token string `json:"token,omitempty"` // Used by simpleHTTP challenges Path string `json:"path,omitempty"` TLS *bool `json:"tls,omitempty"` // Used by dvsni challenges R string `json:"r,omitempty"` S string `json:"s,omitempty"` Nonce string `json:"nonce,omitempty"` } // Check the sanity of a challenge object before issued to the client (completed = false) // and before validation (completed = true). func (ch Challenge) IsSane(completed bool) bool { if ch.Status != StatusPending { return false } switch ch.Type { case ChallengeTypeSimpleHTTP: // check extra fields aren't used if ch.R != "" || ch.S != "" || ch.Nonce != "" { return false } // If the client has marked the challenge as completed, there should be a // non-empty path provided. Otherwise there should be no default path. if completed { if ch.Path == "" { return false } // Composed path should be a clean filepath (i.e. no double slashes, dot segments, etc) vaUrl := fmt.Sprintf("/.well-known/acme-challenge/%s", ch.Path) if vaUrl != filepath.Clean(vaUrl) { return false } } else { if ch.Path != "" { return false } // TLS should set set to true by default if ch.TLS == nil || !*ch.TLS { return false } } // check token is present, corrent length, and contains b64 encoded string if ch.Token == "" || len(ch.Token) != 43 { return false } if _, err := B64dec(ch.Token); err != nil { return false } case ChallengeTypeDVSNI: // check extra fields aren't used if ch.Path != "" || ch.Token != "" || ch.TLS != nil { return false } if ch.Nonce == "" || len(ch.Nonce) != 32 { return false } if _, err := hex.DecodeString(ch.Nonce); err != nil { return false } // Check R & S are sane if ch.R == "" || len(ch.R) != 43 { return false } if _, err := B64dec(ch.R); err != nil { return false } if completed { if ch.S == "" || len(ch.S) != 43 { return false } if _, err := B64dec(ch.S); err != nil { return false } } else { if ch.S != "" { return false } } case ChallengeTypeDNS: // check extra fields aren't used if ch.R != "" || ch.S != "" || ch.Nonce != "" || ch.TLS != nil { return false } // check token is present, corrent length, and contains b64 encoded string if ch.Token == "" || len(ch.Token) != 43 { return false } if _, err := B64dec(ch.Token); err != nil { return false } default: return false } return true } // Merge a client-provide response to a challenge with the issued challenge // Note: This method does not update the challenge on the left side of the '.' func (ch Challenge) MergeResponse(resp Challenge) Challenge { // Only override fields that are supposed to be client-provided if len(ch.Path) == 0 { ch.Path = resp.Path } if len(ch.S) == 0 { ch.S = resp.S } if resp.TLS != nil { ch.TLS = resp.TLS } return ch } // An ACME authorization object represents the authorization // of an account key holder to act on behalf of a domain. This // struct is intended to be used both internally and for JSON // marshaling on the wire. Any fields that should be suppressed // on the wire (e.g., ID, regID) must be made empty before marshaling. type Authorization struct { // An identifier for this authorization, unique across // authorizations and certificates within this instance. ID string `json:"id,omitempty" db:"id"` // The identifier for which authorization is being given Identifier AcmeIdentifier `json:"identifier,omitempty" db:"identifier"` // The registration ID associated with the authorization RegistrationID int64 `json:"regId,omitempty" db:"registrationID"` // The status of the validation of this authorization Status AcmeStatus `json:"status,omitempty" db:"status"` // The date after which this authorization will be no // longer be considered valid Expires time.Time `json:"expires,omitempty" db:"expires"` // An array of challenges objects used to validate the // applicant's control of the identifier. For authorizations // 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 []Challenge `json:"challenges,omitempty" db:"challenges"` // The server may suggest combinations of challenges if it // requires more than one challenge to be completed. Combinations [][]int `json:"combinations,omitempty" db:"combinations"` } // Fields of this type get encoded and decoded JOSE-style, in base64url encoding // with stripped padding. type JsonBuffer []byte // Url-safe base64 encode that strips padding func base64URLEncode(data []byte) string { var result = base64.URLEncoding.EncodeToString(data) return strings.TrimRight(result, "=") } // Url-safe base64 decoder that adds padding func base64URLDecode(data string) ([]byte, error) { var missing = (4 - len(data)%4) % 4 data += strings.Repeat("=", missing) return base64.URLEncoding.DecodeString(data) } func (jb JsonBuffer) MarshalJSON() (result []byte, err error) { return json.Marshal(base64URLEncode(jb)) } func (jb *JsonBuffer) UnmarshalJSON(data []byte) (err error) { var str string err = json.Unmarshal(data, &str) if err != nil { return err } *jb, err = base64URLDecode(str) return } // Certificate objects are entirely internal to the server. The only // thing exposed on the wire is the certificate itself. type Certificate struct { RegistrationID int64 `db:"registrationID"` // The revocation status of the certificate. // * "valid" - not revoked // * "revoked" - revoked Status AcmeStatus `db:"status"` Serial string `db:"serial"` Digest string `db:"digest"` DER JsonBuffer `db:"der"` Issued time.Time `db:"issued"` Expires time.Time `db:"expires"` } // Certificate.MatchesCSR tests the contents of a generated certificate to // make sure that the PublicKey, CommonName, and DNSNames match those provided // in the CSR that was used to generate the certificate. It also checks the // following fields for: // * notAfter is after earliestExpiry // * notBefore is not more than 24 hours ago // * BasicConstraintsValid is true // * IsCA is false // * ExtKeyUsage only contains ExtKeyUsageServerAuth & ExtKeyUsageClientAuth // * Subject only contains CommonName & Names func (cert Certificate) MatchesCSR(csr *x509.CertificateRequest, earliestExpiry time.Time) (err error) { parsedCertificate, err := x509.ParseCertificate([]byte(cert.DER)) if err != nil { return } // Check issued certificate matches what was expected from the CSR hostNames := make([]string, len(csr.DNSNames)) copy(hostNames, csr.DNSNames) if len(csr.Subject.CommonName) > 0 { hostNames = append(hostNames, csr.Subject.CommonName) } hostNames = UniqueNames(hostNames) if !KeyDigestEquals(parsedCertificate.PublicKey, csr.PublicKey) { err = InternalServerError("Generated certificate public key doesn't match CSR public key") return } if len(csr.Subject.CommonName) > 0 && parsedCertificate.Subject.CommonName != csr.Subject.CommonName { err = InternalServerError("Generated certificate CommonName doesn't match CSR CommonName") return } if !cmpStrSlice(parsedCertificate.DNSNames, hostNames) { err = InternalServerError("Generated certificate DNSNames don't match CSR DNSNames") return } if len(parsedCertificate.Subject.Country) > 0 || len(parsedCertificate.Subject.Organization) > 0 || len(parsedCertificate.Subject.OrganizationalUnit) > 0 || len(parsedCertificate.Subject.Locality) > 0 || len(parsedCertificate.Subject.Province) > 0 || len(parsedCertificate.Subject.StreetAddress) > 0 || len(parsedCertificate.Subject.PostalCode) > 0 || len(parsedCertificate.Subject.SerialNumber) > 0 { err = InternalServerError("Generated certificate Subject contains fields other than CommonName or Names") return } if parsedCertificate.NotAfter.After(earliestExpiry) { err = InternalServerError("Generated certificate expires before earliest expiration") return } now := time.Now() if now.Sub(parsedCertificate.NotBefore) > time.Hour*24 { err = InternalServerError(fmt.Sprintf("Generated certificate is back dated %s", now.Sub(parsedCertificate.NotBefore))) return } if !parsedCertificate.BasicConstraintsValid { err = InternalServerError("Generated certificate doesn't have basic constraints set") return } if parsedCertificate.IsCA { err = InternalServerError("Generated certificate can sign other certificates") return } if !cmpExtKeyUsageSlice(parsedCertificate.ExtKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}) { err = InternalServerError("Generated certificate doesn't have correct key usage extensions") return } return } // CertificateStatus structs are internal to the server. They represent the // latest data about the status of the certificate, required for OCSP updating // and for validating that the subscriber has accepted the certificate. type CertificateStatus struct { Serial string `db:"serial"` // subscriberApproved: true iff the subscriber has posted back to the server // that they accept the certificate, otherwise 0. SubscriberApproved bool `db:"subscriberApproved"` // status: 'good' or 'revoked'. Note that good, expired certificates remain // with status 'good' but don't necessarily get fresh OCSP responses. Status OCSPStatus `db:"status"` // ocspLastUpdated: The date and time of the last time we generated an OCSP // response. If we have never generated one, this has the zero value of // time.Time, i.e. Jan 1 1970. OCSPLastUpdated time.Time `db:"ocspLastUpdated"` // revokedDate: If status is 'revoked', this is the date and time it was // revoked. Otherwise it has the zero value of time.Time, i.e. Jan 1 1970. RevokedDate time.Time `db:"revokedDate"` // revokedReason: If status is 'revoked', this is the reason code for the // revocation. Otherwise it is zero (which happens to be the reason // code for 'unspecified'). RevokedReason int `db:"revokedReason"` LockCol int64 `json:"-"` } // A large table of OCSP responses. This contains all historical OCSP // responses we've signed, is append-only, and is likely to get quite // large. We'll probably want administratively truncate it at some point. type OCSPResponse struct { ID int `db:"id"` // serial: Same as certificate serial. Serial string `db:"serial"` // createdAt: The date the response was signed. CreatedAt time.Time `db:"createdAt"` // response: The encoded and signed CRL. Response []byte `db:"response"` } // A large table of signed CRLs. This contains all historical CRLs // we've signed, is append-only, and is likely to get quite large. type CRL struct { // serial: Same as certificate serial. Serial string `db:"serial"` // createdAt: The date the CRL was signed. CreatedAt time.Time `db:"createdAt"` // crl: The encoded and signed CRL. CRL string `db:"crl"` } type DeniedCSR struct { ID int `db:"id"` Names string `db:"names"` } // OCSPSigningRequest is a transfer object representing an OCSP Signing Request type OCSPSigningRequest struct { CertDER []byte Status string Reason int RevokedAt time.Time }