Initial import
This commit is contained in:
114
internal/cryptutil/key.go
Normal file
114
internal/cryptutil/key.go
Normal 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
242
internal/cryptutil/x509.go
Normal 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)
|
||||
}
|
Reference in New Issue
Block a user