@@ -3,11 +3,12 @@ package agentssh
33import (
44"bufio"
55"context"
6- "crypto/rand"
76"crypto/rsa"
87"errors"
98"fmt"
109"io"
10+ "math/big"
11+ "math/rand"
1112"net"
1213"os"
1314"os/exec"
@@ -128,17 +129,6 @@ type Server struct {
128129}
129130
130131func NewServer (ctx context.Context ,logger slog.Logger ,prometheusRegistry * prometheus.Registry ,fs afero.Fs ,execer agentexec.Execer ,config * Config ) (* Server ,error ) {
131- // Clients' should ignore the host key when connecting.
132- // The agent needs to authenticate with coderd to SSH,
133- // so SSH authentication doesn't improve security.
134- randomHostKey ,err := rsa .GenerateKey (rand .Reader ,2048 )
135- if err != nil {
136- return nil ,err
137- }
138- randomSigner ,err := gossh .NewSignerFromKey (randomHostKey )
139- if err != nil {
140- return nil ,err
141- }
142132if config == nil {
143133config = & Config {}
144134}
@@ -205,8 +195,10 @@ func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prom
205195slog .F ("local_addr" ,conn .LocalAddr ()),
206196slog .Error (err ))
207197},
208- Handler :s .sessionHandler ,
209- HostSigners : []ssh.Signer {randomSigner },
198+ Handler :s .sessionHandler ,
199+ // HostSigners are intentionally empty, as the host key will
200+ // be set before we start listening.
201+ HostSigners : []ssh.Signer {},
210202LocalPortForwardingCallback :func (ctx ssh.Context ,destinationHost string ,destinationPort uint32 )bool {
211203// Allow local port forwarding all!
212204s .logger .Debug (ctx ,"local port forward" ,
@@ -844,7 +836,13 @@ func (s *Server) CreateCommand(ctx context.Context, script string, env []string,
844836return cmd ,nil
845837}
846838
839+ // Serve starts the server to handle incoming connections on the provided listener.
840+ // It returns an error if no host keys are set or if there is an issue accepting connections.
847841func (s * Server )Serve (l net.Listener ) (retErr error ) {
842+ if len (s .srv .HostSigners )== 0 {
843+ return xerrors .New ("no host keys set" )
844+ }
845+
848846s .logger .Info (context .Background (),"started serving listener" ,slog .F ("listen_addr" ,l .Addr ()))
849847defer func () {
850848s .logger .Info (context .Background (),"stopped serving listener" ,
@@ -1099,3 +1097,99 @@ func userHomeDir() (string, error) {
10991097}
11001098return u .HomeDir ,nil
11011099}
1100+
1101+ // UpdateHostSigner updates the host signer with a new key generated from the provided seed.
1102+ // If an existing host key exists with the same algorithm, it is overwritten
1103+ func (s * Server )UpdateHostSigner (seed int64 )error {
1104+ key ,err := CoderSigner (seed )
1105+ if err != nil {
1106+ return err
1107+ }
1108+
1109+ s .mu .Lock ()
1110+ defer s .mu .Unlock ()
1111+
1112+ s .srv .AddHostKey (key )
1113+
1114+ return nil
1115+ }
1116+
1117+ // CoderSigner generates a deterministic SSH signer based on the provided seed.
1118+ // It uses RSA with a key size of 2048 bits.
1119+ func CoderSigner (seed int64 ) (gossh.Signer ,error ) {
1120+ // Clients should ignore the host key when connecting.
1121+ // The agent needs to authenticate with coderd to SSH,
1122+ // so SSH authentication doesn't improve security.
1123+
1124+ // Since the standard lib purposefully does not generate
1125+ // deterministic rsa keys, we need to do it ourselves.
1126+ coderHostKey := func ()* rsa.PrivateKey {
1127+ // Create deterministic random source
1128+ // nolint: gosec
1129+ deterministicRand := rand .New (rand .NewSource (seed ))
1130+
1131+ // Use fixed values for p and q based on the seed
1132+ p := big .NewInt (0 )
1133+ q := big .NewInt (0 )
1134+ e := big .NewInt (65537 )// Standard RSA public exponent
1135+
1136+ // Generate deterministic primes using the seeded random
1137+ // Each prime should be ~1024 bits to get a 2048-bit key
1138+ for {
1139+ p .SetBit (p ,1024 ,1 )// Ensure it's large enough
1140+ for i := 0 ;i < 1024 ;i ++ {
1141+ if deterministicRand .Int63 ()% 2 == 1 {
1142+ p .SetBit (p ,i ,1 )
1143+ }else {
1144+ p .SetBit (p ,i ,0 )
1145+ }
1146+ }
1147+ if p .ProbablyPrime (20 ) {
1148+ break
1149+ }
1150+ }
1151+
1152+ for {
1153+ q .SetBit (q ,1024 ,1 )// Ensure it's large enough
1154+ for i := 0 ;i < 1024 ;i ++ {
1155+ if deterministicRand .Int63 ()% 2 == 1 {
1156+ q .SetBit (q ,i ,1 )
1157+ }else {
1158+ q .SetBit (q ,i ,0 )
1159+ }
1160+ }
1161+ if q .ProbablyPrime (20 )&& p .Cmp (q )!= 0 {
1162+ break
1163+ }
1164+ }
1165+
1166+ // Calculate n = p * q
1167+ n := new (big.Int ).Mul (p ,q )
1168+
1169+ // Calculate phi = (p-1) * (q-1)
1170+ p1 := new (big.Int ).Sub (p ,big .NewInt (1 ))
1171+ q1 := new (big.Int ).Sub (q ,big .NewInt (1 ))
1172+ phi := new (big.Int ).Mul (p1 ,q1 )
1173+
1174+ // Calculate private exponent d
1175+ d := new (big.Int ).ModInverse (e ,phi )
1176+
1177+ // Create the private key
1178+ privateKey := & rsa.PrivateKey {
1179+ PublicKey : rsa.PublicKey {
1180+ N :n ,
1181+ E :int (e .Int64 ()),
1182+ },
1183+ D :d ,
1184+ Primes : []* big.Int {p ,q },
1185+ }
1186+
1187+ // Compute precomputed values
1188+ privateKey .Precompute ()
1189+
1190+ return privateKey
1191+ }()
1192+
1193+ coderSigner ,err := gossh .NewSignerFromKey (coderHostKey )
1194+ return coderSigner ,err
1195+ }