845 lines
27 KiB
Go
845 lines
27 KiB
Go
// 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/subtle"
|
|
"crypto/x509"
|
|
"encoding/asn1"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"net"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
|
|
)
|
|
|
|
// AcmeStatus defines the state of a given authorization
|
|
type AcmeStatus string
|
|
|
|
// AcmeResource values identify different types of ACME resources
|
|
type AcmeResource string
|
|
|
|
// Buffer is a variable-length collection of bytes
|
|
type Buffer []byte
|
|
|
|
// IdentifierType defines the available identification mechanisms for domains
|
|
type IdentifierType string
|
|
|
|
// OCSPStatus defines the state of OCSP for a domain
|
|
type OCSPStatus string
|
|
|
|
// ProblemType defines the error types in the ACME protocol
|
|
type ProblemType string
|
|
|
|
// ProblemDetails objects represent problem documents
|
|
// https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00
|
|
type ProblemDetails struct {
|
|
Type ProblemType `json:"type,omitempty"`
|
|
Detail string `json:"detail,omitempty"`
|
|
}
|
|
|
|
// These statuses are the states of authorizations
|
|
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
|
|
)
|
|
|
|
// These types are the available identification mechanisms
|
|
const (
|
|
IdentifierDNS = IdentifierType("dns")
|
|
)
|
|
|
|
// The types of ACME resources
|
|
const (
|
|
ResourceNewReg = AcmeResource("new-reg")
|
|
ResourceNewAuthz = AcmeResource("new-authz")
|
|
ResourceNewCert = AcmeResource("new-cert")
|
|
ResourceRevokeCert = AcmeResource("revoke-cert")
|
|
ResourceRegistration = AcmeResource("reg")
|
|
ResourceChallenge = AcmeResource("challenge")
|
|
)
|
|
|
|
// These status are the states of OCSP
|
|
const (
|
|
OCSPStatusGood = OCSPStatus("good")
|
|
OCSPStatusRevoked = OCSPStatus("revoked")
|
|
)
|
|
|
|
// Error types that can be used in ACME payloads
|
|
const (
|
|
ConnectionProblem = ProblemType("urn:acme:error:connection")
|
|
MalformedProblem = ProblemType("urn:acme:error:malformed")
|
|
ServerInternalProblem = ProblemType("urn:acme:error:serverInternal")
|
|
TLSProblem = ProblemType("urn:acme:error:tls")
|
|
UnauthorizedProblem = ProblemType("urn:acme:error:unauthorized")
|
|
UnknownHostProblem = ProblemType("urn:acme:error:unknownHost")
|
|
RateLimitedProblem = ProblemType("urn:acme:error:rateLimited")
|
|
)
|
|
|
|
// These types are the available challenges
|
|
const (
|
|
ChallengeTypeSimpleHTTP = "simpleHttp"
|
|
ChallengeTypeDVSNI = "dvsni"
|
|
ChallengeTypeHTTP01 = "http-01"
|
|
ChallengeTypeTLSSNI01 = "tls-sni-01"
|
|
ChallengeTypeDNS01 = "dns-01"
|
|
)
|
|
|
|
// The suffix appended to pseudo-domain names in DVSNI challenges
|
|
const TLSSNISuffix = "acme.invalid"
|
|
|
|
// The label attached to DNS names in DNS challenges
|
|
const DNSPrefix = "_acme-challenge"
|
|
|
|
func (pd *ProblemDetails) Error() string {
|
|
return fmt.Sprintf("%s :: %s", pd.Type, pd.Detail)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// CertificateRequest is just a CSR
|
|
//
|
|
// This data is unmarshalled from JSON by way of rawCertificateRequest, which
|
|
// represents the actual structure received from the client.
|
|
type CertificateRequest struct {
|
|
CSR *x509.CertificateRequest // The CSR
|
|
Bytes []byte // The original bytes of the CSR, for logging.
|
|
}
|
|
|
|
type rawCertificateRequest struct {
|
|
CSR JSONBuffer `json:"csr"` // The encoded CSR
|
|
}
|
|
|
|
// UnmarshalJSON provides an implementation for decoding CertificateRequest objects.
|
|
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.Bytes = raw.CSR
|
|
return nil
|
|
}
|
|
|
|
// MarshalJSON provides an implementation for encoding CertificateRequest objects.
|
|
func (cr CertificateRequest) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(rawCertificateRequest{
|
|
CSR: cr.CSR.Raw,
|
|
})
|
|
}
|
|
|
|
// 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"`
|
|
|
|
// Contact URIs
|
|
Contact []*AcmeURL `json:"contact,omitempty"`
|
|
|
|
// Agreement with terms of service
|
|
Agreement string `json:"agreement,omitempty"`
|
|
|
|
// InitialIP is the IP address from which the registration was created
|
|
InitialIP net.IP `json:"initialIp"`
|
|
|
|
// CreatedAt is the time the registration was created.
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
}
|
|
|
|
// MergeUpdate copies a subset of information from the input Registration
|
|
// into this one.
|
|
func (r *Registration) MergeUpdate(input Registration) {
|
|
if len(input.Contact) > 0 {
|
|
r.Contact = input.Contact
|
|
}
|
|
|
|
if len(input.Agreement) > 0 {
|
|
r.Agreement = input.Agreement
|
|
}
|
|
}
|
|
|
|
// ValidationRecord represents a validation attempt against a specific URL/hostname
|
|
// and the IP addresses that were resolved and used
|
|
type ValidationRecord struct {
|
|
// SimpleHTTP only
|
|
URL string `json:"url,omitempty"`
|
|
|
|
// Shared
|
|
Hostname string `json:"hostname"`
|
|
Port string `json:"port"`
|
|
AddressesResolved []net.IP `json:"addressesResolved"`
|
|
AddressUsed net.IP `json:"addressUsed"`
|
|
}
|
|
|
|
// KeyAuthorization represents a domain holder's authorization for a
|
|
// specific account key to satisfy a specific challenge.
|
|
type KeyAuthorization struct {
|
|
Token string
|
|
Thumbprint string
|
|
}
|
|
|
|
// NewKeyAuthorization computes the thumbprint and assembles the object
|
|
func NewKeyAuthorization(token string, key *jose.JsonWebKey) (KeyAuthorization, error) {
|
|
if key == nil {
|
|
return KeyAuthorization{}, fmt.Errorf("Cannot authorize a nil key")
|
|
}
|
|
|
|
thumbprint, err := Thumbprint(key)
|
|
if err != nil {
|
|
return KeyAuthorization{}, err
|
|
}
|
|
|
|
return KeyAuthorization{
|
|
Token: token,
|
|
Thumbprint: thumbprint,
|
|
}, nil
|
|
}
|
|
|
|
// NewKeyAuthorizationFromString parses the string and composes a key authorization struct
|
|
func NewKeyAuthorizationFromString(input string) (ka KeyAuthorization, err error) {
|
|
parts := strings.Split(input, ".")
|
|
if len(parts) != 2 {
|
|
err = fmt.Errorf("Invalid key authorization: %d parts", len(parts))
|
|
return
|
|
} else if !LooksLikeAToken(parts[0]) {
|
|
err = fmt.Errorf("Invalid key authorization: malformed token")
|
|
return
|
|
} else if !LooksLikeAToken(parts[1]) {
|
|
// Thumbprints have the same syntax as tokens in boulder
|
|
// Both are base64-encoded and 32 octets
|
|
err = fmt.Errorf("Invalid key authorization: malformed key thumbprint")
|
|
return
|
|
}
|
|
|
|
ka = KeyAuthorization{
|
|
Token: parts[0],
|
|
Thumbprint: parts[1],
|
|
}
|
|
return
|
|
}
|
|
|
|
// String produces the string representation of a key authorization
|
|
func (ka KeyAuthorization) String() string {
|
|
return ka.Token + "." + ka.Thumbprint
|
|
}
|
|
|
|
// Match determines whether this KeyAuthorization matches the given token and key
|
|
func (ka KeyAuthorization) Match(token string, key *jose.JsonWebKey) bool {
|
|
if key == nil {
|
|
return false
|
|
}
|
|
|
|
thumbprint, err := Thumbprint(key)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
tokensEqual := subtle.ConstantTimeCompare([]byte(token), []byte(ka.Token))
|
|
thumbprintsEqual := subtle.ConstantTimeCompare([]byte(thumbprint), []byte(ka.Thumbprint))
|
|
|
|
return tokensEqual == 1 && thumbprintsEqual == 1
|
|
}
|
|
|
|
// MarshalJSON packs a key authorization into its string representation
|
|
func (ka KeyAuthorization) MarshalJSON() (result []byte, err error) {
|
|
return json.Marshal(ka.String())
|
|
}
|
|
|
|
// UnmarshalJSON unpacks a key authorization from a string
|
|
func (ka *KeyAuthorization) UnmarshalJSON(data []byte) (err error) {
|
|
var str string
|
|
err = json.Unmarshal(data, &str)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
parsed, err := NewKeyAuthorizationFromString(str)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
*ka = parsed
|
|
return
|
|
}
|
|
|
|
// Challenge is an aggregate of all data needed for any challenges.
|
|
//
|
|
// 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 {
|
|
ID int64 `json:"id,omitempty"`
|
|
|
|
// The type of challenge
|
|
Type string `json:"type"`
|
|
|
|
// The status of this challenge
|
|
Status AcmeStatus `json:"status,omitempty"`
|
|
|
|
// Contains the error that occured during challenge validation, if any
|
|
Error *ProblemDetails `json:"error,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 string `json:"uri"`
|
|
|
|
// Used by simpleHttp, http-00, tls-sni-00, and dns-00 challenges
|
|
Token string `json:"token,omitempty"`
|
|
|
|
// Used by simpleHttp challenges
|
|
TLS *bool `json:"tls,omitempty"`
|
|
|
|
// Used by dvsni challenges
|
|
Validation *jose.JsonWebSignature `json:"validation,omitempty"`
|
|
|
|
// Used by http-00, tls-sni-00, and dns-00 challenges
|
|
KeyAuthorization *KeyAuthorization `json:"keyAuthorization,omitempty"`
|
|
|
|
// Contains information about URLs used or redirected to and IPs resolved and
|
|
// used
|
|
ValidationRecord []ValidationRecord `json:"validationRecord,omitempty"`
|
|
|
|
// The account key used to create this challenge. This is not part of the
|
|
// spec, but clients are required to ignore unknown fields, so it's harmless
|
|
// to include.
|
|
//
|
|
// Boulder needs to remember what key was used to create a challenge in order
|
|
// to prevent an attacker from re-using a validation signature with a different,
|
|
// unauthorized key. See:
|
|
// https://mailarchive.ietf.org/arch/msg/acme/F71iz6qq1o_QPVhJCV4dqWf-4Yc
|
|
AccountKey *jose.JsonWebKey `json:"accountKey,omitempty"`
|
|
}
|
|
|
|
// RecordsSane checks the sanity of a ValidationRecord object before sending it
|
|
// back to the RA to be stored.
|
|
func (ch Challenge) RecordsSane() bool {
|
|
if ch.ValidationRecord == nil || len(ch.ValidationRecord) == 0 {
|
|
return false
|
|
}
|
|
|
|
switch ch.Type {
|
|
case ChallengeTypeSimpleHTTP:
|
|
// TODO(https://github.com/letsencrypt/boulder/issues/894): Remove this case
|
|
fallthrough
|
|
case ChallengeTypeHTTP01:
|
|
for _, rec := range ch.ValidationRecord {
|
|
if rec.URL == "" || rec.Hostname == "" || rec.Port == "" || rec.AddressUsed == nil ||
|
|
len(rec.AddressesResolved) == 0 {
|
|
return false
|
|
}
|
|
}
|
|
case ChallengeTypeDVSNI:
|
|
// TODO(https://github.com/letsencrypt/boulder/issues/894): Remove this case
|
|
fallthrough
|
|
case ChallengeTypeTLSSNI01:
|
|
if len(ch.ValidationRecord) > 1 {
|
|
return false
|
|
}
|
|
if ch.ValidationRecord[0].URL != "" {
|
|
return false
|
|
}
|
|
if ch.ValidationRecord[0].Hostname == "" || ch.ValidationRecord[0].Port == "" ||
|
|
ch.ValidationRecord[0].AddressUsed == nil || len(ch.ValidationRecord[0].AddressesResolved) == 0 {
|
|
return false
|
|
}
|
|
case ChallengeTypeDNS01:
|
|
// Nothing for now
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// isLegacy returns true if the challenge is of a legacy type (i.e., one defined
|
|
// before draft-ietf-acme-acme-00)
|
|
// TODO(https://github.com/letsencrypt/boulder/issues/894): Delete this method
|
|
func (ch Challenge) isLegacy() bool {
|
|
return (ch.Type == ChallengeTypeSimpleHTTP) ||
|
|
(ch.Type == ChallengeTypeDVSNI)
|
|
}
|
|
|
|
// legacyIsSane performs sanity checks for legacy challenge types, which have
|
|
// a different structure / logic than current challenges.
|
|
// TODO(https://github.com/letsencrypt/boulder/issues/894): Delete this method
|
|
func (ch Challenge) legacyIsSane(completed bool) bool {
|
|
if ch.Status != StatusPending {
|
|
return false
|
|
}
|
|
|
|
if ch.AccountKey == nil {
|
|
return false
|
|
}
|
|
|
|
switch ch.Type {
|
|
case ChallengeTypeSimpleHTTP:
|
|
// check extra fields aren't used
|
|
if ch.Validation != nil {
|
|
return false
|
|
}
|
|
|
|
if completed && 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
|
|
}
|
|
case ChallengeTypeDVSNI:
|
|
// check extra fields aren't used
|
|
if 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
|
|
}
|
|
|
|
// If completed, check that there's a validation object
|
|
if completed && ch.Validation == nil {
|
|
return false
|
|
}
|
|
default:
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// legacyMergeResponse copies a subset of client-provided data to the current Challenge.
|
|
// Note: This method does not update the challenge on the left side of the '.'
|
|
// TODO(https://github.com/letsencrypt/boulder/issues/894): Delete this method
|
|
func (ch Challenge) legacyMergeResponse(resp Challenge) Challenge {
|
|
switch ch.Type {
|
|
case ChallengeTypeSimpleHTTP:
|
|
// For simpleHttp, only "tls" is client-provided
|
|
// If "tls" is not provided, default to "true"
|
|
if resp.TLS != nil {
|
|
ch.TLS = resp.TLS
|
|
} else {
|
|
ch.TLS = new(bool)
|
|
*ch.TLS = true
|
|
}
|
|
|
|
case ChallengeTypeDVSNI:
|
|
// For dvsni and dns, only "validation" is client-provided
|
|
if resp.Validation != nil {
|
|
ch.Validation = resp.Validation
|
|
}
|
|
}
|
|
|
|
return ch
|
|
}
|
|
|
|
// IsSane checks 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 {
|
|
// TODO(https://github.com/letsencrypt/boulder/issues/894): Delete this branch
|
|
if ch.isLegacy() {
|
|
return ch.legacyIsSane(completed)
|
|
}
|
|
|
|
if ch.Status != StatusPending {
|
|
return false
|
|
}
|
|
|
|
// There always needs to be an account key and token
|
|
if ch.AccountKey == nil || !LooksLikeAToken(ch.Token) {
|
|
return false
|
|
}
|
|
|
|
// Before completion, the key authorization field should be empty
|
|
if !completed && ch.KeyAuthorization != nil {
|
|
return false
|
|
}
|
|
|
|
// If the challenge is completed, then there should be a key authorization,
|
|
// and it should match the challenge.
|
|
if completed {
|
|
if ch.KeyAuthorization == nil {
|
|
return false
|
|
}
|
|
|
|
if !ch.KeyAuthorization.Match(ch.Token, ch.AccountKey) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// MergeResponse copies a subset of client-provided data to the current Challenge.
|
|
// Note: This method does not update the challenge on the left side of the '.'
|
|
func (ch Challenge) MergeResponse(resp Challenge) Challenge {
|
|
// TODO(https://github.com/letsencrypt/boulder/issues/894): Delete this branch
|
|
if ch.isLegacy() {
|
|
return ch.legacyMergeResponse(resp)
|
|
}
|
|
|
|
// The only client-provided field is the key authorization, and all current
|
|
// challenge types use it.
|
|
switch ch.Type {
|
|
case ChallengeTypeHTTP01:
|
|
fallthrough
|
|
case ChallengeTypeTLSSNI01:
|
|
fallthrough
|
|
case ChallengeTypeDNS01:
|
|
ch.KeyAuthorization = resp.KeyAuthorization
|
|
}
|
|
|
|
return ch
|
|
}
|
|
|
|
// Authorization 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. Note: a certificate may be issued even on the
|
|
// last day of an authorization's lifetime. The last day for which someone can
|
|
// hold a valid certificate based on an authorization is authorization
|
|
// lifetime + certificate lifetime.
|
|
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:"-"`
|
|
|
|
// The server may suggest combinations of challenges if it
|
|
// requires more than one challenge to be completed.
|
|
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
|
|
|
|
// 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)
|
|
}
|
|
|
|
// MarshalJSON encodes a JSONBuffer for transmission.
|
|
func (jb JSONBuffer) MarshalJSON() (result []byte, err error) {
|
|
return json.Marshal(base64URLEncode(jb))
|
|
}
|
|
|
|
// UnmarshalJSON decodes a JSONBuffer to an object.
|
|
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"`
|
|
|
|
Serial string `db:"serial"`
|
|
Digest string `db:"digest"`
|
|
DER []byte `db:"der"`
|
|
Issued time.Time `db:"issued"`
|
|
Expires time.Time `db:"expires"`
|
|
}
|
|
|
|
// IdentifierData holds information about what certificates are known for a
|
|
// given identifier. This is used to present Proof of Posession challenges in
|
|
// the case where a certificate already exists. The DB table holding
|
|
// IdentifierData rows contains information about certs issued by Boulder and
|
|
// also information about certs observed from third parties.
|
|
type IdentifierData struct {
|
|
ReversedName string `db:"reversedName"` // The label-wise reverse of an identifier, e.g. com.example or com.example.*
|
|
CertSHA1 string `db:"certSHA1"` // The hex encoding of the SHA-1 hash of a cert containing the identifier
|
|
}
|
|
|
|
// ExternalCert holds information about certificates issued by other CAs,
|
|
// obtained through Certificate Transparency, the SSL Observatory, or scans.io.
|
|
type ExternalCert struct {
|
|
SHA1 string `db:"sha1"` // The hex encoding of the SHA-1 hash of this cert
|
|
Issuer string `db:"issuer"` // The Issuer field of this cert
|
|
Subject string `db:"subject"` // The Subject field of this cert
|
|
NotAfter time.Time `db:"notAfter"` // Date after which this cert should be considered invalid
|
|
SPKI []byte `db:"spki"` // The hex encoding of the certificate's SubjectPublicKeyInfo in DER form
|
|
Valid bool `db:"valid"` // Whether this certificate was valid at LastUpdated time
|
|
EV bool `db:"ev"` // Whether this cert was EV valid
|
|
CertDER []byte `db:"rawDERCert"` // DER (binary) encoding of the raw certificate
|
|
}
|
|
|
|
// 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 RevocationCode `db:"revokedReason"`
|
|
|
|
LastExpirationNagSent time.Time `db:"lastExpirationNagSent"`
|
|
|
|
LockCol int64 `json:"-"`
|
|
}
|
|
|
|
// OCSPResponse is 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.
|
|
// It must be administratively truncated outside of Boulder.
|
|
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"`
|
|
}
|
|
|
|
// CRL is a large table of signed CRLs. This contains all historical CRLs
|
|
// we've signed, is append-only, and is likely to get quite large.
|
|
// It must be administratively truncated outside of Boulder.
|
|
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"`
|
|
}
|
|
|
|
// DeniedCSR is a list of names we deny issuing.
|
|
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 RevocationCode
|
|
RevokedAt time.Time
|
|
}
|
|
|
|
// SignedCertificateTimestamp represents objects used by Certificate Transparency
|
|
// to demonstrate that a certificate was submitted to a CT log. See RFC 6962.
|
|
type SignedCertificateTimestamp struct {
|
|
ID int `db:"id"`
|
|
// The version of the protocol to which the SCT conforms
|
|
SCTVersion uint8 `db:"sctVersion"`
|
|
// the SHA-256 hash of the log's public key, calculated over
|
|
// the DER encoding of the key represented as SubjectPublicKeyInfo.
|
|
LogID string `db:"logID"`
|
|
// Timestamp (in ms since unix epoc) at which the SCT was issued
|
|
Timestamp uint64 `db:"timestamp"`
|
|
// For future extensions to the protocol
|
|
Extensions []byte `db:"extensions"`
|
|
// The Log's signature for this SCT
|
|
Signature []byte `db:"signature"`
|
|
|
|
// The serial of the certificate this SCT is for
|
|
CertificateSerial string `db:"certificateSerial"`
|
|
|
|
LockCol int64
|
|
}
|
|
|
|
// RPCSignedCertificateTimestamp is a wrapper around SignedCertificateTimestamp
|
|
// so that it can be passed through the RPC layer properly. Without this wrapper
|
|
// the UnmarshalJSON method below will be used when marshaling/unmarshaling the
|
|
// object, which is not what we want as it is not symmetrical (as it is intended
|
|
// to unmarshal a rawSignedCertificateTimestamp into a SignedCertificateTimestamp)
|
|
type RPCSignedCertificateTimestamp SignedCertificateTimestamp
|
|
|
|
type rawSignedCertificateTimestamp struct {
|
|
Version uint8 `json:"sct_version"`
|
|
LogID string `json:"id"`
|
|
Timestamp uint64 `json:"timestamp"`
|
|
Signature string `json:"signature"`
|
|
Extensions string `json:"extensions"`
|
|
}
|
|
|
|
// UnmarshalJSON parses the add-chain response from a CT log. It fills all of
|
|
// the fields in the SignedCertificateTimestamp struct except for ID and
|
|
// CertificateSerial, which are used for local recordkeeping in the Boulder DB.
|
|
func (sct *SignedCertificateTimestamp) UnmarshalJSON(data []byte) error {
|
|
var err error
|
|
var rawSCT rawSignedCertificateTimestamp
|
|
if err = json.Unmarshal(data, &rawSCT); err != nil {
|
|
return fmt.Errorf("Failed to unmarshal SCT receipt, %s", err)
|
|
}
|
|
sct.LogID = rawSCT.LogID
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to decode log ID, %s", err)
|
|
}
|
|
sct.Signature, err = base64.StdEncoding.DecodeString(rawSCT.Signature)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to decode SCT signature, %s", err)
|
|
}
|
|
sct.Extensions, err = base64.StdEncoding.DecodeString(rawSCT.Extensions)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to decode SCT extensions, %s", err)
|
|
}
|
|
sct.SCTVersion = rawSCT.Version
|
|
sct.Timestamp = rawSCT.Timestamp
|
|
return nil
|
|
}
|
|
|
|
const (
|
|
sctHashSHA256 = 4
|
|
sctSigECDSA = 3
|
|
)
|
|
|
|
// CheckSignature validates that the returned SCT signature is a valid SHA256 +
|
|
// ECDSA signature but does not verify that a specific public key signed it.
|
|
func (sct *SignedCertificateTimestamp) CheckSignature() error {
|
|
if len(sct.Signature) < 4 {
|
|
return errors.New("SCT signature is truncated")
|
|
}
|
|
// Since all of the known logs currently only use SHA256 hashes and ECDSA
|
|
// keys, only allow those
|
|
if sct.Signature[0] != sctHashSHA256 {
|
|
return fmt.Errorf("Unsupported SCT hash function [%d]", sct.Signature[0])
|
|
}
|
|
if sct.Signature[1] != sctSigECDSA {
|
|
return fmt.Errorf("Unsupported SCT signature algorithm [%d]", sct.Signature[1])
|
|
}
|
|
|
|
var ecdsaSig struct {
|
|
R, S *big.Int
|
|
}
|
|
// Ignore the two length bytes and attempt to unmarshal the signature directly
|
|
signatureBytes := sct.Signature[4:]
|
|
signatureBytes, err := asn1.Unmarshal(signatureBytes, &ecdsaSig)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to parse SCT signature, %s", err)
|
|
}
|
|
if len(signatureBytes) > 0 {
|
|
return fmt.Errorf("Trailing garbage after signature")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RevocationCode is used to specify a certificate revocation reason
|
|
type RevocationCode int
|
|
|
|
// RevocationReasons provides a map from reason code to string explaining the
|
|
// code
|
|
var RevocationReasons = map[RevocationCode]string{
|
|
0: "unspecified",
|
|
1: "keyCompromise",
|
|
2: "cACompromise",
|
|
3: "affiliationChanged",
|
|
4: "superseded",
|
|
5: "cessationOfOperation",
|
|
6: "certificateHold",
|
|
// 7 is unused
|
|
8: "removeFromCRL", // needed?
|
|
9: "privilegeWithdrawn",
|
|
10: "aAcompromise",
|
|
}
|