Initial import

This commit is contained in:
2025-09-26 08:49:53 +02:00
commit a76650da35
35 changed files with 4660 additions and 0 deletions

114
internal/cryptutil/key.go Normal file
View File

@@ -0,0 +1,114 @@
package cryptutil
import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"os"
)
type publicKeyer interface {
Public() any
}
// PublicKey returns the public part of a crypto.PrivateKey.
func PublicKey(key crypto.PrivateKey) any {
switch key := key.(type) {
case ed25519.PublicKey:
return key
case ed25519.PrivateKey:
return key.Public()
case ecdsa.PublicKey:
return &key
case *ecdsa.PublicKey:
return key
case *ecdsa.PrivateKey:
return &key.PublicKey
case rsa.PublicKey:
return &key
case *rsa.PublicKey:
return key
case *rsa.PrivateKey:
return &key.PublicKey
default:
if p, ok := key.(publicKeyer); ok {
return p.Public()
}
panic(fmt.Sprintf("don't know how to extract a public key from %T", key))
}
}
// LoadPrivateKey loads a private key from disk.
func LoadPrivateKey(name string) (crypto.PrivateKey, error) {
b, err := os.ReadFile(name)
if err != nil {
return nil, err
}
return decodePEMPrivateKey(b)
}
func decodePEMPrivateKey(b []byte) (key crypto.PrivateKey, err error) {
var (
rest = b
block *pem.Block
)
for {
if block, rest = pem.Decode(rest); block == nil {
return nil, errors.New("mitm: no private key PEM block could be decoded")
}
switch block.Type {
case "EC PRIVATE KEY":
return x509.ParseECPrivateKey(block.Bytes)
case "RSA PRIVATE KEY":
return x509.ParsePKCS1PrivateKey(block.Bytes)
case "PRIVATE KEY":
return x509.ParsePKCS8PrivateKey(block.Bytes)
}
}
}
// GenerateKeyID generates the PKIX public key ID.
func GenerateKeyID(key crypto.PublicKey) []byte {
b, err := x509.MarshalPKIXPublicKey(key)
if err != nil {
return nil
}
return sha1.New().Sum(b)
}
func keyType(key any) string {
switch key := key.(type) {
case ed25519.PrivateKey:
return "ed25519"
case *ecdsa.PrivateKey:
return "ecdsa (" + curveType(key.Curve) + ")"
case *rsa.PrivateKey:
return "rsa"
default:
return fmt.Sprintf("%T", key)
}
}
func curveType(c elliptic.Curve) string {
switch c {
case elliptic.P224():
return "p224"
case elliptic.P256():
return "p256"
case elliptic.P384():
return "p384"
case elliptic.P521():
return "p521"
default:
return fmt.Sprintf("%T", c)
}
}

242
internal/cryptutil/x509.go Normal file
View File

