boulder/core/objects.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",
}