mirror your GitHub repos to tangled.org automatically
1import crypto from 'node:crypto'
2
3/**
4 * Generate an ed25519 SSH keypair. Returns the OpenSSH-formatted public key
5 * (suitable for `sh.tangled.publicKey` records / GitHub deploy keys / authorized_keys)
6 * and the PKCS#8-PEM-encoded private key (suitable for storage).
7 *
8 * We store PKCS#8 because Node loads it natively via `crypto.createPrivateKey`.
9 * Conversion to OpenSSH private key format (what `git`/`ssh-agent` consumes) is
10 * deferred until commit 12, where it lives next to the SSH push code.
11 */
12export interface GeneratedKeypair {
13 publicKeyOpenSsh: string
14 privateKeyPem: string
15}
16
17export function generateKeypair(comment: string): GeneratedKeypair {
18 const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519', {
19 publicKeyEncoding: { type: 'spki', format: 'der' },
20 privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
21 })
22
23 // SPKI-DER for ed25519 is a fixed 44-byte ASN.1 wrapper; the last 32 bytes
24 // are the raw public key. (See RFC 8410 §4.) Skip the wrapper.
25 const rawPublic = (publicKey as Buffer).subarray(-32)
26
27 return {
28 publicKeyOpenSsh: encodeOpenSshEd25519(rawPublic, comment),
29 privateKeyPem: privateKey as string,
30 }
31}
32
33/**
34 * Encode a 32-byte ed25519 public key in OpenSSH `authorized_keys` format:
35 * ssh-ed25519 <base64(string("ssh-ed25519") + string(rawKey))> <comment>
36 *
37 * The base64 payload uses SSH's length-prefixed string format (uint32 big-endian
38 * length + bytes), per RFC 4253 §6.6 and the ed25519 draft.
39 */
40function encodeOpenSshEd25519(rawPublicKey: Buffer, comment: string): string {
41 if (rawPublicKey.length !== 32) {
42 throw new Error(`expected 32 raw bytes for ed25519 public key, got ${rawPublicKey.length}`)
43 }
44
45 const algo = Buffer.from('ssh-ed25519', 'utf8')
46 const payload = Buffer.concat([
47 sshString(algo),
48 sshString(rawPublicKey),
49 ])
50 return `ssh-ed25519 ${payload.toString('base64')} ${comment}`
51}
52
53function sshString(buf: Buffer): Buffer {
54 const len = Buffer.alloc(4)
55 len.writeUInt32BE(buf.length, 0)
56 return Buffer.concat([len, buf])
57}