@andrewheberle/serverless-ssh-ca 0.17.5 → 0.17.7

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 (48) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +268 -0
  3. package/dist/api/v3/schema.js +2 -1
  4. package/dist/proof.js +1 -1
  5. package/dist/sshsig/formats.js +1 -1
  6. package/dist/utils.d.ts +2 -1
  7. package/dist/utils.js +1 -2
  8. package/package.json +9 -13
  9. package/dist/src/api/v3/index.d.ts +0 -1
  10. package/dist/src/api/v3/index.js +0 -263
  11. package/dist/src/api/v3/schema.d.ts +0 -217
  12. package/dist/src/api/v3/schema.js +0 -216
  13. package/dist/src/certificate.d.ts +0 -30
  14. package/dist/src/certificate.js +0 -154
  15. package/dist/src/db.d.ts +0 -36
  16. package/dist/src/db.js +0 -111
  17. package/dist/src/index.d.ts +0 -6
  18. package/dist/src/index.js +0 -8
  19. package/dist/src/krl.d.ts +0 -39
  20. package/dist/src/krl.js +0 -317
  21. package/dist/src/logger.d.ts +0 -2
  22. package/dist/src/logger.js +0 -3
  23. package/dist/src/proof.d.ts +0 -51
  24. package/dist/src/proof.js +0 -154
  25. package/dist/src/router.d.ts +0 -14
  26. package/dist/src/router.js +0 -22
  27. package/dist/src/sshsig/armor.d.ts +0 -7
  28. package/dist/src/sshsig/armor.js +0 -18
  29. package/dist/src/sshsig/formats.d.ts +0 -52
  30. package/dist/src/sshsig/formats.js +0 -174
  31. package/dist/src/sshsig/index.d.ts +0 -15
  32. package/dist/src/sshsig/index.js +0 -31
  33. package/dist/src/sshsig/reader.d.ts +0 -20
  34. package/dist/src/sshsig/reader.js +0 -54
  35. package/dist/src/sshsig/sig.d.ts +0 -16
  36. package/dist/src/sshsig/sig.js +0 -1
  37. package/dist/src/sshsig/sig_parser.d.ts +0 -9
  38. package/dist/src/sshsig/sig_parser.js +0 -83
  39. package/dist/src/sshsig/verifier.d.ts +0 -2
  40. package/dist/src/sshsig/verifier.js +0 -37
  41. package/dist/src/sshsig/writer.d.ts +0 -17
  42. package/dist/src/sshsig/writer.js +0 -51
  43. package/dist/src/types.d.ts +0 -42
  44. package/dist/src/types.js +0 -20
  45. package/dist/src/utils.d.ts +0 -76
  46. package/dist/src/utils.js +0 -370
  47. package/dist/src/verify.d.ts +0 -6
  48. package/dist/src/verify.js +0 -17
