smallstep-certificates/acme/db/nosql/certificate.go
2023-05-09 23:47:28 -07:00

142 lines
3.9 KiB
Go

package nosql
import (
"context"
"crypto/x509"
"encoding/json"
"encoding/pem"
"time"
"github.com/pkg/errors"
"github.com/smallstep/certificates/acme"
"github.com/smallstep/nosql"
)
type dbCert struct {
ID string `json:"id"`
CreatedAt time.Time `json:"createdAt"`
AccountID string `json:"accountID"`
OrderID string `json:"orderID"`
Leaf []byte `json:"leaf"`
Intermediates []byte `json:"intermediates"`
}
type dbSerial struct {
Serial string `json:"serial"`
CertificateID string `json:"certificateID"`
}
// CreateCertificate creates and stores an ACME certificate type.
func (db *DB) CreateCertificate(ctx context.Context, cert *acme.Certificate) error {
var err error
cert.ID, err = randID()
if err != nil {
return err
}
leaf := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Leaf.Raw,
})
var intermediates []byte
for _, cert := range cert.Intermediates {
intermediates = append(intermediates, pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Raw,
})...)
}
dbch := &dbCert{
ID: cert.ID,
AccountID: cert.AccountID,
OrderID: cert.OrderID,
Leaf: leaf,
Intermediates: intermediates,
CreatedAt: time.Now().UTC(),
}
err = db.save(ctx, cert.ID, dbch, nil, "certificate", certTable)
if err != nil {
return err
}
serial := cert.Leaf.SerialNumber.String()
dbSerial := &dbSerial{
Serial: serial,
CertificateID: cert.ID,
}
return db.save(ctx, serial, dbSerial, nil, "serial", certBySerialTable)
}
// GetCertificate retrieves and unmarshals an ACME certificate type from the
// datastore.
func (db *DB) GetCertificate(_ context.Context, id string) (*acme.Certificate, error) {
b, err := db.db.Get(certTable, []byte(id))
if nosql.IsErrNotFound(err) {
return nil, acme.NewError(acme.ErrorMalformedType, "certificate %s not found", id)
} else if err != nil {
return nil, errors.Wrapf(err, "error loading certificate %s", id)
}
dbC := new(dbCert)
if err := json.Unmarshal(b, dbC); err != nil {
return nil, errors.Wrapf(err, "error unmarshaling certificate %s", id)
}
certs, err := parseBundle(append(dbC.Leaf, dbC.Intermediates...))
if err != nil {
return nil, errors.Wrapf(err, "error parsing certificate chain for ACME certificate with ID %s", id)
}
return &acme.Certificate{
ID: dbC.ID,
AccountID: dbC.AccountID,
OrderID: dbC.OrderID,
Leaf: certs[0],
Intermediates: certs[1:],
}, nil
}
// GetCertificateBySerial retrieves and unmarshals an ACME certificate type from the
// datastore based on a certificate serial number.
func (db *DB) GetCertificateBySerial(ctx context.Context, serial string) (*acme.Certificate, error) {
b, err := db.db.Get(certBySerialTable, []byte(serial))
if nosql.IsErrNotFound(err) {
return nil, acme.NewError(acme.ErrorMalformedType, "certificate with serial %s not found", serial)
} else if err != nil {
return nil, errors.Wrapf(err, "error loading certificate ID for serial %s", serial)
}
dbSerial := new(dbSerial)
if err := json.Unmarshal(b, dbSerial); err != nil {
return nil, errors.Wrapf(err, "error unmarshaling certificate with serial %s", serial)
}
return db.GetCertificate(ctx, dbSerial.CertificateID)
}
func parseBundle(b []byte) ([]*x509.Certificate, error) {
var (
err error
block *pem.Block
bundle []*x509.Certificate
)
for len(b) > 0 {
block, b = pem.Decode(b)
if block == nil {
break
}
if block.Type != "CERTIFICATE" {
return nil, errors.New("error decoding PEM: data contains block that is not a certificate")
}
var crt *x509.Certificate
crt, err = x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, errors.Wrapf(err, "error parsing x509 certificate")
}
bundle = append(bundle, crt)
}
if len(b) > 0 {
return nil, errors.New("error decoding PEM: unexpected data")
}
return bundle, nil
}