376 lines
10 KiB
Go
376 lines
10 KiB
Go
package sa
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"net"
|
|
"time"
|
|
|
|
jose "gopkg.in/square/go-jose.v2"
|
|
|
|
"github.com/letsencrypt/boulder/core"
|
|
corepb "github.com/letsencrypt/boulder/core/proto"
|
|
"github.com/letsencrypt/boulder/probs"
|
|
"github.com/letsencrypt/boulder/revocation"
|
|
)
|
|
|
|
// A `dbOneSelector` is anything that provides a `SelectOne` function.
|
|
type dbOneSelector interface {
|
|
SelectOne(interface{}, string, ...interface{}) error
|
|
}
|
|
|
|
// A `dbSelector` is anything that provides a `Select` function.
|
|
type dbSelector interface {
|
|
Select(interface{}, string, ...interface{}) ([]interface{}, error)
|
|
}
|
|
|
|
const regFields = "id, jwk, jwk_sha256, contact, agreement, initialIP, createdAt, LockCol, status"
|
|
|
|
// selectRegistration selects all fields of one registration model
|
|
func selectRegistration(s dbOneSelector, q string, args ...interface{}) (*regModel, error) {
|
|
var model regModel
|
|
err := s.SelectOne(
|
|
&model,
|
|
"SELECT "+regFields+" FROM registrations "+q,
|
|
args...,
|
|
)
|
|
return &model, err
|
|
}
|
|
|
|
// selectPendingAuthz selects all fields of one pending authorization model
|
|
func selectPendingAuthz(s dbOneSelector, q string, args ...interface{}) (*pendingauthzModel, error) {
|
|
var model pendingauthzModel
|
|
err := s.SelectOne(
|
|
&model,
|
|
"SELECT id, identifier, registrationID, status, expires, combinations, LockCol FROM pendingAuthorizations "+q,
|
|
args...,
|
|
)
|
|
return &model, err
|
|
}
|
|
|
|
const authzFields = "id, identifier, registrationID, status, expires, combinations"
|
|
|
|
// selectAuthz selects all fields of one authorization model
|
|
func selectAuthz(s dbOneSelector, q string, args ...interface{}) (*authzModel, error) {
|
|
var model authzModel
|
|
err := s.SelectOne(
|
|
&model,
|
|
"SELECT "+authzFields+" FROM authz "+q,
|
|
args...,
|
|
)
|
|
return &model, err
|
|
}
|
|
|
|
// selectSctReceipt selects all fields of one SignedCertificateTimestamp object
|
|
func selectSctReceipt(s dbOneSelector, q string, args ...interface{}) (core.SignedCertificateTimestamp, error) {
|
|
var model core.SignedCertificateTimestamp
|
|
err := s.SelectOne(
|
|
&model,
|
|
"SELECT id, sctVersion, logID, timestamp, extensions, signature, certificateSerial, LockCol FROM sctReceipts "+q,
|
|
args...,
|
|
)
|
|
return model, err
|
|
}
|
|
|
|
const certFields = "registrationID, serial, digest, der, issued, expires"
|
|
|
|
// SelectCertificate selects all fields of one certificate object
|
|
func SelectCertificate(s dbOneSelector, q string, args ...interface{}) (core.Certificate, error) {
|
|
var model core.Certificate
|
|
err := s.SelectOne(
|
|
&model,
|
|
"SELECT "+certFields+" FROM certificates "+q,
|
|
args...,
|
|
)
|
|
return model, err
|
|
}
|
|
|
|
// SelectCertificates selects all fields of multiple certificate objects
|
|
func SelectCertificates(s dbSelector, q string, args map[string]interface{}) ([]core.Certificate, error) {
|
|
var models []core.Certificate
|
|
_, err := s.Select(
|
|
&models,
|
|
"SELECT "+certFields+" FROM certificates "+q, args)
|
|
return models, err
|
|
}
|
|
|
|
const certStatusFields = "serial, status, ocspLastUpdated, revokedDate, revokedReason, lastExpirationNagSent, ocspResponse, notAfter, isExpired"
|
|
|
|
// SelectCertificateStatus selects all fields of one certificate status model
|
|
func SelectCertificateStatus(s dbOneSelector, q string, args ...interface{}) (certStatusModel, error) {
|
|
var model certStatusModel
|
|
err := s.SelectOne(
|
|
&model,
|
|
"SELECT "+certStatusFields+" FROM certificateStatus "+q,
|
|
args...,
|
|
)
|
|
return model, err
|
|
}
|
|
|
|
// SelectCertificateStatuses selects all fields of multiple certificate status objects
|
|
func SelectCertificateStatuses(s dbSelector, q string, args ...interface{}) ([]core.CertificateStatus, error) {
|
|
var models []core.CertificateStatus
|
|
_, err := s.Select(
|
|
&models,
|
|
"SELECT "+certStatusFields+" FROM certificateStatus "+q,
|
|
args...,
|
|
)
|
|
return models, err
|
|
}
|
|
|
|
var mediumBlobSize = int(math.Pow(2, 24))
|
|
|
|
type issuedNameModel struct {
|
|
ID int64 `db:"id"`
|
|
ReversedName string `db:"reversedName"`
|
|
NotBefore time.Time `db:"notBefore"`
|
|
Serial string `db:"serial"`
|
|
}
|
|
|
|
// regModel is the description of a core.Registration in the database before
|
|
type regModel struct {
|
|
ID int64 `db:"id"`
|
|
Key []byte `db:"jwk"`
|
|
KeySHA256 string `db:"jwk_sha256"`
|
|
Contact []string `db:"contact"`
|
|
Agreement string `db:"agreement"`
|
|
// InitialIP is stored as sixteen binary bytes, regardless of whether it
|
|
// represents a v4 or v6 IP address.
|
|
InitialIP []byte `db:"initialIp"`
|
|
CreatedAt time.Time `db:"createdAt"`
|
|
LockCol int64
|
|
Status string `db:"status"`
|
|
}
|
|
|
|
type certStatusModel struct {
|
|
Serial string `db:"serial"`
|
|
Status core.OCSPStatus `db:"status"`
|
|
OCSPLastUpdated time.Time `db:"ocspLastUpdated"`
|
|
RevokedDate time.Time `db:"revokedDate"`
|
|
RevokedReason revocation.Reason `db:"revokedReason"`
|
|
LastExpirationNagSent time.Time `db:"lastExpirationNagSent"`
|
|
OCSPResponse []byte `db:"ocspResponse"`
|
|
NotAfter time.Time `db:"notAfter"`
|
|
IsExpired bool `db:"isExpired"`
|
|
|
|
// TODO(#856, #873): Deprecated, remove once #2882 has been deployed
|
|
// to production
|
|
SubscribedApproved bool `db:"subscriberApproved"`
|
|
LockCol int
|
|
}
|
|
|
|
// challModel is the description of a core.Challenge in the database
|
|
//
|
|
// The Validation field is a stub; the column is only there for backward compatibility.
|
|
type challModel struct {
|
|
ID int64 `db:"id"`
|
|
AuthorizationID string `db:"authorizationID"`
|
|
|
|
Type string `db:"type"`
|
|
Status core.AcmeStatus `db:"status"`
|
|
Error []byte `db:"error"`
|
|
Token string `db:"token"`
|
|
KeyAuthorization string `db:"keyAuthorization"`
|
|
ValidationRecord []byte `db:"validationRecord"`
|
|
|
|
// TODO(#1818): Remove, this field is unused, but is kept temporarily to avoid a database migration.
|
|
Validated bool `db:"validated"`
|
|
|
|
LockCol int64
|
|
}
|
|
|
|
// getChallengesQuery fetches exactly the fields in challModel from the
|
|
// challenges table.
|
|
const getChallengesQuery = `
|
|
SELECT id, authorizationID, type, status, error, token,
|
|
keyAuthorization, validationRecord
|
|
FROM challenges WHERE authorizationID = :authID ORDER BY id ASC`
|
|
|
|
// newReg creates a reg model object from a core.Registration
|
|
func registrationToModel(r *core.Registration) (*regModel, error) {
|
|
key, err := json.Marshal(r.Key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sha, err := core.KeyDigest(r.Key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if r.InitialIP == nil {
|
|
return nil, fmt.Errorf("initialIP was nil")
|
|
}
|
|
if r.Contact == nil {
|
|
r.Contact = &[]string{}
|
|
}
|
|
rm := regModel{
|
|
ID: r.ID,
|
|
Key: key,
|
|
KeySHA256: sha,
|
|
Contact: *r.Contact,
|
|
Agreement: r.Agreement,
|
|
InitialIP: []byte(r.InitialIP.To16()),
|
|
CreatedAt: r.CreatedAt,
|
|
Status: string(r.Status),
|
|
}
|
|
|
|
return &rm, nil
|
|
}
|
|
|
|
func modelToRegistration(reg *regModel) (core.Registration, error) {
|
|
k := &jose.JSONWebKey{}
|
|
err := json.Unmarshal(reg.Key, k)
|
|
if err != nil {
|
|
err = fmt.Errorf("unable to unmarshal JSONWebKey in db: %s", err)
|
|
return core.Registration{}, err
|
|
}
|
|
var contact *[]string
|
|
// Contact can be nil when the DB contains the literal string "null". We
|
|
// prefer to represent this in memory as a pointer to an empty slice rather
|
|
// than a nil pointer.
|
|
if reg.Contact == nil {
|
|
contact = &[]string{}
|
|
} else {
|
|
contact = ®.Contact
|
|
}
|
|
r := core.Registration{
|
|
ID: reg.ID,
|
|
Key: k,
|
|
Contact: contact,
|
|
Agreement: reg.Agreement,
|
|
InitialIP: net.IP(reg.InitialIP),
|
|
CreatedAt: reg.CreatedAt,
|
|
Status: core.AcmeStatus(reg.Status),
|
|
}
|
|
|
|
return r, nil
|
|
}
|
|
|
|
func challengeToModel(c *core.Challenge, authID string) (*challModel, error) {
|
|
cm := challModel{
|
|
ID: c.ID,
|
|
AuthorizationID: authID,
|
|
Type: c.Type,
|
|
Status: c.Status,
|
|
Token: c.Token,
|
|
KeyAuthorization: c.ProvidedKeyAuthorization,
|
|
}
|
|
if c.Error != nil {
|
|
errJSON, err := json.Marshal(c.Error)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(errJSON) > mediumBlobSize {
|
|
return nil, fmt.Errorf("Error object is too large to store in the database")
|
|
}
|
|
cm.Error = errJSON
|
|
}
|
|
if len(c.ValidationRecord) > 0 {
|
|
vrJSON, err := json.Marshal(c.ValidationRecord)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(vrJSON) > mediumBlobSize {
|
|
return nil, fmt.Errorf("Validation Record object is too large to store in the database")
|
|
}
|
|
cm.ValidationRecord = vrJSON
|
|
}
|
|
return &cm, nil
|
|
}
|
|
|
|
func modelToChallenge(cm *challModel) (core.Challenge, error) {
|
|
c := core.Challenge{
|
|
ID: cm.ID,
|
|
Type: cm.Type,
|
|
Status: cm.Status,
|
|
Token: cm.Token,
|
|
ProvidedKeyAuthorization: cm.KeyAuthorization,
|
|
}
|
|
if len(cm.Error) > 0 {
|
|
var problem probs.ProblemDetails
|
|
err := json.Unmarshal(cm.Error, &problem)
|
|
if err != nil {
|
|
return core.Challenge{}, err
|
|
}
|
|
c.Error = &problem
|
|
}
|
|
if len(cm.ValidationRecord) > 0 {
|
|
var vr []core.ValidationRecord
|
|
err := json.Unmarshal(cm.ValidationRecord, &vr)
|
|
if err != nil {
|
|
return core.Challenge{}, err
|
|
}
|
|
c.ValidationRecord = vr
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
type orderModel struct {
|
|
ID int64
|
|
RegistrationID int64
|
|
Expires time.Time
|
|
Created time.Time
|
|
Error []byte
|
|
CertificateSerial string
|
|
BeganProcessing bool
|
|
}
|
|
|
|
type requestedNameModel struct {
|
|
ID int64
|
|
OrderID int64
|
|
ReversedName string
|
|
}
|
|
|
|
type orderToAuthzModel struct {
|
|
OrderID int64
|
|
AuthzID string
|
|
}
|
|
|
|
func orderToModel(order *corepb.Order) (*orderModel, error) {
|
|
om := &orderModel{
|
|
ID: *order.Id,
|
|
RegistrationID: *order.RegistrationID,
|
|
Expires: time.Unix(0, *order.Expires),
|
|
Created: time.Unix(0, *order.Created),
|
|
BeganProcessing: *order.BeganProcessing,
|
|
}
|
|
if order.CertificateSerial != nil {
|
|
om.CertificateSerial = *order.CertificateSerial
|
|
}
|
|
|
|
if order.Error != nil {
|
|
errJSON, err := json.Marshal(order.Error)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(errJSON) > mediumBlobSize {
|
|
return nil, fmt.Errorf("Error object is too large to store in the database")
|
|
}
|
|
om.Error = errJSON
|
|
}
|
|
return om, nil
|
|
}
|
|
|
|
func modelToOrder(om *orderModel) (*corepb.Order, error) {
|
|
expires := om.Expires.UnixNano()
|
|
created := om.Created.UnixNano()
|
|
order := &corepb.Order{
|
|
Id: &om.ID,
|
|
RegistrationID: &om.RegistrationID,
|
|
Expires: &expires,
|
|
Created: &created,
|
|
CertificateSerial: &om.CertificateSerial,
|
|
BeganProcessing: &om.BeganProcessing,
|
|
}
|
|
if len(om.Error) > 0 {
|
|
var problem corepb.ProblemDetails
|
|
err := json.Unmarshal(om.Error, &problem)
|
|
if err != nil {
|
|
return &corepb.Order{}, err
|
|
}
|
|
order.Error = &problem
|
|
}
|
|
return order, nil
|
|
}
|