@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.
Files changed (122) hide show
  1. package/README.md +174 -1
  2. package/dist/api/operations.d.ts +50 -0
  3. package/dist/api/operations.d.ts.map +1 -0
  4. package/dist/api/operations.js +264 -0
  5. package/dist/api/operations.js.map +1 -0
  6. package/dist/api/schema.d.ts +72 -0
  7. package/dist/api/schema.d.ts.map +1 -0
  8. package/dist/api/schema.js +114 -0
  9. package/dist/api/schema.js.map +1 -0
  10. package/dist/api/v3/index.d.ts.map +1 -1
  11. package/dist/api/v3/index.js +13 -202
  12. package/dist/api/v3/index.js.map +1 -1
  13. package/dist/api/v3/schema.d.ts +23 -68
  14. package/dist/api/v3/schema.d.ts.map +1 -1
  15. package/dist/api/v3/schema.js +18 -104
  16. package/dist/api/v3/schema.js.map +1 -1
  17. package/dist/api/v4/index.d.ts +8 -0
  18. package/dist/api/v4/index.d.ts.map +1 -0
  19. package/dist/api/v4/index.js +155 -0
  20. package/dist/api/v4/index.js.map +1 -0
  21. package/dist/api/v4/schema.d.ts +268 -0
  22. package/dist/api/v4/schema.d.ts.map +1 -0
  23. package/dist/api/v4/schema.js +186 -0
  24. package/dist/api/v4/schema.js.map +1 -0
  25. package/dist/certificate.d.ts +9 -6
  26. package/dist/certificate.d.ts.map +1 -1
  27. package/dist/certificate.js +55 -99
  28. package/dist/certificate.js.map +1 -1
  29. package/dist/db/index.d.ts +68 -9
  30. package/dist/db/index.d.ts.map +1 -1
  31. package/dist/db/index.js +165 -49
  32. package/dist/db/index.js.map +1 -1
  33. package/dist/db/migrations/0003_krl_revision.d.ts +3 -0
  34. package/dist/db/migrations/0003_krl_revision.d.ts.map +1 -0
  35. package/dist/db/migrations/0003_krl_revision.js +14 -0
  36. package/dist/db/migrations/0003_krl_revision.js.map +1 -0
  37. package/dist/db/migrations/index.d.ts.map +1 -1
  38. package/dist/db/migrations/index.js +2 -0
  39. package/dist/db/migrations/index.js.map +1 -1
  40. package/dist/db/types.d.ts +2 -0
  41. package/dist/db/types.d.ts.map +1 -1
  42. package/dist/index.d.ts +5 -2
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +4 -2
  45. package/dist/index.js.map +1 -1
  46. package/dist/krl.d.ts +3 -2
  47. package/dist/krl.d.ts.map +1 -1
  48. package/dist/krl.js +12 -165
  49. package/dist/krl.js.map +1 -1
  50. package/dist/node/cache.d.ts +21 -0
  51. package/dist/node/cache.d.ts.map +1 -0
  52. package/dist/node/cache.js +49 -0
  53. package/dist/node/cache.js.map +1 -0
  54. package/dist/node/env.d.ts +6 -3
  55. package/dist/node/env.d.ts.map +1 -1
  56. package/dist/node/env.js +4 -3
  57. package/dist/node/env.js.map +1 -1
  58. package/dist/node/index.d.ts +1 -0
  59. package/dist/node/index.d.ts.map +1 -1
  60. package/dist/node/index.js +1 -0
  61. package/dist/node/index.js.map +1 -1
  62. package/dist/proof.d.ts +36 -7
  63. package/dist/proof.d.ts.map +1 -1
  64. package/dist/proof.js +53 -21
  65. package/dist/proof.js.map +1 -1
  66. package/dist/router.d.ts +25 -1
  67. package/dist/router.d.ts.map +1 -1
  68. package/dist/router.js +41 -2
  69. package/dist/router.js.map +1 -1
  70. package/dist/ssh/certificate.d.ts +93 -0
  71. package/dist/ssh/certificate.d.ts.map +1 -0
  72. package/dist/ssh/certificate.js +178 -0
  73. package/dist/ssh/certificate.js.map +1 -0
  74. package/dist/ssh/encoding.d.ts +41 -0
  75. package/dist/ssh/encoding.d.ts.map +1 -0
  76. package/dist/ssh/encoding.js +70 -0
  77. package/dist/ssh/encoding.js.map +1 -0
  78. package/dist/ssh/keys.d.ts +92 -0
  79. package/dist/ssh/keys.d.ts.map +1 -0
  80. package/dist/ssh/keys.js +277 -0
  81. package/dist/ssh/keys.js.map +1 -0
  82. package/dist/ssh/signer.d.ts +35 -0
  83. package/dist/ssh/signer.d.ts.map +1 -0
  84. package/dist/ssh/signer.js +99 -0
  85. package/dist/ssh/signer.js.map +1 -0
  86. package/dist/types.d.ts +64 -19
  87. package/dist/types.d.ts.map +1 -1
  88. package/dist/utils.d.ts +107 -21
  89. package/dist/utils.d.ts.map +1 -1
  90. package/dist/utils.js +251 -65
  91. package/dist/utils.js.map +1 -1
  92. package/dist/verify.d.ts +17 -5
  93. package/dist/verify.d.ts.map +1 -1
  94. package/dist/verify.js +47 -9
  95. package/dist/verify.js.map +1 -1
  96. package/openapi.json +1942 -1
  97. package/package.json +4 -3
  98. package/src/api/operations.ts +314 -0
  99. package/src/api/schema.ts +146 -0
  100. package/src/api/v3/index.ts +21 -248
  101. package/src/api/v3/schema.ts +36 -128
  102. package/src/api/v4/index.ts +266 -0
  103. package/src/api/v4/schema.ts +241 -0
  104. package/src/certificate.ts +62 -123
  105. package/src/db/index.ts +196 -56
  106. package/src/db/migrations/0003_krl_revision.ts +15 -0
  107. package/src/db/migrations/index.ts +2 -0
  108. package/src/db/types.ts +2 -0
  109. package/src/index.ts +6 -4
  110. package/src/krl.ts +13 -197
  111. package/src/node/cache.ts +62 -0
  112. package/src/node/env.ts +6 -5
  113. package/src/node/index.ts +1 -0
  114. package/src/proof.ts +65 -31
  115. package/src/router.ts +67 -3
  116. package/src/ssh/certificate.ts +236 -0
  117. package/src/ssh/encoding.ts +81 -0
  118. package/src/ssh/keys.ts +327 -0
  119. package/src/ssh/signer.ts +129 -0
  120. package/src/types.ts +60 -20
  121. package/src/utils.ts +285 -81
  122. package/src/verify.ts +54 -14