@@ -1,216 +0,0 @@
1
- import z from "zod";
2
- import { split, refineCertificateRequest, transformAuthorizationHeader, transformProofOfPossession, transformPublicKey, transformRenewalProofOfPossession, refineHostCertificateRequest, transformCertificate, refineHostCertificateRenewal, refineRevokeCertificate, transformIdentityToken, } from "../../utils";
3
- import { ConflictException, contentJson, ForbiddenException, InputValidationException, InternalServerErrorException, UnauthorizedException, UnprocessableEntityException, } from "chanfana";
4
- import { env } from "cloudflare:workers";
5
- import { seconds } from "itty-time";
6
- const openapiStringByte = z.base64()
7
- .transform((v) => {
8
- const a = z.util.base64ToUint8Array(v);
9
- return Buffer.from(a.buffer, a.byteOffset, a.length);
10
- })
11
- .openapi({ format: "byte" });
12
- const proofOfPossession = z.string()
13
- .meta({
14
- description: "Proof of possession comprising of ${timestamp}.${fingerprint}.${signature}",
15
- example: "1765696780805.SHA256:79mq0wtpQMTvS4+Of8VzLN0qmYWNUyTXYmqKwhEgSLs.LS0tLS1CRUdJTiBTU0ggU0lHTkFUVVJFLS0tLS0KVTFOSVUwbEhBQUFBQVFBQUFHZ0FBQUFUWldOa2MyRXRjMmhoTWkxdWFYTjBjREkxTmdBQUFBaHVhWE4wY0RJMQpOZ0FBQUVFRUR6Ymk4OE10alBYSDFCb1JURU9HaS96aEc4QnhQNVZUUW5OQkFNMGZtMExtekErUUZkUlNVYVRFCkdNN2l0QjhINmZNZFAydlAxcElQcGUyOWVLQkJZd0FBQUFSbWFXeGxBQUFBQUFBQUFBWnphR0UxTVRJQUFBQmsKQUFBQUUyVmpaSE5oTFhOb1lUSXRibWx6ZEhBeU5UWUFBQUJKQUFBQUlHdmZVZGNDQkRXa3ZwL0ZHS3NkakgrRQpKZzZXN0EvWUJVZng2Z0srQ01raEFBQUFJUUQxOFd5cDNZejZ0eXNvdllXbEJHTXprRnlYbFRrSHM5blVVRXJqCkczNWVPQT09Ci0tLS0tRU5EIFNTSCBTSUdOQVRVUkUtLS0tLQo="
16
- });
17
- const publicKey = openapiStringByte
18
- .transform(transformPublicKey)
19
- .meta({
20
- description: "SSH public key to sign",
21
- example: "ZWNkc2Etc2hhMi1uaXN0cDI1NiBBQUFBRTJWalpITmhMWE5vWVRJdGJtbHpkSEF5TlRZQUFBQUlibWx6ZEhBeU5UWUFBQUJCQkdNcFNKdHRmMEl0dE5DVmMyOFR6WEZSMUQweHZPM25wNjdWemVBUXNiZFpza3JEY1lqU2x3SjZIUERtVHBYYVUwbEVhWDlMNFloYy9jQ2YxTWU5RlRrPQo="
22
- });
23
- const identityToken = z.string()
24
- .meta({
25
- description: "Identity Token JWT from OIDC IdP",
26
- example: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZW1haWwiOiJ1c2VyQGV4YW1wbGUuY29tIiwiaWF0IjoxNzc1NzAzMDk1fQ.ri_neAWnhMNK3LzlsrBcQYymSM4yRjmNSZZSeZiXhrEqtEz6c3cXk0Esq765umGjpUsWcosL-OFrDJlyAjTDnhrd9oV08uc_CW0rQRsJIGEuRo3ryxkLdVu9mGoZWEUb9KwjGJrwxvr-0cPWx5jaDyKwJcqMvtV_bEITUD51sDB1Vm89QfYRO_pGJo2vrRzSvMjpUenRpwPay4lYIBxl41_4YpR9Rc6VrIZuYsjV2iqEZ4eBrygMA7zPR_hN7l7s95FddLOzj5NsK57VT4uLHwYohx2oqMzw3M-B9HsZIQin_9q61pZFQXepzJth0woXiZheU27llnfHX967PhNQyg"
27
- })
28
- .transform(transformIdentityToken);
29
- const accessToken = z.string()
30
- .meta({
31
- description: "Access Token JWT from OIDC IdP",
32
- example: "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZW1haWwiOiJ1c2VyQGV4YW1wbGUuY29tIiwiaWF0IjoxNzc1NzAzMDk1fQ.ri_neAWnhMNK3LzlsrBcQYymSM4yRjmNSZZSeZiXhrEqtEz6c3cXk0Esq765umGjpUsWcosL-OFrDJlyAjTDnhrd9oV08uc_CW0rQRsJIGEuRo3ryxkLdVu9mGoZWEUb9KwjGJrwxvr-0cPWx5jaDyKwJcqMvtV_bEITUD51sDB1Vm89QfYRO_pGJo2vrRzSvMjpUenRpwPay4lYIBxl41_4YpR9Rc6VrIZuYsjV2iqEZ4eBrygMA7zPR_hN7l7s95FddLOzj5NsK57VT4uLHwYohx2oqMzw3M-B9HsZIQin_9q61pZFQXepzJth0woXiZheU27llnfHX967PhNQyg"
33
- })
34
- .startsWith("Bearer ")
35
- .transform(transformAuthorizationHeader);
36
- const krl = openapiStringByte
37
- .meta({ description: "Key Revocation List" });
38
- const HeaderSchema = z.object({
39
- "Authorization": accessToken
40
- });
41
- const certificate = openapiStringByte
42
- .meta({ description: "Issued Certificate" });
43
- export const CaPublicKeyEndpointSchema = {
44
- responses: {
45
- "200": {
46
- content: {
47
- "text/plain": {
48
- schema: z.string()
49
- }
50
- },
51
- description: "Returns SSH Certificate Authority Public Key",
52
- },
53
- ...InternalServerErrorException.schema(),
54
- }
55
- };
56
- export const UserCertificateRequestEndpointSchema = {
57
- security: [
58
- {
59
- oidcAuth: []
60
- }
61
- ],
62
- request: {
63
- headers: HeaderSchema,
64
- body: contentJson(z.object({
65
- public_key: publicKey,
66
- proof: proofOfPossession
67
- .transform(transformProofOfPossession),
68
- identity: identityToken,
69
- extensions: z.array(z.string())
70
- .default(split(env.SSH_CERTIFICATE_EXTENSIONS))
71
- .meta({ description: "Extensions to include in the issued SSH certificate" }),
72
- lifetime: z.int()
73
- .min(seconds("5 minutes"))
74
- .max(seconds(env.SSH_CERTIFICATE_LIFETIME))
75
- .default(seconds(env.SSH_CERTIFICATE_LIFETIME))
76
- .meta({ description: "Lifetime of issued SSH certificate" }),
77
- })
78
- .superRefine(refineCertificateRequest)
79
- .openapi("User Certificate Request"))
80
- },
81
- responses: {
82
- "200": {
83
- description: "SSH User Certificate issued successfully",
84
- ...contentJson(z.object({
85
- certificate: certificate,
86
- })
87
- .openapi("Certificate Response"))
88
- },
89
- ...UnauthorizedException.schema(),
90
- ...ForbiddenException.schema(),
91
- ...InputValidationException.schema(),
92
- ...UnprocessableEntityException.schema(),
93
- ...InternalServerErrorException.schema(),
94
- }
95
- };
96
- export const RevocationListEndpointSchema = {
97
- request: {
98
- params: z.object({
99
- certificateType: z.enum(["user", "host"])
100
- }),
101
- },
102
- responses: {
103
- "200": {
104
- description: "Returns an Open SSH Key Revocation List as BASE64 and an SSHSIG signature for verification",
105
- ...contentJson(z.object({
106
- krl: krl,
107
- signature: z.string()
108
- })
109
- .openapi("Key Revocation List Response"))
110
- },
111
- ...InternalServerErrorException.schema(),
112
- }
113
- };
114
- export const HostCertificateRequestEndpointSchema = {
115
- security: [
116
- {
117
- oidcAuth: []
118
- }
119
- ],
120
- request: {
121
- headers: HeaderSchema,
122
- body: contentJson(z.object({
123
- public_key: publicKey,
124
- proof: proofOfPossession
125
- .transform(transformProofOfPossession),
126
- identity: identityToken,
127
- principals: z.array(z.string()).min(1)
128
- .meta({ description: "List of principals to include on the issued certificate" }),
129
- lifetime: z.int()
130
- .min(seconds("24 hours"))
131
- .max(seconds(env.SSH_HOST_CERTIFICATE_LIFETIME))
132
- .default(seconds(env.SSH_HOST_CERTIFICATE_LIFETIME))
133
- .meta({ description: "Lifetime of issued Host SSH certificate" }),
134
- })
135
- .superRefine(refineHostCertificateRequest)
136
- .openapi("Host Certificate Request"))
137
- },
138
- responses: {
139
- "200": {
140
- description: "SSH Host Certificate issued successfully",
141
- ...contentJson(z.object({
142
- certificate: certificate,
143
- })
144
- .openapi("Certificate Response"))
145
- },
146
- ...UnauthorizedException.schema(),
147
- ...ForbiddenException.schema(),
148
- ...InputValidationException.schema(),
149
- ...UnprocessableEntityException.schema(),
150
- ...InternalServerErrorException.schema(),
151
- }
152
- };
153
- export const HostCertificateRenewEndpointSchema = {
154
- request: {
155
- body: contentJson(z.object({
156
- certificate: openapiStringByte
157
- .transform(transformCertificate)
158
- .meta({ description: "SSH certificate to renew" }),
159
- public_key: publicKey,
160
- proof: proofOfPossession
161
- .transform(transformRenewalProofOfPossession),
162
- lifetime: z.int()
163
- .min(seconds("24 hours"))
164
- .max(seconds(env.SSH_HOST_CERTIFICATE_LIFETIME))
165
- .default(seconds(env.SSH_HOST_CERTIFICATE_LIFETIME))
166
- .meta({ description: "Lifetime of renewed Host SSH certificate" }),
167
- })
168
- .superRefine(refineHostCertificateRenewal)
169
- .openapi("Host Certificate Renew"))
170
- },
171
- responses: {
172
- "200": {
173
- description: "SSH Host Certificate renewed successfully",
174
- ...contentJson(z.object({
175
- certificate: certificate
176
- })
177
- .openapi("Certificate Response")),
178
- },
179
- ...UnauthorizedException.schema(),
180
- ...ForbiddenException.schema(),
181
- ...InputValidationException.schema(),
182
- ...UnprocessableEntityException.schema(),
183
- ...InternalServerErrorException.schema(),
184
- }
185
- };
186
- export const RevokeCertificateEndpointSchema = {
187
- request: {
188
- params: z.object({
189
- certificateType: z.enum(["user", "host"])
190
- }),
191
- body: contentJson(z.object({
192
- serial: z.bigint()
193
- .meta({ description: "Serial number of certificate to revoke" }),
194
- public_key: publicKey,
195
- proof: proofOfPossession
196
- .transform(transformProofOfPossession),
197
- })
198
- .openapi("Certificate Revocation")
199
- .superRefine(refineRevokeCertificate))
200
- },
201
- responses: {
202
- "200": {
203
- description: "SSH certificate revoked successfully",
204
- ...contentJson(z.object({
205
- revoked_at: z.iso.datetime(),
206
- })
207
- .openapi("Revocation Response")),
208
- },
209
- ...UnauthorizedException.schema(),
210
- ...ForbiddenException.schema(),
211
- ...InputValidationException.schema(),
212
- ...UnprocessableEntityException.schema(),
213
- ...InternalServerErrorException.schema(),
214
- ...ConflictException.schema()
215
- }
216
- };
@@ -1,30 +0,0 @@
1
- import { Certificate, Identity, Key, PrivateKey } from "sshpk";
2
- import { SshCaBindings } from "./types";
3
- export type CreateCertificateOptions = {
4
- lifetime?: number;
5
- principals?: string[];
6
- extensions?: string[];
7
- };
8
- export declare class CertificateError extends Error {
9
- constructor(message: string);
10
- }
11
- export type GenerateCertificateOptions = {
12
- /**
13
- * @internal
14
- * Override the certificate serial number.
15
- * Should only be set in tests.
16
- */
17
- serial?: bigint;
18
- } & CreateCertificateOptions;
19
- export declare const generateCertificate: (env: SshCaBindings, email: string, key: PrivateKey, public_key: Key, options?: GenerateCertificateOptions) => Certificate;
20
- export declare function createSignedCertificate(env: SshCaBindings, email: string, public_key: Key, options?: CreateCertificateOptions): Promise<Certificate>;
21
- export type CreateHostCertificateOptions = {
22
- lifetime?: number;
23
- principals?: string[];
24
- subjects?: Identity[];
25
- };
26
- export declare class BadIssuerError extends Error {
27
- constructor(message: string);
28
- }
29
- export declare function createSignedHostCertificate(env: SshCaBindings, public_key: Key, options?: CreateHostCertificateOptions): Promise<Certificate>;
30
- export declare const generateSerial: (serial?: bigint) => Buffer<ArrayBuffer>;
@@ -1,154 +0,0 @@
1
- import { seconds } from "itty-time";
2
- import { createCertificate, identityForHost, identityForUser, identityFromDN, parsePrivateKey } from "sshpk";
3
- import { split } from "./utils";
4
- /* const DefaultCreateCertificateOptions: CreateCertificateOptions = {
5
- lifetime: seconds(env.SSH_CERTIFICATE_LIFETIME),
6
- principals: [],
7
- extensions: sshCertificateExtensions
8
- } */
9
- export class CertificateError extends Error {
10
- constructor(message) {
11
- super(message);
12
- this.name = "CertificateError";
13
- // This is necessary for proper stack trace in TypeScript
14
- Object.setPrototypeOf(this, CertificateError.prototype);
15
- }
16
- }
17
- export const generateCertificate = (env, email, key, public_key, options) => {
18
- let { lifetime, principals, extensions, serial } = options ?? {};
19
- // add identities
20
- const identity = env.SSH_CERTIFICATE_INCLUDE_SELF === "true"
21
- ? [identityForUser(email.split("@")[0])]
22
- : [];
23
- if (principals !== undefined) {
24
- for (const p of principals) {
25
- identity.push(identityForUser(p));
26
- }
27
- }
28
- if (env.SSH_CERTIFICATE_PRINCIPALS !== "") {
29
- for (const p of split(env.SSH_CERTIFICATE_PRINCIPALS)) {
30
- identity.push(identityForUser(p));
31
- }
32
- }
33
- // lifetime is the smaller of what was provided in the options or the default
34
- lifetime = lifetime !== undefined
35
- ? Math.min(lifetime, seconds(env.SSH_CERTIFICATE_LIFETIME))
36
- : seconds(env.SSH_CERTIFICATE_LIFETIME);
37
- // generate value for serial of certificate
38
- const s = generateSerial(serial);
39
- // set issuer of certificate based on ISSUER_DN
40
- const issuer = identityFromDN(env.ISSUER_DN);
41
- // create certificate
42
- const certificate = createCertificate(identity, public_key, issuer, key, { lifetime: lifetime, serial: s });
43
- // ensure openssh info is included in certificate (should not occur)
44
- if (certificate.signatures.openssh === undefined) {
45
- throw new CertificateError("missing openssh information in certificate");
46
- }
47
- const sshCertificateExtensions = split(env.SSH_CERTIFICATE_EXTENSIONS);
48
- // add usage extensions
49
- const sshextensions = [];
50
- if (extensions !== undefined) {
51
- // if extensions are provided in request, ensure they do not include extra extensions beyond the defaults
52
- for (const ext of extensions) {
53
- if (!sshCertificateExtensions.includes(ext)) {
54
- throw new CertificateError(`${ext} is not allowed`);
55
- }
56
- // add to list of allowed extensions
57
- sshextensions.push({
58
- critical: false,
59
- name: ext,
60
- data: Buffer.alloc(0)
61
- });
62
- }
63
- }
64
- else {
65
- // use defaults if not provided
66
- sshCertificateExtensions.forEach((ext) => {
67
- sshextensions.push({
68
- critical: false,
69
- name: ext,
70
- data: Buffer.alloc(0)
71
- });
72
- });
73
- }
74
- // add info to certificate
75
- certificate.signatures = {
76
- openssh: {
77
- nonce: certificate.signatures.openssh.nonce,
78
- keyId: email,
79
- signature: certificate.signatures.openssh.signature,
80
- exts: sshextensions
81
- }
82
- };
83
- // re-sign after changes
84
- certificate.signWith(key);
85
- return certificate;
86
- };
87
- export async function createSignedCertificate(env, email, public_key, options) {
88
- // grab private key from secret store
89
- const secret = await env.PRIVATE_KEY.get();
90
- // parse key
91
- const key = parsePrivateKey(secret);
92
- if (options === undefined) {
93
- options = {
94
- lifetime: seconds(env.SSH_CERTIFICATE_LIFETIME),
95
- principals: [],
96
- extensions: split(env.SSH_CERTIFICATE_EXTENSIONS)
97
- };
98
- }
99
- return generateCertificate(env, email, key, public_key, options);
100
- }
101
- /* const DefaultCreateHostertificateOptions: CreateHostCertificateOptions = {
102
- lifetime: seconds(env.SSH_HOST_CERTIFICATE_LIFETIME),
103
- principals: [],
104
- } */
105
- export class BadIssuerError extends Error {
106
- constructor(message) {
107
- super(message);
108
- this.name = "BadIssuerError";
109
- Object.setPrototypeOf(this, BadIssuerError.prototype);
110
- }
111
- }
112
- export async function createSignedHostCertificate(env, public_key, options) {
113
- if (options === undefined) {
114
- options = {
115
- lifetime: seconds(env.SSH_HOST_CERTIFICATE_LIFETIME),
116
- principals: []
117
- };
118
- }
119
- // generate list of identities for host key
120
- const identity = [];
121
- if (options.principals !== undefined) {
122
- for (const p of options.principals) {
123
- identity.push(identityForHost(p));
124
- }
125
- }
126
- // add any already provided subjects (for renewals only)
127
- if (options.subjects !== undefined) {
128
- identity.push(...options.subjects);
129
- }
130
- // grab private key from secret store
131
- const secret = await env.PRIVATE_KEY.get();
132
- // parse private key
133
- const key = parsePrivateKey(secret);
134
- // lifetime is the smaller of what was provided in the options or the default
135
- const lifetime = options.lifetime !== undefined
136
- ? Math.min(options.lifetime, seconds(env.SSH_HOST_CERTIFICATE_LIFETIME))
137
- : seconds(env.SSH_HOST_CERTIFICATE_LIFETIME);
138
- // generate value for serial of certificate
139
- const serial = generateSerial();
140
- // set issuer of certificate based on ISSUER_DN
141
- const issuer = identityFromDN(env.ISSUER_DN);
142
- // create certificate
143
- const certificate = createCertificate(identity, public_key, issuer, key, { lifetime: lifetime, serial: serial });
144
- return certificate;
145
- }
146
- export const generateSerial = (serial) => {
147
- if (serial !== undefined) {
148
- const s = Buffer.alloc(8);
149
- s.writeBigUInt64BE(serial);
150
- return s;
151
- }
152
- const randomBytes = crypto.getRandomValues(new Uint8Array(8));
153
- return Buffer.from(randomBytes);
154
- };
package/dist/src/db.d.ts DELETED
@@ -1,36 +0,0 @@
1
- import { Certificate } from "sshpk";
2
- import { SshCaBindings } from "./types";
3
- export declare enum CertificateType {
4
- User = 0,
5
- Host = 1
6
- }
7
- export type CertificateRecord = {
8
- id: number;
9
- serial: string;
10
- key_id: string;
11
- principals: string;
12
- extensions: string | null;
13
- valid_after: Date;
14
- valid_before: Date;
15
- revoked_at: Date | null;
16
- certificate_type: CertificateType;
17
- public_key: string | null;
18
- };
19
- export declare const runStatement: <T = Record<string, unknown>>(stmt: D1PreparedStatement) => Promise<D1Result<T>>;
20
- export declare const shouldRetry: (err: unknown, nextAttempt: number) => boolean;
21
- export declare const dbCleanup: (env: SshCaBindings) => Promise<void>;
22
- export declare const recordCertificate: (env: SshCaBindings, certificate: Certificate, keyid: string, certificateType?: CertificateType) => Promise<void>;
23
- export declare const isRevoked: (env: SshCaBindings, serial: bigint) => Promise<boolean>;
24
- export declare enum RevocationStatus {
25
- NotFound = 0,
26
- Revoked = 1,
27
- Revokable = 2,
28
- Unrevokable = 3
29
- }
30
- export declare const revocationStatus: (env: SshCaBindings, serial: bigint, publickey: string, certificateType: CertificateType) => Promise<RevocationStatus>;
31
- export declare const getRevocationList: (env: SshCaBindings, certificateType: CertificateType) => Promise<string[]>;
32
- type RevokedCertificate = {
33
- revoked_at: string;
34
- };
35
- export declare const revokeCertificate: (env: SshCaBindings, serial: bigint) => Promise<RevokedCertificate[]>;
36
- export {};
package/dist/src/db.js DELETED
@@ -1,111 +0,0 @@
1
- import { tryWhile } from "@cloudflare/actors";
2
- import { logger } from "./logger";
3
- export var CertificateType;
4
- (function (CertificateType) {
5
- CertificateType[CertificateType["User"] = 0] = "User";
6
- CertificateType[CertificateType["Host"] = 1] = "Host";
7
- })(CertificateType || (CertificateType = {}));
8
- export const runStatement = async (stmt) => {
9
- return await tryWhile(async () => {
10
- return await stmt.run();
11
- }, shouldRetry);
12
- };
13
- export const shouldRetry = (err, nextAttempt) => {
14
- const errMsg = String(err);
15
- const isRetryableError = errMsg.includes("Network connection lost") ||
16
- errMsg.includes("storage caused object to be reset") ||
17
- errMsg.includes("reset because its code was updated") ||
18
- errMsg.includes("stream because client disconnected");
19
- if (nextAttempt <= 5 && isRetryableError) {
20
- return true;
21
- }
22
- return false;
23
- };
24
- export const dbCleanup = async (env) => {
25
- const l = logger.with("retention", env.DB_CERTIFICATE_RETENTION);
26
- if (env.DB_CERTIFICATE_RETENTION === "infinite") {
27
- l.info("skipping database cleanup");
28
- }
29
- l.info("starting database cleanup");
30
- try {
31
- const stmt = env.DB.prepare("DELETE FROM certificates WHERE unixepoch('subsec') > unixepoch(valid_before,'subsec',?)")
32
- .bind(env.DB_CERTIFICATE_RETENTION);
33
- const res = await stmt.run();
34
- l.info("completed database cleanup", "changes", res.meta.changes);
35
- }
36
- catch (err) {
37
- l.error("error during database cleanup", "retention", env.DB_CERTIFICATE_RETENTION);
38
- }
39
- };
40
- export const recordCertificate = async (env, certificate, keyid, certificateType = CertificateType.User) => {
41
- const serial = certificate.serial.readBigUInt64BE(0);
42
- const subjects = certificate.subjects.map((v) => {
43
- return v.toString();
44
- }).join(",");
45
- const extensions = certificate.getExtensions().map((v) => {
46
- // @ts-ignore: the name property does exist
47
- return v.name;
48
- }).join(",");
49
- const stmt = env.DB
50
- .prepare("INSERT INTO certificates (serial, key_id, principals, extensions, valid_after, valid_before, certificate_type, public_key) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
51
- .bind(`${serial}`, keyid, subjects, extensions, certificate.validFrom.toISOString(), certificate.validUntil.toISOString(), certificateType, certificate.subjectKey.toString("ssh"));
52
- const res = await runStatement(stmt);
53
- if (!res.success) {
54
- if (res.error !== undefined) {
55
- throw new Error(res.error);
56
- }
57
- throw new Error("error during query");
58
- }
59
- };
60
- export const isRevoked = async (env, serial) => {
61
- const stmt = env.DB
62
- .prepare("SELECT serial FROM certificates WHERE serial = ? AND revoked_at NOTNULL")
63
- .bind(`${serial}`);
64
- const res = await runStatement(stmt);
65
- return res.results.length > 0;
66
- };
67
- export var RevocationStatus;
68
- (function (RevocationStatus) {
69
- RevocationStatus[RevocationStatus["NotFound"] = 0] = "NotFound";
70
- RevocationStatus[RevocationStatus["Revoked"] = 1] = "Revoked";
71
- RevocationStatus[RevocationStatus["Revokable"] = 2] = "Revokable";
72
- RevocationStatus[RevocationStatus["Unrevokable"] = 3] = "Unrevokable";
73
- })(RevocationStatus || (RevocationStatus = {}));
74
- export const revocationStatus = async (env, serial, publickey, certificateType) => {
75
- const stmt = env.DB
76
- .prepare("SELECT public_key, revoked_at as count FROM certificates WHERE serial = ? AND public_key = ? AND certificate_type = ?")
77
- .bind(`${serial}`, publickey, certificateType);
78
- const res = await runStatement(stmt);
79
- if (res.results.length === 0) {
80
- return RevocationStatus.NotFound;
81
- }
82
- const row = res.results?.[0];
83
- if (!row) {
84
- return RevocationStatus.NotFound;
85
- }
86
- if (row.revoked_at !== null) {
87
- return RevocationStatus.Revoked;
88
- }
89
- if (publickey === row.public_key) {
90
- return RevocationStatus.Revokable;
91
- }
92
- return RevocationStatus.Unrevokable;
93
- };
94
- export const getRevocationList = async (env, certificateType) => {
95
- const stmt = env.DB
96
- .prepare("SELECT serial FROM certificates WHERE revoked_at NOTNULL AND certificate_type = ? AND unixepoch('subsec') < unixepoch(valid_before,'subsec','24 hours')")
97
- .bind(certificateType);
98
- const res = await runStatement(stmt);
99
- const result = [];
100
- for (const item of res.results) {
101
- result.push(item.serial);
102
- }
103
- return result;
104
- };
105
- export const revokeCertificate = async (env, serial) => {
106
- const stmt = env.DB
107
- .prepare("UPDATE certificates SET revoked_at = ? WHERE serial = ? RETURNING revoked_at")
108
- .bind((new Date).toISOString(), `${serial}`);
109
- const res = await runStatement(stmt);
110
- return res.results;
111
- };
@@ -1,6 +0,0 @@
1
- import { SshCaBindings } from "./types";
2
- declare const _default: {
3
- fetch: (request: Request, Env?: unknown, executionCtx?: import("hono").ExecutionContext) => Response | Promise<Response>;
4
- scheduled(controller: ScheduledController, env: SshCaBindings, ctx: ExecutionContext): Promise<void>;
5
- };
6
- export default _default;
package/dist/src/index.js DELETED
@@ -1,8 +0,0 @@
1
- import { dbCleanup } from "./db";
2
- import { app } from "./router";
3
- export default {
4
- fetch: app.fetch,
5
- async scheduled(controller, env, ctx) {
6
- ctx.waitUntil(dbCleanup(env));
7
- },
8
- };
package/dist/src/krl.d.ts DELETED
@@ -1,39 +0,0 @@
1
- import type { PrivateKey } from "sshpk";
2
- export declare class KRLBuilder {
3
- private readonly _caKey;
4
- private readonly _serials;
5
- private readonly _keyIds;
6
- private _krl;
7
- constructor(caKey: PrivateKey);
8
- /**
9
- * Add certificate serial numbers to be revoked.
10
- * May be called multiple times all serials are merged into one section.
11
- *
12
- * This must not be called after generate() has been called
13
- */
14
- addSerials(serials: bigint[]): this;
15
- /**
16
- * Add certificate key ID strings to be revoked.
17
- * May be called multiple times all key IDs are merged into one section.
18
- *
19
- * This must not be called after generate() has been called
20
- */
21
- addKeyIds(keyIds: string[]): this;
22
- /**
23
- * Generate the binary KRL
24
- *
25
- * @param krlVersion Optional KRL version number (default: 1).
26
- * @param comment Optional comment string embedded in the header.
27
- */
28
- generate(krlVersion?: bigint, comment?: string): ArrayBuffer;
29
- /**
30
- * Return the SSHSIG signature of the KRL.
31
- *
32
- * This must be called after generate()
33
- *
34
- * The returned string can be verified with ssh-keygen:
35
- * echo -n "$KRL_BYTES" | ssh-keygen -Y verify -f allowed_signers \
36
- * -I <identity> -n file -s <sig_file>
37
- */
38
- signature(): Promise<string>;
39
- }