prime8dev

prime8dev
git clone https://git.prime8.dev/prime8dev.git
Log | Files | Refs | README | LICENSE

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:
MDockerfile | 2+-
Mgo.mod | 13++++++-------
Mgo.sum | 24++++++++++++------------
Ainternal/ssh/kex_test.go | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minternal/ssh/server.go | 16++++++++++++++++
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 == "" {