155 lines
4.5 KiB
Go
155 lines
4.5 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"log"
|
|
"math/big"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
"github.com/aws/aws-sdk-go-v2/config"
|
|
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
|
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
|
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
|
)
|
|
|
|
// ddb is fulfilled by a dynamodb.Client and is used for mocking in tests.
|
|
type ddb interface {
|
|
BatchWriteItem(context.Context, *dynamodb.BatchWriteItemInput, ...func(*dynamodb.Options)) (*dynamodb.BatchWriteItemOutput, error)
|
|
PutItem(context.Context, *dynamodb.PutItemInput, ...func(*dynamodb.Options)) (*dynamodb.PutItemOutput, error)
|
|
Scan(context.Context, *dynamodb.ScanInput, ...func(*dynamodb.Options)) (*dynamodb.ScanOutput, error)
|
|
}
|
|
|
|
type Database struct {
|
|
Table string
|
|
Dynamo ddb
|
|
}
|
|
|
|
func New(ctx context.Context, table, dynamoEndpoint string) (*Database, error) {
|
|
cfg, err := config.LoadDefaultConfig(ctx)
|
|
if err != nil {
|
|
log.Fatalf("Error creating AWS config: %v", err)
|
|
}
|
|
|
|
return &Database{
|
|
Table: table,
|
|
Dynamo: dynamodb.NewFromConfig(cfg, func(o *dynamodb.Options) {
|
|
if dynamoEndpoint != "" {
|
|
o.BaseEndpoint = aws.String(dynamoEndpoint)
|
|
}
|
|
}),
|
|
}, nil
|
|
}
|
|
|
|
// CertMetadata is the entire set of attributes stored in Dynamo.
|
|
// That is the CertKey plus the revocation time today.
|
|
type CertMetadata struct {
|
|
CertKey
|
|
RevocationTime time.Time `dynamodbav:"RT,unixtime"`
|
|
CRLDistributionPoint string `dynamodbav:"DP,string,omitempty"`
|
|
}
|
|
|
|
// CertKey is the DynamoDB primary key, which is the serial number.
|
|
type CertKey struct {
|
|
SerialNumber []byte `dynamodbav:"SN"`
|
|
}
|
|
|
|
func NewCertKey(sn *big.Int) CertKey {
|
|
return CertKey{SerialNumber: sn.Bytes()}
|
|
}
|
|
|
|
// SerialString returns a consistent string representation of a SerialNumber
|
|
// It is intended for use as a map key, and is equivalent to boulder's SerialToString
|
|
func (ck CertKey) SerialString() string {
|
|
return fmt.Sprintf("%036x", ck.SerialNumber)
|
|
}
|
|
|
|
// AddCert inserts the metadata for monitoring
|
|
func (db *Database) AddCert(ctx context.Context, certificate *x509.Certificate, revocationTime time.Time) error {
|
|
var crlDistributionPoint string
|
|
// TODO: Once all issued certificates have a CRLDistributionPoint, error out when
|
|
// the extension is absent.
|
|
if len(certificate.CRLDistributionPoints) > 0 {
|
|
crlDistributionPoint = certificate.CRLDistributionPoints[0]
|
|
}
|
|
if len(certificate.CRLDistributionPoints) > 1 {
|
|
return fmt.Errorf("too many CRLDistributionPoints in certificate: %d", len(certificate.CRLDistributionPoints))
|
|
}
|
|
item, err := attributevalue.MarshalMap(CertMetadata{
|
|
CertKey: NewCertKey(certificate.SerialNumber),
|
|
RevocationTime: revocationTime,
|
|
CRLDistributionPoint: crlDistributionPoint,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = db.Dynamo.PutItem(ctx, &dynamodb.PutItemInput{
|
|
Item: item,
|
|
TableName: &db.Table,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetAllCerts returns all the certificates in the DynamoDB. This set is
|
|
// intended to be much smaller than the set of certificates in a CRL, so it's
|
|
// more efficient to just load the entire set instead of conditional querying.
|
|
// The map key is the serial's CertKey.SerialString.
|
|
// TODO: This could be more efficient if it was a query over issuer or shard
|
|
// TODO: However, the dataset is small enough to not matter much.
|
|
func (db *Database) GetAllCerts(ctx context.Context) (map[string]CertMetadata, error) {
|
|
resp, err := db.Dynamo.Scan(ctx, &dynamodb.ScanInput{
|
|
TableName: &db.Table,
|
|
Select: types.SelectAllAttributes,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var certList []CertMetadata
|
|
err = attributevalue.UnmarshalListOfMaps(resp.Items, &certList)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
certs := make(map[string]CertMetadata, len(certList))
|
|
for _, cert := range certList {
|
|
certs[cert.SerialString()] = cert
|
|
}
|
|
return certs, nil
|
|
}
|
|
|
|
// DeleteSerials takes a list of serials that we've seen in the CRL and thus
|
|
// no longer need to keep an eye out for.
|
|
func (db *Database) DeleteSerials(ctx context.Context, serialNumbers [][]byte) error {
|
|
if len(serialNumbers) == 0 {
|
|
return nil
|
|
}
|
|
var deletes []types.WriteRequest
|
|
for _, serial := range serialNumbers {
|
|
key, err := attributevalue.MarshalMap(CertKey{SerialNumber: serial})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
deletes = append(deletes, types.WriteRequest{
|
|
DeleteRequest: &types.DeleteRequest{
|
|
Key: key,
|
|
},
|
|
})
|
|
}
|
|
|
|
_, err := db.Dynamo.BatchWriteItem(ctx, &dynamodb.BatchWriteItemInput{
|
|
RequestItems: map[string][]types.WriteRequest{db.Table: deletes},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|