You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
637 lines
20 KiB
637 lines
20 KiB
// Package tlscert owns the protected HTTPS certificate library. Public callers
|
|
// receive metadata or CSR material only; private keys leave this package only
|
|
// when an active key pair is written to the local HTTPS runtime paths.
|
|
package tlscert
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/sha256"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/hex"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"tera-clone/services/tera-api/internal/storage/atomicfile"
|
|
)
|
|
|
|
const (
|
|
FactoryCertificateID = "factory-default"
|
|
MaximumCertificates = 16
|
|
maximumPEMBytes = 1 << 20
|
|
)
|
|
|
|
type Subject struct {
|
|
Country string
|
|
Province string
|
|
Locality string
|
|
Organization string
|
|
OrganizationalUnit string
|
|
CommonName string
|
|
Email string
|
|
}
|
|
|
|
type Metadata struct {
|
|
CertificateID string
|
|
Subject string
|
|
Issuer string
|
|
CommonName string
|
|
NotBefore time.Time
|
|
NotAfter time.Time
|
|
FingerprintSHA256 string
|
|
Source string
|
|
PrivateKeyStored bool
|
|
KeyBits int
|
|
}
|
|
|
|
type ImportRequest struct {
|
|
CertificateID string
|
|
CertificatePEM []byte
|
|
PrivateKeyPEM []byte
|
|
Password string
|
|
}
|
|
|
|
type Options struct {
|
|
Root string
|
|
ActiveCert string
|
|
ActiveKey string
|
|
Clock func() time.Time
|
|
}
|
|
|
|
type record struct {
|
|
metadata Metadata
|
|
certPEM []byte
|
|
keyPEM []byte
|
|
}
|
|
|
|
type Repository struct {
|
|
mu sync.Mutex
|
|
root string
|
|
activeCert string
|
|
activeKey string
|
|
clock func() time.Time
|
|
records map[string]record
|
|
}
|
|
|
|
var (
|
|
memoryFactoryOnce sync.Once
|
|
memoryFactory record
|
|
memoryFactoryErr error
|
|
)
|
|
|
|
func Open(options Options) (*Repository, error) {
|
|
clock := options.Clock
|
|
if clock == nil {
|
|
clock = time.Now
|
|
}
|
|
repository := &Repository{
|
|
root: strings.TrimSpace(options.Root), activeCert: strings.TrimSpace(options.ActiveCert),
|
|
activeKey: strings.TrimSpace(options.ActiveKey), clock: clock, records: make(map[string]record),
|
|
}
|
|
if repository.root != "" {
|
|
if !filepath.IsAbs(repository.root) {
|
|
return nil, errors.New("TLS certificate store root must be an absolute path")
|
|
}
|
|
if err := os.MkdirAll(repository.root, 0o700); err != nil {
|
|
return nil, fmt.Errorf("create TLS certificate store: %w", err)
|
|
}
|
|
if err := repository.load(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if _, ok := repository.records[FactoryCertificateID]; !ok {
|
|
var generated record
|
|
var err error
|
|
if repository.root == "" {
|
|
memoryFactoryOnce.Do(func() {
|
|
memoryFactory, memoryFactoryErr = generateRecord(FactoryCertificateID, Subject{Organization: "Teraclone Co.,Ltd", CommonName: "tera-management.local"}, "factory", time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC))
|
|
})
|
|
generated, err = cloneRecord(memoryFactory), memoryFactoryErr
|
|
} else {
|
|
generated, err = generateRecord(FactoryCertificateID, Subject{Organization: "Teraclone Co.,Ltd", CommonName: "tera-management.local"}, "factory", clock())
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
repository.records[FactoryCertificateID] = generated
|
|
if err := repository.persist(generated); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return repository, nil
|
|
}
|
|
|
|
func cloneRecord(source record) record {
|
|
source.certPEM = append([]byte(nil), source.certPEM...)
|
|
source.keyPEM = append([]byte(nil), source.keyPEM...)
|
|
return source
|
|
}
|
|
|
|
func (r *Repository) List(activeID string) []Metadata {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
result := make([]Metadata, 0, len(r.records))
|
|
for _, item := range r.records {
|
|
result = append(result, item.metadata)
|
|
}
|
|
sort.Slice(result, func(i, j int) bool { return result[i].CertificateID < result[j].CertificateID })
|
|
return result
|
|
}
|
|
|
|
func (r *Repository) Metadata(certificateID string) (Metadata, error) {
|
|
id, err := normalizeID(certificateID)
|
|
if err != nil {
|
|
return Metadata{}, err
|
|
}
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
item, ok := r.records[id]
|
|
if !ok {
|
|
return Metadata{}, errors.New("TLS certificate does not exist")
|
|
}
|
|
return item.metadata, nil
|
|
}
|
|
|
|
func (r *Repository) Generate(certificateID string, subject Subject) (Metadata, error) {
|
|
id, err := normalizeID(certificateID)
|
|
if err != nil {
|
|
return Metadata{}, err
|
|
}
|
|
if err := validateSubject(subject); err != nil {
|
|
return Metadata{}, err
|
|
}
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if len(r.records) >= MaximumCertificates {
|
|
return Metadata{}, fmt.Errorf("TLS certificate key limit of %d reached", MaximumCertificates)
|
|
}
|
|
if _, exists := r.records[id]; exists {
|
|
return Metadata{}, errors.New("TLS certificate already exists")
|
|
}
|
|
item, err := generateRecord(id, subject, "generated", r.clock())
|
|
if err != nil {
|
|
return Metadata{}, err
|
|
}
|
|
if err := r.persist(item); err != nil {
|
|
return Metadata{}, err
|
|
}
|
|
r.records[id] = item
|
|
return item.metadata, nil
|
|
}
|
|
|
|
func (r *Repository) Import(request ImportRequest) (Metadata, error) {
|
|
id, err := normalizeID(request.CertificateID)
|
|
if err != nil {
|
|
return Metadata{}, err
|
|
}
|
|
if len(request.CertificatePEM) == 0 || len(request.CertificatePEM)+len(request.PrivateKeyPEM) > maximumPEMBytes {
|
|
return Metadata{}, errors.New("TLS certificate import must contain at most 1 MiB of PEM data")
|
|
}
|
|
keySource := request.PrivateKeyPEM
|
|
if len(keySource) == 0 {
|
|
keySource = request.CertificatePEM
|
|
}
|
|
certPEM, certificate, err := normalizeCertificates(request.CertificatePEM)
|
|
if err != nil {
|
|
return Metadata{}, err
|
|
}
|
|
keyPEM, privateKey, err := normalizePrivateKey(keySource, request.Password)
|
|
if err != nil {
|
|
return Metadata{}, err
|
|
}
|
|
if _, err := tls.X509KeyPair(certPEM, keyPEM); err != nil {
|
|
return Metadata{}, fmt.Errorf("TLS certificate private key does not match certificate: %w", err)
|
|
}
|
|
item := record{metadata: metadataFromCertificate(id, certificate, privateKey, "imported"), certPEM: certPEM, keyPEM: keyPEM}
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if len(r.records) >= MaximumCertificates {
|
|
return Metadata{}, fmt.Errorf("TLS certificate key limit of %d reached", MaximumCertificates)
|
|
}
|
|
if _, exists := r.records[id]; exists {
|
|
return Metadata{}, errors.New("TLS certificate already exists")
|
|
}
|
|
if err := r.persist(item); err != nil {
|
|
return Metadata{}, err
|
|
}
|
|
r.records[id] = item
|
|
return item.metadata, nil
|
|
}
|
|
|
|
func (r *Repository) CSR(certificateID string, subject Subject) ([]byte, error) {
|
|
id, err := normalizeID(certificateID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r.mu.Lock()
|
|
item, ok := r.records[id]
|
|
r.mu.Unlock()
|
|
if !ok {
|
|
return nil, errors.New("TLS certificate private key does not exist")
|
|
}
|
|
if strings.TrimSpace(subject.CommonName) == "" {
|
|
subject.CommonName = item.metadata.CommonName
|
|
}
|
|
if err := validateSubject(subject); err != nil {
|
|
return nil, err
|
|
}
|
|
privateKey, err := parsePrivateKeyDER(item.keyPEM)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
request := x509.CertificateRequest{Subject: subjectName(subject), EmailAddresses: nonEmpty(subject.Email)}
|
|
if ip := net.ParseIP(subject.CommonName); ip != nil {
|
|
request.IPAddresses = []net.IP{ip}
|
|
} else {
|
|
request.DNSNames = []string{subject.CommonName}
|
|
}
|
|
der, err := x509.CreateCertificateRequest(rand.Reader, &request, privateKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create TLS certificate signing request: %w", err)
|
|
}
|
|
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: der}), nil
|
|
}
|
|
|
|
func (r *Repository) Activate(certificateID string) (Metadata, error) {
|
|
id, err := normalizeID(certificateID)
|
|
if err != nil {
|
|
return Metadata{}, err
|
|
}
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
item, ok := r.records[id]
|
|
if !ok {
|
|
return Metadata{}, errors.New("TLS certificate does not exist")
|
|
}
|
|
now := r.clock().UTC()
|
|
// A factory certificate created before the device acquired a clock must
|
|
// be reissued after bootstrap. Keep its key and subject; never replace a
|
|
// user certificate or a normally dated expired factory certificate.
|
|
if id == FactoryCertificateID && item.metadata.Source == "factory" && item.metadata.NotBefore.Year() < 2000 && now.Year() >= 2020 {
|
|
repaired, err := reissueBootstrapFactory(item, now)
|
|
if err != nil {
|
|
return Metadata{}, err
|
|
}
|
|
if err := r.persist(repaired); err != nil {
|
|
return Metadata{}, err
|
|
}
|
|
r.records[id] = repaired
|
|
item = repaired
|
|
}
|
|
if now.Before(item.metadata.NotBefore) || !now.Before(item.metadata.NotAfter) {
|
|
return Metadata{}, errors.New("TLS certificate is not currently valid")
|
|
}
|
|
if r.activeCert != "" || r.activeKey != "" {
|
|
if !filepath.IsAbs(r.activeCert) || !filepath.IsAbs(r.activeKey) {
|
|
return Metadata{}, errors.New("TLS active certificate and key paths must be absolute")
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(r.activeKey), 0o700); err != nil {
|
|
return Metadata{}, err
|
|
}
|
|
if err := atomicfile.Write(r.activeKey, item.keyPEM, 0o600); err != nil {
|
|
return Metadata{}, fmt.Errorf("write active TLS private key: %w", err)
|
|
}
|
|
if err := atomicfile.Write(r.activeCert, item.certPEM, 0o644); err != nil {
|
|
return Metadata{}, fmt.Errorf("write active TLS certificate: %w", err)
|
|
}
|
|
}
|
|
return item.metadata, nil
|
|
}
|
|
|
|
func (r *Repository) Delete(certificateID string) error {
|
|
id, err := normalizeID(certificateID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if id == FactoryCertificateID {
|
|
return errors.New("factory TLS certificate cannot be deleted")
|
|
}
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if _, ok := r.records[id]; !ok {
|
|
return errors.New("TLS certificate does not exist")
|
|
}
|
|
if r.root != "" {
|
|
for _, extension := range []string{".crt", ".key"} {
|
|
if err := os.Remove(filepath.Join(r.root, id+extension)); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("delete TLS certificate material: %w", err)
|
|
}
|
|
}
|
|
}
|
|
delete(r.records, id)
|
|
return nil
|
|
}
|
|
|
|
func (r *Repository) load() error {
|
|
entries, err := os.ReadDir(r.root)
|
|
if err != nil {
|
|
return fmt.Errorf("read TLS certificate store: %w", err)
|
|
}
|
|
for _, entry := range entries {
|
|
if entry.IsDir() || filepath.Ext(entry.Name()) != ".crt" {
|
|
continue
|
|
}
|
|
id := strings.TrimSuffix(entry.Name(), ".crt")
|
|
if _, err := normalizeID(id); err != nil {
|
|
return fmt.Errorf("unsafe TLS certificate store entry %q", entry.Name())
|
|
}
|
|
certPEM, err := os.ReadFile(filepath.Join(r.root, id+".crt"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
keyPEM, err := os.ReadFile(filepath.Join(r.root, id+".key"))
|
|
if err != nil {
|
|
return fmt.Errorf("read TLS private key %s: %w", id, err)
|
|
}
|
|
normalizedCert, certificate, err := normalizeCertificates(certPEM)
|
|
if err != nil {
|
|
return fmt.Errorf("load TLS certificate %s: %w", id, err)
|
|
}
|
|
normalizedKey, privateKey, err := normalizePrivateKey(keyPEM, "")
|
|
if err != nil {
|
|
return fmt.Errorf("load TLS private key %s: %w", id, err)
|
|
}
|
|
if _, err := tls.X509KeyPair(normalizedCert, normalizedKey); err != nil {
|
|
return fmt.Errorf("load TLS key pair %s: %w", id, err)
|
|
}
|
|
source := "protected-store"
|
|
if id == FactoryCertificateID {
|
|
source = "factory"
|
|
}
|
|
r.records[id] = record{metadata: metadataFromCertificate(id, certificate, privateKey, source), certPEM: normalizedCert, keyPEM: normalizedKey}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Repository) persist(item record) error {
|
|
if r.root == "" {
|
|
return nil
|
|
}
|
|
if err := atomicfile.Write(filepath.Join(r.root, item.metadata.CertificateID+".key"), item.keyPEM, 0o600); err != nil {
|
|
return fmt.Errorf("persist TLS private key: %w", err)
|
|
}
|
|
if err := atomicfile.Write(filepath.Join(r.root, item.metadata.CertificateID+".crt"), item.certPEM, 0o644); err != nil {
|
|
return fmt.Errorf("persist TLS certificate: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func generateRecord(id string, subject Subject, source string, now time.Time) (record, error) {
|
|
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
return record{}, fmt.Errorf("generate RSA-2048 TLS private key: %w", err)
|
|
}
|
|
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
|
if err != nil {
|
|
return record{}, err
|
|
}
|
|
template := &x509.Certificate{
|
|
SerialNumber: serial, Subject: subjectName(subject), Issuer: subjectName(subject),
|
|
NotBefore: now.UTC().Add(-5 * time.Minute), NotAfter: now.UTC().AddDate(10, 0, 0),
|
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true,
|
|
EmailAddresses: nonEmpty(subject.Email),
|
|
}
|
|
if ip := net.ParseIP(subject.CommonName); ip != nil {
|
|
template.IPAddresses = []net.IP{ip}
|
|
} else if subject.CommonName != "" {
|
|
template.DNSNames = []string{subject.CommonName}
|
|
}
|
|
der, err := x509.CreateCertificate(rand.Reader, template, template, &privateKey.PublicKey, privateKey)
|
|
if err != nil {
|
|
return record{}, err
|
|
}
|
|
keyDER, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
|
if err != nil {
|
|
return record{}, err
|
|
}
|
|
certificate, err := x509.ParseCertificate(der)
|
|
if err != nil {
|
|
return record{}, err
|
|
}
|
|
return record{
|
|
metadata: metadataFromCertificate(id, certificate, privateKey, source),
|
|
certPEM: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}),
|
|
keyPEM: pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}),
|
|
}, nil
|
|
}
|
|
|
|
func normalizeCertificates(payload []byte) ([]byte, *x509.Certificate, error) {
|
|
remainder := payload
|
|
var output []byte
|
|
var leaf *x509.Certificate
|
|
for {
|
|
block, rest := pem.Decode(remainder)
|
|
if block == nil {
|
|
break
|
|
}
|
|
remainder = rest
|
|
if block.Type != "CERTIFICATE" {
|
|
continue
|
|
}
|
|
certificate, err := x509.ParseCertificate(block.Bytes)
|
|
if err != nil {
|
|
return nil, nil, errors.New("TLS certificate contains invalid PEM certificate data")
|
|
}
|
|
if leaf == nil {
|
|
leaf = certificate
|
|
}
|
|
output = append(output, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: block.Bytes})...)
|
|
}
|
|
if leaf == nil {
|
|
return nil, nil, errors.New("TLS certificate import requires PEM certificate data")
|
|
}
|
|
return output, leaf, nil
|
|
}
|
|
|
|
func normalizePrivateKey(payload []byte, password string) ([]byte, crypto.PrivateKey, error) {
|
|
remainder := payload
|
|
for {
|
|
block, rest := pem.Decode(remainder)
|
|
if block == nil {
|
|
break
|
|
}
|
|
remainder = rest
|
|
if !strings.Contains(block.Type, "PRIVATE KEY") {
|
|
continue
|
|
}
|
|
der := block.Bytes
|
|
if x509.IsEncryptedPEMBlock(block) {
|
|
if password == "" {
|
|
return nil, nil, errors.New("TLS private key password is required")
|
|
}
|
|
var err error
|
|
der, err = x509.DecryptPEMBlock(block, []byte(password))
|
|
if err != nil {
|
|
return nil, nil, errors.New("TLS private key password is incorrect")
|
|
}
|
|
}
|
|
privateKey, err := parsePrivateKeyBytes(der)
|
|
if err != nil {
|
|
return nil, nil, errors.New("TLS private key uses an unsupported PEM format")
|
|
}
|
|
normalizedDER, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
|
if err != nil {
|
|
return nil, nil, errors.New("TLS private key uses an unsupported key type")
|
|
}
|
|
return pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: normalizedDER}), privateKey, nil
|
|
}
|
|
return nil, nil, errors.New("TLS certificate import requires a PEM private key")
|
|
}
|
|
|
|
func parsePrivateKeyDER(payload []byte) (crypto.PrivateKey, error) {
|
|
block, _ := pem.Decode(payload)
|
|
if block == nil {
|
|
return nil, errors.New("invalid stored TLS private key")
|
|
}
|
|
return parsePrivateKeyBytes(block.Bytes)
|
|
}
|
|
|
|
func parsePrivateKeyBytes(der []byte) (crypto.PrivateKey, error) {
|
|
if key, err := x509.ParsePKCS8PrivateKey(der); err == nil {
|
|
return key, nil
|
|
}
|
|
if key, err := x509.ParsePKCS1PrivateKey(der); err == nil {
|
|
return key, nil
|
|
}
|
|
if key, err := x509.ParseECPrivateKey(der); err == nil {
|
|
return key, nil
|
|
}
|
|
return nil, errors.New("unsupported private key")
|
|
}
|
|
|
|
func metadataFromCertificate(id string, certificate *x509.Certificate, privateKey crypto.PrivateKey, source string) Metadata {
|
|
fingerprint := sha256.Sum256(certificate.Raw)
|
|
bits := 0
|
|
if key, ok := privateKey.(*rsa.PrivateKey); ok {
|
|
bits = key.N.BitLen()
|
|
}
|
|
return Metadata{
|
|
CertificateID: id, Subject: certificate.Subject.String(), Issuer: certificate.Issuer.String(),
|
|
CommonName: certificate.Subject.CommonName, NotBefore: certificate.NotBefore.UTC(), NotAfter: certificate.NotAfter.UTC(),
|
|
FingerprintSHA256: hex.EncodeToString(fingerprint[:]), Source: source, PrivateKeyStored: true, KeyBits: bits,
|
|
}
|
|
}
|
|
|
|
func subjectName(subject Subject) pkix.Name {
|
|
return pkix.Name{
|
|
Country: nonEmpty(subject.Country), Province: nonEmpty(subject.Province), Locality: nonEmpty(subject.Locality),
|
|
Organization: nonEmpty(subject.Organization), OrganizationalUnit: nonEmpty(subject.OrganizationalUnit), CommonName: strings.TrimSpace(subject.CommonName),
|
|
}
|
|
}
|
|
|
|
func validateSubject(subject Subject) error {
|
|
values := []string{subject.Country, subject.Province, subject.Locality, subject.Organization, subject.OrganizationalUnit, subject.CommonName, subject.Email}
|
|
for _, value := range values {
|
|
if len(value) > 128 || strings.ContainsAny(value, "\r\n\x00") {
|
|
return errors.New("TLS certificate subject fields must contain at most 128 non-control characters")
|
|
}
|
|
}
|
|
if strings.TrimSpace(subject.CommonName) == "" {
|
|
return errors.New("TLS certificate common name must not be empty")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func normalizeID(value string) (string, error) {
|
|
id := strings.ToLower(strings.TrimSpace(value))
|
|
if id == "" || len(id) > 64 {
|
|
return "", errors.New("TLS certificate file name must contain 1 through 64 characters")
|
|
}
|
|
for _, char := range id {
|
|
if char >= 'a' && char <= 'z' || char >= '0' && char <= '9' || char == '.' || char == '_' || char == '-' {
|
|
continue
|
|
}
|
|
return "", errors.New("TLS certificate file name may contain only letters, digits, dot, underscore, or hyphen")
|
|
}
|
|
return id, nil
|
|
}
|
|
|
|
func nonEmpty(value string) []string {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
return nil
|
|
}
|
|
return []string{value}
|
|
}
|
|
|
|
// Reset deletes customer key material and generates a fresh factory identity.
|
|
func (r *Repository) Reset() (Metadata, error) {
|
|
fresh, err := generateRecord(FactoryCertificateID, Subject{Organization: "Teraclone Co.,Ltd", CommonName: "tera-management.local"}, "factory", r.clock())
|
|
if err != nil {
|
|
return Metadata{}, err
|
|
}
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if r.root != "" {
|
|
entries, err := os.ReadDir(r.root)
|
|
if err != nil {
|
|
return Metadata{}, err
|
|
}
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
return Metadata{}, errors.New("unexpected directory in TLS store")
|
|
}
|
|
if err := os.Remove(filepath.Join(r.root, entry.Name())); err != nil {
|
|
return Metadata{}, err
|
|
}
|
|
}
|
|
}
|
|
if err := r.persist(fresh); err != nil {
|
|
return Metadata{}, err
|
|
}
|
|
r.records = map[string]record{FactoryCertificateID: fresh}
|
|
for path, content := range map[string][]byte{r.activeKey: fresh.keyPEM, r.activeCert: fresh.certPEM} {
|
|
if path == "" {
|
|
continue
|
|
}
|
|
if err := atomicfile.Write(path, content, 0o600); err != nil {
|
|
return Metadata{}, err
|
|
}
|
|
}
|
|
return fresh.metadata, nil
|
|
}
|
|
|
|
func reissueBootstrapFactory(item record, now time.Time) (record, error) {
|
|
pair, err := tls.X509KeyPair(item.certPEM, item.keyPEM)
|
|
if err != nil {
|
|
return record{}, err
|
|
}
|
|
certificate, err := x509.ParseCertificate(pair.Certificate[0])
|
|
if err != nil {
|
|
return record{}, err
|
|
}
|
|
signer, ok := pair.PrivateKey.(crypto.Signer)
|
|
if !ok {
|
|
return record{}, errors.New("factory certificate signer unavailable")
|
|
}
|
|
certificate.SerialNumber, err = rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
|
if err != nil {
|
|
return record{}, err
|
|
}
|
|
certificate.NotBefore = now.Add(-5 * time.Minute)
|
|
certificate.NotAfter = now.AddDate(10, 0, 0)
|
|
der, err := x509.CreateCertificate(rand.Reader, certificate, certificate, signer.Public(), signer)
|
|
if err != nil {
|
|
return record{}, err
|
|
}
|
|
certificate, err = x509.ParseCertificate(der)
|
|
if err != nil {
|
|
return record{}, err
|
|
}
|
|
item.certPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
|
|
item.metadata = metadataFromCertificate(FactoryCertificateID, certificate, pair.PrivateKey, "factory")
|
|
return item, nil
|
|
}
|
|
|