Skip to content

Commit cd3a104

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 546d915 commit cd3a104

File tree

3 files changed

+139
-72
lines changed

3 files changed

+139
-72
lines changed

agent/agentrsa/key.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package agentrsa
2+
3+
import (
4+
"crypto/rsa"
5+
"math/big"
6+
"math/rand"
7+
)
8+
9+
// GenerateDeterministicKey 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+
//
13+
// Reference: https://pkg.go.dev/crypto/rsa#GenerateKey
14+
func GenerateDeterministicKey(seed int64) *rsa.PrivateKey {
15+
// Since the standard lib purposefully does not generate
16+
// deterministic rsa keys, we need to do it ourselves.
17+
18+
// Create deterministic random source
19+
// nolint: gosec
20+
deterministicRand := rand.New(rand.NewSource(seed))
21+
22+
// Use fixed values for p and q based on the seed
23+
p := big.NewInt(0)
24+
q := big.NewInt(0)
25+
e := big.NewInt(65537) // Standard RSA public exponent
26+
27+
for {
28+
// Generate deterministic primes using the seeded random
29+
// Each prime should be ~1024 bits to get a 2048-bit key
30+
for {
31+
p.SetBit(p, 1024, 1) // Ensure it's large enough
32+
for i := range 1024 {
33+
if deterministicRand.Int63()%2 == 1 {
34+
p.SetBit(p, i, 1)
35+
} else {
36+
p.SetBit(p, i, 0)
37+
}
38+
}
39+
p1 := new(big.Int).Sub(p, big.NewInt(1))
40+
if p.ProbablyPrime(20) && new(big.Int).GCD(nil, nil, e, p1).Cmp(big.NewInt(1)) == 0 {
41+
break
42+
}
43+
}
44+
45+
for {
46+
q.SetBit(q, 1024, 1) // Ensure it's large enough
47+
for i := range 1024 {
48+
if deterministicRand.Int63()%2 == 1 {
49+
q.SetBit(q, i, 1)
50+
} else {
51+
q.SetBit(q, i, 0)
52+
}
53+
}
54+
q1 := new(big.Int).Sub(q, big.NewInt(1))
55+
if q.ProbablyPrime(20) && p.Cmp(q) != 0 && new(big.Int).GCD(nil, nil, e, q1).Cmp(big.NewInt(1)) == 0 {
56+
break
57+
}
58+
}
59+
60+
// Calculate phi = (p-1) * (q-1)
61+
p1 := new(big.Int).Sub(p, big.NewInt(1))
62+
q1 := new(big.Int).Sub(q, big.NewInt(1))
63+
phi := new(big.Int).Mul(p1, q1)
64+
65+
// Calculate private exponent d
66+
d := new(big.Int).ModInverse(e, phi)
67+
if d != nil {
68+
// Calculate n = p * q
69+
n := new(big.Int).Mul(p, q)
70+
71+
// Create the private key
72+
privateKey := &rsa.PrivateKey{
73+
PublicKey: rsa.PublicKey{
74+
N: n,
75+
E: int(e.Int64()),
76+
},
77+
D: d,
78+
Primes: []*big.Int{p, q},
79+
}
80+
81+
// Compute precomputed values
82+
privateKey.Precompute()
83+
84+
return privateKey
85+
}
86+
}
87+
}

agent/agentrsa/key_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package agentrsa_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/agentrsa"
11+
)
12+
13+
func TestGenerateDeterministicKey(t *testing.T) {
14+
t.Parallel()
15+
16+
key1 := agentrsa.GenerateDeterministicKey(1234)
17+
key2 := agentrsa.GenerateDeterministicKey(1234)
18+
19+
assert.Equal(t, key1, key2)
20+
assert.EqualExportedValues(t, key1, key2)
21+
}
22+
23+
var result *rsa.PrivateKey
24+
25+
func BenchmarkGenerateDeterministicKey(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 = agentrsa.GenerateDeterministicKey(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 FuzzGenerateDeterministicKey(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 := agentrsa.GenerateDeterministicKey(seed)
46+
key2 := agentrsa.GenerateDeterministicKey(seed)
47+
assert.Equal(t, key1, key2)
48+
assert.EqualExportedValues(t, key1, key2)
49+
})
50+
}

agent/agentssh/agentssh.go

Lines changed: 2 additions & 72 deletions
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/agentrsa"
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 := agentrsa.GenerateDeterministicKey(seed)
11921122

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

0 commit comments

Comments
 (0)