CI / go-test (push) Canceled after 0s
CI / postgres-durable-first (push) Canceled after 0s
CI / codegen (push) Canceled after 0s
CI / govulncheck (push) Canceled after 0s
CI / race (push) Canceled after 0s
CI / fuzz-smoke (push) Canceled after 0s
CI / golangci (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CodeQL Advanced / Analyze (go) (push) Canceled after 0s
CodeQL Advanced / Analyze (actions) (push) Canceled after 0s
CodeQL Advanced / Analyze (javascript-typescript) (push) Canceled after 0s
Docs CI / build (push) Canceled after 0s
Docs Deploy (GitHub Pages) / build (push) Canceled after 0s
Release 3X-UI / build (386) (push) Canceled after 0s
Release 3X-UI / build (amd64) (push) Canceled after 0s
Release 3X-UI / build (arm64) (push) Canceled after 0s
Release 3X-UI / build (armv5) (push) Canceled after 0s
Release 3X-UI / build (armv6) (push) Canceled after 0s
Release 3X-UI / build (armv7) (push) Canceled after 0s
Release 3X-UI / build (s390x) (push) Canceled after 0s
Release 3X-UI / Build for Windows (amd64) (push) Canceled after 0s
Deploy Smoke Tests / noninteractive-install (ubuntu-24.04-arm) (push) Canceled after 0s
Deploy Smoke Tests / noninteractive-install (ubuntu-latest) (push) Canceled after 0s
Deploy Smoke Tests / release-tag-install (ubuntu-24.04-arm) (push) Canceled after 0s
Deploy Smoke Tests / release-tag-install (ubuntu-latest) (push) Canceled after 0s
Docs Deploy (GitHub Pages) / deploy (push) Canceled after 0s
Release 3X-UI / Publish rolling dev release (push) Canceled after 0s
123 lines
3.6 KiB
Go
123 lines
3.6 KiB
Go
package crypto
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/pem"
|
|
"errors"
|
|
"math/big"
|
|
"time"
|
|
)
|
|
|
|
// nodeCertValidity is how long the node-auth CA and the leaves it issues stay
|
|
// valid. It is deliberately long: v1 has no rotation flow, so expiry would mean
|
|
// a fleet-wide outage. Rotation is tracked as follow-up work.
|
|
const nodeCertValidity = 10 * 365 * 24 * time.Hour
|
|
|
|
// CertKeyPEM is a PEM-encoded certificate together with its private key.
|
|
type CertKeyPEM struct {
|
|
CertPEM []byte
|
|
KeyPEM []byte
|
|
}
|
|
|
|
func randomSerial() (*big.Int, error) {
|
|
return rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
|
}
|
|
|
|
func marshalCertKey(certDER []byte, key *ecdsa.PrivateKey) (CertKeyPEM, error) {
|
|
keyDER, err := x509.MarshalECPrivateKey(key)
|
|
if err != nil {
|
|
return CertKeyPEM{}, err
|
|
}
|
|
return CertKeyPEM{
|
|
CertPEM: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}),
|
|
KeyPEM: pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}),
|
|
}, nil
|
|
}
|
|
|
|
// GenerateNodeCA mints a self-signed ECDSA P-256 CA used to authenticate node
|
|
// API traffic. It can sign leaf certificates only (path length 0).
|
|
func GenerateNodeCA(commonName string) (CertKeyPEM, error) {
|
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
return CertKeyPEM{}, err
|
|
}
|
|
serial, err := randomSerial()
|
|
if err != nil {
|
|
return CertKeyPEM{}, err
|
|
}
|
|
now := time.Now()
|
|
tmpl := &x509.Certificate{
|
|
SerialNumber: serial,
|
|
Subject: pkix.Name{CommonName: commonName},
|
|
NotBefore: now.Add(-time.Hour),
|
|
NotAfter: now.Add(nodeCertValidity),
|
|
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
|
BasicConstraintsValid: true,
|
|
IsCA: true,
|
|
MaxPathLenZero: true,
|
|
}
|
|
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key)
|
|
if err != nil {
|
|
return CertKeyPEM{}, err
|
|
}
|
|
return marshalCertKey(der, key)
|
|
}
|
|
|
|
// IssueClientCert signs a client-auth leaf (ExtKeyUsageClientAuth) with the
|
|
// given CA. The leaf authenticates the managing panel to a node.
|
|
func IssueClientCert(ca CertKeyPEM, commonName string) (CertKeyPEM, error) {
|
|
caCert, caKey, err := LoadCAFromPEM(ca)
|
|
if err != nil {
|
|
return CertKeyPEM{}, err
|
|
}
|
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
return CertKeyPEM{}, err
|
|
}
|
|
serial, err := randomSerial()
|
|
if err != nil {
|
|
return CertKeyPEM{}, err
|
|
}
|
|
now := time.Now()
|
|
tmpl := &x509.Certificate{
|
|
SerialNumber: serial,
|
|
Subject: pkix.Name{CommonName: commonName},
|
|
NotBefore: now.Add(-time.Hour),
|
|
NotAfter: now.Add(nodeCertValidity),
|
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
|
}
|
|
der, err := x509.CreateCertificate(rand.Reader, tmpl, caCert, key.Public(), caKey)
|
|
if err != nil {
|
|
return CertKeyPEM{}, err
|
|
}
|
|
return marshalCertKey(der, key)
|
|
}
|
|
|
|
// LoadCAFromPEM parses a CA cert+key pair into a certificate and its signer,
|
|
// ready to issue or to populate a trust pool.
|
|
func LoadCAFromPEM(ca CertKeyPEM) (*x509.Certificate, crypto.Signer, error) {
|
|
certBlock, _ := pem.Decode(ca.CertPEM)
|
|
if certBlock == nil || certBlock.Type != "CERTIFICATE" {
|
|
return nil, nil, errors.New("invalid CA certificate PEM")
|
|
}
|
|
cert, err := x509.ParseCertificate(certBlock.Bytes)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
keyBlock, _ := pem.Decode(ca.KeyPEM)
|
|
if keyBlock == nil {
|
|
return nil, nil, errors.New("invalid CA key PEM")
|
|
}
|
|
key, err := x509.ParseECPrivateKey(keyBlock.Bytes)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return cert, key, nil
|
|
}
|