diff options
| author | Adnan Maolood <[email protected]> | 2021-01-14 20:42:12 -0500 |
|---|---|---|
| committer | Adnan Maolood <[email protected]> | 2021-01-14 20:42:12 -0500 |
| commit | 14d89f304ad022bb61158d498869d63bc5109e7a (patch) | |
| tree | 3af9d2f1c08a4053da94e23181a2d792e04af0f0 /certificate | |
| parent | tofu: Fix example (diff) | |
| download | go-gemini-14d89f304ad022bb61158d498869d63bc5109e7a.tar.xz go-gemini-14d89f304ad022bb61158d498869d63bc5109e7a.zip | |
Move cert.go to a subpackage
Diffstat (limited to 'certificate')
| -rw-r--r-- | certificate/certificate.go | 226 |
1 files changed, 226 insertions, 0 deletions
diff --git a/certificate/certificate.go b/certificate/certificate.go new file mode 100644 index 0000000..185ac4d --- /dev/null +++ b/certificate/certificate.go @@ -0,0 +1,226 @@ +// Package certificate provides utility functions for TLS certificates. +package certificate + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "net" + "os" + "path/filepath" + "strings" + "sync" + "time" +) + +// Dir represents a directory of certificates. +// The zero value of Dir is an empty directory ready to use. +// +// Dir is safe for concurrent use by multiple goroutines. +type Dir struct { + certs map[string]tls.Certificate + path *string + mu sync.RWMutex +} + +// Add adds a certificate for the given scope to the directory. +// It tries to parse the certificate if it is not already parsed. +func (d *Dir) Add(scope string, cert tls.Certificate) error { + d.mu.Lock() + defer d.mu.Unlock() + if d.certs == nil { + d.certs = map[string]tls.Certificate{} + } + // Parse certificate if not already parsed + if cert.Leaf == nil { + parsed, err := x509.ParseCertificate(cert.Certificate[0]) + if err == nil { + cert.Leaf = parsed + } + } + + if d.path != nil { + // Escape slash character + scope = strings.ReplaceAll(scope, "/", ":") + certPath := filepath.Join(*d.path, scope+".crt") + keyPath := filepath.Join(*d.path, scope+".key") + if err := Write(cert, certPath, keyPath); err != nil { + return err + } + } + + d.certs[scope] = cert + return nil +} + +// Lookup returns the certificate for the provided scope. +func (d *Dir) Lookup(scope string) (tls.Certificate, bool) { + d.mu.RLock() + defer d.mu.RUnlock() + cert, ok := d.certs[scope] + return cert, ok +} + +// Load loads certificates from the given path. +// The path should lead to a directory containing certificates and private keys +// in the form scope.crt and scope.key. +// For example, the hostname "localhost" would have the corresponding files +// localhost.crt (certificate) and localhost.key (private key). +// New certificates will be written to this directory. +func (d *Dir) Load(path string) error { + matches, err := filepath.Glob(filepath.Join(path, "*.crt")) + if err != nil { + return err + } + for _, crtPath := range matches { + keyPath := strings.TrimSuffix(crtPath, ".crt") + ".key" + cert, err := tls.LoadX509KeyPair(crtPath, keyPath) + if err != nil { + continue + } + scope := strings.TrimSuffix(filepath.Base(crtPath), ".crt") + // Unescape slash character + scope = strings.ReplaceAll(scope, ":", "/") + d.Add(scope, cert) + } + d.SetPath(path) + return nil +} + +// SetPath sets the directory that new certificates will be written to. +func (d *Dir) SetPath(path string) { + d.mu.Lock() + defer d.mu.Unlock() + d.path = &path +} + +// CreateOptions configures the creation of a TLS certificate. +type CreateOptions struct { + // Subject Alternate Name values. + // Should contain the DNS names that this certificate is valid for. + // E.g. example.com, *.example.com + DNSNames []string + + // Subject Alternate Name values. + // Should contain the IP addresses that the certificate is valid for. + IPAddresses []net.IP + + // Subject specifies the certificate Subject. + // + // Subject.CommonName can contain the DNS name that this certificate + // is valid for. Server certificates should specify both a Subject + // and a Subject Alternate Name. + Subject pkix.Name + + // Duration specifies the amount of time that the certificate is valid for. + Duration time.Duration + + // Ed25519 specifies whether to generate an Ed25519 key pair. + // If false, an ECDSA key will be generated instead. + // Ed25519 is not as widely supported as ECDSA. + Ed25519 bool +} + +// Create creates a new TLS certificate. +func Create(options CreateOptions) (tls.Certificate, error) { + crt, priv, err := newX509KeyPair(options) + if err != nil { + return tls.Certificate{}, err + } + var cert tls.Certificate + cert.Leaf = crt + cert.Certificate = append(cert.Certificate, crt.Raw) + cert.PrivateKey = priv + return cert, nil +} + +// newX509KeyPair creates and returns a new certificate and private key. +func newX509KeyPair(options CreateOptions) (*x509.Certificate, crypto.PrivateKey, error) { + var pub crypto.PublicKey + var priv crypto.PrivateKey + if options.Ed25519 { + // Generate an Ed25519 private key + var err error + pub, priv, err = ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, nil, err + } + } else { + // Generate an ECDSA private key + private, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, err + } + priv = private + pub = &private.PublicKey + } + + // ECDSA and Ed25519 keys should have the DigitalSignature KeyUsage bits + // set in the x509.Certificate template + keyUsage := x509.KeyUsageDigitalSignature + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, nil, err + } + + notBefore := time.Now() + notAfter := notBefore.Add(options.Duration) + + template := x509.Certificate{ + SerialNumber: serialNumber, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: keyUsage, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + IPAddresses: options.IPAddresses, + DNSNames: options.DNSNames, + Subject: options.Subject, + } + + crt, err := x509.CreateCertificate(rand.Reader, &template, &template, pub, priv) + if err != nil { + return nil, nil, err + } + cert, err := x509.ParseCertificate(crt) + if err != nil { + return nil, nil, err + } + return cert, priv, nil +} + +// Write writes the provided certificate and its private key +// to certPath and keyPath respectively. +func Write(cert tls.Certificate, certPath, keyPath string) error { + certOut, err := os.OpenFile(certPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer certOut.Close() + if err := pem.Encode(certOut, &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Leaf.Raw, + }); err != nil { + return err + } + + keyOut, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer keyOut.Close() + privBytes, err := x509.MarshalPKCS8PrivateKey(cert.PrivateKey) + if err != nil { + return err + } + return pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}) +} |