@@ -0,0 +1,242 @@
package cryptutil
import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"math/big"
"os"
"path/filepath"
"strings"
"time"
"git.maze.io/maze/styx/internal/log"
)
// Supported key types.
const (
TypeRSA = "rsa"
TypeECDSA = "ecdsa"
TypeED25519 = "ed25519"
)
// Supported PEM block types.
const (
pemTypeCert = "CERTIFICATE"
pemTypeRSA = "RSA PRIVATE KEY"
pemTypeECDSA = "EC PRIVATE KEY"
pemTypeAny = "PRIVATE KEY"
)
// LoadKeyPair loads a certificate and private key, certdata and keydata can be a PEM encoded block or a file.
//
// If [keydata] is empty, then the private key is assumed to be contained in [certdata].
func LoadKeyPair(certdata, keydata string) (cert *x509.Certificate, key crypto.PrivateKey, err error) {
if keydata == "" {
keydata = certdata
}
if strings.Contains(certdata, "-----BEGIN "+pemTypeCert) {
log.Trace().Msg("parsing X.509 certificate")
if cert, err = decodePEMBCertificate([]byte(certdata)); err != nil {
return
}
} else {
log.Trace().Str("name", certdata).Msg("loading X.509 certificate")
if cert, err = LoadCertificate(certdata); err != nil {
return
}
}
if strings.Contains(keydata, pemTypeAny+"-----") {
log.Trace().Msg("parsing private key")
if key, err = decodePEMPrivateKey([]byte(keydata)); err != nil {
return
}
} else if key, err = LoadPrivateKey(keydata); err != nil {
log.Trace().Str("name", keydata).Msg("loading private key")
return
}
return
}
// SaveKeyPair saves a certificate and private key in PEM encoding.
//
// If [keyFile] is empty, then the private key is stored in [certFile] alongside the certificate.
//
// Attempts are made to use secure file modes for files that contains private keys.
func SaveKeyPair(cert *x509.Certificate, key crypto.PrivateKey, certFile, keyFile string) (err error) {
var (
keyDER []byte
keyPEMType = pemTypeAny
)
switch key := key.(type) {
case *ecdsa.PrivateKey:
if keyDER, err = x509.MarshalECPrivateKey(key); err != nil {
return
}
keyPEMType = pemTypeECDSA
case ed25519.PrivateKey:
if keyDER, err = x509.MarshalPKCS8PrivateKey(key); err != nil {
return
}
case *rsa.PrivateKey:
keyDER = x509.MarshalPKCS1PrivateKey(key)
keyPEMType = pemTypeRSA
default:
return fmt.Errorf("mitm: don't know how to marshal %T", key)
}
var certf, keyf *os.File
if certf, err = os.OpenFile(certFile, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0o644); err != nil {
return
}
defer func() { _ = certf.Close() }()
if filepath.Clean(certFile) == filepath.Clean(keyFile) || keyFile == "" {
if err = certf.Chmod(0o600); err != nil {
return
}
keyf, keyFile = certf, certFile
} else {
if keyf, err = os.OpenFile(keyFile, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0o600); err != nil {
return
}
defer func() { _ = keyf.Close() }()
}
log.Debug().Str("file", certFile).Msg("saving X.509 certificate")
if err = pem.Encode(certf, &pem.Block{Type: pemTypeCert, Bytes: cert.Raw}); err != nil {
return
}
log.Debug().Str("fiile", keyFile).Msg("saving private key")
if err = pem.Encode(keyf, &pem.Block{Type: keyPEMType, Bytes: keyDER}); err != nil {
return
}
return
}
// GenerateKeyPair generates a private key and self-signed certificate.
func GenerateKeyPair(name pkix.Name, days int, keyType string, keyBits int) (cert *x509.Certificate, key crypto.PrivateKey, err error) {
if key, err = GeneratePrivateKey(keyType, keyBits); err != nil {
return
}
if cert, err = GenerateCertificateAuthority(name, days, key); err != nil {
return
}
return
}
func GenerateCertificateAuthority(name pkix.Name, days int, key crypto.PrivateKey) (cert *x509.Certificate, err error) {
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return nil, fmt.Errorf("mtim: failed to generate serial number: %w", err)
}
keyUsage := x509.KeyUsageCertSign
if _, ok := key.(*rsa.PrivateKey); ok {
keyUsage |= x509.KeyUsageDigitalSignature
}
notBefore := roundToDay(time.Now())
notAfter := notBefore.Add(time.Duration(days) * 24 * time.Hour)
template := &x509.Certificate{
Subject: name,
SerialNumber: serialNumber,
KeyUsage: keyUsage,
SubjectKeyId: GenerateKeyID(key),
IsCA: true,
BasicConstraintsValid: true,
NotBefore: notBefore,
NotAfter: notAfter,
}
log.Info().
Str("name", name.CommonName).
Int("days", days).
Str("key", keyType(key)).
Str("serial", serialNumber.String()).
Msg("generating self-signed CA certificate")
var der []byte
if der, err = x509.CreateCertificate(rand.Reader, template, template, PublicKey(key), key); err != nil {
return
}
return x509.ParseCertificate(der)
}
func GeneratePrivateKey(kind string, bits int) (key crypto.PrivateKey, err error) {
switch strings.ToLower(kind) {
case TypeRSA, "":
if bits == 0 {
bits = 2048
}
log.Trace().Int("bits", bits).Str("type", TypeRSA).Msg("generating private key")
return rsa.GenerateKey(rand.Reader, bits)
case TypeECDSA, "ec", "ecc":
if bits == 0 {
bits = 256
}
var curve elliptic.Curve
switch bits {
case 224:
curve = elliptic.P224()
case 256:
curve = elliptic.P256()
case 384:
curve = elliptic.P384()
case 521:
curve = elliptic.P521()
default:
return nil, fmt.Errorf("mitm: elliptic curve %d bits not supported", bits)
}
log.Trace().Int("bits", bits).Str("type", TypeECDSA).Msg("generating private key")
return ecdsa.GenerateKey(curve, rand.Reader)
case TypeED25519:
log.Trace().Str("type", TypeED25519).Msg("generating ED25519 private key")
_, key, err = ed25519.GenerateKey(rand.Reader)
return
default:
return nil, fmt.Errorf("mitm: don't know how to generate %s private key", kind)
}
}
func decodePEMBCertificate(b []byte) (cert *x509.Certificate, err error) {
var (
rest = b
block *pem.Block
)
for {
if block, rest = pem.Decode(rest); block == nil {
return nil, errors.New("mitm: no CERTIFICATE PEM block could be decoded")
} else if block.Type == "CERTIFICATE" {
return x509.ParseCertificate(block.Bytes)
}
}
}
func LoadCertificate(name string) (*x509.Certificate, error) {
b, err := os.ReadFile(name)
if err != nil {
return nil, err
}
return decodePEMBCertificate(b)
}
func roundToDay(t time.Time) time.Time {
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC)
}