Speak protocol 3.0 and fall back to 2.0 only when that is the reflector's maximum.
A reflector that has moved to 3.0 will not accept a station that always announces 2.0, and a 2.x reflector still needs one explicit plaintext session. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+279
@@ -0,0 +1,279 @@
|
||||
package station
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"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 {
|
||||
csr, err1 := os.Stat(filepath.Join(dir, csrFileName))
|
||||
cert, err2 := os.Stat(filepath.Join(dir, certFileName))
|
||||
if err1 != nil || err2 != nil {
|
||||
return false
|
||||
}
|
||||
return csr.ModTime().After(cert.ModTime())
|
||||
}
|
||||
|
||||
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) {
|
||||
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
|
||||
}
|
||||
if keyErr != nil && !errors.Is(keyErr, fs.ErrNotExist) {
|
||||
return tls.Certificate{}, false, "Cannot create " + keyPath
|
||||
}
|
||||
if certExists && !keyExists {
|
||||
return tls.Certificate{}, false, "Certificate key is missing"
|
||||
}
|
||||
if certExists && keyExists {
|
||||
pair, msg := loadPair(certPath, keyPath, callsign)
|
||||
if msg != "" {
|
||||
return tls.Certificate{}, false, msg
|
||||
}
|
||||
return pair, true, ""
|
||||
}
|
||||
if msg := ensureKeyAndCSR(dir, callsign, !keyExists); msg != "" {
|
||||
return tls.Certificate{}, false, msg
|
||||
}
|
||||
return tls.Certificate{}, false, ""
|
||||
}
|
||||
|
||||
func loadPair(certPath, keyPath, callsign string) (tls.Certificate, string) {
|
||||
cert, err := loadCert(certPath)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, "Certificate does not match the key"
|
||||
}
|
||||
key, err := loadKey(keyPath)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, "Certificate does not match the key"
|
||||
}
|
||||
pub, ok := cert.PublicKey.(*rsa.PublicKey)
|
||||
if !ok || !pub.Equal(&key.PublicKey) {
|
||||
return tls.Certificate{}, "Certificate does not match the key"
|
||||
}
|
||||
if cert.Subject.CommonName != callsign {
|
||||
return tls.Certificate{}, "Certificate callsign does not match"
|
||||
}
|
||||
return tls.Certificate{
|
||||
Certificate: [][]byte{cert.Raw},
|
||||
PrivateKey: key,
|
||||
Leaf: cert,
|
||||
}, ""
|
||||
}
|
||||
|
||||
func ensureKeyAndCSR(dir, callsign string, created bool) string {
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
} else {
|
||||
key, err = loadKey(keyPath)
|
||||
if err != nil {
|
||||
return "Cannot create " + keyPath
|
||||
}
|
||||
}
|
||||
csrPath := filepath.Join(dir, csrFileName)
|
||||
_, csrErr := os.Stat(csrPath)
|
||||
if created || errors.Is(csrErr, fs.ErrNotExist) {
|
||||
if err := writeCSR(csrPath, key, callsign); err != nil {
|
||||
return "Cannot create " + csrPath
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user