@andrewheberle/serverless-ssh-ca 0.17.5 → 0.17.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +268 -0
- package/dist/api/v3/schema.js +2 -1
- package/dist/proof.js +1 -1
- package/dist/sshsig/formats.js +1 -1
- package/dist/utils.d.ts +2 -1
- package/dist/utils.js +1 -2
- package/package.json +8 -13
- package/dist/src/api/v3/index.d.ts +0 -1
- package/dist/src/api/v3/index.js +0 -263
- package/dist/src/api/v3/schema.d.ts +0 -217
- package/dist/src/api/v3/schema.js +0 -216
- package/dist/src/certificate.d.ts +0 -30
- package/dist/src/certificate.js +0 -154
- package/dist/src/db.d.ts +0 -36
- package/dist/src/db.js +0 -111
- package/dist/src/index.d.ts +0 -6
- package/dist/src/index.js +0 -8
- package/dist/src/krl.d.ts +0 -39
- package/dist/src/krl.js +0 -317
- package/dist/src/logger.d.ts +0 -2
- package/dist/src/logger.js +0 -3
- package/dist/src/proof.d.ts +0 -51
- package/dist/src/proof.js +0 -154
- package/dist/src/router.d.ts +0 -14
- package/dist/src/router.js +0 -22
- package/dist/src/sshsig/armor.d.ts +0 -7
- package/dist/src/sshsig/armor.js +0 -18
- package/dist/src/sshsig/formats.d.ts +0 -52
- package/dist/src/sshsig/formats.js +0 -174
- package/dist/src/sshsig/index.d.ts +0 -15
- package/dist/src/sshsig/index.js +0 -31
- package/dist/src/sshsig/reader.d.ts +0 -20
- package/dist/src/sshsig/reader.js +0 -54
- package/dist/src/sshsig/sig.d.ts +0 -16
- package/dist/src/sshsig/sig.js +0 -1
- package/dist/src/sshsig/sig_parser.d.ts +0 -9
- package/dist/src/sshsig/sig_parser.js +0 -83
- package/dist/src/sshsig/verifier.d.ts +0 -2
- package/dist/src/sshsig/verifier.js +0 -37
- package/dist/src/sshsig/writer.d.ts +0 -17
- package/dist/src/sshsig/writer.js +0 -51
- package/dist/src/types.d.ts +0 -42
- package/dist/src/types.js +0 -20
- package/dist/src/utils.d.ts +0 -76
- package/dist/src/utils.js +0 -370
- package/dist/src/verify.d.ts +0 -6
- package/dist/src/verify.js +0 -17
package/dist/src/krl.js
DELETED
|
@@ -1,317 +0,0 @@
|
|
|
1
|
-
// ── Constants ────────────────────────────────────────────────────────────────
|
|
2
|
-
const KRL_MAGIC = 0x5353484b524c0a00n; // "SSHKRL\n\0"
|
|
3
|
-
const KRL_FORMAT_VERSION = 1;
|
|
4
|
-
const KRL_SECTION_CERTIFICATES = 1;
|
|
5
|
-
const KRL_SECTION_CERT_SERIAL_LIST = 0x20;
|
|
6
|
-
const KRL_SECTION_CERT_KEY_ID = 0x23;
|
|
7
|
-
// ── Low-level encoding helpers ───────────────────────────────────────────────
|
|
8
|
-
/** Encode a uint32 big-endian. */
|
|
9
|
-
function encodeUint32(value) {
|
|
10
|
-
const buf = new Uint8Array(4);
|
|
11
|
-
new DataView(buf.buffer).setUint32(0, value, false);
|
|
12
|
-
return buf;
|
|
13
|
-
}
|
|
14
|
-
/** Encode a uint64 big-endian from a bigint. */
|
|
15
|
-
function encodeUint64(value) {
|
|
16
|
-
const buf = new Uint8Array(8);
|
|
17
|
-
new DataView(buf.buffer).setBigUint64(0, value, false);
|
|
18
|
-
return buf;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Encode an SSH "string" (RFC 4251 §5): uint32 length followed by raw bytes.
|
|
22
|
-
* Accepts either a Uint8Array (raw bytes) or a JS string (encoded as UTF-8).
|
|
23
|
-
*/
|
|
24
|
-
function encodeString(value) {
|
|
25
|
-
const bytes = typeof value === "string" ? new TextEncoder().encode(value) : value;
|
|
26
|
-
const out = new Uint8Array(4 + bytes.length);
|
|
27
|
-
new DataView(out.buffer).setUint32(0, bytes.length, false);
|
|
28
|
-
out.set(bytes, 4);
|
|
29
|
-
return out;
|
|
30
|
-
}
|
|
31
|
-
/** Concatenate an arbitrary number of Uint8Arrays into one. */
|
|
32
|
-
function concat(...parts) {
|
|
33
|
-
const total = parts.reduce((n, p) => n + p.length, 0);
|
|
34
|
-
const out = new Uint8Array(total);
|
|
35
|
-
let offset = 0;
|
|
36
|
-
for (const part of parts) {
|
|
37
|
-
out.set(part, offset);
|
|
38
|
-
offset += part.length;
|
|
39
|
-
}
|
|
40
|
-
return out;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Wrap bytes as a KRL section: byte section_type + string section_data.
|
|
44
|
-
* The spec defines each section as: byte type | string data
|
|
45
|
-
*/
|
|
46
|
-
function encodeSection(sectionType, data) {
|
|
47
|
-
return concat(new Uint8Array([sectionType]), encodeString(data));
|
|
48
|
-
}
|
|
49
|
-
// ── CA public-key extraction ─────────────────────────────────────────────────
|
|
50
|
-
/**
|
|
51
|
-
* Extract the SSH wire-format public key bytes from an sshpk PrivateKey.
|
|
52
|
-
*
|
|
53
|
-
* sshpk's "ssh" format produces a string like:
|
|
54
|
-
* ssh-ed25519 AAAA...base64... [comment]
|
|
55
|
-
*
|
|
56
|
-
* The base64 blob *is* the SSH wire encoding of the public key, so we just
|
|
57
|
-
* decode it. This works for both Ed25519 and ECDSA keys.
|
|
58
|
-
*/
|
|
59
|
-
function caKeyWireBytes(caKey) {
|
|
60
|
-
const sshString = caKey.toPublic().toString("ssh");
|
|
61
|
-
// Format: "<type> <base64> [<comment>]" — we want the second token.
|
|
62
|
-
const base64 = sshString.trim().split(/\s+/)[1];
|
|
63
|
-
if (!base64) {
|
|
64
|
-
throw new Error("Unexpected sshpk 'ssh' format output: " + sshString);
|
|
65
|
-
}
|
|
66
|
-
const binary = atob(base64);
|
|
67
|
-
const bytes = new Uint8Array(binary.length);
|
|
68
|
-
for (let i = 0; i < binary.length; i++) {
|
|
69
|
-
bytes[i] = binary.charCodeAt(i);
|
|
70
|
-
}
|
|
71
|
-
return bytes;
|
|
72
|
-
}
|
|
73
|
-
// ── Constants ────────────────────────────────────────────────────────────────
|
|
74
|
-
const SSHSIG_MAGIC = new TextEncoder().encode("SSHSIG");
|
|
75
|
-
const SIG_VERSION = 1;
|
|
76
|
-
const NAMESPACE = "krl@com.github.serverless-ssh-ca.andrewheberle";
|
|
77
|
-
const HASH_ALGORITHM = "sha512";
|
|
78
|
-
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
79
|
-
/**
|
|
80
|
-
* PEM string → raw DER bytes (strips header/footer and decodes base64).
|
|
81
|
-
* Works for both PKCS8 ("-----BEGIN PRIVATE KEY-----") and any other PEM type.
|
|
82
|
-
*/
|
|
83
|
-
function pemToDer(pem) {
|
|
84
|
-
const lines = pem
|
|
85
|
-
.trim()
|
|
86
|
-
.split("\n")
|
|
87
|
-
.filter((l) => !l.startsWith("-----"));
|
|
88
|
-
const binary = atob(lines.join(""));
|
|
89
|
-
const bytes = new Uint8Array(binary.length);
|
|
90
|
-
for (let i = 0; i < binary.length; i++) {
|
|
91
|
-
bytes[i] = binary.charCodeAt(i);
|
|
92
|
-
}
|
|
93
|
-
return bytes;
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Import an sshpk PrivateKey into a WebCrypto CryptoKey.
|
|
97
|
-
* Supports Ed25519 and ECDSA (P-256 / P-384 / P-521).
|
|
98
|
-
*/
|
|
99
|
-
async function importPrivateKey(caKey) {
|
|
100
|
-
const pkcs8Pem = caKey.toString("pkcs8");
|
|
101
|
-
const der = pemToDer(pkcs8Pem);
|
|
102
|
-
const type = caKey.type; // "ed25519" | "ecdsa" | etc.
|
|
103
|
-
const curve = caKey.curve;
|
|
104
|
-
if (type === "ed25519") {
|
|
105
|
-
const cryptoKey = await crypto.subtle.importKey("pkcs8", der, { name: "Ed25519" }, false, ["sign"]);
|
|
106
|
-
return {
|
|
107
|
-
cryptoKey,
|
|
108
|
-
sigAlgo: "ssh-ed25519",
|
|
109
|
-
webCryptoAlgo: { name: "Ed25519" },
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
else if (type === "ecdsa") {
|
|
113
|
-
let namedCurve;
|
|
114
|
-
let sigAlgo;
|
|
115
|
-
let hash;
|
|
116
|
-
if (curve === "nistp256") {
|
|
117
|
-
namedCurve = "P-256";
|
|
118
|
-
sigAlgo = "ecdsa-sha2-nistp256";
|
|
119
|
-
hash = "SHA-256";
|
|
120
|
-
}
|
|
121
|
-
else if (curve === "nistp384") {
|
|
122
|
-
namedCurve = "P-384";
|
|
123
|
-
sigAlgo = "ecdsa-sha2-nistp384";
|
|
124
|
-
hash = "SHA-384";
|
|
125
|
-
}
|
|
126
|
-
else if (curve === "nistp521") {
|
|
127
|
-
namedCurve = "P-521";
|
|
128
|
-
sigAlgo = "ecdsa-sha2-nistp521";
|
|
129
|
-
hash = "SHA-512";
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
throw new Error(`Unsupported ECDSA curve: ${curve}`);
|
|
133
|
-
}
|
|
134
|
-
const cryptoKey = await crypto.subtle.importKey("pkcs8", der, { name: "ECDSA", namedCurve }, false, ["sign"]);
|
|
135
|
-
return {
|
|
136
|
-
cryptoKey,
|
|
137
|
-
sigAlgo,
|
|
138
|
-
webCryptoAlgo: { name: "ECDSA", hash: { name: hash } },
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
else {
|
|
142
|
-
throw new Error(`Unsupported key type: ${type}`);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Convert a WebCrypto ECDSA signature (IEEE P1363: raw r||s) to the SSH wire
|
|
147
|
-
* encoding: string(algo) + string(mpint(r) + mpint(s)).
|
|
148
|
-
*
|
|
149
|
-
* SSH mpints are big-endian, with a leading 0x00 byte if the high bit is set.
|
|
150
|
-
*/
|
|
151
|
-
function p1363ToSshEcdsaSig(p1363, sigAlgo) {
|
|
152
|
-
const half = p1363.length / 2;
|
|
153
|
-
const r = p1363.slice(0, half);
|
|
154
|
-
const s = p1363.slice(half);
|
|
155
|
-
function encodeMpint(n) {
|
|
156
|
-
// Strip leading zero bytes, then re-pad if high bit set.
|
|
157
|
-
let start = 0;
|
|
158
|
-
while (start < n.length - 1 && n[start] === 0)
|
|
159
|
-
start++;
|
|
160
|
-
const trimmed = n.slice(start);
|
|
161
|
-
const padded = trimmed[0] & 0x80 ? concat(new Uint8Array([0x00]), trimmed) : trimmed;
|
|
162
|
-
return encodeString(padded);
|
|
163
|
-
}
|
|
164
|
-
const innerSig = concat(encodeMpint(r), encodeMpint(s));
|
|
165
|
-
return concat(encodeString(sigAlgo), encodeString(innerSig));
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Wrap an Ed25519 WebCrypto signature into SSH wire encoding:
|
|
169
|
-
* string(algo) + string(sig_bytes)
|
|
170
|
-
*/
|
|
171
|
-
function wrapEd25519Sig(sig) {
|
|
172
|
-
return concat(encodeString("ssh-ed25519"), encodeString(sig));
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Base64-encode bytes and wrap in SSH signature armor at 76 chars per line.
|
|
176
|
-
*/
|
|
177
|
-
function armorSignature(bytes) {
|
|
178
|
-
let b64 = btoa(String.fromCharCode(...bytes));
|
|
179
|
-
const lines = [];
|
|
180
|
-
for (let i = 0; i < b64.length; i += 76) {
|
|
181
|
-
lines.push(b64.slice(i, i + 76));
|
|
182
|
-
}
|
|
183
|
-
return [
|
|
184
|
-
"-----BEGIN SSH SIGNATURE-----",
|
|
185
|
-
...lines,
|
|
186
|
-
"-----END SSH SIGNATURE-----",
|
|
187
|
-
].join("\n");
|
|
188
|
-
}
|
|
189
|
-
// ── KRLBuilder ───────────────────────────────────────────────────────────────
|
|
190
|
-
export class KRLBuilder {
|
|
191
|
-
_caKey;
|
|
192
|
-
_serials = [];
|
|
193
|
-
_keyIds = [];
|
|
194
|
-
_krl = null;
|
|
195
|
-
constructor(caKey) {
|
|
196
|
-
this._caKey = caKey;
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* Add certificate serial numbers to be revoked.
|
|
200
|
-
* May be called multiple times all serials are merged into one section.
|
|
201
|
-
*
|
|
202
|
-
* This must not be called after generate() has been called
|
|
203
|
-
*/
|
|
204
|
-
addSerials(serials) {
|
|
205
|
-
if (this._krl !== null) {
|
|
206
|
-
throw new Error("addSerials cannot be called after KRL has been generated");
|
|
207
|
-
}
|
|
208
|
-
this._serials.push(...serials);
|
|
209
|
-
return this;
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* Add certificate key ID strings to be revoked.
|
|
213
|
-
* May be called multiple times all key IDs are merged into one section.
|
|
214
|
-
*
|
|
215
|
-
* This must not be called after generate() has been called
|
|
216
|
-
*/
|
|
217
|
-
addKeyIds(keyIds) {
|
|
218
|
-
if (this._krl !== null) {
|
|
219
|
-
throw new Error("addKeyIds cannot be called after KRL has been generated");
|
|
220
|
-
}
|
|
221
|
-
this._keyIds.push(...keyIds);
|
|
222
|
-
return this;
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* Generate the binary KRL
|
|
226
|
-
*
|
|
227
|
-
* @param krlVersion Optional KRL version number (default: 1).
|
|
228
|
-
* @param comment Optional comment string embedded in the header.
|
|
229
|
-
*/
|
|
230
|
-
generate(krlVersion = 1n, comment = "") {
|
|
231
|
-
if (this._krl !== null) {
|
|
232
|
-
return this._krl;
|
|
233
|
-
}
|
|
234
|
-
// ── Header ──────────────────────────────────────────────────────────────
|
|
235
|
-
const generatedDate = BigInt(Math.floor(Date.now() / 1000));
|
|
236
|
-
const header = concat(encodeUint64(KRL_MAGIC), encodeUint32(KRL_FORMAT_VERSION), encodeUint64(krlVersion), encodeUint64(generatedDate), encodeUint64(0n), // flags — none defined
|
|
237
|
-
encodeString(""), // reserved
|
|
238
|
-
encodeString(comment));
|
|
239
|
-
// ── Certificate sub-sections ─────────────────────────────────────────────
|
|
240
|
-
const certSubSections = [];
|
|
241
|
-
// KRL_SECTION_CERT_SERIAL_LIST
|
|
242
|
-
if (this._serials.length > 0) {
|
|
243
|
-
const serialBytes = concat(...this._serials.map(encodeUint64));
|
|
244
|
-
certSubSections.push(encodeSection(KRL_SECTION_CERT_SERIAL_LIST, serialBytes));
|
|
245
|
-
}
|
|
246
|
-
// KRL_SECTION_CERT_KEY_ID
|
|
247
|
-
if (this._keyIds.length > 0) {
|
|
248
|
-
const keyIdBytes = concat(...this._keyIds.map((id) => encodeString(id)));
|
|
249
|
-
certSubSections.push(encodeSection(KRL_SECTION_CERT_KEY_ID, keyIdBytes));
|
|
250
|
-
}
|
|
251
|
-
// ── KRL_SECTION_CERTIFICATES wrapper ────────────────────────────────────
|
|
252
|
-
//
|
|
253
|
-
// Format (inside the outer section's string data):
|
|
254
|
-
// string ca_key
|
|
255
|
-
// string reserved
|
|
256
|
-
// <one or more cert sub-sections>
|
|
257
|
-
//
|
|
258
|
-
const certSectionData = concat(encodeString(caKeyWireBytes(this._caKey)), // ca_key
|
|
259
|
-
encodeString(""), // reserved
|
|
260
|
-
...certSubSections);
|
|
261
|
-
const certificatesSection = encodeSection(KRL_SECTION_CERTIFICATES, certSectionData);
|
|
262
|
-
// ── Assemble final KRL ───────────────────────────────────────────────────
|
|
263
|
-
const krl = concat(header, certificatesSection);
|
|
264
|
-
this._krl = krl.buffer;
|
|
265
|
-
return this._krl;
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Return the SSHSIG signature of the KRL.
|
|
269
|
-
*
|
|
270
|
-
* This must be called after generate()
|
|
271
|
-
*
|
|
272
|
-
* The returned string can be verified with ssh-keygen:
|
|
273
|
-
* echo -n "$KRL_BYTES" | ssh-keygen -Y verify -f allowed_signers \
|
|
274
|
-
* -I <identity> -n file -s <sig_file>
|
|
275
|
-
*/
|
|
276
|
-
async signature() {
|
|
277
|
-
let krl;
|
|
278
|
-
if (this._krl === null) {
|
|
279
|
-
krl = this.generate();
|
|
280
|
-
}
|
|
281
|
-
else {
|
|
282
|
-
krl = this._krl;
|
|
283
|
-
}
|
|
284
|
-
const { cryptoKey, sigAlgo, webCryptoAlgo } = await importPrivateKey(this._caKey);
|
|
285
|
-
// 1. Hash the KRL bytes with SHA-512.
|
|
286
|
-
const krlHash = new Uint8Array(await crypto.subtle.digest("SHA-512", krl));
|
|
287
|
-
// 2. Build the signed data blob.
|
|
288
|
-
//
|
|
289
|
-
// byte[6] "SSHSIG"
|
|
290
|
-
// string namespace
|
|
291
|
-
// string reserved (empty)
|
|
292
|
-
// string hash_algorithm
|
|
293
|
-
// string H(message)
|
|
294
|
-
const signedData = concat(SSHSIG_MAGIC, encodeString(NAMESPACE), encodeString(""), // reserved
|
|
295
|
-
encodeString(HASH_ALGORITHM), encodeString(krlHash));
|
|
296
|
-
// 3. Sign the blob.
|
|
297
|
-
const rawSig = new Uint8Array(await crypto.subtle.sign(webCryptoAlgo, cryptoKey, signedData));
|
|
298
|
-
// 4. Encode the signature into SSH wire format.
|
|
299
|
-
const sshSig = sigAlgo === "ssh-ed25519"
|
|
300
|
-
? wrapEd25519Sig(rawSig)
|
|
301
|
-
: p1363ToSshEcdsaSig(rawSig, sigAlgo);
|
|
302
|
-
// 5. Assemble the outer SSHSIG blob.
|
|
303
|
-
//
|
|
304
|
-
// byte[6] "SSHSIG"
|
|
305
|
-
// uint32 SIG_VERSION (1)
|
|
306
|
-
// string publickey
|
|
307
|
-
// string namespace
|
|
308
|
-
// string reserved (empty)
|
|
309
|
-
// string hash_algorithm
|
|
310
|
-
// string signature
|
|
311
|
-
const publicKeyWire = caKeyWireBytes(this._caKey);
|
|
312
|
-
const sigBlob = concat(SSHSIG_MAGIC, encodeUint32(SIG_VERSION), encodeString(publicKeyWire), encodeString(NAMESPACE), encodeString(""), // reserved
|
|
313
|
-
encodeString(HASH_ALGORITHM), encodeString(sshSig));
|
|
314
|
-
// 6. Armor and return.
|
|
315
|
-
return armorSignature(sigBlob);
|
|
316
|
-
}
|
|
317
|
-
}
|
package/dist/src/logger.d.ts
DELETED
package/dist/src/logger.js
DELETED
package/dist/src/proof.d.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { Fingerprint, Key } from "sshpk";
|
|
2
|
-
import { Sig } from "./sshsig/sig";
|
|
3
|
-
import { Logger } from "@andrewheberle/ts-slog";
|
|
4
|
-
export declare class PossessionParseError extends Error {
|
|
5
|
-
constructor(message: string, cause?: unknown);
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* ProofOfPossession is used to check/enforce proof of possession for a SSH
|
|
9
|
-
* private key.
|
|
10
|
-
*
|
|
11
|
-
* This is used to verify that the requester of a certificate actually
|
|
12
|
-
* possesses the private key corresponding to the public key in the certificate
|
|
13
|
-
* request.
|
|
14
|
-
*
|
|
15
|
-
* The proof of possession is a signed message containing a timestamp and the
|
|
16
|
-
* fingerprint of the public key.
|
|
17
|
-
*
|
|
18
|
-
* The signature is made with the private key corresponding to the public key
|
|
19
|
-
* in the certificate request and is a BASE64 encoded SSHSIG signature.
|
|
20
|
-
*/
|
|
21
|
-
export declare class ProofOfPossession {
|
|
22
|
-
readonly timestamp: number;
|
|
23
|
-
readonly fingerprint: Fingerprint;
|
|
24
|
-
readonly signature: Sig;
|
|
25
|
-
readonly signaturePubkey: Key;
|
|
26
|
-
readonly logger: Logger;
|
|
27
|
-
private readonly data;
|
|
28
|
-
constructor(proof: string, options?: {
|
|
29
|
-
from?: number;
|
|
30
|
-
logger?: Logger;
|
|
31
|
-
});
|
|
32
|
-
/**
|
|
33
|
-
*
|
|
34
|
-
* @returns true if signature is valid, false otherwise
|
|
35
|
-
*/
|
|
36
|
-
verify(namespace?: string): Promise<boolean>;
|
|
37
|
-
/**
|
|
38
|
-
*
|
|
39
|
-
* @param keys array of keys to verify finterprint against
|
|
40
|
-
* @returns true if the proof of possession fingerprint matches any of the provided keys, false otherwise
|
|
41
|
-
*/
|
|
42
|
-
matches(key: Key): boolean;
|
|
43
|
-
matches(...keys: Key[]): boolean;
|
|
44
|
-
}
|
|
45
|
-
export declare class PossessionMatchError extends Error {
|
|
46
|
-
constructor(message: string, cause?: unknown);
|
|
47
|
-
}
|
|
48
|
-
export declare class RenewalProofOfPossession extends ProofOfPossession {
|
|
49
|
-
matches(key: Key): never;
|
|
50
|
-
matches(...keys: Key[]): boolean;
|
|
51
|
-
}
|
package/dist/src/proof.js
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { env } from "cloudflare:workers";
|
|
2
|
-
import { ms } from "itty-time";
|
|
3
|
-
import { FingerprintFormatError, parseFingerprint, parseKey } from "sshpk";
|
|
4
|
-
import { verify } from "./sshsig";
|
|
5
|
-
import { parse } from "./sshsig/sig_parser";
|
|
6
|
-
import { group, Logger, LogLevel } from "@andrewheberle/ts-slog";
|
|
7
|
-
const Namespace = "proof-of-possession@com.github.serverless-ssh-ca.andrewheberle";
|
|
8
|
-
export class PossessionParseError extends Error {
|
|
9
|
-
constructor(message, cause) {
|
|
10
|
-
super(message);
|
|
11
|
-
this.name = "PossessionParseError";
|
|
12
|
-
this.cause = cause;
|
|
13
|
-
// This is necessary for proper stack trace in TypeScript
|
|
14
|
-
Object.setPrototypeOf(this, PossessionParseError.prototype);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* ProofOfPossession is used to check/enforce proof of possession for a SSH
|
|
19
|
-
* private key.
|
|
20
|
-
*
|
|
21
|
-
* This is used to verify that the requester of a certificate actually
|
|
22
|
-
* possesses the private key corresponding to the public key in the certificate
|
|
23
|
-
* request.
|
|
24
|
-
*
|
|
25
|
-
* The proof of possession is a signed message containing a timestamp and the
|
|
26
|
-
* fingerprint of the public key.
|
|
27
|
-
*
|
|
28
|
-
* The signature is made with the private key corresponding to the public key
|
|
29
|
-
* in the certificate request and is a BASE64 encoded SSHSIG signature.
|
|
30
|
-
*/
|
|
31
|
-
export class ProofOfPossession {
|
|
32
|
-
timestamp;
|
|
33
|
-
fingerprint;
|
|
34
|
-
signature;
|
|
35
|
-
signaturePubkey;
|
|
36
|
-
logger;
|
|
37
|
-
data;
|
|
38
|
-
constructor(proof, options) {
|
|
39
|
-
this.logger = options?.logger !== undefined ? options.logger : new Logger({ minLevel: LogLevel.None });
|
|
40
|
-
const parts = proof.split(".");
|
|
41
|
-
if (parts.length !== 3) {
|
|
42
|
-
throw new PossessionParseError("invalid proof of possession format");
|
|
43
|
-
}
|
|
44
|
-
const [timestampStr, fingerprintString, signatureBase64] = parts;
|
|
45
|
-
// verify timestamp
|
|
46
|
-
const timestamp = parseInt(timestampStr, 10);
|
|
47
|
-
if (isNaN(timestamp)) {
|
|
48
|
-
throw new PossessionParseError("timestamp was not a number");
|
|
49
|
-
}
|
|
50
|
-
const now = options?.from !== undefined ? options?.from : Date.now();
|
|
51
|
-
const age = now - timestamp;
|
|
52
|
-
const skew = ms(env.CERTIFICATE_REQUEST_TIME_SKEW_MAX);
|
|
53
|
-
if (age > skew) {
|
|
54
|
-
throw new PossessionParseError("timestamp too old");
|
|
55
|
-
}
|
|
56
|
-
if (age + skew < 0) {
|
|
57
|
-
throw new PossessionParseError("timestamp was from the future");
|
|
58
|
-
}
|
|
59
|
-
try {
|
|
60
|
-
// parse fingerprint
|
|
61
|
-
const fingerprint = parseFingerprint(fingerprintString);
|
|
62
|
-
if (fingerprint === undefined) {
|
|
63
|
-
throw new PossessionParseError("fingerprint did not parse");
|
|
64
|
-
}
|
|
65
|
-
// convert siganture from base64
|
|
66
|
-
const signature = Buffer.from(signatureBase64, "base64").toString();
|
|
67
|
-
// set values
|
|
68
|
-
this.timestamp = timestamp;
|
|
69
|
-
try {
|
|
70
|
-
this.signature = parse(signature);
|
|
71
|
-
}
|
|
72
|
-
catch (err) {
|
|
73
|
-
throw new PossessionParseError("proof of possession signature could not be parsed", err);
|
|
74
|
-
}
|
|
75
|
-
this.fingerprint = fingerprint;
|
|
76
|
-
this.data = `${timestamp}.${fingerprintString}`;
|
|
77
|
-
this.signaturePubkey = parseKey(this.signature.publickey.toString());
|
|
78
|
-
// ensure the provided fingerprint matches the signature public key
|
|
79
|
-
if (!this.fingerprint.matches(this.signaturePubkey)) {
|
|
80
|
-
throw new PossessionParseError("proof of possession fingerprint does not match signature public key");
|
|
81
|
-
}
|
|
82
|
-
this.logger.debug("constructor completed", ...group("this", "timestamp", this.timestamp, "fingerprint", this.fingerprint.toString("base64"), "signaturePubkey.fingerprint()", this.signaturePubkey.fingerprint().toString("base64")));
|
|
83
|
-
}
|
|
84
|
-
catch (err) {
|
|
85
|
-
switch (true) {
|
|
86
|
-
case (err instanceof FingerprintFormatError):
|
|
87
|
-
throw new PossessionParseError("proof of possession fingerprint was an invalid format", err);
|
|
88
|
-
default:
|
|
89
|
-
throw err;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
*
|
|
95
|
-
* @returns true if signature is valid, false otherwise
|
|
96
|
-
*/
|
|
97
|
-
async verify(namespace) {
|
|
98
|
-
return await verify(this.signature, this.data, { namespace: namespace || Namespace });
|
|
99
|
-
}
|
|
100
|
-
matches(...keys) {
|
|
101
|
-
this.logger.debug("matches() called", "in", "ProofOfPossession", "keys", keys.length);
|
|
102
|
-
try {
|
|
103
|
-
for (const key of keys) {
|
|
104
|
-
// confirm proof of possession fingerprint matches keys
|
|
105
|
-
this.logger.debug("this.fingerprint.matches()", "in", "ProofOfPossession", "fingerprint", this.fingerprint.toString("base64"), "key", key.toString("ssh"));
|
|
106
|
-
if (!this.fingerprint.matches(key))
|
|
107
|
-
return false;
|
|
108
|
-
// also confirm key used to sign proof of possession matches
|
|
109
|
-
this.logger.debug("this.signaturePubkey.fingerprint().matches()", "in", "ProofOfPossession", "signaturePubkey", this.signaturePubkey.fingerprint().toString("base64"), "key", key.toString("ssh"));
|
|
110
|
-
if (!this.signaturePubkey.fingerprint().matches(key))
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
this.logger.debug("matches() completed", "in", "ProofOfPossession", "keys", keys.length);
|
|
114
|
-
return true;
|
|
115
|
-
}
|
|
116
|
-
catch (err) {
|
|
117
|
-
this.logger.error("matches() error", "in", "ProofOfPossession", "error", err);
|
|
118
|
-
throw err;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
export class PossessionMatchError extends Error {
|
|
123
|
-
constructor(message, cause) {
|
|
124
|
-
super(message);
|
|
125
|
-
this.name = "PossessionMatchError";
|
|
126
|
-
this.cause = cause;
|
|
127
|
-
// This is necessary for proper stack trace in TypeScript
|
|
128
|
-
Object.setPrototypeOf(this, PossessionMatchError.prototype);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
export class RenewalProofOfPossession extends ProofOfPossession {
|
|
132
|
-
matches(...keys) {
|
|
133
|
-
this.logger.debug("matches() called", "in", "RenewalProofOfPossession", "keys", keys.length);
|
|
134
|
-
if (keys.length === 1) {
|
|
135
|
-
throw new PossessionMatchError("must verify both public key and certificate");
|
|
136
|
-
}
|
|
137
|
-
try {
|
|
138
|
-
for (const key of keys) {
|
|
139
|
-
this.logger.debug("this.fingerprint.matches()", "in", "RenewalProofOfPossession", "fingerprint", this.fingerprint.toString("base64"), "key", key.toString("ssh"));
|
|
140
|
-
if (!this.fingerprint.matches(key))
|
|
141
|
-
return false;
|
|
142
|
-
this.logger.debug("this.signaturePubkey.fingerprint().matches()", "in", "RenewalProofOfPossession", "signaturePubkey", this.signaturePubkey.fingerprint().toString("base64"), "key", key.toString("ssh"));
|
|
143
|
-
if (!this.signaturePubkey.fingerprint().matches(key))
|
|
144
|
-
return false;
|
|
145
|
-
}
|
|
146
|
-
this.logger.debug("matches() completed", "in", "RenewalProofOfPossession", "keys", keys.length);
|
|
147
|
-
return true;
|
|
148
|
-
}
|
|
149
|
-
catch (err) {
|
|
150
|
-
this.logger.error("error in matches()", "in", "RenewalProofOfPossession", "error", err);
|
|
151
|
-
throw err;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
package/dist/src/router.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { Hono, type Context } from "hono";
|
|
2
|
-
import { SshCaBindings } from "./types";
|
|
3
|
-
export type CFArgs = [SshCaBindings, ExecutionContext];
|
|
4
|
-
export type AppContext = Context<{
|
|
5
|
-
Bindings: SshCaBindings;
|
|
6
|
-
}>;
|
|
7
|
-
export declare const app: Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
|
|
8
|
-
export declare const openapi: import("chanfana").HonoOpenAPIRouterType<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
|
|
9
|
-
export declare const oidcAuth: {
|
|
10
|
-
name: string;
|
|
11
|
-
ref: {
|
|
12
|
-
$ref: string;
|
|
13
|
-
};
|
|
14
|
-
};
|
package/dist/src/router.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { fromHono } from "chanfana";
|
|
2
|
-
import { Hono } from "hono";
|
|
3
|
-
import { api as apiv3 } from "./api/v3";
|
|
4
|
-
import { HTTPException } from "hono/http-exception";
|
|
5
|
-
import { logger } from "./logger";
|
|
6
|
-
export const app = new Hono();
|
|
7
|
-
// add error handling
|
|
8
|
-
app.onError((err, c) => {
|
|
9
|
-
if (err instanceof HTTPException) {
|
|
10
|
-
return err.getResponse();
|
|
11
|
-
}
|
|
12
|
-
// Handle other errors
|
|
13
|
-
logger.error("unexpected error from router", "error", err);
|
|
14
|
-
return c.json({ success: false, errors: [{ code: 7000, message: "Internal Server Error" }] }, 500);
|
|
15
|
-
});
|
|
16
|
-
export const openapi = fromHono(app);
|
|
17
|
-
export const oidcAuth = openapi.registry.registerComponent("securitySchemes", "oidcAuth", {
|
|
18
|
-
type: 'http',
|
|
19
|
-
scheme: 'bearer',
|
|
20
|
-
bearerFormat: 'JWT',
|
|
21
|
-
});
|
|
22
|
-
openapi.route("/api/v3", apiv3);
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Removes armoring from text SSH signatures and decodes it to raw bytes.
|
|
3
|
-
*
|
|
4
|
-
* @param {string} text SSH signature in armored format.
|
|
5
|
-
* @returns {Uint8Array} Raw bytes of the signature.
|
|
6
|
-
*/
|
|
7
|
-
export declare function dearmor(text: string): Uint8Array<ArrayBuffer>;
|
package/dist/src/sshsig/armor.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Removes armoring from text SSH signatures and decodes it to raw bytes.
|
|
3
|
-
*
|
|
4
|
-
* @param {string} text SSH signature in armored format.
|
|
5
|
-
* @returns {Uint8Array} Raw bytes of the signature.
|
|
6
|
-
*/
|
|
7
|
-
export function dearmor(text) {
|
|
8
|
-
const lines = text.trim().split("\n");
|
|
9
|
-
const first = lines.shift();
|
|
10
|
-
if (first !== "-----BEGIN SSH SIGNATURE-----") {
|
|
11
|
-
throw new Error("Bad header line, expected -----BEGIN SSH SIGNATURE----- got: " + first);
|
|
12
|
-
}
|
|
13
|
-
const last = lines.pop();
|
|
14
|
-
if (last !== "-----END SSH SIGNATURE-----") {
|
|
15
|
-
throw new Error("Bad trailer line, expected -----END SSH SIGNATURE----- got: " + last);
|
|
16
|
-
}
|
|
17
|
-
return Uint8Array.from(atob(lines.join("")).split("").map((x) => x.charCodeAt(0)));
|
|
18
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { Reader } from "./reader";
|
|
2
|
-
/**
|
|
3
|
-
* Represents an SSH public key.
|
|
4
|
-
*/
|
|
5
|
-
export type Pubkey = {
|
|
6
|
-
pk_algo: "ssh-rsa";
|
|
7
|
-
e: Uint8Array;
|
|
8
|
-
n: Uint8Array;
|
|
9
|
-
toString(): string;
|
|
10
|
-
} | {
|
|
11
|
-
pk_algo: "ssh-ed25519";
|
|
12
|
-
key: Uint8Array;
|
|
13
|
-
toString(): string;
|
|
14
|
-
} | {
|
|
15
|
-
pk_algo: "ecdsa-sha2-nistp256" | "ecdsa-sha2-nistp384" | "ecdsa-sha2-nistp521";
|
|
16
|
-
curve: string;
|
|
17
|
-
point: Uint8Array;
|
|
18
|
-
toString(): string;
|
|
19
|
-
} | {
|
|
20
|
-
pk_algo: "sk-ecdsa-sha2-nistp256@openssh.com";
|
|
21
|
-
curve: string;
|
|
22
|
-
point: Uint8Array;
|
|
23
|
-
application: string;
|
|
24
|
-
toString(): string;
|
|
25
|
-
} | {
|
|
26
|
-
pk_algo: "sk-ssh-ed25519@openssh.com";
|
|
27
|
-
key: Uint8Array;
|
|
28
|
-
application: string;
|
|
29
|
-
toString(): string;
|
|
30
|
-
};
|
|
31
|
-
export declare function parsePubkey(pk_algo: string, publickey: Reader, raw_publickey: ArrayBuffer): Pubkey;
|
|
32
|
-
export declare function convertPublicKey(publickey: Pubkey): {
|
|
33
|
-
format: "jwk";
|
|
34
|
-
keyData: JsonWebKey;
|
|
35
|
-
} | {
|
|
36
|
-
format: "raw";
|
|
37
|
-
keyData: ArrayBuffer;
|
|
38
|
-
};
|
|
39
|
-
export declare function convertAlgorithm(sig_algo: string): {
|
|
40
|
-
name: string;
|
|
41
|
-
hash: {
|
|
42
|
-
name: string;
|
|
43
|
-
};
|
|
44
|
-
namedCurve?: undefined;
|
|
45
|
-
} | {
|
|
46
|
-
name: string;
|
|
47
|
-
namedCurve: string;
|
|
48
|
-
hash: {
|
|
49
|
-
name: string;
|
|
50
|
-
};
|
|
51
|
-
};
|
|
52
|
-
export declare function convertHash(hashName: string): string;
|