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).
This commit is contained in:
Jakob Borg
2025-06-09 07:48:01 +02:00
committed by GitHub
parent 4215058911
commit 8afc9855f2
7 changed files with 48 additions and 18 deletions

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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) {

View File

@@ -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)
}
}