163 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			Go
		
	
	
	
		
		
			
		
	
	
			163 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			Go
		
	
	
	
| 
								 | 
							
								/*
							 | 
						||
| 
								 | 
							
								Package ca implements a disk file-backed certificate authority with a built-in
							 | 
						||
| 
								 | 
							
								CRL signer. The certificate authority can issue new certificates (including the
							 | 
						||
| 
								 | 
							
								creation of intermediate CAs) and revoke existing certificates, resulting in a
							 | 
						||
| 
								 | 
							
								fresh CRL.
							 | 
						||
| 
								 | 
							
								*/
							 | 
						||
| 
								 | 
							
								package ca
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								import (
							 | 
						||
| 
								 | 
							
									"crypto/rand"
							 | 
						||
| 
								 | 
							
									"crypto/rsa"
							 | 
						||
| 
								 | 
							
									"crypto/x509"
							 | 
						||
| 
								 | 
							
									"errors"
							 | 
						||
| 
								 | 
							
									"fmt"
							 | 
						||
| 
								 | 
							
									"io/fs"
							 | 
						||
| 
								 | 
							
									"math/big"
							 | 
						||
| 
								 | 
							
									"os"
							 | 
						||
| 
								 | 
							
									"path/filepath"
							 | 
						||
| 
								 | 
							
									"time"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									"src.lwithers.me.uk/go/rsa/pkg/pemfile"
							 | 
						||
| 
								 | 
							
									"src.lwithers.me.uk/go/rsa/pkg/serial"
							 | 
						||
| 
								 | 
							
								)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const (
							 | 
						||
| 
								 | 
							
									crlSignerBits = 3072
							 | 
						||
| 
								 | 
							
								)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								func Create(dir string, template *x509.Certificate, key *rsa.PrivateKey) (*CA, error) {
							 | 
						||
| 
								 | 
							
									return createCA(dir, template, template, key, key)
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								func (ca *CA) CreateIntermediate(dir string, template *x509.Certificate, key *rsa.PrivateKey) (*CA, error) {
							 | 
						||
| 
								 | 
							
									return createCA(dir, template, ca.root, key, ca.key)
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								// createCA holds the shared code between Create and CreateIntermediate. All
							 | 
						||
| 
								 | 
							
								// that really differs between those operations is the signing key and whether
							 | 
						||
| 
								 | 
							
								// or not there is a parent certificate.
							 | 
						||
| 
								 | 
							
								func createCA(dir string, template, parent *x509.Certificate, newRootKey, signingKey *rsa.PrivateKey) (*CA, error) {
							 | 
						||
| 
								 | 
							
									// template must have at least the common name set
							 | 
						||
| 
								 | 
							
									if template.Subject.CommonName == "" {
							 | 
						||
| 
								 | 
							
										return nil, errors.New("template CA certificate must have common name set")
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// directory must not exist
							 | 
						||
| 
								 | 
							
									if _, err := os.Lstat(dir); !errors.Is(err, fs.ErrNotExist) {
							 | 
						||
| 
								 | 
							
										return nil, &fs.PathError{
							 | 
						||
| 
								 | 
							
											Op:   "mkdir",
							 | 
						||
| 
								 | 
							
											Path: dir,
							 | 
						||
| 
								 | 
							
											Err:  fs.ErrExist,
							 | 
						||
| 
								 | 
							
										}
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// create the directory
							 | 
						||
| 
								 | 
							
									if err := os.Mkdir(dir, 0700); err != nil {
							 | 
						||
| 
								 | 
							
										return nil, err
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// write out the private key
							 | 
						||
| 
								 | 
							
									der, err := x509.MarshalPKCS8PrivateKey(newRootKey)
							 | 
						||
| 
								 | 
							
									if err != nil {
							 | 
						||
| 
								 | 
							
										return nil, fmt.Errorf("failed to marshal private key: %w", err)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
									if err = pemfile.Write(filepath.Join(dir, rootKeyFilename), pemfile.TypePKCS8PrivateKey, der); err != nil {
							 | 
						||
| 
								 | 
							
										return nil, err
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// ensure the template is valid for a root CA
							 | 
						||
| 
								 | 
							
									template.BasicConstraintsValid = true
							 | 
						||
| 
								 | 
							
									template.IsCA = true
							 | 
						||
| 
								 | 
							
									template.KeyUsage |= x509.KeyUsageCertSign
							 | 
						||
| 
								 | 
							
									if template.SerialNumber == nil {
							 | 
						||
| 
								 | 
							
										template.SerialNumber = serial.Rand()
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
									if template.NotBefore.IsZero() {
							 | 
						||
| 
								 | 
							
										template.NotBefore = time.Now().Add(-2 * time.Hour)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
									if template.NotAfter.IsZero() {
							 | 
						||
| 
								 | 
							
										template.NotAfter = time.Now().Add(24 * 365 * 40 * time.Hour)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// create the self-signed certificate; ensure we can parse it, and write
							 | 
						||
| 
								 | 
							
									// it to disk
							 | 
						||
| 
								 | 
							
									der, err = x509.CreateCertificate(rand.Reader, template, parent, &newRootKey.PublicKey, signingKey)
							 | 
						||
| 
								 | 
							
									if err != nil {
							 | 
						||
| 
								 | 
							
										return nil, fmt.Errorf("failed to create/sign new root CA certificate: %w", err)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
									root, err := x509.ParseCertificate(der)
							 | 
						||
| 
								 | 
							
									if err != nil {
							 | 
						||
| 
								 | 
							
										return nil, fmt.Errorf("failed to parse just-signed root CA certificate: %w", err)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
									if err = pemfile.Write(filepath.Join(dir, rootCertFilename), pemfile.TypeX509Certificate, der); err != nil {
							 | 
						||
| 
								 | 
							
										return nil, err
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// now create a CRL signer key and write it to disk
							 | 
						||
| 
								 | 
							
									crlKey, err := rsa.GenerateKey(rand.Reader, crlSignerBits)
							 | 
						||
| 
								 | 
							
									if err != nil {
							 | 
						||
| 
								 | 
							
										return nil, fmt.Errorf("failed to generate new CRL signer key: %w", err)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
									der, err = x509.MarshalPKCS8PrivateKey(crlKey)
							 | 
						||
| 
								 | 
							
									if err != nil {
							 | 
						||
| 
								 | 
							
										return nil, fmt.Errorf("failed to marshal CRL signer private key: %w", err)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
									if err = pemfile.Write(filepath.Join(dir, crlKeyFilename), pemfile.TypePKCS8PrivateKey, der); err != nil {
							 | 
						||
| 
								 | 
							
										return nil, err
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// create a template for a CRL signing certificate
							 | 
						||
| 
								 | 
							
									crlTemplate := &x509.Certificate{
							 | 
						||
| 
								 | 
							
										SerialNumber:          serial.Rand(),
							 | 
						||
| 
								 | 
							
										Subject:               template.Subject,
							 | 
						||
| 
								 | 
							
										SubjectKeyId:          ComputeSubjectKeyId(&crlKey.PublicKey),
							 | 
						||
| 
								 | 
							
										NotBefore:             template.NotBefore,
							 | 
						||
| 
								 | 
							
										NotAfter:              template.NotAfter,
							 | 
						||
| 
								 | 
							
										KeyUsage:              x509.KeyUsageCRLSign,
							 | 
						||
| 
								 | 
							
										BasicConstraintsValid: true,
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
									crlTemplate.Subject.CommonName += " CRL signer"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// create the CRL signing certificate, parse it, and write it to disk
							 | 
						||
| 
								 | 
							
									der, err = x509.CreateCertificate(rand.Reader, crlTemplate, root, &crlKey.PublicKey, newRootKey)
							 | 
						||
| 
								 | 
							
									if err != nil {
							 | 
						||
| 
								 | 
							
										return nil, fmt.Errorf("failed to sign CRL signer certificate: %w", err)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
									crlCert, err := x509.ParseCertificate(der)
							 | 
						||
| 
								 | 
							
									if err != nil {
							 | 
						||
| 
								 | 
							
										return nil, fmt.Errorf("failed to parse just-signed CRL signer certificate: %w", err)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
									if err = pemfile.Write(filepath.Join(dir, crlCertFilename), pemfile.TypeX509Certificate, der); err != nil {
							 | 
						||
| 
								 | 
							
										return nil, err
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// finally, create an empty CRL
							 | 
						||
| 
								 | 
							
									crl := &x509.RevocationList{
							 | 
						||
| 
								 | 
							
										Number:     big.NewInt(1),
							 | 
						||
| 
								 | 
							
										ThisUpdate: template.NotBefore,
							 | 
						||
| 
								 | 
							
										NextUpdate: template.NotAfter,
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
									der, err = x509.CreateRevocationList(rand.Reader, crl, crlCert, crlKey)
							 | 
						||
| 
								 | 
							
									if err != nil {
							 | 
						||
| 
								 | 
							
										return nil, fmt.Errorf("failed to create empty CRL: %w", err)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
									crl, err = x509.ParseRevocationList(der)
							 | 
						||
| 
								 | 
							
									if err != nil {
							 | 
						||
| 
								 | 
							
										return nil, fmt.Errorf("failed to parse just-created CRL: %w", err)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
									if err = pemfile.Write(filepath.Join(dir, crlFilename), pemfile.TypeX509CRL, der); err != nil {
							 | 
						||
| 
								 | 
							
										return nil, err
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// all done
							 | 
						||
| 
								 | 
							
									return &CA{
							 | 
						||
| 
								 | 
							
										dir:     dir,
							 | 
						||
| 
								 | 
							
										key:     newRootKey,
							 | 
						||
| 
								 | 
							
										root:    root,
							 | 
						||
| 
								 | 
							
										crl:     crl,
							 | 
						||
| 
								 | 
							
										crlKey:  crlKey,
							 | 
						||
| 
								 | 
							
										crlCert: crlCert,
							 | 
						||
| 
								 | 
							
									}, nil
							 | 
						||
| 
								 | 
							
								}
							 |