@andrewheberle/serverless-ssh-ca 0.22.0 → 0.24.0
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/README.md +174 -1
- package/dist/api/operations.d.ts +50 -0
- package/dist/api/operations.d.ts.map +1 -0
- package/dist/api/operations.js +264 -0
- package/dist/api/operations.js.map +1 -0
- package/dist/api/schema.d.ts +72 -0
- package/dist/api/schema.d.ts.map +1 -0
- package/dist/api/schema.js +114 -0
- package/dist/api/schema.js.map +1 -0
- package/dist/api/v3/index.d.ts.map +1 -1
- package/dist/api/v3/index.js +13 -202
- package/dist/api/v3/index.js.map +1 -1
- package/dist/api/v3/schema.d.ts +23 -68
- package/dist/api/v3/schema.d.ts.map +1 -1
- package/dist/api/v3/schema.js +18 -104
- package/dist/api/v3/schema.js.map +1 -1
- package/dist/api/v4/index.d.ts +8 -0
- package/dist/api/v4/index.d.ts.map +1 -0
- package/dist/api/v4/index.js +155 -0
- package/dist/api/v4/index.js.map +1 -0
- package/dist/api/v4/schema.d.ts +268 -0
- package/dist/api/v4/schema.d.ts.map +1 -0
- package/dist/api/v4/schema.js +186 -0
- package/dist/api/v4/schema.js.map +1 -0
- package/dist/certificate.d.ts +9 -6
- package/dist/certificate.d.ts.map +1 -1
- package/dist/certificate.js +55 -99
- package/dist/certificate.js.map +1 -1
- package/dist/db/index.d.ts +68 -9
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +165 -49
- package/dist/db/index.js.map +1 -1
- package/dist/db/migrations/0003_krl_revision.d.ts +3 -0
- package/dist/db/migrations/0003_krl_revision.d.ts.map +1 -0
- package/dist/db/migrations/0003_krl_revision.js +14 -0
- package/dist/db/migrations/0003_krl_revision.js.map +1 -0
- package/dist/db/migrations/index.d.ts.map +1 -1
- package/dist/db/migrations/index.js +2 -0
- package/dist/db/migrations/index.js.map +1 -1
- package/dist/db/types.d.ts +2 -0
- package/dist/db/types.d.ts.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/krl.d.ts +3 -2
- package/dist/krl.d.ts.map +1 -1
- package/dist/krl.js +12 -165
- package/dist/krl.js.map +1 -1
- package/dist/node/cache.d.ts +21 -0
- package/dist/node/cache.d.ts.map +1 -0
- package/dist/node/cache.js +49 -0
- package/dist/node/cache.js.map +1 -0
- package/dist/node/env.d.ts +6 -3
- package/dist/node/env.d.ts.map +1 -1
- package/dist/node/env.js +4 -3
- package/dist/node/env.js.map +1 -1
- package/dist/node/index.d.ts +1 -0
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +1 -0
- package/dist/node/index.js.map +1 -1
- package/dist/proof.d.ts +36 -7
- package/dist/proof.d.ts.map +1 -1
- package/dist/proof.js +53 -21
- package/dist/proof.js.map +1 -1
- package/dist/router.d.ts +25 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +41 -2
- package/dist/router.js.map +1 -1
- package/dist/ssh/certificate.d.ts +93 -0
- package/dist/ssh/certificate.d.ts.map +1 -0
- package/dist/ssh/certificate.js +178 -0
- package/dist/ssh/certificate.js.map +1 -0
- package/dist/ssh/encoding.d.ts +41 -0
- package/dist/ssh/encoding.d.ts.map +1 -0
- package/dist/ssh/encoding.js +70 -0
- package/dist/ssh/encoding.js.map +1 -0
- package/dist/ssh/keys.d.ts +92 -0
- package/dist/ssh/keys.d.ts.map +1 -0
- package/dist/ssh/keys.js +277 -0
- package/dist/ssh/keys.js.map +1 -0
- package/dist/ssh/signer.d.ts +35 -0
- package/dist/ssh/signer.d.ts.map +1 -0
- package/dist/ssh/signer.js +99 -0
- package/dist/ssh/signer.js.map +1 -0
- package/dist/types.d.ts +64 -19
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +107 -21
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +251 -65
- package/dist/utils.js.map +1 -1
- package/dist/verify.d.ts +17 -5
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +47 -9
- package/dist/verify.js.map +1 -1
- package/openapi.json +1942 -1
- package/package.json +4 -3
- package/src/api/operations.ts +314 -0
- package/src/api/schema.ts +146 -0
- package/src/api/v3/index.ts +21 -248
- package/src/api/v3/schema.ts +36 -128
- package/src/api/v4/index.ts +266 -0
- package/src/api/v4/schema.ts +241 -0
- package/src/certificate.ts +62 -123
- package/src/db/index.ts +196 -56
- package/src/db/migrations/0003_krl_revision.ts +15 -0
- package/src/db/migrations/index.ts +2 -0
- package/src/db/types.ts +2 -0
- package/src/index.ts +6 -4
- package/src/krl.ts +13 -197
- package/src/node/cache.ts +62 -0
- package/src/node/env.ts +6 -5
- package/src/node/index.ts +1 -0
- package/src/proof.ts +65 -31
- package/src/router.ts +67 -3
- package/src/ssh/certificate.ts +236 -0
- package/src/ssh/encoding.ts +81 -0
- package/src/ssh/keys.ts +327 -0
- package/src/ssh/signer.ts +129 -0
- package/src/types.ts +60 -20
- package/src/utils.ts +285 -81
- package/src/verify.ts +54 -14
package/dist/certificate.js
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
import { seconds } from "itty-time";
|
|
2
|
-
import sshpk from "sshpk";
|
|
3
2
|
import { getPrivateKey, split } from "./utils.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
/* const DefaultCreateCertificateOptions: CreateCertificateOptions = {
|
|
7
|
-
lifetime: seconds(env.SSH_CERTIFICATE_LIFETIME),
|
|
8
|
-
principals: [],
|
|
9
|
-
extensions: sshCertificateExtensions
|
|
10
|
-
} */
|
|
3
|
+
import { signCertificate } from "./ssh/certificate.js";
|
|
4
|
+
import { createSigner } from "./ssh/signer.js";
|
|
11
5
|
export class CertificateError extends Error {
|
|
12
6
|
constructor(message) {
|
|
13
7
|
super(message);
|
|
@@ -16,17 +10,32 @@ export class CertificateError extends Error {
|
|
|
16
10
|
Object.setPrototypeOf(this, CertificateError.prototype);
|
|
17
11
|
}
|
|
18
12
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Signs a certificate with the CA key
|
|
15
|
+
*
|
|
16
|
+
* @throws {@link CertificateError} when the certificate has no principals,
|
|
17
|
+
* as OpenSSH accepts a certificate without principals for any user or host
|
|
18
|
+
*/
|
|
19
|
+
const sign = async (key, fields, lifetime) => {
|
|
20
|
+
if (fields.principals.length === 0) {
|
|
21
|
+
throw new CertificateError("a certificate must have at least one principal");
|
|
22
|
+
}
|
|
23
|
+
// the validity is to the second, so starts at the current second rather
|
|
24
|
+
// than in the future
|
|
25
|
+
const validAfter = new Date(Math.floor(Date.now() / 1000) * 1000);
|
|
26
|
+
const validBefore = new Date(validAfter.getTime() + lifetime * 1000);
|
|
27
|
+
return await signCertificate({ ...fields, validAfter, validBefore }, await createSigner(key));
|
|
28
|
+
};
|
|
29
|
+
export const generateCertificate = async (env, email, key, public_key, options) => {
|
|
30
|
+
const { lifetime, principals, extensions, serial } = options ?? {};
|
|
31
|
+
// add principals
|
|
23
32
|
const includeSelf = env.SSH_CERTIFICATE_INCLUDE_SELF === undefined
|
|
24
33
|
? false
|
|
25
34
|
: typeof env.SSH_CERTIFICATE_INCLUDE_SELF === "string"
|
|
26
35
|
? env.SSH_CERTIFICATE_INCLUDE_SELF === "true"
|
|
27
36
|
: env.SSH_CERTIFICATE_INCLUDE_SELF;
|
|
28
|
-
const
|
|
29
|
-
? [
|
|
37
|
+
const certificatePrincipals = includeSelf
|
|
38
|
+
? [email.split("@")[0]]
|
|
30
39
|
: [];
|
|
31
40
|
// add email?
|
|
32
41
|
const includeSelfEmail = env.SSH_CERTIFICATE_INCLUDE_SELF_EMAIL === undefined
|
|
@@ -35,69 +44,34 @@ export const generateCertificate = (env, email, key, public_key, options) => {
|
|
|
35
44
|
? env.SSH_CERTIFICATE_INCLUDE_SELF_EMAIL === "true"
|
|
36
45
|
: env.SSH_CERTIFICATE_INCLUDE_SELF_EMAIL;
|
|
37
46
|
if (includeSelfEmail) {
|
|
38
|
-
|
|
47
|
+
certificatePrincipals.push(email);
|
|
39
48
|
}
|
|
40
49
|
if (principals !== undefined) {
|
|
41
|
-
|
|
42
|
-
identity.push(identityForUser(p));
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
for (const p of split(env.SSH_CERTIFICATE_PRINCIPALS)) {
|
|
46
|
-
identity.push(identityForUser(p));
|
|
47
|
-
}
|
|
48
|
-
// lifetime is the smaller of what was provided in the options or the default
|
|
49
|
-
lifetime = lifetime !== undefined
|
|
50
|
-
? Math.min(lifetime, seconds(env.SSH_CERTIFICATE_LIFETIME))
|
|
51
|
-
: seconds(env.SSH_CERTIFICATE_LIFETIME);
|
|
52
|
-
// generate value for serial of certificate
|
|
53
|
-
const s = generateSerial(serial);
|
|
54
|
-
// set issuer of certificate based on ISSUER_DN
|
|
55
|
-
const issuer = identityFromDN(env.ISSUER_DN);
|
|
56
|
-
// create certificate
|
|
57
|
-
const certificate = createCertificate(identity, public_key, issuer, key, { lifetime: lifetime, serial: s });
|
|
58
|
-
// ensure openssh info is included in certificate (should not occur)
|
|
59
|
-
if (certificate.signatures.openssh === undefined) {
|
|
60
|
-
throw new CertificateError("missing openssh information in certificate");
|
|
50
|
+
certificatePrincipals.push(...principals);
|
|
61
51
|
}
|
|
52
|
+
certificatePrincipals.push(...split(env.SSH_CERTIFICATE_PRINCIPALS));
|
|
62
53
|
const sshCertificateExtensions = split(env.SSH_CERTIFICATE_EXTENSIONS);
|
|
63
|
-
//
|
|
64
|
-
const sshextensions = [];
|
|
54
|
+
// if extensions are provided in request, ensure they do not include extra extensions beyond the defaults
|
|
65
55
|
if (extensions !== undefined) {
|
|
66
|
-
// if extensions are provided in request, ensure they do not include extra extensions beyond the defaults
|
|
67
56
|
for (const ext of extensions) {
|
|
68
57
|
if (!sshCertificateExtensions.includes(ext)) {
|
|
69
58
|
throw new CertificateError(`${ext} is not allowed`);
|
|
70
59
|
}
|
|
71
|
-
// add to list of allowed extensions
|
|
72
|
-
sshextensions.push({
|
|
73
|
-
critical: false,
|
|
74
|
-
name: ext,
|
|
75
|
-
data: Buffer.alloc(0)
|
|
76
|
-
});
|
|
77
60
|
}
|
|
78
61
|
}
|
|
79
|
-
|
|
62
|
+
// lifetime is the smaller of what was provided in the options or the default
|
|
63
|
+
const certificateLifetime = lifetime !== undefined
|
|
64
|
+
? Math.min(lifetime, seconds(env.SSH_CERTIFICATE_LIFETIME))
|
|
65
|
+
: seconds(env.SSH_CERTIFICATE_LIFETIME);
|
|
66
|
+
return await sign(key, {
|
|
67
|
+
kind: "user",
|
|
68
|
+
publicKey: public_key,
|
|
69
|
+
serial: serial ?? generateSerial(),
|
|
70
|
+
keyId: email,
|
|
71
|
+
principals: certificatePrincipals,
|
|
80
72
|
// use defaults if not provided
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
critical: false,
|
|
84
|
-
name: ext,
|
|
85
|
-
data: Buffer.alloc(0)
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
// add info to certificate
|
|
90
|
-
certificate.signatures = {
|
|
91
|
-
openssh: {
|
|
92
|
-
nonce: certificate.signatures.openssh.nonce,
|
|
93
|
-
keyId: email,
|
|
94
|
-
signature: certificate.signatures.openssh.signature,
|
|
95
|
-
exts: sshextensions
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
// re-sign after changes
|
|
99
|
-
certificate.signWith(key);
|
|
100
|
-
return certificate;
|
|
73
|
+
extensions: extensions ?? sshCertificateExtensions,
|
|
74
|
+
}, certificateLifetime);
|
|
101
75
|
};
|
|
102
76
|
export async function createSignedCertificate(env, email, public_key, options) {
|
|
103
77
|
// grab private key from secret store
|
|
@@ -109,7 +83,7 @@ export async function createSignedCertificate(env, email, public_key, options) {
|
|
|
109
83
|
extensions: split(env.SSH_CERTIFICATE_EXTENSIONS)
|
|
110
84
|
};
|
|
111
85
|
}
|
|
112
|
-
return generateCertificate(env, email, key, public_key, options);
|
|
86
|
+
return await generateCertificate(env, email, key, public_key, options);
|
|
113
87
|
}
|
|
114
88
|
export class BadIssuerError extends Error {
|
|
115
89
|
constructor(message) {
|
|
@@ -119,44 +93,26 @@ export class BadIssuerError extends Error {
|
|
|
119
93
|
}
|
|
120
94
|
}
|
|
121
95
|
export async function createSignedHostCertificate(env, public_key, options) {
|
|
122
|
-
|
|
123
|
-
options = {
|
|
124
|
-
lifetime: seconds(env.SSH_HOST_CERTIFICATE_LIFETIME),
|
|
125
|
-
principals: []
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
// generate list of identities for host key
|
|
129
|
-
const identity = [];
|
|
130
|
-
if (options.principals !== undefined) {
|
|
131
|
-
for (const p of options.principals) {
|
|
132
|
-
identity.push(identityForHost(p));
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
// add any already provided subjects (for renewals only)
|
|
136
|
-
if (options.subjects !== undefined) {
|
|
137
|
-
identity.push(...options.subjects);
|
|
138
|
-
}
|
|
96
|
+
const principals = options?.principals ?? [];
|
|
139
97
|
// grab private key from secret store
|
|
140
98
|
const key = await getPrivateKey(env);
|
|
141
99
|
// lifetime is the smaller of what was provided in the options or the default
|
|
142
|
-
const lifetime = options
|
|
100
|
+
const lifetime = options?.lifetime !== undefined
|
|
143
101
|
? Math.min(options.lifetime, seconds(env.SSH_HOST_CERTIFICATE_LIFETIME))
|
|
144
102
|
: seconds(env.SSH_HOST_CERTIFICATE_LIFETIME);
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
103
|
+
return await sign(key, {
|
|
104
|
+
kind: "host",
|
|
105
|
+
publicKey: public_key,
|
|
106
|
+
serial: generateSerial(),
|
|
107
|
+
keyId: `host_${principals[0]}`,
|
|
108
|
+
principals,
|
|
109
|
+
extensions: [],
|
|
110
|
+
}, lifetime);
|
|
152
111
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
const randomBytes = crypto.getRandomValues(new Uint8Array(8));
|
|
160
|
-
return Buffer.from(randomBytes);
|
|
112
|
+
/**
|
|
113
|
+
* Returns a random certificate serial number
|
|
114
|
+
*/
|
|
115
|
+
export const generateSerial = () => {
|
|
116
|
+
return new DataView(crypto.getRandomValues(new Uint8Array(8)).buffer).getBigUint64(0);
|
|
161
117
|
};
|
|
162
118
|
//# sourceMappingURL=certificate.js.map
|
package/dist/certificate.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"certificate.js","sourceRoot":"","sources":["../src/certificate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"certificate.js","sourceRoot":"","sources":["../src/certificate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEnC,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAEjD,OAAO,EAAE,eAAe,EAA+C,MAAM,sBAAsB,CAAA;AACnG,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAQ9C,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IAC1C,YAAY,OAAe;QAC1B,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAA;QAE9B,yDAAyD;QACzD,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,gBAAgB,CAAC,SAAS,CAAC,CAAA;IACxD,CAAC;CACD;AAWD;;;;;GAKG;AACH,MAAM,IAAI,GAAG,KAAK,EAAE,GAAe,EAAE,MAA6D,EAAE,QAAgB,EAA2B,EAAE;IAChJ,IAAI,MAAM,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,gBAAgB,CAAC,gDAAgD,CAAC,CAAA;IAC7E,CAAC;IAED,wEAAwE;IACxE,qBAAqB;IACrB,MAAM,UAAU,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,CAAA;IACjE,MAAM,WAAW,GAAG,IAAI,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,QAAQ,GAAG,IAAI,CAAC,CAAA;IAEpE,OAAO,MAAM,eAAe,CAAC,EAAE,GAAG,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,EAAE,MAAM,YAAY,CAAC,GAAG,CAAC,CAAC,CAAA;AAC9F,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,EAAE,GAAkB,EAAE,KAAa,EAAE,GAAe,EAAE,UAAqB,EAAE,OAAoC,EAA2B,EAAE;IACrL,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,IAAI,EAAE,CAAA;IAElE,iBAAiB;IACjB,MAAM,WAAW,GAAG,GAAG,CAAC,4BAA4B,KAAK,SAAS;QACjE,CAAC,CAAC,KAAK;QACP,CAAC,CAAC,OAAO,GAAG,CAAC,4BAA4B,KAAK,QAAQ;YACrD,CAAC,CAAC,GAAG,CAAC,4BAA4B,KAAK,MAAM;YAC7C,CAAC,CAAC,GAAG,CAAC,4BAA4B,CAAA;IACpC,MAAM,qBAAqB,GAAG,WAAW;QACxC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC;QACxB,CAAC,CAAC,EAAE,CAAA;IAEL,aAAa;IACb,MAAM,gBAAgB,GAAG,GAAG,CAAC,kCAAkC,KAAK,SAAS;QAC5E,CAAC,CAAC,KAAK;QACP,CAAC,CAAC,OAAO,GAAG,CAAC,kCAAkC,KAAK,QAAQ;YAC3D,CAAC,CAAC,GAAG,CAAC,kCAAkC,KAAK,MAAM;YACnD,CAAC,CAAC,GAAG,CAAC,kCAAkC,CAAA;IAC1C,IAAI,gBAAgB,EAAE,CAAC;QACtB,qBAAqB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAClC,CAAC;IAED,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC9B,qBAAqB,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,CAAA;IAC1C,CAAC;IAED,qBAAqB,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAC,CAAA;IAEpE,MAAM,wBAAwB,GAAG,KAAK,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAA;IAEtE,yGAAyG;IACzG,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC9B,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;YAC9B,IAAI,CAAC,wBAAwB,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC7C,MAAM,IAAI,gBAAgB,CAAC,GAAG,GAAG,iBAAiB,CAAC,CAAA;YACpD,CAAC;QACF,CAAC;IACF,CAAC;IAED,6EAA6E;IAC7E,MAAM,mBAAmB,GAAG,QAAQ,KAAK,SAAS;QACjD,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC;QAC3D,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAA;IAExC,OAAO,MAAM,IAAI,CAAC,GAAG,EAAE;QACtB,IAAI,EAAE,MAAM;QACZ,SAAS,EAAE,UAAU;QACrB,MAAM,EAAE,MAAM,IAAI,cAAc,EAAE;QAClC,KAAK,EAAE,KAAK;QACZ,UAAU,EAAE,qBAAqB;QACjC,+BAA+B;QAC/B,UAAU,EAAE,UAAU,IAAI,wBAAwB;KAClD,EAAE,mBAAmB,CAAC,CAAA;AACxB,CAAC,CAAA;AAED,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,GAAkB,EAAE,KAAa,EAAE,UAAqB,EAAE,OAAkC;IACzI,qCAAqC;IACrC,MAAM,GAAG,GAAG,MAAM,aAAa,CAAC,GAAG,CAAC,CAAA;IAEpC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO,GAAG;YACT,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC;YAC/C,UAAU,EAAE,EAAE;YACd,UAAU,EAAE,KAAK,CAAC,GAAG,CAAC,0BAA0B,CAAC;SACjD,CAAA;IACF,CAAC;IAED,OAAO,MAAM,mBAAmB,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,UAAU,EAAE,OAAO,CAAC,CAAA;AACvE,CAAC;AAOD,MAAM,OAAO,cAAe,SAAQ,KAAK;IACxC,YAAY,OAAe;QAC1B,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAA;QAE5B,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,cAAc,CAAC,SAAS,CAAC,CAAA;IACtD,CAAC;CACD;AAED,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAAC,GAAkB,EAAE,UAAqB,EAAE,OAAsC;IAClI,MAAM,UAAU,GAAG,OAAO,EAAE,UAAU,IAAI,EAAE,CAAA;IAE5C,qCAAqC;IACrC,MAAM,GAAG,GAAG,MAAM,aAAa,CAAC,GAAG,CAAC,CAAA;IAEpC,6EAA6E;IAC7E,MAAM,QAAQ,GAAG,OAAO,EAAE,QAAQ,KAAK,SAAS;QAC/C,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,6BAA6B,CAAC,CAAC;QACxE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,6BAA6B,CAAC,CAAA;IAE7C,OAAO,MAAM,IAAI,CAAC,GAAG,EAAE;QACtB,IAAI,EAAE,MAAM;QACZ,SAAS,EAAE,UAAU;QACrB,MAAM,EAAE,cAAc,EAAE;QACxB,KAAK,EAAE,QAAQ,UAAU,CAAC,CAAC,CAAC,EAAE;QAC9B,UAAU;QACV,UAAU,EAAE,EAAE;KACd,EAAE,QAAQ,CAAC,CAAA;AACb,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,GAAW,EAAE;IAC1C,OAAO,IAAI,QAAQ,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;AACtF,CAAC,CAAA"}
|
package/dist/db/index.d.ts
CHANGED
|
@@ -1,19 +1,77 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { PublicKey } from "../ssh/keys.js";
|
|
2
|
+
import type { SshCertificate } from "../ssh/certificate.js";
|
|
2
3
|
import type { SshCaBindings } from "../types.js";
|
|
3
|
-
import { CertificateType
|
|
4
|
+
import { CertificateType } from "./types.js";
|
|
4
5
|
export declare const dbCleanup: (env: SshCaBindings) => Promise<void>;
|
|
5
|
-
export declare const recordCertificate: (env: SshCaBindings, certificate:
|
|
6
|
+
export declare const recordCertificate: (env: SshCaBindings, certificate: SshCertificate, keyid: string, certificateType?: CertificateType) => Promise<void>;
|
|
7
|
+
/**
|
|
8
|
+
* Records a host certificate issued by renewing another, revoking the
|
|
9
|
+
* certificate that was renewed so it cannot be used or renewed again.
|
|
10
|
+
*
|
|
11
|
+
* This is done in a single transaction, and the renewed certificate is only
|
|
12
|
+
* recorded if this call revoked the previous certificate, so when a
|
|
13
|
+
* certificate is renewed more than once concurrently only one renewal is
|
|
14
|
+
* recorded. A previous certificate that was never recorded (for example one
|
|
15
|
+
* signed with the CA key outside of the CA) is recorded so that it can be
|
|
16
|
+
* revoked.
|
|
17
|
+
*
|
|
18
|
+
* @param env The CA bindings
|
|
19
|
+
* @param previous The certificate that was presented for renewal
|
|
20
|
+
* @param certificate The certificate issued by the renewal
|
|
21
|
+
* @returns true if the renewal was recorded, or false if the previous certificate was already revoked
|
|
22
|
+
*/
|
|
23
|
+
export declare const recordRenewal: (env: SshCaBindings, previous: SshCertificate, certificate: SshCertificate) => Promise<boolean>;
|
|
6
24
|
export declare const isRevoked: (env: SshCaBindings, serial: bigint) => Promise<boolean>;
|
|
7
25
|
export declare enum RevocationStatus {
|
|
8
26
|
NotFound = 0,
|
|
9
27
|
Revoked = 1,
|
|
10
|
-
Revokable = 2
|
|
11
|
-
Unrevokable = 3
|
|
28
|
+
Revokable = 2
|
|
12
29
|
}
|
|
13
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Returns whether a certificate can be revoked by the holder of a public key.
|
|
32
|
+
*
|
|
33
|
+
* The public key is compared by fingerprint, so a key with a different
|
|
34
|
+
* comment to the one the certificate was issued for still matches.
|
|
35
|
+
*
|
|
36
|
+
* @param env The CA bindings
|
|
37
|
+
* @param serial The serial of the certificate
|
|
38
|
+
* @param publicKey The public key the requester has proven possession of
|
|
39
|
+
* @param certificateType The type of certificate
|
|
40
|
+
* @returns The revocation status of the certificate
|
|
41
|
+
*/
|
|
42
|
+
export declare const revocationStatus: (env: SshCaBindings, serial: bigint, publicKey: PublicKey, certificateType: CertificateType) => Promise<RevocationStatus>;
|
|
14
43
|
export declare const getRevocationList: (env: SshCaBindings, certificateType: CertificateType) => Promise<string[]>;
|
|
15
|
-
|
|
44
|
+
/** The time a certificate was revoked, as an ISO 8601 string */
|
|
45
|
+
type RevokedCertificate = {
|
|
46
|
+
revoked_at: string;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Revokes a certificate that has not already been revoked.
|
|
50
|
+
*
|
|
51
|
+
* A certificate that is already revoked is left unchanged, so when several
|
|
52
|
+
* requests revoke the same certificate concurrently only one of them revokes
|
|
53
|
+
* it and the time it was first revoked is kept.
|
|
54
|
+
*
|
|
55
|
+
* @param env The CA bindings
|
|
56
|
+
* @param serial The serial of the certificate to revoke
|
|
57
|
+
* @returns The revocation time of the certificate, or an empty array if it was not revoked by this call
|
|
58
|
+
*/
|
|
16
59
|
export declare const revokeCertificate: (env: SshCaBindings, serial: bigint) => Promise<RevokedCertificate[]>;
|
|
60
|
+
/**
|
|
61
|
+
* Returns the revision of the KRL for a certificate type, which is
|
|
62
|
+
* incremented whenever a certificate of that type is revoked.
|
|
63
|
+
*
|
|
64
|
+
* @param env The CA bindings
|
|
65
|
+
* @param certificateType The type of certificates in the KRL
|
|
66
|
+
* @returns The current revision
|
|
67
|
+
*/
|
|
68
|
+
export declare const getKrlRevision: (env: SshCaBindings, certificateType: CertificateType) => Promise<bigint>;
|
|
69
|
+
/** The version of a KRL and when that version was generated */
|
|
70
|
+
export type KrlVersion = {
|
|
71
|
+
version: bigint;
|
|
72
|
+
/** When the version was first generated, which is the same for every KRL with this version */
|
|
73
|
+
generatedAt: Date;
|
|
74
|
+
};
|
|
17
75
|
/**
|
|
18
76
|
* Get the version of the KRL for a certificate type, incrementing it if the
|
|
19
77
|
* content of the KRL has changed since the version was last returned.
|
|
@@ -25,8 +83,9 @@ export declare const revokeCertificate: (env: SshCaBindings, serial: bigint) =>
|
|
|
25
83
|
* @param env The bindings for the CA, see {@link SshCaBindings}
|
|
26
84
|
* @param certificateType The type of certificates in the KRL, see {@link CertificateType}
|
|
27
85
|
* @param digest A digest of the content of the KRL, which changes whenever the content does
|
|
28
|
-
* @returns The version to use for the KRL
|
|
86
|
+
* @returns The version to use for the KRL, and when that version was first
|
|
87
|
+
* generated so that every KRL with the version has the same generation date
|
|
29
88
|
*/
|
|
30
|
-
export declare const getKrlVersion: (env: SshCaBindings, certificateType: CertificateType, digest: string) => Promise<
|
|
89
|
+
export declare const getKrlVersion: (env: SshCaBindings, certificateType: CertificateType, digest: string) => Promise<KrlVersion>;
|
|
31
90
|
export {};
|
|
32
91
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/db/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/db/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/db/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAE3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAKhD,OAAO,EAAE,eAAe,EAAuC,MAAM,YAAY,CAAA;AA2BjF,eAAO,MAAM,SAAS,QAAe,aAAa,kBA4BjD,CAAA;AA4BD,eAAO,MAAM,iBAAiB,QAAe,aAAa,eAAe,cAAc,SAAS,MAAM,oBAAmB,eAAe,kBAUvI,CAAA;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,aAAa,QAAe,aAAa,YAAY,cAAc,eAAe,cAAc,KAAG,OAAO,CAAC,OAAO,CAyB9H,CAAA;AAED,eAAO,MAAM,SAAS,QAAe,aAAa,UAAU,MAAM,KAAG,OAAO,CAAC,OAAO,CAYnF,CAAA;AAED,oBAAY,gBAAgB;IAC3B,QAAQ,IAAA;IACR,OAAO,IAAA;IACP,SAAS,IAAA;CACT;AAYD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gBAAgB,QAAe,aAAa,UAAU,MAAM,aAAa,SAAS,mBAAmB,eAAe,KAAG,OAAO,CAAC,gBAAgB,CAsB3J,CAAA;AAED,eAAO,MAAM,iBAAiB,QAAe,aAAa,mBAAmB,eAAe,KAAG,OAAO,CAAC,MAAM,EAAE,CAsB9G,CAAA;AAED,gEAAgE;AAChE,KAAK,kBAAkB,GAAG;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,CAAA;AAEhD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iBAAiB,QAAe,aAAa,UAAU,MAAM,KAAG,OAAO,CAAC,kBAAkB,EAAE,CAoBxG,CAAA;AAMD;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,QAAe,aAAa,mBAAmB,eAAe,KAAG,OAAO,CAAC,MAAM,CAYzG,CAAA;AAgBD,+DAA+D;AAC/D,MAAM,MAAM,UAAU,GAAG;IACxB,OAAO,EAAE,MAAM,CAAA;IACf,8FAA8F;IAC9F,WAAW,EAAE,IAAI,CAAA;CACjB,CAAA;AAKD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,aAAa,QAAe,aAAa,mBAAmB,eAAe,UAAU,MAAM,KAAG,OAAO,CAAC,UAAU,CA8C5H,CAAA"}
|
package/dist/db/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PublicKey } from "../ssh/keys.js";
|
|
1
2
|
import { logger } from "../logger.js";
|
|
2
3
|
import { D1QB } from "workers-qb";
|
|
3
4
|
import { z } from "zod";
|
|
@@ -17,6 +18,12 @@ const connect = async (env) => {
|
|
|
17
18
|
throw err;
|
|
18
19
|
}
|
|
19
20
|
};
|
|
21
|
+
// statements that increment the KRL revision, so a KRL cached for an earlier
|
|
22
|
+
// revision is not used again
|
|
23
|
+
const BumpAllRevisions = "UPDATE krl SET revision = revision + 1";
|
|
24
|
+
// increments the revision of the KRL for a certificate type, but only if the
|
|
25
|
+
// previous statement in the same batch changed a row
|
|
26
|
+
const BumpRevisionIfChanged = "UPDATE krl SET revision = revision + 1 WHERE certificate_type = ? AND changes() = 1";
|
|
20
27
|
export const dbCleanup = async (env) => {
|
|
21
28
|
const l = logger(env).with("retention", env.DB_CERTIFICATE_RETENTION);
|
|
22
29
|
if (env.DB_CERTIFICATE_RETENTION === "infinite") {
|
|
@@ -34,38 +41,86 @@ export const dbCleanup = async (env) => {
|
|
|
34
41
|
}
|
|
35
42
|
}).execute();
|
|
36
43
|
l.info("completed database cleanup", "changes", res.meta?.changes);
|
|
44
|
+
// revoked certificates may have been removed from the KRLs, so cached
|
|
45
|
+
// KRLs must not be used again
|
|
46
|
+
if ((res.meta?.changes ?? 0) > 0) {
|
|
47
|
+
await env.DB.prepare(BumpAllRevisions).run();
|
|
48
|
+
}
|
|
37
49
|
}
|
|
38
50
|
catch (err) {
|
|
39
51
|
l.error("error during database cleanup", "retention", env.DB_CERTIFICATE_RETENTION, "error", err);
|
|
40
52
|
}
|
|
41
53
|
};
|
|
54
|
+
// the columns recorded for an issued certificate, in the order of the values
|
|
55
|
+
// returned by certificateValues
|
|
56
|
+
const CertificateColumns = ["serial", "key_id", "principals", "extensions", "valid_after", "valid_before", "certificate_type", "public_key"];
|
|
57
|
+
const certificateRecord = (certificate, keyid, certificateType) => {
|
|
58
|
+
// principals are recorded as distinguished names, as they were when
|
|
59
|
+
// certificates were created with sshpk
|
|
60
|
+
const principals = certificate.principals.map((p) => `${certificate.kind === "host" ? "CN" : "UID"}=${p}`).join(",");
|
|
61
|
+
return {
|
|
62
|
+
serial: `${certificate.serial}`,
|
|
63
|
+
key_id: keyid,
|
|
64
|
+
principals: principals,
|
|
65
|
+
extensions: certificate.extensions.join(","),
|
|
66
|
+
valid_after: certificate.validAfter.toISOString(),
|
|
67
|
+
valid_before: certificate.validBefore.toISOString(),
|
|
68
|
+
certificate_type: certificateType,
|
|
69
|
+
public_key: certificate.publicKey.toString()
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
const certificateValues = (certificate, keyid, certificateType) => {
|
|
73
|
+
const record = certificateRecord(certificate, keyid, certificateType);
|
|
74
|
+
return CertificateColumns.map((column) => record[column]);
|
|
75
|
+
};
|
|
42
76
|
export const recordCertificate = async (env, certificate, keyid, certificateType = CertificateType.User) => {
|
|
43
|
-
const serial = certificate.serial.readBigUInt64BE(0);
|
|
44
|
-
const subjects = certificate.subjects.map((v) => {
|
|
45
|
-
return v.toString();
|
|
46
|
-
}).join(",");
|
|
47
|
-
const extensions = certificate.getExtensions().map((v) => {
|
|
48
|
-
// @ts-expect-error: the name property does exist
|
|
49
|
-
return v.name;
|
|
50
|
-
}).join(",");
|
|
51
77
|
const qb = await connect(env);
|
|
52
78
|
const res = await qb.insert({
|
|
53
79
|
tableName: "certificates",
|
|
54
|
-
data:
|
|
55
|
-
serial: `${serial}`,
|
|
56
|
-
key_id: keyid,
|
|
57
|
-
principals: subjects,
|
|
58
|
-
extensions: extensions,
|
|
59
|
-
valid_after: certificate.validFrom.toISOString(),
|
|
60
|
-
valid_before: certificate.validUntil.toISOString(),
|
|
61
|
-
certificate_type: certificateType,
|
|
62
|
-
public_key: certificate.subjectKey.toString("ssh")
|
|
63
|
-
},
|
|
80
|
+
data: certificateRecord(certificate, keyid, certificateType),
|
|
64
81
|
}).execute();
|
|
65
82
|
if (!res.success) {
|
|
66
83
|
throw new Error("error during query");
|
|
67
84
|
}
|
|
68
85
|
};
|
|
86
|
+
/**
|
|
87
|
+
* Records a host certificate issued by renewing another, revoking the
|
|
88
|
+
* certificate that was renewed so it cannot be used or renewed again.
|
|
89
|
+
*
|
|
90
|
+
* This is done in a single transaction, and the renewed certificate is only
|
|
91
|
+
* recorded if this call revoked the previous certificate, so when a
|
|
92
|
+
* certificate is renewed more than once concurrently only one renewal is
|
|
93
|
+
* recorded. A previous certificate that was never recorded (for example one
|
|
94
|
+
* signed with the CA key outside of the CA) is recorded so that it can be
|
|
95
|
+
* revoked.
|
|
96
|
+
*
|
|
97
|
+
* @param env The CA bindings
|
|
98
|
+
* @param previous The certificate that was presented for renewal
|
|
99
|
+
* @param certificate The certificate issued by the renewal
|
|
100
|
+
* @returns true if the renewal was recorded, or false if the previous certificate was already revoked
|
|
101
|
+
*/
|
|
102
|
+
export const recordRenewal = async (env, previous, certificate) => {
|
|
103
|
+
// ensure migrations have been applied
|
|
104
|
+
await connect(env);
|
|
105
|
+
const columns = CertificateColumns.join(", ");
|
|
106
|
+
const placeholders = CertificateColumns.map(() => "?").join(", ");
|
|
107
|
+
const revokedAt = (new Date).toISOString();
|
|
108
|
+
const results = await env.DB.batch([
|
|
109
|
+
env.DB.prepare(`INSERT INTO certificates (${columns}) VALUES (${placeholders}) ON CONFLICT (serial) DO NOTHING`)
|
|
110
|
+
.bind(...certificateValues(previous, previous.keyId, CertificateType.Host)),
|
|
111
|
+
env.DB.prepare("UPDATE certificates SET revoked_at = ? WHERE serial = ? AND certificate_type = ? AND revoked_at IS NULL")
|
|
112
|
+
.bind(revokedAt, `${previous.serial}`, CertificateType.Host),
|
|
113
|
+
// changes() is the number of rows revoked by the previous statement
|
|
114
|
+
env.DB.prepare(`INSERT INTO certificates (${columns}) SELECT ${placeholders} WHERE changes() = 1`)
|
|
115
|
+
.bind(...certificateValues(certificate, certificate.keyId, CertificateType.Host)),
|
|
116
|
+
// the previous certificate was revoked if the renewed one was recorded
|
|
117
|
+
env.DB.prepare(BumpRevisionIfChanged).bind(CertificateType.Host),
|
|
118
|
+
]);
|
|
119
|
+
if (results.some((res) => !res.success)) {
|
|
120
|
+
throw new Error("error during query");
|
|
121
|
+
}
|
|
122
|
+
return results[2]?.meta?.changes === 1;
|
|
123
|
+
};
|
|
69
124
|
export const isRevoked = async (env, serial) => {
|
|
70
125
|
const qb = await connect(env);
|
|
71
126
|
const res = await qb.fetchOne({
|
|
@@ -83,28 +138,48 @@ export var RevocationStatus;
|
|
|
83
138
|
RevocationStatus[RevocationStatus["NotFound"] = 0] = "NotFound";
|
|
84
139
|
RevocationStatus[RevocationStatus["Revoked"] = 1] = "Revoked";
|
|
85
140
|
RevocationStatus[RevocationStatus["Revokable"] = 2] = "Revokable";
|
|
86
|
-
RevocationStatus[RevocationStatus["Unrevokable"] = 3] = "Unrevokable";
|
|
87
141
|
})(RevocationStatus || (RevocationStatus = {}));
|
|
88
|
-
|
|
142
|
+
// whether a public key recorded in OpenSSH format is the same as another key,
|
|
143
|
+
// treating a recorded key that cannot be parsed as a different key
|
|
144
|
+
const isKey = (recorded, publicKey) => {
|
|
145
|
+
try {
|
|
146
|
+
return PublicKey.parse(recorded).equals(publicKey);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* Returns whether a certificate can be revoked by the holder of a public key.
|
|
154
|
+
*
|
|
155
|
+
* The public key is compared by fingerprint, so a key with a different
|
|
156
|
+
* comment to the one the certificate was issued for still matches.
|
|
157
|
+
*
|
|
158
|
+
* @param env The CA bindings
|
|
159
|
+
* @param serial The serial of the certificate
|
|
160
|
+
* @param publicKey The public key the requester has proven possession of
|
|
161
|
+
* @param certificateType The type of certificate
|
|
162
|
+
* @returns The revocation status of the certificate
|
|
163
|
+
*/
|
|
164
|
+
export const revocationStatus = async (env, serial, publicKey, certificateType) => {
|
|
89
165
|
const qb = await connect(env);
|
|
90
166
|
const res = await qb.fetchOne({
|
|
91
167
|
tableName: "certificates",
|
|
92
168
|
fields: ["public_key", "revoked_at"],
|
|
93
169
|
where: {
|
|
94
|
-
conditions: "serial = ? AND
|
|
95
|
-
params: [`${serial}`,
|
|
170
|
+
conditions: "serial = ? AND certificate_type = ?",
|
|
171
|
+
params: [`${serial}`, certificateType]
|
|
96
172
|
}
|
|
97
173
|
}).execute();
|
|
98
|
-
|
|
174
|
+
// a certificate issued for another key is reported as not found, so
|
|
175
|
+
// requests cannot be used to discover which serials exist
|
|
176
|
+
if (res.results?.public_key == null || !isKey(res.results.public_key, publicKey)) {
|
|
99
177
|
return RevocationStatus.NotFound;
|
|
100
178
|
}
|
|
101
179
|
if (res.results.revoked_at !== null) {
|
|
102
180
|
return RevocationStatus.Revoked;
|
|
103
181
|
}
|
|
104
|
-
|
|
105
|
-
return RevocationStatus.Revokable;
|
|
106
|
-
}
|
|
107
|
-
return RevocationStatus.Unrevokable;
|
|
182
|
+
return RevocationStatus.Revokable;
|
|
108
183
|
};
|
|
109
184
|
export const getRevocationList = async (env, certificateType) => {
|
|
110
185
|
const qb = await connect(env);
|
|
@@ -126,31 +201,69 @@ export const getRevocationList = async (env, certificateType) => {
|
|
|
126
201
|
}
|
|
127
202
|
return res.results.map(item => item.serial);
|
|
128
203
|
};
|
|
204
|
+
/**
|
|
205
|
+
* Revokes a certificate that has not already been revoked.
|
|
206
|
+
*
|
|
207
|
+
* A certificate that is already revoked is left unchanged, so when several
|
|
208
|
+
* requests revoke the same certificate concurrently only one of them revokes
|
|
209
|
+
* it and the time it was first revoked is kept.
|
|
210
|
+
*
|
|
211
|
+
* @param env The CA bindings
|
|
212
|
+
* @param serial The serial of the certificate to revoke
|
|
213
|
+
* @returns The revocation time of the certificate, or an empty array if it was not revoked by this call
|
|
214
|
+
*/
|
|
129
215
|
export const revokeCertificate = async (env, serial) => {
|
|
216
|
+
// ensure migrations have been applied
|
|
217
|
+
await connect(env);
|
|
218
|
+
const revokedAt = (new Date).toISOString();
|
|
219
|
+
// the KRL revision is incremented in the same transaction, so a cached KRL
|
|
220
|
+
// that does not include this certificate is never used again
|
|
221
|
+
const results = await env.DB.batch([
|
|
222
|
+
env.DB.prepare("UPDATE certificates SET revoked_at = ? WHERE serial = ? AND revoked_at IS NULL")
|
|
223
|
+
.bind(revokedAt, `${serial}`),
|
|
224
|
+
env.DB.prepare("UPDATE krl SET revision = revision + 1 WHERE certificate_type = (SELECT certificate_type FROM certificates WHERE serial = ?) AND changes() = 1")
|
|
225
|
+
.bind(`${serial}`),
|
|
226
|
+
]);
|
|
227
|
+
if (results.some((res) => !res.success)) {
|
|
228
|
+
throw new Error("error during query");
|
|
229
|
+
}
|
|
230
|
+
return results[0]?.meta?.changes === 1 ? [{ revoked_at: revokedAt }] : [];
|
|
231
|
+
};
|
|
232
|
+
const KrlRevisionRow = z.object({
|
|
233
|
+
revision: z.number().int().nonnegative(),
|
|
234
|
+
});
|
|
235
|
+
/**
|
|
236
|
+
* Returns the revision of the KRL for a certificate type, which is
|
|
237
|
+
* incremented whenever a certificate of that type is revoked.
|
|
238
|
+
*
|
|
239
|
+
* @param env The CA bindings
|
|
240
|
+
* @param certificateType The type of certificates in the KRL
|
|
241
|
+
* @returns The current revision
|
|
242
|
+
*/
|
|
243
|
+
export const getKrlRevision = async (env, certificateType) => {
|
|
130
244
|
const qb = await connect(env);
|
|
131
|
-
const res = await qb.
|
|
132
|
-
tableName: "
|
|
133
|
-
|
|
134
|
-
revoked_at: (new Date).toISOString()
|
|
135
|
-
},
|
|
245
|
+
const res = await qb.fetchOne({
|
|
246
|
+
tableName: "krl",
|
|
247
|
+
fields: ["revision"],
|
|
136
248
|
where: {
|
|
137
|
-
conditions: "
|
|
138
|
-
params:
|
|
139
|
-
}
|
|
140
|
-
returning: ["revoked_at"]
|
|
249
|
+
conditions: "certificate_type = ?",
|
|
250
|
+
params: certificateType
|
|
251
|
+
}
|
|
141
252
|
}).execute();
|
|
142
|
-
|
|
143
|
-
throw new Error("error during query");
|
|
144
|
-
}
|
|
145
|
-
if (res.results === undefined) {
|
|
146
|
-
return [];
|
|
147
|
-
}
|
|
148
|
-
return res.results;
|
|
253
|
+
return BigInt(KrlRevisionRow.parse(res.results).revision);
|
|
149
254
|
};
|
|
150
255
|
const KrlVersionRow = z.object({
|
|
151
256
|
version: z.number().int().nonnegative(),
|
|
152
257
|
digest: z.string().nullable(),
|
|
258
|
+
updated_at: z.string(),
|
|
153
259
|
});
|
|
260
|
+
/**
|
|
261
|
+
* Parses a timestamp from the database, which is either an ISO 8601 string or,
|
|
262
|
+
* when set by SQLite's CURRENT_TIMESTAMP, "YYYY-MM-DD HH:MM:SS" in UTC.
|
|
263
|
+
*/
|
|
264
|
+
const parseTimestamp = (value) => {
|
|
265
|
+
return new Date(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(value) ? `${value.replace(" ", "T")}Z` : value);
|
|
266
|
+
};
|
|
154
267
|
// the number of times to retry when another request updates the KRL version concurrently
|
|
155
268
|
const KRL_VERSION_ATTEMPTS = 5;
|
|
156
269
|
/**
|
|
@@ -164,14 +277,15 @@ const KRL_VERSION_ATTEMPTS = 5;
|
|
|
164
277
|
* @param env The bindings for the CA, see {@link SshCaBindings}
|
|
165
278
|
* @param certificateType The type of certificates in the KRL, see {@link CertificateType}
|
|
166
279
|
* @param digest A digest of the content of the KRL, which changes whenever the content does
|
|
167
|
-
* @returns The version to use for the KRL
|
|
280
|
+
* @returns The version to use for the KRL, and when that version was first
|
|
281
|
+
* generated so that every KRL with the version has the same generation date
|
|
168
282
|
*/
|
|
169
283
|
export const getKrlVersion = async (env, certificateType, digest) => {
|
|
170
284
|
const qb = await connect(env);
|
|
171
285
|
for (let attempt = 0; attempt < KRL_VERSION_ATTEMPTS; attempt++) {
|
|
172
286
|
const res = await qb.fetchOne({
|
|
173
287
|
tableName: "krl",
|
|
174
|
-
fields: ["version", "digest"],
|
|
288
|
+
fields: ["version", "digest", "updated_at"],
|
|
175
289
|
where: {
|
|
176
290
|
conditions: "certificate_type = ?",
|
|
177
291
|
params: certificateType
|
|
@@ -179,16 +293,18 @@ export const getKrlVersion = async (env, certificateType, digest) => {
|
|
|
179
293
|
}).execute();
|
|
180
294
|
const current = KrlVersionRow.parse(res.results);
|
|
181
295
|
if (current.digest === digest) {
|
|
182
|
-
return BigInt(current.version);
|
|
296
|
+
return { version: BigInt(current.version), generatedAt: parseTimestamp(current.updated_at) };
|
|
183
297
|
}
|
|
184
|
-
// only update if the version has not been changed by another request
|
|
298
|
+
// only update if the version has not been changed by another request,
|
|
299
|
+
// recording the time to the second as that is all a KRL records
|
|
185
300
|
const next = current.version + 1;
|
|
301
|
+
const generatedAt = new Date(Math.floor(Date.now() / 1000) * 1000);
|
|
186
302
|
const updated = await qb.update({
|
|
187
303
|
tableName: "krl",
|
|
188
304
|
data: {
|
|
189
305
|
version: next,
|
|
190
306
|
digest: digest,
|
|
191
|
-
updated_at:
|
|
307
|
+
updated_at: generatedAt.toISOString()
|
|
192
308
|
},
|
|
193
309
|
where: {
|
|
194
310
|
conditions: "certificate_type = ? AND version = ?",
|
|
@@ -200,7 +316,7 @@ export const getKrlVersion = async (env, certificateType, digest) => {
|
|
|
200
316
|
throw new Error("error during query");
|
|
201
317
|
}
|
|
202
318
|
if (updated.results !== undefined && updated.results.length > 0) {
|
|
203
|
-
return BigInt(next);
|
|
319
|
+
return { version: BigInt(next), generatedAt };
|
|
204
320
|
}
|
|
205
321
|
}
|
|
206
322
|
throw new Error("unable to update KRL version due to concurrent updates");
|