@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.
- package/LICENSE +201 -0
- package/README.md +268 -0
- package/dist/api/v3/schema.js +2 -1
- package/dist/proof.js +1 -1
- package/dist/sshsig/formats.js +1 -1
- package/dist/utils.d.ts +2 -1
- package/dist/utils.js +1 -2
- package/package.json +9 -13
- package/dist/src/api/v3/index.d.ts +0 -1
- package/dist/src/api/v3/index.js +0 -263
- package/dist/src/api/v3/schema.d.ts +0 -217
- package/dist/src/api/v3/schema.js +0 -216
- package/dist/src/certificate.d.ts +0 -30
- package/dist/src/certificate.js +0 -154
- package/dist/src/db.d.ts +0 -36
- package/dist/src/db.js +0 -111
- package/dist/src/index.d.ts +0 -6
- package/dist/src/index.js +0 -8
- package/dist/src/krl.d.ts +0 -39
- package/dist/src/krl.js +0 -317
- package/dist/src/logger.d.ts +0 -2
- package/dist/src/logger.js +0 -3
- package/dist/src/proof.d.ts +0 -51
- package/dist/src/proof.js +0 -154
- package/dist/src/router.d.ts +0 -14
- package/dist/src/router.js +0 -22
- package/dist/src/sshsig/armor.d.ts +0 -7
- package/dist/src/sshsig/armor.js +0 -18
- package/dist/src/sshsig/formats.d.ts +0 -52
- package/dist/src/sshsig/formats.js +0 -174
- package/dist/src/sshsig/index.d.ts +0 -15
- package/dist/src/sshsig/index.js +0 -31
- package/dist/src/sshsig/reader.d.ts +0 -20
- package/dist/src/sshsig/reader.js +0 -54
- package/dist/src/sshsig/sig.d.ts +0 -16
- package/dist/src/sshsig/sig.js +0 -1
- package/dist/src/sshsig/sig_parser.d.ts +0 -9
- package/dist/src/sshsig/sig_parser.js +0 -83
- package/dist/src/sshsig/verifier.d.ts +0 -2
- package/dist/src/sshsig/verifier.js +0 -37
- package/dist/src/sshsig/writer.d.ts +0 -17
- package/dist/src/sshsig/writer.js +0 -51
- package/dist/src/types.d.ts +0 -42
- package/dist/src/types.js +0 -20
- package/dist/src/utils.d.ts +0 -76
- package/dist/src/utils.js +0 -370
- package/dist/src/verify.d.ts +0 -6
- package/dist/src/verify.js +0 -17
|
@@ -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>;
|
package/dist/src/certificate.js
DELETED
|
@@ -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
|
-
};
|
package/dist/src/index.d.ts
DELETED
|
@@ -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
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
|
-
}
|