Skip to content

Commit 8df1f21

Browse files
committed
default to SHA2-512 for RSA signature
1 parent 413b7f2 commit 8df1f21

File tree

5 files changed

+91
-21
lines changed

5 files changed

+91
-21
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ A seed is used to generate the secret, it's recommended you don't use the same s
3131
## ⚠️ Limitations
3232

3333
- Can't use ECDSA keys, they always give different signatures
34+
- [RFC8332](https://www.rfc-editor.org/info/rfc8332) compatible agent (e.g. OpenSSH 7.6+) mandatory to use SHA2-512 signature scheme. You can still use deprecated SHA1 signatures with `rsaSignatureFlag: 0` option in `SSHAgentClient` constructor.
3435

3536
## 💻 CLI usage
3637

@@ -93,3 +94,10 @@ const decrypted = await agent.decrypt(
9394
)
9495
console.log('Decrypted data:', decrypted.toString('utf8'))
9596
```
97+
98+
## Local test
99+
100+
```bash
101+
ssh-agent -D
102+
SSH_AUTH_SOCK= ssh-add id_ecdsa id_ed25519 id_rsa
103+
```

src/lib/ssh_agent_client.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ const Protocol = {
1515

1616
type Protocol = (typeof Protocol)[keyof typeof Protocol]
1717

18+
export const RsaSignatureFlag = {
19+
DEFAULT: 0,
20+
SSH_AGENT_RSA_SHA2_256: 2,
21+
SSH_AGENT_RSA_SHA2_512: 4,
22+
} as const
23+
24+
type RsaSignatureFlag = (typeof RsaSignatureFlag)[keyof typeof RsaSignatureFlag]
25+
1826
export interface SSHKey {
1927
/** E.g. "ssh-rsa" */
2028
type: string
@@ -40,6 +48,8 @@ export interface SSHAgentClientOptions {
4048
sockFile?: string
4149
cipherAlgo?: string
4250
digestAlgo?: string
51+
/** RSA signature method flag for signing request when using RSA keys */
52+
rsaSignatureFlag?: RsaSignatureFlag
4353
}
4454

4555
/** Read a length-prefixed string (uint32 BE length + bytes) from a buffer. */
@@ -71,6 +81,7 @@ export class SSHAgentClient {
7181
private readonly sockFile: string
7282
private readonly cipherAlgo: string
7383
private readonly digestAlgo: string
84+
private readonly rsaSignatureFlag: RsaSignatureFlag
7485

7586
/**
7687
* @param options - Optional configuration.
@@ -84,6 +95,8 @@ export class SSHAgentClient {
8495
this.cipherAlgo = options.cipherAlgo ?? 'aes-256-cbc'
8596
this.digestAlgo = options.digestAlgo ?? 'sha256'
8697

98+
this.rsaSignatureFlag = options.rsaSignatureFlag ?? RsaSignatureFlag.SSH_AGENT_RSA_SHA2_512
99+
87100
const sockFile = options.sockFile ?? process.env.SSH_AUTH_SOCK
88101
if (!sockFile || !existsSync(sockFile)) {
89102
throw new Error(`Socket ${sockFile ?? '?'} not found`)
@@ -161,7 +174,7 @@ export class SSHAgentClient {
161174
let offset = writeHeader(req, Protocol.SSH2_AGENTC_SIGN_REQUEST)
162175
offset = writeString(req, key.raw, offset)
163176
offset = writeString(req, data, offset)
164-
req.writeUInt32BE(0, offset) // Flags = 0
177+
req.writeUInt32BE(this.rsaSignatureFlag, offset)
165178
return req
166179
}
167180

test/ssh_agent_cli.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ describe('ssh-crypt cli tests', () => {
4242
})
4343
it('should decrypt', () => {
4444
const data =
45-
'ecfd6bb57f4891ba7226886e90d2eb848022a495b15ffd91ffe760bca5605f9062c305ee14226d9daf7faa58460c8f50'
45+
'5f1979820d75926171e7028d5938f64ba5872683334f21b14947df1a4cce1f9ff1bb7c9c91e28e49aa8807b02c18c48c'
4646
const output = execSync(
47-
'npm exec -- tsx src/cli.ts -k key_rsa -s not_a_secret --decryptEncoding hex decrypt',
47+
'npm exec -- tsx src/cli.ts -k key_ed25519 -s not_a_secret --decryptEncoding hex decrypt',
4848
{
4949
encoding: 'utf8',
5050
input: data,

test/ssh_agent_client.spec.ts

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as chai from 'chai'
22
import { describe, it } from 'mocha'
3+
import { RsaSignatureFlag, SSHAgentClient } from '../src/lib/ssh_agent_client.ts'
34
import chaiAsPromised from 'chai-as-promised'
4-
import { SSHAgentClient } from '../src/lib/ssh_agent_client.ts'
55

66
chai.use(chaiAsPromised)
77

@@ -65,8 +65,8 @@ describe('SSHAgentClient tests', () => {
6565
chai.assert.strictEqual(identity.type, 'ssh-rsa')
6666
chai.assert.strictEqual(identity.comment, 'key_rsa')
6767
})
68-
it('should sign', async () => {
69-
const agent = new SSHAgentClient()
68+
it('should sign with key_rsa and default ssh-rsa signature', async () => {
69+
const agent = new SSHAgentClient({ rsaSignatureFlag: RsaSignatureFlag.DEFAULT })
7070
const identity = await agent.getIdentity('key_rsa')
7171
if (!identity) {
7272
throw new Error()
@@ -78,6 +78,55 @@ describe('SSHAgentClient tests', () => {
7878
'2Ox3c3wtOkmZwINZ0nSi31NP2ysWnERdaLWMcY/CONPcLK3DPBfEolG307yMdmYAsJY5VW/sgw0ye58zOu5daos2xtWxYTHUvY7peDMcJWgQJ5YFBQWGY6ku++tqR31FSlLl9KJMUnXGdE88T1RFbaWOsg8U37IsMd5juxCeakgmcvo4PXOcWlRKBnQKRaxoOl+lQxcBNb3GM2T/kqnKkae5NebaUMOI+v7U95tzNPq0xLrZ0805rNETEcLdMszi6XS8Gbh4iDZNZGV7sA5hh9rB/avhKQHYplJ9YzyLfLEX3S8bZf41xIynt62PXeEUuM5UcYRj1lUC6quGC59Z4P2pBujjvJqj9gmKjwVcwnVo28J9OEugmRnO2QmamR0LIJIhIJmdmRRFqD86vkfyYBz733KRkvA80gXvbvligTI5LZwpUoTX7YWGkpz8fCCrMJ4WRyu9nerp9EGk4afhlAb92wMSqQV3QmXI/uRjHkbs+8rfeZS6i77Xgj4AYkHp',
7979
)
8080
})
81+
it('should sign with key_rsa and SHA2-256 signature', async () => {
82+
const agent = new SSHAgentClient({ rsaSignatureFlag: RsaSignatureFlag.SSH_AGENT_RSA_SHA2_256 })
83+
const identity = await agent.getIdentity('key_rsa')
84+
if (!identity) {
85+
throw new Error()
86+
}
87+
const signature = await agent.sign(identity, Buffer.from('hello', 'utf8'))
88+
chai.assert.strictEqual(signature.type, 'rsa-sha2-256')
89+
chai.assert.strictEqual(
90+
signature.signature,
91+
'InYgWb2UN3RCyQwKMWNSjljkzmMQ+6D+gwol2PfSrYaXejyKIEFH2ZwzueT0bTD8CEi0l51BZ6Lx5Pdi8JLTgdk3fneJQPayBkzq+QUINNeK8qLICYol36T2Huy2oS1TrqVvlhRZQSvJB8En6jRbsgo9qEoK4GtitiDYqNIdsG3mIEpmP8M+gA/iis6PHAxlT2cHPF8gQu43KXorZ1txvkOnahJ0LAfjB5axz+NvUkEgSbXbK3l4REFm3+TNXq/El9yqnC+5NW09v+m0dctW/YBY0eGpYZoO8pl/oXJ/47M8SOmDZjrmmAwG2zPzTq6Pctv3glVROY8sndcy32VpJs1DEEbk3MGgj2R2HkvCX6PPuhPDpMdcakpfSbOFvRDuYityeijJLA5d6v7EaWBTeH4nX10Dq4O+5CCsdb1dOhzS5gay4aedy2TwU8zpUF0GqzJwxLq7w26nokZLOoLmNFbkGsRXQqVfTPUN5aNNZqMNIdeXcjmvAb7fmcz6Gxdv',
92+
)
93+
})
94+
it('should sign with key_rsa and SHA2-512 signature', async () => {
95+
const agent = new SSHAgentClient({ rsaSignatureFlag: RsaSignatureFlag.SSH_AGENT_RSA_SHA2_512 })
96+
const identity = await agent.getIdentity('key_rsa')
97+
if (!identity) {
98+
throw new Error()
99+
}
100+
const signature = await agent.sign(identity, Buffer.from('hello', 'utf8'))
101+
chai.assert.strictEqual(signature.type, 'rsa-sha2-512')
102+
chai.assert.strictEqual(
103+
signature.signature,
104+
'PEOu4DK+GXsdgd84PEEAFzQgsSz6kgCGiVgf4d464mBGyXhebFR5HBZ4RPCKDIPAu6zFt7DgAYBDmBio0LdwqgAs561ytLO+pQ1UCS1nmzE8f9n8220vGp18PSIXzDPAwlbAk9tPv940kFWbQOr1GwxmyERWC0XOdMLvueeCx5alThYWOAKbjHLhMSAry9E0I02g44UkoFV0VAgrSff03t9Y31c+n5ogpa2bii02IFg3khycrzaYv+3B+aU9kew7MhFH9awkJLFbFuQbLtOiINwhkZRnTAMbrqZPqeYpKrhHr+D6gjNhYXqNhAfZKBJFUurVAkccmkFWttmAZJwCpoiDD+yrOTYj7s5iQq5M0YlrIv3N+RP7MyN9GhvWwlD/Ti5Mnc7EckAIBIrFAja8hdJqmNbeVKD2o5tqJopvz7vvGpOiUHAZyL9Gbd74W3yjX971rZKSHsKtC7ngi3GqUK64QJ/sLjutQXnsxCWvEPaltnsT8dlyjdi2iizP1Prn',
105+
)
106+
})
107+
it('should sign with ed25519', async () => {
108+
const agent = new SSHAgentClient()
109+
const identity = await agent.getIdentity('key_ed25519')
110+
if (!identity) {
111+
throw new Error()
112+
}
113+
const signature = await agent.sign(identity, Buffer.from('hello', 'utf8'))
114+
chai.assert.strictEqual(signature.type, 'ssh-ed25519')
115+
chai.assert.strictEqual(
116+
signature.signature,
117+
'Uolma4H0fzvBSu1G/6pUYZthmFH/NjXRjP4Zx80SloXMIlTFsF/++HqOi4ooEhLoTh/ZlhAlyEONjVqqcAnWAQ==',
118+
)
119+
})
120+
it('can sign with ecdsa', async () => {
121+
const agent = new SSHAgentClient()
122+
const identity = await agent.getIdentity('key_ecdsa')
123+
if (!identity) {
124+
throw new Error()
125+
}
126+
const signature = await agent.sign(identity, Buffer.from('hello', 'utf8'))
127+
chai.assert.strictEqual(signature.type, 'ecdsa-sha2-nistp256')
128+
// Can't assert signature value as it is not deterministic
129+
})
81130
it('should throw if wrong secret', async () => {
82131
const agent = new SSHAgentClient()
83132
const identity = await agent.getIdentity('key_rsa')
@@ -153,13 +202,13 @@ describe('SSHAgentClient cipher combination tests', () => {
153202
const decrypted = await agent.decrypt(
154203
identity,
155204
SEED,
156-
'ecfd6bb57f4891ba7226886e90d2eb848022a495b15ffd91ffe760bca5605f9062c305ee14226d9daf7faa58460c8f50',
205+
'5af153e6fbf83b40cf98ed8bf5710321aa3234b2121cc3d8c47ef6854007f35ece319b056ddba7791b1db776b26a7ea7',
157206
)
158207
chai.assert.strictEqual(decrypted.toString('utf8'), DECODED_STRING)
159208
})
160209
it('should encrypt and decrypt with aes128/shake128', async () => {
161210
const agent = new SSHAgentClient({ cipherAlgo: 'aes-128-cbc', digestAlgo: 'shake128' })
162-
const identity = await agent.getIdentity('key_rsa')
211+
const identity = await agent.getIdentity('key_ed25519')
163212
if (!identity) {
164213
throw new Error()
165214
}
@@ -168,13 +217,13 @@ describe('SSHAgentClient cipher combination tests', () => {
168217
const decrypted = await agent.decrypt(
169218
identity,
170219
SEED,
171-
'9126f351eb84b1d9316b2808c69a09d379fa9f20d4f4a8d4e30135dbba262cbbfc3ad3774fcade60e6d1ae7c75af0a9c',
220+
'9a9bdd5451fd1ed8220f4ce23a17aa0981d69521cadd73ff219fd85aa01bc8a3a44cbe95502854a5b37296e93a91db91',
172221
)
173222
chai.assert.strictEqual(decrypted.toString('utf8'), DECODED_STRING)
174223
})
175-
it('should encrypt with non matching aes192/sha512', async () => {
224+
it('should encrypt and decrypt with non matching aes192/sha512', async () => {
176225
const agent = new SSHAgentClient({ cipherAlgo: 'aes-192-cbc', digestAlgo: 'sha512' })
177-
const identity = await agent.getIdentity('key_rsa')
226+
const identity = await agent.getIdentity('key_ed25519')
178227
if (!identity) {
179228
throw new Error()
180229
}
@@ -183,7 +232,7 @@ describe('SSHAgentClient cipher combination tests', () => {
183232
const decrypted = await agent.decrypt(
184233
identity,
185234
SEED,
186-
'280cd2993bb8f9d7ec14ef0c1d94d4ac8e128a39f3f9cba9f5730a7d99674057766e099c17a6786a4a6b33670d1b45b7',
235+
'1e69398e2ad6ab5e1f748d538fafe6e54e9824b0646af6666437c556398a7495b48f76db5df52d52f9adde0a232465ab',
187236
)
188237
chai.assert.strictEqual(decrypted.toString('utf8'), DECODED_STRING)
189238
})
@@ -192,7 +241,7 @@ describe('SSHAgentClient cipher combination tests', () => {
192241
describe('SSHAgentClient encodings tests', () => {
193242
it('should encrypt to base64', async () => {
194243
const agent = new SSHAgentClient()
195-
const identity = await agent.getIdentity('key_rsa')
244+
const identity = await agent.getIdentity('key_ed25519')
196245
if (!identity) {
197246
throw new Error()
198247
}
@@ -201,14 +250,14 @@ describe('SSHAgentClient encodings tests', () => {
201250
})
202251
it('should decrypt from base64', async () => {
203252
const agent = new SSHAgentClient()
204-
const identity = await agent.getIdentity('key_rsa')
253+
const identity = await agent.getIdentity('key_ed25519')
205254
if (!identity) {
206255
throw new Error()
207256
}
208257
const decrypted = await agent.decrypt(
209258
identity,
210259
SEED,
211-
'8epe+B3bWcSGTPpyW2MRqHeAKjTj2NVnR4q1YNVB5LfXw9JE02wsb3RqZBTFbMXc',
260+
'QMf1r1/ZONTJImZyY1qO+ibDTqLCZNwD2dMs/tpfVpfLKovb7flyiU1Au/01xffv',
212261
'base64',
213262
)
214263
chai.assert.strictEqual(decrypted.toString('utf8'), DECODED_STRING)

test/ssh_agent_stream.spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const SEED = 'not_a_secret'
1414
describe('SSHAgentClient streams tests', () => {
1515
it('should encrypt', async () => {
1616
const agent = new SSHAgentClient()
17-
const identity = await agent.getIdentity('key_rsa')
17+
const identity = await agent.getIdentity('key_ed25519')
1818
if (!identity) {
1919
throw new Error()
2020
}
@@ -25,13 +25,13 @@ describe('SSHAgentClient streams tests', () => {
2525
})
2626
it('should decrypt', async () => {
2727
const agent = new SSHAgentClient()
28-
const identity = await agent.getIdentity('key_rsa')
28+
const identity = await agent.getIdentity('key_ed25519')
2929
if (!identity) {
3030
throw new Error()
3131
}
3232
const transform = await agent.getDecryptTransform(identity, SEED, 'hex')
3333
const stream = Readable.from(
34-
'ecfd6bb57f4891ba7226886e90d2eb848022a495b15ffd91ffe760bca5605f9062c305ee14226d9daf7faa58460c8f50',
34+
'2e306f1403b72eb17fe7187545dbd863be234ad1128ac3f34d60d102ced7bc43a7c4506d341463cff257b4d007b39143',
3535
)
3636
const decrypted = await text(stream.pipe(transform))
3737
chai.assert.strictEqual(decrypted, DECODED_STRING)
@@ -41,7 +41,7 @@ describe('SSHAgentClient streams tests', () => {
4141
describe('SSHAgentClient streams encodings tests', () => {
4242
it('should encrypt to base64', async () => {
4343
const agent = new SSHAgentClient()
44-
const identity = await agent.getIdentity('key_rsa')
44+
const identity = await agent.getIdentity('key_ed25519')
4545
if (!identity) {
4646
throw new Error()
4747
}
@@ -52,12 +52,12 @@ describe('SSHAgentClient streams encodings tests', () => {
5252
})
5353
it('should decrypt from base64', async () => {
5454
const agent = new SSHAgentClient()
55-
const identity = await agent.getIdentity('key_rsa')
55+
const identity = await agent.getIdentity('key_ed25519')
5656
if (!identity) {
5757
throw new Error()
5858
}
5959
const transform = await agent.getDecryptTransform(identity, SEED, 'base64')
60-
const stream = Readable.from('8epe+B3bWcSGTPpyW2MRqHeAKjTj2NVnR4q1YNVB5LfXw9JE02wsb3RqZBTFbMXc')
60+
const stream = Readable.from('5e0UbJX4+Rad+byLPnRNho3Qvjbeeqcwmg7yrXinHrTZ0788uuyvTl9jjbcpErF6')
6161
const decrypted = await text(stream.pipe(transform))
6262
chai.assert.strictEqual(decrypted, DECODED_STRING)
6363
})

0 commit comments

Comments
 (0)