207 lines
5.1 KiB
Go
207 lines
5.1 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/pem"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"math/big"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"shamilnunhuck/saml-oidc-bridge/internal/config"
|
|
)
|
|
|
|
type rotateOpts struct {
|
|
ConfigPath string
|
|
ID string
|
|
Algo string
|
|
Days int
|
|
CN string
|
|
Org string
|
|
OutK8s string
|
|
ActiveOnly bool
|
|
}
|
|
|
|
func RunCert(args []string) error {
|
|
fs := flag.NewFlagSet("cert", flag.ContinueOnError)
|
|
var ro rotateOpts
|
|
fs.StringVar(&ro.ConfigPath, "config", "example.config.yaml", "path to config yaml")
|
|
fs.StringVar(&ro.ID, "id", "", "key id (e.g. k-2025-10)")
|
|
fs.StringVar(&ro.Algo, "algo", "rsa3072", "rsa2048|rsa3072|rsa4096|p256|p384")
|
|
fs.IntVar(&ro.Days, "days", 825, "validity in days")
|
|
fs.StringVar(&ro.CN, "cn", "id.example.com", "certificate CN")
|
|
fs.StringVar(&ro.Org, "org", "YourOrg", "certificate O")
|
|
fs.StringVar(&ro.OutK8s, "k8s-secret-out", "", "write a Kubernetes Secret manifest to this path")
|
|
fs.BoolVar(&ro.ActiveOnly, "active-only", false, "only set active_key to -id (no new cert)")
|
|
if err := fs.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
if ro.ID == "" {
|
|
return errors.New("missing -id")
|
|
}
|
|
|
|
cfg, raw, err := loadConfig(ro.ConfigPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if ro.ActiveOnly {
|
|
cfg.Crypto.ActiveKey = ro.ID
|
|
return saveConfig(ro.ConfigPath, raw, cfg)
|
|
}
|
|
|
|
certPEM, keyPEM, notAfter, err := genSelfSigned(ro)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cfg.Crypto.Keys = append(cfg.Crypto.Keys, config.KeyPair{
|
|
ID: ro.ID,
|
|
CertPEM: string(certPEM),
|
|
KeyPEM: string(keyPEM),
|
|
NotAfter: notAfter.UTC(),
|
|
})
|
|
cfg.Crypto.ActiveKey = ro.ID
|
|
|
|
if err := saveConfig(ro.ConfigPath, raw, cfg); err != nil {
|
|
return err
|
|
}
|
|
if ro.OutK8s != "" {
|
|
if err := os.WriteFile(ro.OutK8s, []byte(k8sSecretYAML(ro, certPEM, keyPEM)), 0o600); err != nil {
|
|
return fmt.Errorf("write secret: %w", err)
|
|
}
|
|
}
|
|
fmt.Printf("OK: generated %s (algo=%s, not_after=%s) and set active_key\n", ro.ID, ro.Algo, notAfter.UTC().Format(time.RFC3339))
|
|
return nil
|
|
}
|
|
|
|
func genSelfSigned(ro rotateOpts) (certPEM, keyPEM []byte, notAfter time.Time, err error) {
|
|
nb := time.Now().Add(-5 * time.Minute)
|
|
na := nb.Add(time.Duration(ro.Days) * 24 * time.Hour)
|
|
|
|
serial, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
|
template := x509.Certificate{
|
|
SerialNumber: serial,
|
|
Subject: pkix.Name{
|
|
CommonName: ro.CN,
|
|
Organization: []string{ro.Org},
|
|
OrganizationalUnit: []string{"SAML Signing"},
|
|
},
|
|
NotBefore: nb,
|
|
NotAfter: na,
|
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
|
BasicConstraintsValid: true,
|
|
}
|
|
|
|
var der []byte
|
|
var keyPKCS8 []byte
|
|
|
|
switch strings.ToLower(ro.Algo) {
|
|
case "rsa2048", "rsa3072", "rsa4096":
|
|
bits := 2048
|
|
if ro.Algo == "rsa3072" {
|
|
bits = 3072
|
|
}
|
|
if ro.Algo == "rsa4096" {
|
|
bits = 4096
|
|
}
|
|
priv, e := rsa.GenerateKey(rand.Reader, bits)
|
|
if e != nil {
|
|
return nil, nil, time.Time{}, e
|
|
}
|
|
der, err = x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
|
if err != nil {
|
|
return nil, nil, time.Time{}, err
|
|
}
|
|
keyPKCS8, err = x509.MarshalPKCS8PrivateKey(priv)
|
|
if err != nil {
|
|
return nil, nil, time.Time{}, err
|
|
}
|
|
case "p256", "p384":
|
|
curve := elliptic.P256()
|
|
if ro.Algo == "p384" {
|
|
curve = elliptic.P384()
|
|
}
|
|
priv, e := ecdsa.GenerateKey(curve, rand.Reader)
|
|
if e != nil {
|
|
return nil, nil, time.Time{}, e
|
|
}
|
|
der, err = x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
|
if err != nil {
|
|
return nil, nil, time.Time{}, err
|
|
}
|
|
keyPKCS8, err = x509.MarshalPKCS8PrivateKey(priv)
|
|
if err != nil {
|
|
return nil, nil, time.Time{}, err
|
|
}
|
|
default:
|
|
return nil, nil, time.Time{}, fmt.Errorf("unknown -algo %q", ro.Algo)
|
|
}
|
|
|
|
certPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
|
|
keyPEM = pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyPKCS8})
|
|
return certPEM, keyPEM, na, nil
|
|
}
|
|
|
|
func loadConfig(path string) (*config.Config, *yaml.Node, error) {
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
var root yaml.Node
|
|
if err := yaml.Unmarshal(b, &root); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
var c config.Config
|
|
if err := root.Decode(&c); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return &c, &root, nil
|
|
}
|
|
|
|
func saveConfig(path string, _ *yaml.Node, c *config.Config) error {
|
|
var buf bytes.Buffer
|
|
enc := yaml.NewEncoder(&buf)
|
|
enc.SetIndent(2)
|
|
if err := enc.Encode(c); err != nil {
|
|
return err
|
|
}
|
|
_ = enc.Close()
|
|
return os.WriteFile(path, buf.Bytes(), 0o644)
|
|
}
|
|
|
|
func k8sSecretYAML(ro rotateOpts, certPEM, keyPEM []byte) string {
|
|
name := strings.ToLower(strings.ReplaceAll(ro.ID, "_", "-"))
|
|
return fmt.Sprintf(`apiVersion: v1
|
|
kind: Secret
|
|
metadata:
|
|
name: saml-signing-%s
|
|
type: Opaque
|
|
stringData:
|
|
cert.pem: |-
|
|
%s
|
|
key.pem: |-
|
|
%s
|
|
`, name, indent(string(certPEM), 4), indent(string(keyPEM), 4))
|
|
}
|
|
|
|
func indent(s string, n int) string {
|
|
pad := strings.Repeat(" ", n)
|
|
lines := strings.Split(strings.TrimRight(s, "\n"), "\n")
|
|
for i := range lines {
|
|
lines[i] = pad + lines[i]
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|