commit d0c8b01c519ed449b88837f2414e4294c4b10f67
parent 3ba66b208f9d28bb221d6740c5e15a1f6751b244
Author: Damian Myrda <damian@prime8.dev>
Date: Wed, 21 Oct 2026 08:23:48 +0000
feat(ssh): offer post-quantum mlkem768x25519-sha256 key exchange
OpenSSH 10 warns when the connection doesn't use a post-quantum KEX.
Bump golang.org/x/crypto to v0.48.0 (mlkem768x25519-sha256, needs Go
1.24 for crypto/mlkem) and set the server's KEX list explicitly with it
first, curve25519/ECDH/DH as fallbacks. go directive 1.24, and the build
image is pinned to golang:1.27 so a stale cached golang:latest can't
silently build without ML-KEM.
Diffstat:
5 files changed, 93 insertions(+), 20 deletions(-)
diff --git a/Dockerfile b/Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:latest AS build
+FROM golang:1.27 AS build
WORKDIR /app
COPY go.mod go.sum ./
diff --git a/go.mod b/go.mod
@@ -1,6 +1,6 @@
module git.prime8.dev/moncheeta/prime8dev
-go 1.23.0
+go 1.24.0
require (
github.com/MichaelMure/go-term-markdown v0.1.4
@@ -20,7 +20,7 @@ require (
github.com/evanw/esbuild v0.27.4
github.com/joho/godotenv v1.5.1
github.com/muesli/termenv v0.16.0
- golang.org/x/crypto v0.36.0
+ golang.org/x/crypto v0.48.0
)
require (
@@ -65,9 +65,8 @@ require (
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect
- golang.org/x/net v0.36.0 // indirect
- golang.org/x/sync v0.13.0 // indirect
- golang.org/x/sys v0.32.0 // indirect
- golang.org/x/term v0.31.0 // indirect
- golang.org/x/text v0.24.0 // indirect
+ golang.org/x/net v0.49.0 // indirect
+ golang.org/x/sync v0.19.0 // indirect
+ golang.org/x/sys v0.41.0 // indirect
+ golang.org/x/text v0.34.0 // indirect
)
diff --git a/go.sum b/go.sum
@@ -143,8 +143,8 @@ github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
-golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
+golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
+golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@@ -152,10 +152,10 @@ golang.org/x/image v0.0.0-20191206065243-da761ea9ff43/go.mod h1:FeLwcggjj3mMvU+o
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
-golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
-golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
-golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
+golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -164,14 +164,14 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
-golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
-golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
-golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
+golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
+golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
+golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
-golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
+golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
+golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/ssh/kex_test.go b/internal/ssh/kex_test.go
@@ -0,0 +1,58 @@
+package ssh
+
+import (
+ "crypto/ed25519"
+ "crypto/rand"
+ "net"
+ "testing"
+
+ gossh "golang.org/x/crypto/ssh"
+)
+
+// TestPostQuantumKeyExchange checks that a client offering only
+// mlkem768x25519-sha256 completes the handshake against our KEX list.
+func TestPostQuantumKeyExchange(t *testing.T) {
+ _, priv, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ t.Fatal(err)
+ }
+ signer, err := gossh.NewSignerFromKey(priv)
+ if err != nil {
+ t.Fatal(err)
+ }
+ cfg := &gossh.ServerConfig{Config: gossh.Config{KeyExchanges: keyExchanges}, NoClientAuth: true}
+ cfg.AddHostKey(signer)
+
+ ln, err := net.Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer ln.Close()
+ errc := make(chan error, 1)
+ go func() {
+ serverConn, err := ln.Accept()
+ if err != nil {
+ errc <- err
+ return
+ }
+ defer serverConn.Close()
+ conn, _, _, err := gossh.NewServerConn(serverConn, cfg)
+ if err == nil {
+ conn.Close()
+ }
+ errc <- err
+ }()
+
+ client, err := gossh.Dial("tcp", ln.Addr().String(), &gossh.ClientConfig{
+ User: "visitor",
+ Config: gossh.Config{KeyExchanges: []string{gossh.KeyExchangeMLKEM768X25519}},
+ HostKeyCallback: gossh.InsecureIgnoreHostKey(),
+ })
+ if err != nil {
+ t.Fatalf("mlkem768x25519-sha256 handshake failed: %v", err)
+ }
+ client.Close()
+ if err := <-errc; err != nil {
+ t.Fatalf("server side: %v", err)
+ }
+}
diff --git a/internal/ssh/server.go b/internal/ssh/server.go
@@ -56,6 +56,9 @@ func Run(ctx context.Context, logger *slog.Logger) error {
return conn
}),
func(s *charmssh.Server) error {
+ s.ServerConfigCallback = func(_ charmssh.Context) *gossh.ServerConfig {
+ return &gossh.ServerConfig{Config: gossh.Config{KeyExchanges: keyExchanges}}
+ }
s.ConnectionFailedCallback = func(conn net.Conn, err error) {
logger.Warn("connection failed",
"ip", remoteIP(conn.RemoteAddr()),
@@ -94,6 +97,19 @@ func Run(ctx context.Context, logger *slog.Logger) error {
return nil
}
+// keyExchanges puts the hybrid post-quantum KEX first so OpenSSH 10 clients
+// stop warning about a non-PQ key exchange, with classic fallbacks for older
+// clients. Needs Go 1.24+ (crypto/mlkem).
+var keyExchanges = []string{
+ gossh.KeyExchangeMLKEM768X25519,
+ gossh.KeyExchangeCurve25519,
+ gossh.KeyExchangeECDHP256,
+ gossh.KeyExchangeECDHP384,
+ gossh.KeyExchangeECDHP521,
+ gossh.KeyExchangeDH14SHA256,
+ gossh.KeyExchangeDH16SHA512,
+}
+
func sshListenAddr() string {
port := strings.TrimSpace(os.Getenv("SSH_PORT"))
if port == "" {