From 8afc9855f2e0037ba40806ff964f259fccc355ce Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Mon, 9 Jun 2025 07:48:01 +0200 Subject: [PATCH] feat: use Ed25519 keys for sync connections (#10162) This updates our key generation to use Ed25519 keys/certificates for sync connections. Certificates for browser use remain ECDSA for wider compatibility. Ed25519 is more modern and has fewer concerns for the future than the ECDSA curves we used previously. It is supported from Go 1.13 and forwards, which is Syncthing 1.3.0 (October 2019). --- cmd/infra/strelaypoolsrv/main.go | 2 +- cmd/stdiscosrv/apisrv_test.go | 2 +- cmd/stdiscosrv/main.go | 2 +- cmd/strelaysrv/main.go | 2 +- lib/api/api.go | 2 +- lib/syncthing/utils.go | 4 +-- lib/tlsutil/tlsutil.go | 52 +++++++++++++++++++++++++------- 7 files changed, 48 insertions(+), 18 deletions(-) diff --git a/cmd/infra/strelaypoolsrv/main.go b/cmd/infra/strelaypoolsrv/main.go index a2e5f65ef..ee982633f 100644 --- a/cmd/infra/strelaypoolsrv/main.go +++ b/cmd/infra/strelaypoolsrv/main.go @@ -620,7 +620,7 @@ func createTestCertificate() tls.Certificate { } certFile, keyFile := filepath.Join(tmpDir, "cert.pem"), filepath.Join(tmpDir, "key.pem") - cert, err := tlsutil.NewCertificate(certFile, keyFile, "relaypoolsrv", 20*365) + cert, err := tlsutil.NewCertificate(certFile, keyFile, "relaypoolsrv", 20*365, false) if err != nil { log.Fatalln("Failed to create test X509 key pair:", err) } diff --git a/cmd/stdiscosrv/apisrv_test.go b/cmd/stdiscosrv/apisrv_test.go index 07dfa2f17..99f788763 100644 --- a/cmd/stdiscosrv/apisrv_test.go +++ b/cmd/stdiscosrv/apisrv_test.go @@ -115,7 +115,7 @@ func BenchmarkAPIRequests(b *testing.B) { srv := httptest.NewServer(http.HandlerFunc(api.handler)) kf := b.TempDir() + "/cert" - crt, err := tlsutil.NewCertificate(kf+".crt", kf+".key", "localhost", 7) + crt, err := tlsutil.NewCertificate(kf+".crt", kf+".key", "localhost", 7, true) if err != nil { b.Fatal(err) } diff --git a/cmd/stdiscosrv/main.go b/cmd/stdiscosrv/main.go index a870651d1..c704f4697 100644 --- a/cmd/stdiscosrv/main.go +++ b/cmd/stdiscosrv/main.go @@ -107,7 +107,7 @@ func main() { cert, err = tls.LoadX509KeyPair(cli.Cert, cli.Key) if os.IsNotExist(err) { log.Println("Failed to load keypair. Generating one, this might take a while...") - cert, err = tlsutil.NewCertificate(cli.Cert, cli.Key, "stdiscosrv", 20*365) + cert, err = tlsutil.NewCertificate(cli.Cert, cli.Key, "stdiscosrv", 20*365, false) if err != nil { log.Fatalln("Failed to generate X509 key pair:", err) } diff --git a/cmd/strelaysrv/main.go b/cmd/strelaysrv/main.go index c1d264081..dca94b703 100644 --- a/cmd/strelaysrv/main.go +++ b/cmd/strelaysrv/main.go @@ -157,7 +157,7 @@ func main() { cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { log.Println("Failed to load keypair. Generating one, this might take a while...") - cert, err = tlsutil.NewCertificate(certFile, keyFile, "strelaysrv", 20*365) + cert, err = tlsutil.NewCertificate(certFile, keyFile, "strelaysrv", 20*365, false) if err != nil { log.Fatalln("Failed to generate X509 key pair:", err) } diff --git a/lib/api/api.go b/lib/api/api.go index c7952d6af..4cb7a97ed 100644 --- a/lib/api/api.go +++ b/lib/api/api.go @@ -166,7 +166,7 @@ func (s *service) getListener(guiCfg config.GUIConfiguration) (net.Listener, err name = s.tlsDefaultCommonName } - cert, err = tlsutil.NewCertificate(httpsCertFile, httpsKeyFile, name, httpsCertLifetimeDays) + cert, err = tlsutil.NewCertificate(httpsCertFile, httpsKeyFile, name, httpsCertLifetimeDays, true) } if err != nil { return nil, err diff --git a/lib/syncthing/utils.go b/lib/syncthing/utils.go index 79d2cd6b3..79ffd8c46 100644 --- a/lib/syncthing/utils.go +++ b/lib/syncthing/utils.go @@ -60,8 +60,8 @@ func LoadOrGenerateCertificate(certFile, keyFile string) (tls.Certificate, error } func GenerateCertificate(certFile, keyFile string) (tls.Certificate, error) { - l.Infof("Generating ECDSA key and certificate for %s...", tlsDefaultCommonName) - return tlsutil.NewCertificate(certFile, keyFile, tlsDefaultCommonName, deviceCertLifetimeDays) + l.Infof("Generating key and certificate for %s...", tlsDefaultCommonName) + return tlsutil.NewCertificate(certFile, keyFile, tlsDefaultCommonName, deviceCertLifetimeDays, false) } func DefaultConfig(path string, myID protocol.DeviceID, evLogger events.Logger, skipPortProbing bool) (config.Wrapper, error) { diff --git a/lib/tlsutil/tlsutil.go b/lib/tlsutil/tlsutil.go index a105c9e87..e1b9b7e86 100644 --- a/lib/tlsutil/tlsutil.go +++ b/lib/tlsutil/tlsutil.go @@ -8,6 +8,7 @@ package tlsutil import ( "crypto/ecdsa" + "crypto/ed25519" "crypto/elliptic" "crypto/rsa" "crypto/tls" @@ -87,9 +88,28 @@ func SecureDefaultWithTLS12() *tls.Config { } } -// generateCertificate generates a PEM formatted key pair and self-signed certificate in memory. -func generateCertificate(commonName string, lifetimeDays int) (*pem.Block, *pem.Block, error) { - priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) +// generateCertificate generates a PEM formatted key pair and self-signed +// certificate in memory. The compatible flag indicates whether we aim for +// compatibility (browsers) or maximum efficiency/security (sync +// connections). +func generateCertificate(commonName string, lifetimeDays int, compatible bool) (*pem.Block, *pem.Block, error) { + var pub, priv any + var err error + var sigAlgo x509.SignatureAlgorithm + if compatible { + // For browser connections we prefer ECDSA-P256 + sigAlgo = x509.ECDSAWithSHA256 + var pk *ecdsa.PrivateKey + pk, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err == nil { + priv = pk + pub = pk.Public() + } + } else { + // For sync connections we use Ed25519 + sigAlgo = x509.PureEd25519 + pub, priv, err = ed25519.GenerateKey(rand.Reader) + } if err != nil { return nil, nil, fmt.Errorf("generate key: %w", err) } @@ -110,13 +130,13 @@ func generateCertificate(commonName string, lifetimeDays int) (*pem.Block, *pem. DNSNames: []string{commonName}, NotBefore: notBefore, NotAfter: notAfter, - SignatureAlgorithm: x509.ECDSAWithSHA256, + SignatureAlgorithm: sigAlgo, KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, BasicConstraintsValid: true, } - derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, priv.Public(), priv) + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, pub, priv) if err != nil { return nil, nil, fmt.Errorf("create cert: %w", err) } @@ -130,9 +150,12 @@ func generateCertificate(commonName string, lifetimeDays int) (*pem.Block, *pem. return certBlock, keyBlock, nil } -// NewCertificate generates and returns a new TLS certificate, saved to the given PEM files. -func NewCertificate(certFile, keyFile string, commonName string, lifetimeDays int) (tls.Certificate, error) { - certBlock, keyBlock, err := generateCertificate(commonName, lifetimeDays) +// NewCertificate generates and returns a new TLS certificate, saved to the +// given PEM files. The compatible flag indicates whether we aim for +// compatibility (browsers) or maximum efficiency/security (sync +// connections). +func NewCertificate(certFile, keyFile string, commonName string, lifetimeDays int, compatible bool) (tls.Certificate, error) { + certBlock, keyBlock, err := generateCertificate(commonName, lifetimeDays, compatible) if err != nil { return tls.Certificate{}, err } @@ -162,9 +185,10 @@ func NewCertificate(certFile, keyFile string, commonName string, lifetimeDays in return tls.X509KeyPair(pem.EncodeToMemory(certBlock), pem.EncodeToMemory(keyBlock)) } -// NewCertificateInMemory generates and returns a new TLS certificate, kept only in memory. +// NewCertificateInMemory generates and returns a new TLS certificate, kept +// only in memory. func NewCertificateInMemory(commonName string, lifetimeDays int) (tls.Certificate, error) { - certBlock, keyBlock, err := generateCertificate(commonName, lifetimeDays) + certBlock, keyBlock, err := generateCertificate(commonName, lifetimeDays, false) if err != nil { return tls.Certificate{}, err } @@ -246,7 +270,13 @@ func pemBlockForKey(priv interface{}) (*pem.Block, error) { return nil, err } return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}, nil + case ed25519.PrivateKey: + bs, err := x509.MarshalPKCS8PrivateKey(k) + if err != nil { + return nil, err + } + return &pem.Block{Type: "PRIVATE KEY", Bytes: bs}, nil default: - return nil, errors.New("unknown key type") + return nil, fmt.Errorf("unknown key type: %T", priv) } }