Add SSO login support
- Add [sso] config section with redirect_uri - Create mcdsl/sso client when SSO is configured - Add /login (landing page), /sso/redirect, /sso/callback routes - Add /logout route - Update login template with SSO landing page variant - Bump mcdsl to v1.6.0 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
276
vendor/git.wntrmute.dev/kyle/goutils/certlib/certgen/config.go
vendored
Normal file
276
vendor/git.wntrmute.dev/kyle/goutils/certlib/certgen/config.go
vendored
Normal file
@@ -0,0 +1,276 @@
|
||||
package certgen
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.wntrmute.dev/kyle/goutils/lib"
|
||||
)
|
||||
|
||||
type KeySpec struct {
|
||||
Algorithm string `yaml:"algorithm"`
|
||||
Size int `yaml:"size"`
|
||||
}
|
||||
|
||||
func (ks KeySpec) String() string {
|
||||
if strings.ToLower(ks.Algorithm) == nameEd25519 {
|
||||
return nameEd25519
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s-%d", ks.Algorithm, ks.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 nameEd25519:
|
||||
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 nameEd25519:
|
||||
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) Request(priv crypto.PrivateKey) (*x509.CertificateRequest, error) {
|
||||
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, fmt.Errorf("invalid IP address: %s", ip)
|
||||
}
|
||||
}
|
||||
|
||||
dnsNames := cs.Subject.DNSNames
|
||||
if isFQDN(cs.Subject.CommonName) && !slices.Contains(dnsNames, cs.Subject.CommonName) {
|
||||
dnsNames = append(dnsNames, cs.Subject.CommonName)
|
||||
}
|
||||
|
||||
req := &x509.CertificateRequest{
|
||||
PublicKeyAlgorithm: 0,
|
||||
PublicKey: getPublic(priv),
|
||||
Subject: subject,
|
||||
EmailAddresses: cs.Subject.Email,
|
||||
DNSNames: dnsNames,
|
||||
IPAddresses: ipAddresses,
|
||||
}
|
||||
|
||||
reqBytes, err := x509.CreateCertificateRequest(rand.Reader, req, priv)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create certificate request: %w", err)
|
||||
}
|
||||
|
||||
req, err = x509.ParseCertificateRequest(reqBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse certificate request: %w", err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (cs CertificateRequest) Generate() (crypto.PrivateKey, *x509.CertificateRequest, error) {
|
||||
_, priv, err := cs.KeySpec.Generate()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := cs.Request(priv)
|
||||
if err != nil {
|
||||
return nil, nil, 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"`
|
||||
OCSPServer []string `yaml:"ocsp_server,omitempty"`
|
||||
IssuingCertificateURL []string `yaml:"issuing_certificate_url,omitempty"`
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
for _, sku := range p.KeyUse {
|
||||
ku, ok := keyUsageStrings[sku]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid key usage: %s", p.KeyUse)
|
||||
}
|
||||
|
||||
certTemplate.KeyUsage |= ku
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if len(p.OCSPServer) > 0 {
|
||||
certTemplate.OCSPServer = p.OCSPServer
|
||||
}
|
||||
if len(p.IssuingCertificateURL) > 0 {
|
||||
certTemplate.IssuingCertificateURL = p.IssuingCertificateURL
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// isFQDN returns true if s looks like a fully-qualified domain name.
|
||||
func isFQDN(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
// Must contain at least one dot and no spaces.
|
||||
if !strings.Contains(s, ".") || strings.ContainsAny(s, " \t") {
|
||||
return false
|
||||
}
|
||||
// Each label must be non-empty and consist of letters, digits, or hyphens.
|
||||
for label := range strings.SplitSeq(strings.TrimSuffix(s, "."), ".") {
|
||||
if label == "" {
|
||||
return false
|
||||
}
|
||||
for _, c := range label {
|
||||
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if label[0] == '-' || label[len(label)-1] == '-' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
90
vendor/git.wntrmute.dev/kyle/goutils/certlib/certgen/keygen.go
vendored
Normal file
90
vendor/git.wntrmute.dev/kyle/goutils/certlib/certgen/keygen.go
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
package certgen
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// var (
|
||||
// oidEd25519 = asn1.ObjectIdentifier{1, 3, 101, 110}
|
||||
//)
|
||||
|
||||
const (
|
||||
nameEd25519 = "ed25519"
|
||||
)
|
||||
|
||||
func GenerateKey(algorithm x509.PublicKeyAlgorithm, bitSize int) (crypto.PublicKey, crypto.PrivateKey, error) {
|
||||
var key crypto.PrivateKey
|
||||
var pub crypto.PublicKey
|
||||
var err error
|
||||
|
||||
switch algorithm {
|
||||
case x509.Ed25519:
|
||||
pub, key, err = ed25519.GenerateKey(rand.Reader)
|
||||
case x509.RSA:
|
||||
key, err = rsa.GenerateKey(rand.Reader, bitSize)
|
||||
if err == nil {
|
||||
rsaPriv, ok := key.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
panic("failed to cast RSA private key to *rsa.PrivateKey")
|
||||
}
|
||||
|
||||
pub = rsaPriv.Public()
|
||||
}
|
||||
case x509.ECDSA:
|
||||
var curve elliptic.Curve
|
||||
|
||||
switch bitSize {
|
||||
case 256:
|
||||
curve = elliptic.P256()
|
||||
case 384:
|
||||
curve = elliptic.P384()
|
||||
case 521:
|
||||
curve = elliptic.P521()
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("unsupported curve size %d", bitSize)
|
||||
}
|
||||
|
||||
key, err = ecdsa.GenerateKey(curve, rand.Reader)
|
||||
if err == nil {
|
||||
ecPriv, ok := key.(*ecdsa.PrivateKey)
|
||||
if !ok {
|
||||
panic("failed to cast ECDSA private key to *ecdsa.PrivateKey")
|
||||
}
|
||||
|
||||
pub = ecPriv.Public()
|
||||
}
|
||||
case x509.DSA:
|
||||
fallthrough
|
||||
case x509.UnknownPublicKeyAlgorithm:
|
||||
fallthrough
|
||||
default:
|
||||
err = errors.New("unsupported algorithm")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return pub, key, nil
|
||||
}
|
||||
|
||||
func getPublic(priv crypto.PrivateKey) crypto.PublicKey {
|
||||
switch priv := priv.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
return &priv.PublicKey
|
||||
case *ecdsa.PrivateKey:
|
||||
return &priv.PublicKey
|
||||
case *ed25519.PrivateKey:
|
||||
return priv.Public()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
32
vendor/git.wntrmute.dev/kyle/goutils/certlib/certgen/ku.go
vendored
Normal file
32
vendor/git.wntrmute.dev/kyle/goutils/certlib/certgen/ku.go
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
package certgen
|
||||
|
||||
import "crypto/x509"
|
||||
|
||||
var keyUsageStrings = map[string]x509.KeyUsage{
|
||||
"signing": x509.KeyUsageDigitalSignature,
|
||||
"digital signature": x509.KeyUsageDigitalSignature,
|
||||
"content commitment": x509.KeyUsageContentCommitment,
|
||||
"key encipherment": x509.KeyUsageKeyEncipherment,
|
||||
"key agreement": x509.KeyUsageKeyAgreement,
|
||||
"data encipherment": x509.KeyUsageDataEncipherment,
|
||||
"cert sign": x509.KeyUsageCertSign,
|
||||
"crl sign": x509.KeyUsageCRLSign,
|
||||
"encipher only": x509.KeyUsageEncipherOnly,
|
||||
"decipher only": x509.KeyUsageDecipherOnly,
|
||||
}
|
||||
|
||||
var extKeyUsageStrings = map[string]x509.ExtKeyUsage{
|
||||
"any": x509.ExtKeyUsageAny,
|
||||
"server auth": x509.ExtKeyUsageServerAuth,
|
||||
"client auth": x509.ExtKeyUsageClientAuth,
|
||||
"code signing": x509.ExtKeyUsageCodeSigning,
|
||||
"email protection": x509.ExtKeyUsageEmailProtection,
|
||||
"s/mime": x509.ExtKeyUsageEmailProtection,
|
||||
"ipsec end system": x509.ExtKeyUsageIPSECEndSystem,
|
||||
"ipsec tunnel": x509.ExtKeyUsageIPSECTunnel,
|
||||
"ipsec user": x509.ExtKeyUsageIPSECUser,
|
||||
"timestamping": x509.ExtKeyUsageTimeStamping,
|
||||
"ocsp signing": x509.ExtKeyUsageOCSPSigning,
|
||||
"microsoft sgc": x509.ExtKeyUsageMicrosoftServerGatedCrypto,
|
||||
"netscape sgc": x509.ExtKeyUsageNetscapeServerGatedCrypto,
|
||||
}
|
||||
Reference in New Issue
Block a user