// 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() 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} }