hub: wgsync fix — constrain HostKeyAlgorithms to the pinned key's type
Live S1 validation caught it: a stock multi-hostkey sshd presented ECDSA while we pin ed25519 → FixedHostKey refused a legitimate server. Regression test with an in-process dual-hostkey server (fails without the fix — red-proofed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -79,7 +79,13 @@ func (c *Client) Push(ctx context.Context, payload []byte) error {
|
||||
User: c.user,
|
||||
Auth: []ssh.AuthMethod{ssh.PublicKeys(c.signer)},
|
||||
HostKeyCallback: ssh.FixedHostKey(c.hostKey),
|
||||
Timeout: c.timeout,
|
||||
// Constrain negotiation to the PINNED key's algorithm. Without this the default
|
||||
// algorithm preference makes a multi-hostkey sshd (stock: ECDSA + ed25519) present a
|
||||
// different key type than the pinned one — FixedHostKey then refuses a LEGITIMATE
|
||||
// server. Found live in the S1 validation (host key mismatch against the real sshd);
|
||||
// regression: TestPush_MultiHostkeyServerStillMatchesPin.
|
||||
HostKeyAlgorithms: []string{c.hostKey.Type()},
|
||||
Timeout: c.timeout,
|
||||
}
|
||||
dialer := net.Dialer{Timeout: c.timeout}
|
||||
conn, err := dialer.DialContext(ctx, "tcp", c.addr)
|
||||
|
||||
@@ -7,7 +7,9 @@ package wgsync
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"encoding/pem"
|
||||
"io"
|
||||
@@ -204,6 +206,79 @@ func TestPush_WrongHostKeyRefused(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestPush_MultiHostkeyServerStillMatchesPin reproduces the live S1 failure: a server holding
|
||||
// MULTIPLE host keys (like a stock sshd: ECDSA + ed25519) must still present the PINNED type.
|
||||
// Without constraining ClientConfig.HostKeyAlgorithms to the pinned key's algorithm, default
|
||||
// negotiation can select the other key and FixedHostKey refuses a legitimate server.
|
||||
func TestPush_MultiHostkeyServerStillMatchesPin(t *testing.T) {
|
||||
clientPEM, clientSigner := testKeys(t)
|
||||
_, edSigner := testKeys(t)
|
||||
|
||||
ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("ecdsa: %v", err)
|
||||
}
|
||||
ecSigner, err := ssh.NewSignerFromKey(ecKey)
|
||||
if err != nil {
|
||||
t.Fatalf("ecdsa signer: %v", err)
|
||||
}
|
||||
|
||||
// Server holds BOTH keys, ECDSA added first (the tempting default pick).
|
||||
cfg := &ssh.ServerConfig{
|
||||
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
||||
if bytes.Equal(key.Marshal(), clientSigner.PublicKey().Marshal()) {
|
||||
return &ssh.Permissions{}, nil
|
||||
}
|
||||
return nil, io.EOF
|
||||
},
|
||||
}
|
||||
cfg.AddHostKey(ecSigner)
|
||||
cfg.AddHostKey(edSigner)
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { ln.Close() })
|
||||
go func() {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
sconn, chans, reqs, err := ssh.NewServerConn(conn, cfg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer sconn.Close()
|
||||
go ssh.DiscardRequests(reqs)
|
||||
for newCh := range chans {
|
||||
ch, chReqs, err := newCh.Accept()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
go func() {
|
||||
for req := range chReqs {
|
||||
if req.Type == "exec" {
|
||||
req.Reply(true, nil)
|
||||
io.ReadAll(ch)
|
||||
ch.Write([]byte(`{"status":"ok","applied":0}`))
|
||||
ch.SendRequest("exit-status", false, ssh.Marshal(struct{ Status uint32 }{0}))
|
||||
ch.Close()
|
||||
return
|
||||
}
|
||||
req.Reply(false, nil)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}()
|
||||
|
||||
// Pin the ed25519 key — Push must negotiate exactly that type and succeed.
|
||||
c := newTestClient(t, ln.Addr().String(), hostKeyLine(t, edSigner), clientPEM)
|
||||
if err := c.Push(context.Background(), []byte(`{"peers":[]}`)); err != nil {
|
||||
t.Fatalf("Push against multi-hostkey server with correct pin: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_BadInputsFailEarly(t *testing.T) {
|
||||
clientPEM, _ := testKeys(t)
|
||||
_, hostSigner := testKeys(t)
|
||||
|
||||
Reference in New Issue
Block a user