@@ -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
- // sshpk is CommonJS and Node's ESM loader cannot detect most of its named exports, so values are taken from the default import
5
- const { createCertificate, identityForHost, identityForUser, identityFromDN } = sshpk;
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
- export const generateCertificate = (env, email, key, public_key, options) => {
20
- let { lifetime } = options ?? {};
21
- const { principals, extensions, serial } = options ?? {};
22
- // add identities
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 identity = includeSelf
29
- ? [identityForUser(email.split("@")[0])]
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
- identity.push(identityForUser(email));
47
+ certificatePrincipals.push(email);
39
48
  }
40
49
  if (principals !== undefined) {
41
- for (const p of principals) {
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
- // add usage extensions
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
- else {
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
- sshCertificateExtensions.forEach((ext) => {
82
- sshextensions.push({
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
- if (options === undefined) {
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.lifetime !== undefined
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
- // generate value for serial of certificate
146
- const serial = generateSerial();
147
- // set issuer of certificate based on ISSUER_DN
148
- const issuer = identityFromDN(env.ISSUER_DN);
149
- // create certificate
150
- const certificate = createCertificate(identity, public_key, issuer, key, { lifetime: lifetime, serial: serial });
151
- return certificate;
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
- export const generateSerial = (serial) => {
154
- if (serial !== undefined) {
155
- const s = Buffer.alloc(8);
156
- s.writeBigUInt64BE(serial);
157
- return s;
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
@@ -1 +1 @@
1
- {"version":3,"file":"certificate.js","sourceRoot":"","sources":["../src/certificate.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"certificate.js","sourceRoot":"","sources":["../src/certificate.ts"],"names":[],"mappings":"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"}
@@ -1,19 +1,77 @@
1
- import type { Certificate } from "sshpk";
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, type CertificateRecord } from "./types.js";
4
+ import { CertificateType } from "./types.js";
4
5
  export declare const dbCleanup: (env: SshCaBindings) => Promise<void>;
5
- export declare const recordCertificate: (env: SshCaBindings, certificate: Certificate, keyid: string, certificateType?: CertificateType) => Promise<void>;
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
- export declare const revocationStatus: (env: SshCaBindings, serial: bigint, publickey: string, certificateType: CertificateType) => Promise<RevocationStatus>;
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
- type RevokedCertificate = Pick<CertificateRecord, "revoked_at">;
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<bigint>;
89
+ export declare const getKrlVersion: (env: SshCaBindings, certificateType: CertificateType, digest: string) => Promise<KrlVersion>;
31
90
  export {};
32
91
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/db/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAoB,MAAM,OAAO,CAAA;AAE1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAKhD,OAAO,EAAE,eAAe,EAAE,KAAK,iBAAiB,EAAuC,MAAM,YAAY,CAAA;AAmBzG,eAAO,MAAM,SAAS,QAAe,aAAa,kBAsBjD,CAAA;AAED,eAAO,MAAM,iBAAiB,QAAe,aAAa,eAAe,WAAW,SAAS,MAAM,oBAAmB,eAAe,kBA2BpI,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;IACT,WAAW,IAAA;CACX;AAED,eAAO,MAAM,gBAAgB,QAAe,aAAa,UAAU,MAAM,aAAa,MAAM,mBAAmB,eAAe,KAAG,OAAO,CAAC,gBAAgB,CAwBxJ,CAAA;AAED,eAAO,MAAM,iBAAiB,QAAe,aAAa,mBAAmB,eAAe,KAAG,OAAO,CAAC,MAAM,EAAE,CAsB9G,CAAA;AAED,KAAK,kBAAkB,GAAG,IAAI,CAAC,iBAAiB,EAAE,YAAY,CAAC,CAAA;AAE/D,eAAO,MAAM,iBAAiB,QAAe,aAAa,UAAU,MAAM,KAAG,OAAO,CAAC,kBAAkB,EAAE,CAuBxG,CAAA;AAUD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,aAAa,QAAe,aAAa,mBAAmB,eAAe,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,CA4CxH,CAAA"}
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
- export const revocationStatus = async (env, serial, publickey, certificateType) => {
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 public_key = ? AND certificate_type = ?",
95
- params: [`${serial}`, publickey, certificateType]
170
+ conditions: "serial = ? AND certificate_type = ?",
171
+ params: [`${serial}`, certificateType]
96
172
  }
97
173
  }).execute();
98
- if (res.results === undefined) {
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
- if (publickey === res.results.public_key) {
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.update({
132
- tableName: "certificates",
133
- data: {
134
- revoked_at: (new Date).toISOString()
135
- },
245
+ const res = await qb.fetchOne({
246
+ tableName: "krl",
247
+ fields: ["revision"],
136
248
  where: {
137
- conditions: "serial = ?",
138
- params: `${serial}`
139
- },
140
- returning: ["revoked_at"]
249
+ conditions: "certificate_type = ?",
250
+ params: certificateType
251
+ }
141
252
  }).execute();
142
- if (!res.success) {
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: (new Date).toISOString()
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");