package station import ( "crypto" "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "encoding/pem" "errors" "fmt" "io/fs" "os" "path/filepath" ) const ( caFileName = "ca.pem" keyFileName = "key.pem" csrFileName = "csr.pem" certFileName = "cert.pem" ) var ( oidBasicConstraints = asn1.ObjectIdentifier{2, 5, 29, 19} oidKeyUsage = asn1.ObjectIdentifier{2, 5, 29, 15} oidExtKeyUsage = asn1.ObjectIdentifier{2, 5, 29, 37} oidClientAuth = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 2} ) func ensureCertDir(path string) error { err := os.Mkdir(path, 0o700) if err != nil { if !errors.Is(err, fs.ErrExist) { return err } info, statErr := os.Stat(path) if statErr != nil { return statErr } if !info.IsDir() { return err } return nil } return os.Chmod(path, 0o700) } func caDigest(dir string) (sum [32]byte, ok bool, err error) { body, err := os.ReadFile(filepath.Join(dir, caFileName)) if errors.Is(err, fs.ErrNotExist) { return sum, false, nil } if err != nil { return sum, false, err } return sha256.Sum256(body), true, nil } func verifyCABundle(caPEM string, sig []byte, certPEM string) error { block, _ := pem.Decode([]byte(certPEM)) if block == nil { return errors.New("signing certificate") } cert, err := x509.ParseCertificate(block.Bytes) if err != nil { return err } pub, ok := cert.PublicKey.(*rsa.PublicKey) if !ok { return errors.New("signing certificate") } sum := sha256.Sum256([]byte(caPEM)) return rsa.VerifyPKCS1v15(pub, crypto.SHA256, sum[:], sig) } func writeCABundle(dir, caPEM string) error { return writeMode(filepath.Join(dir, caFileName), []byte(caPEM), 0o644) } func writeClientCert(dir, certPEM string) error { return writeMode(filepath.Join(dir, certFileName), []byte(certPEM), 0o644) } func csrNewerThanCert(dir string) (bool, error) { csrPath := filepath.Join(dir, csrFileName) certPath := filepath.Join(dir, certFileName) csr, err := os.Stat(csrPath) if err != nil && !errors.Is(err, fs.ErrNotExist) { return false, fmt.Errorf("stat %s: %w", csrPath, err) } if err != nil { return false, nil } cert, err := os.Stat(certPath) if err != nil && !errors.Is(err, fs.ErrNotExist) { return false, fmt.Errorf("stat %s: %w", certPath, err) } if err != nil { return false, nil } return csr.ModTime().After(cert.ModTime()), nil } func readCSR(dir string) (string, error) { body, err := os.ReadFile(filepath.Join(dir, csrFileName)) if err != nil { return "", err } return string(body), nil } // prepareIdentity checks a saved certificate or creates the key and signing // request. fatal is a window message when the station must stop. func prepareIdentity(dir, callsign string) (tls.Certificate, bool, string, error) { certPath := filepath.Join(dir, certFileName) keyPath := filepath.Join(dir, keyFileName) _, certErr := os.Stat(certPath) _, keyErr := os.Stat(keyPath) certExists := certErr == nil keyExists := keyErr == nil if certErr != nil && !errors.Is(certErr, fs.ErrNotExist) { return tls.Certificate{}, false, "Cannot create " + certPath, fmt.Errorf("stat certificate: %w", certErr) } if keyErr != nil && !errors.Is(keyErr, fs.ErrNotExist) { return tls.Certificate{}, false, "Cannot create " + keyPath, fmt.Errorf("stat key: %w", keyErr) } if certExists && !keyExists { return tls.Certificate{}, false, "Certificate key is missing", nil } if certExists && keyExists { pair, msg, err := loadPair(certPath, keyPath, callsign) if msg != "" { return tls.Certificate{}, false, msg, err } if msg, err := ensureCSR(dir, callsign); msg != "" { return tls.Certificate{}, false, msg, err } return pair, true, "", nil } if msg, err := ensureKeyAndCSR(dir, callsign, !keyExists); msg != "" { return tls.Certificate{}, false, msg, err } return tls.Certificate{}, false, "", nil } func loadPair(certPath, keyPath, callsign string) (tls.Certificate, string, error) { cert, err := loadCert(certPath) if err != nil { return tls.Certificate{}, "Cannot create " + certPath, fmt.Errorf("read certificate: %w", err) } key, err := loadKey(keyPath) if err != nil { return tls.Certificate{}, "Cannot create " + keyPath, fmt.Errorf("read key: %w", err) } pub, ok := cert.PublicKey.(*rsa.PublicKey) if !ok || !pub.Equal(&key.PublicKey) { return tls.Certificate{}, "Certificate does not match the key", nil } if cert.Subject.CommonName != callsign { return tls.Certificate{}, "Certificate callsign does not match", nil } return tls.Certificate{ Certificate: [][]byte{cert.Raw}, PrivateKey: key, Leaf: cert, }, "", nil } func ensureCSR(dir, callsign string) (string, error) { csrPath := filepath.Join(dir, csrFileName) _, err := os.Stat(csrPath) if err == nil { return "", nil } if !errors.Is(err, fs.ErrNotExist) { return "Cannot create " + csrPath, fmt.Errorf("stat signing request: %w", err) } key, err := loadKey(filepath.Join(dir, keyFileName)) if err != nil { return "Cannot create " + csrPath, fmt.Errorf("read key: %w", err) } if err := writeCSR(csrPath, key, callsign); err != nil { return "Cannot create " + csrPath, err } return "", nil } func ensureKeyAndCSR(dir, callsign string, created bool) (string, error) { keyPath := filepath.Join(dir, keyFileName) var key *rsa.PrivateKey var err error if created { key, err = rsa.GenerateKey(rand.Reader, 2048) if err != nil { return "Cannot create " + keyPath, fmt.Errorf("generate key: %w", err) } der := x509.MarshalPKCS1PrivateKey(key) body := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: der}) if err := writeNew(keyPath, body, 0o600); err != nil { return "Cannot create " + keyPath, err } } else { key, err = loadKey(keyPath) if err != nil { return "Cannot create " + keyPath, fmt.Errorf("read key: %w", err) } } csrPath := filepath.Join(dir, csrFileName) _, csrErr := os.Stat(csrPath) if csrErr != nil && !errors.Is(csrErr, fs.ErrNotExist) { return "Cannot create " + csrPath, fmt.Errorf("stat signing request: %w", csrErr) } if created || errors.Is(csrErr, fs.ErrNotExist) { if err := writeCSR(csrPath, key, callsign); err != nil { return "Cannot create " + csrPath, err } } return "", nil } func writeCSR(path string, key *rsa.PrivateKey, callsign string) error { bc, err := asn1.Marshal(struct { CA bool }{CA: false}) if err != nil { return err } // digitalSignature, keyEncipherment, keyAgreement: bits 0, 2, and 4. ku, err := asn1.Marshal(asn1.BitString{Bytes: []byte{0xA8}, BitLength: 5}) if err != nil { return err } eku, err := asn1.Marshal([]asn1.ObjectIdentifier{oidClientAuth}) if err != nil { return err } der, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ Subject: pkix.Name{CommonName: callsign}, ExtraExtensions: []pkix.Extension{ {Id: oidBasicConstraints, Critical: true, Value: bc}, {Id: oidKeyUsage, Critical: true, Value: ku}, {Id: oidExtKeyUsage, Critical: false, Value: eku}, }, }, key) if err != nil { return err } body := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: der}) return writeMode(path, body, 0o644) } func loadCert(path string) (*x509.Certificate, error) { body, err := os.ReadFile(path) if err != nil { return nil, err } block, _ := pem.Decode(body) if block == nil { return nil, errors.New("certificate") } return x509.ParseCertificate(block.Bytes) } func loadKey(path string) (*rsa.PrivateKey, error) { body, err := os.ReadFile(path) if err != nil { return nil, err } block, _ := pem.Decode(body) if block == nil { return nil, errors.New("key") } switch block.Type { case "RSA PRIVATE KEY": return x509.ParsePKCS1PrivateKey(block.Bytes) case "PRIVATE KEY": k, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { return nil, err } rk, ok := k.(*rsa.PrivateKey) if !ok { return nil, errors.New("key") } return rk, nil default: return nil, errors.New("key") } } func writeNew(path string, body []byte, mode os.FileMode) error { f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, mode) if err != nil { return err } _, werr := f.Write(body) cerr := f.Close() if werr != nil { return werr } if cerr != nil { return cerr } return os.Chmod(path, mode) } func writeMode(path string, body []byte, mode os.FileMode) error { if err := os.WriteFile(path, body, mode); err != nil { return err } return os.Chmod(path, mode) }