Skip to content

Commit f0fe6cd

Browse files
committed
fix(agentssh): ensure RSA key generation always produces valid keys
Change-Id: I0a453e1e1f8c638e40e7a4b87a6d0d7299e1cb5d Signed-off-by: Thomas Kosiewski <tk@coder.com>
1 parent 6bdddd5 commit f0fe6cd

File tree

3 files changed

+137
-72
lines changed

3 files changed

+137
-72
lines changed

agent/agentssh/agentssh.go

+2-72
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,9 @@ package agentssh
33
import (
44
"bufio"
55
"context"
6-
"crypto/rsa"
76
"errors"
87
"fmt"
98
"io"
10-
"math/big"
11-
"math/rand"
129
"net"
1310
"os"
1411
"os/exec"
@@ -33,6 +30,7 @@ import (
3330
"cdr.dev/slog"
3431

3532
"github.com/coder/coder/v2/agent/agentexec"
33+
"github.com/coder/coder/v2/agent/agentssh/agentsshrsa"
3634
"github.com/coder/coder/v2/agent/usershell"
3735
"github.com/coder/coder/v2/codersdk"
3836
"github.com/coder/coder/v2/pty"
@@ -1120,75 +1118,7 @@ func CoderSigner(seed int64) (gossh.Signer, error) {
11201118
// Clients should ignore the host key when connecting.
11211119
// The agent needs to authenticate with coderd to SSH,
11221120
// 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-
}()
1121+
coderHostKey := agentsshrsa.DeterministicPrivateKey(seed)
11921122

11931123
coderSigner, err := gossh.NewSignerFromKey(coderHostKey)
11941124
return coderSigner, err

agent/agentssh/agentsshrsa/key.go

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package agentsshrsa
2+
3+
import (
4+
"crypto/rsa"
5+
"math/big"
6+
"math/rand"
7+
)
8+
9+
// DeterministicPrivateKey generates an RSA private key deterministically based on the provided seed.
10+
// This function uses a deterministic random source to generate the primes p and q, ensuring that the
11+
// same seed will always produce the same private key. The generated key is 2048 bits in size.
12+
func DeterministicPrivateKey(seed int64) *rsa.PrivateKey {
13+
// Since the standard lib purposefully does not generate
14+
// deterministic rsa keys, we need to do it ourselves.
15+
16+
// Create deterministic random source
17+
// nolint: gosec
18+
deterministicRand := rand.New(rand.NewSource(seed))
19+
20+
// Use fixed values for p and q based on the seed
21+
p := big.NewInt(0)
22+
q := big.NewInt(0)
23+
e := big.NewInt(65537) // Standard RSA public exponent
24+
25+
for {
26+
// Generate deterministic primes using the seeded random
27+
// Each prime should be ~1024 bits to get a 2048-bit key
28+
for {
29+
p.SetBit(p, 1024, 1) // Ensure it's large enough
30+
for i := range 1024 {
31+
if deterministicRand.Int63()%2 == 1 {
32+
p.SetBit(p, i, 1)
33+
} else {
34+
p.SetBit(p, i, 0)
35+
}
36+
}
37+
p1 := new(big.Int).Sub(p, big.NewInt(1))
38+
if p.ProbablyPrime(20) && new(big.Int).GCD(nil, nil, e, p1).Cmp(big.NewInt(1)) == 0 {
39+
break
40+
}
41+
}
42+
43+
for {
44+
q.SetBit(q, 1024, 1) // Ensure it's large enough
45+
for i := range 1024 {
46+
if deterministicRand.Int63()%2 == 1 {
47+
q.SetBit(q, i, 1)
48+
} else {
49+
q.SetBit(q, i, 0)
50+
}
51+
}
52+
q1 := new(big.Int).Sub(q, big.NewInt(1))
53+
if q.ProbablyPrime(20) && p.Cmp(q) != 0 && new(big.Int).GCD(nil, nil, e, q1).Cmp(big.NewInt(1)) == 0 {
54+
break
55+
}
56+
}
57+
58+
// Calculate phi = (p-1) * (q-1)
59+
p1 := new(big.Int).Sub(p, big.NewInt(1))
60+
q1 := new(big.Int).Sub(q, big.NewInt(1))
61+
phi := new(big.Int).Mul(p1, q1)
62+
63+
// Calculate private exponent d
64+
d := new(big.Int).ModInverse(e, phi)
65+
if d != nil {
66+
// Calculate n = p * q
67+
n := new(big.Int).Mul(p, q)
68+
69+
// Create the private key
70+
privateKey := &rsa.PrivateKey{
71+
PublicKey: rsa.PublicKey{
72+
N: n,
73+
E: int(e.Int64()),
74+
},
75+
D: d,
76+
Primes: []*big.Int{p, q},
77+
}
78+
79+
// Compute precomputed values
80+
privateKey.Precompute()
81+
82+
return privateKey
83+
}
84+
}
85+
}
+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package agentsshrsa_test
2+
3+
import (
4+
"crypto/rsa"
5+
"math/rand/v2"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
10+
"github.com/coder/coder/v2/agent/agentssh/agentsshrsa"
11+
)
12+
13+
func TestDeterministicPrivateKey(t *testing.T) {
14+
t.Parallel()
15+
16+
key1 := agentsshrsa.DeterministicPrivateKey(1234)
17+
key2 := agentsshrsa.DeterministicPrivateKey(1234)
18+
19+
assert.Equal(t, key1, key2)
20+
assert.EqualExportedValues(t, key1, key2)
21+
}
22+
23+
var result *rsa.PrivateKey
24+
25+
func BenchmarkDeterministicPrivateKey(b *testing.B) {
26+
var r *rsa.PrivateKey
27+
28+
for range b.N {
29+
// always record the result of DeterministicPrivateKey to prevent
30+
// the compiler eliminating the function call.
31+
r = agentsshrsa.DeterministicPrivateKey(rand.Int64())
32+
}
33+
34+
// always store the result to a package level variable
35+
// so the compiler cannot eliminate the Benchmark itself.
36+
result = r
37+
}
38+
39+
func FuzzDeterministicPrivateKey(f *testing.F) {
40+
testcases := []int64{0, 1234, 1010101010}
41+
for _, tc := range testcases {
42+
f.Add(tc) // Use f.Add to provide a seed corpus
43+
}
44+
f.Fuzz(func(t *testing.T, seed int64) {
45+
key1 := agentsshrsa.DeterministicPrivateKey(seed)
46+
key2 := agentsshrsa.DeterministicPrivateKey(seed)
47+
assert.Equal(t, key1, key2)
48+
assert.EqualExportedValues(t, key1, key2)
49+
})
50+
}

0 commit comments

Comments
 (0)