216 lines
6.0 KiB
Go
216 lines
6.0 KiB
Go
package certgen
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"fmt"
|
|
"math/big"
|
|
"net"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.wntrmute.dev/kyle/goutils/lib"
|
|
)
|
|
|
|
type KeySpec struct {
|
|
Algorithm string `yaml:"algorithm"`
|
|
Size int `yaml:"size"`
|
|
}
|
|
|
|
func (ks KeySpec) Generate() (crypto.PublicKey, crypto.PrivateKey, error) {
|
|
switch strings.ToLower(ks.Algorithm) {
|
|
case "rsa":
|
|
return GenerateKey(x509.RSA, ks.Size)
|
|
case "ecdsa":
|
|
return GenerateKey(x509.ECDSA, ks.Size)
|
|
case "ed25519":
|
|
return GenerateKey(x509.Ed25519, 0)
|
|
default:
|
|
return nil, nil, fmt.Errorf("unknown key algorithm: %s", ks.Algorithm)
|
|
}
|
|
}
|
|
|
|
func (ks KeySpec) SigningAlgorithm() (x509.SignatureAlgorithm, error) {
|
|
switch strings.ToLower(ks.Algorithm) {
|
|
case "rsa":
|
|
return x509.SHA512WithRSAPSS, nil
|
|
case "ecdsa":
|
|
return x509.ECDSAWithSHA512, nil
|
|
case "ed25519":
|
|
return x509.PureEd25519, nil
|
|
default:
|
|
return 0, fmt.Errorf("unknown key algorithm: %s", ks.Algorithm)
|
|
}
|
|
}
|
|
|
|
type Subject struct {
|
|
CommonName string `yaml:"common_name"`
|
|
Country string `yaml:"country"`
|
|
Locality string `yaml:"locality"`
|
|
Province string `yaml:"province"`
|
|
Organization string `yaml:"organization"`
|
|
OrganizationalUnit string `yaml:"organizational_unit"`
|
|
Email string `yaml:"email"`
|
|
DNSNames []string `yaml:"dns"`
|
|
IPAddresses []string `yaml:"ips"`
|
|
}
|
|
|
|
type CertificateRequest struct {
|
|
KeySpec KeySpec `yaml:"key"`
|
|
Subject Subject `yaml:"subject"`
|
|
Profile Profile `yaml:"profile"`
|
|
}
|
|
|
|
func (cs CertificateRequest) Generate() (crypto.PrivateKey, *x509.CertificateRequest, error) {
|
|
pub, priv, err := cs.KeySpec.Generate()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
subject := pkix.Name{}
|
|
subject.CommonName = cs.Subject.CommonName
|
|
subject.Country = []string{cs.Subject.Country}
|
|
subject.Locality = []string{cs.Subject.Locality}
|
|
subject.Province = []string{cs.Subject.Province}
|
|
subject.Organization = []string{cs.Subject.Organization}
|
|
subject.OrganizationalUnit = []string{cs.Subject.OrganizationalUnit}
|
|
|
|
ipAddresses := make([]net.IP, 0, len(cs.Subject.IPAddresses))
|
|
for i, ip := range cs.Subject.IPAddresses {
|
|
ipAddresses = append(ipAddresses, net.ParseIP(ip))
|
|
if ipAddresses[i] == nil {
|
|
return nil, nil, fmt.Errorf("invalid IP address: %s", ip)
|
|
}
|
|
}
|
|
|
|
req := &x509.CertificateRequest{
|
|
PublicKeyAlgorithm: 0,
|
|
PublicKey: pub,
|
|
Subject: subject,
|
|
DNSNames: cs.Subject.DNSNames,
|
|
IPAddresses: ipAddresses,
|
|
}
|
|
|
|
reqBytes, err := x509.CreateCertificateRequest(rand.Reader, req, priv)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to create certificate request: %w", err)
|
|
}
|
|
|
|
req, err = x509.ParseCertificateRequest(reqBytes)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to parse certificate request: %w", err)
|
|
}
|
|
|
|
return priv, req, nil
|
|
}
|
|
|
|
type Profile struct {
|
|
IsCA bool `yaml:"is_ca"`
|
|
PathLen int `yaml:"path_len"`
|
|
KeyUse string `yaml:"key_uses"`
|
|
ExtKeyUsages []string `yaml:"ext_key_usages"`
|
|
Expiry string `yaml:"expiry"`
|
|
}
|
|
|
|
func (p Profile) templateFromRequest(req *x509.CertificateRequest) (*x509.Certificate, error) {
|
|
serial, err := SerialNumber()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate serial number: %w", err)
|
|
}
|
|
|
|
expiry, err := lib.ParseDuration(p.Expiry)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing expiry: %w", err)
|
|
}
|
|
|
|
certTemplate := &x509.Certificate{
|
|
SignatureAlgorithm: req.SignatureAlgorithm,
|
|
PublicKeyAlgorithm: req.PublicKeyAlgorithm,
|
|
PublicKey: req.PublicKey,
|
|
SerialNumber: serial,
|
|
Subject: req.Subject,
|
|
NotBefore: time.Now().Add(-1 * time.Hour),
|
|
NotAfter: time.Now().Add(expiry),
|
|
BasicConstraintsValid: true,
|
|
IsCA: p.IsCA,
|
|
MaxPathLen: p.PathLen,
|
|
DNSNames: req.DNSNames,
|
|
IPAddresses: req.IPAddresses,
|
|
}
|
|
|
|
var ok bool
|
|
certTemplate.KeyUsage, ok = keyUsageStrings[p.KeyUse]
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid key usage: %s", p.KeyUse)
|
|
}
|
|
|
|
var eku x509.ExtKeyUsage
|
|
for _, extKeyUsage := range p.ExtKeyUsages {
|
|
eku, ok = extKeyUsageStrings[extKeyUsage]
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid extended key usage: %s", extKeyUsage)
|
|
}
|
|
certTemplate.ExtKeyUsage = append(certTemplate.ExtKeyUsage, eku)
|
|
}
|
|
|
|
return certTemplate, nil
|
|
}
|
|
|
|
func (p Profile) SignRequest(
|
|
parent *x509.Certificate,
|
|
req *x509.CertificateRequest,
|
|
priv crypto.PrivateKey,
|
|
) (*x509.Certificate, error) {
|
|
tpl, err := p.templateFromRequest(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create certificate template: %w", err)
|
|
}
|
|
|
|
certBytes, err := x509.CreateCertificate(rand.Reader, tpl, parent, req.PublicKey, priv)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create certificate: %w", err)
|
|
}
|
|
|
|
cert, err := x509.ParseCertificate(certBytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse certificate: %w", err)
|
|
}
|
|
|
|
return cert, nil
|
|
}
|
|
|
|
func (p Profile) SelfSign(req *x509.CertificateRequest, priv crypto.PrivateKey) (*x509.Certificate, error) {
|
|
certTemplate, err := p.templateFromRequest(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create certificate template: %w", err)
|
|
}
|
|
|
|
return p.SignRequest(certTemplate, req, priv)
|
|
}
|
|
|
|
func SerialNumber() (*big.Int, error) {
|
|
serialNumberBytes := make([]byte, 20)
|
|
_, err := rand.Read(serialNumberBytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate serial number: %w", err)
|
|
}
|
|
return new(big.Int).SetBytes(serialNumberBytes), nil
|
|
}
|
|
|
|
// GenerateSelfSigned generates a self-signed certificate using the given certificate request.
|
|
func GenerateSelfSigned(creq *CertificateRequest) (*x509.Certificate, crypto.PrivateKey, error) {
|
|
priv, req, err := creq.Generate()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to generate certificate request: %w", err)
|
|
}
|
|
|
|
cert, err := creq.Profile.SelfSign(req, priv)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to self-sign certificate: %w", err)
|
|
}
|
|
|
|
return cert, priv, nil
|
|
}
|