Skip to content

Commit a2eafe3

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 a2eafe3

File tree

3 files changed

+136
-72
lines changed

3 files changed

+136
-72
lines changed

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/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

Lines changed: 85 additions & 0 deletions
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+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package agentsshrsa_test
2+
3+
import (
4+
"crypto/rsa"
5+
"math/rand/v2"
6+
"testing"
7+
8+
"github.com/coder/coder/v2/agent/agentssh/agentsshrsa"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestDeterministicPrivateKey(t *testing.T) {
13+
t.Parallel()
14+
15+
key1 := agentsshrsa.DeterministicPrivateKey(1234)
16+
key2 := agentsshrsa.DeterministicPrivateKey(1234)
17+
18+
assert.Equal(t, key1, key2)
19+
assert.EqualExportedValues(t, key1, key2)
20+
}
21+
22+
var result *rsa.PrivateKey
23+
24+
func BenchmarkDeterministicPrivateKey(b *testing.B) {
25+
var r *rsa.PrivateKey
26+
27+
for range b.N {
28+
// always record the result of DeterministicPrivateKey to prevent
29+
// the compiler eliminating the function call.
30+
r = agentsshrsa.DeterministicPrivateKey(rand.Int64())
31+
}
32+
33+
// always store the result to a package level variable
34+
// so the compiler cannot eliminate the Benchmark itself.
35+
result = r
36+
}
37+
38+
func FuzzDeterministicPrivateKey(f *testing.F) {
39+
testcases := []int64{0, 1234, 1010101010}
40+
for _, tc := range testcases {
41+
f.Add(tc) // Use f.Add to provide a seed corpus
42+
}
43+
f.Fuzz(func(t *testing.T, seed int64) {
44+
key1 := agentsshrsa.DeterministicPrivateKey(seed)
45+
key2 := agentsshrsa.DeterministicPrivateKey(seed)
46+
assert.Equal(t, key1, key2)
47+
assert.EqualExportedValues(t, key1, key2)
48+
})
49+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy