@andrewheberle/serverless-ssh-ca 0.17.5

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 (81) hide show
  1. package/dist/api/v3/index.d.ts +2 -0
  2. package/dist/api/v3/index.js +278 -0
  3. package/dist/api/v3/schema.d.ts +218 -0
  4. package/dist/api/v3/schema.js +207 -0
  5. package/dist/certificate.d.ts +30 -0
  6. package/dist/certificate.js +154 -0
  7. package/dist/db.d.ts +36 -0
  8. package/dist/db.js +111 -0
  9. package/dist/index.d.ts +7 -0
  10. package/dist/index.js +14 -0
  11. package/dist/krl.d.ts +39 -0
  12. package/dist/krl.js +317 -0
  13. package/dist/logger.d.ts +3 -0
  14. package/dist/logger.js +2 -0
  15. package/dist/proof.d.ts +52 -0
  16. package/dist/proof.js +153 -0
  17. package/dist/router.d.ts +7 -0
  18. package/dist/router.js +24 -0
  19. package/dist/src/api/v3/index.d.ts +1 -0
  20. package/dist/src/api/v3/index.js +263 -0
  21. package/dist/src/api/v3/schema.d.ts +217 -0
  22. package/dist/src/api/v3/schema.js +216 -0
  23. package/dist/src/certificate.d.ts +30 -0
  24. package/dist/src/certificate.js +154 -0
  25. package/dist/src/db.d.ts +36 -0
  26. package/dist/src/db.js +111 -0
  27. package/dist/src/index.d.ts +6 -0
  28. package/dist/src/index.js +8 -0
  29. package/dist/src/krl.d.ts +39 -0
  30. package/dist/src/krl.js +317 -0
  31. package/dist/src/logger.d.ts +2 -0
  32. package/dist/src/logger.js +3 -0
  33. package/dist/src/proof.d.ts +51 -0
  34. package/dist/src/proof.js +154 -0
  35. package/dist/src/router.d.ts +14 -0
  36. package/dist/src/router.js +22 -0
  37. package/dist/src/sshsig/armor.d.ts +7 -0
  38. package/dist/src/sshsig/armor.js +18 -0
  39. package/dist/src/sshsig/formats.d.ts +52 -0
  40. package/dist/src/sshsig/formats.js +174 -0
  41. package/dist/src/sshsig/index.d.ts +15 -0
  42. package/dist/src/sshsig/index.js +31 -0
  43. package/dist/src/sshsig/reader.d.ts +20 -0
  44. package/dist/src/sshsig/reader.js +54 -0
  45. package/dist/src/sshsig/sig.d.ts +16 -0
  46. package/dist/src/sshsig/sig.js +1 -0
  47. package/dist/src/sshsig/sig_parser.d.ts +9 -0
  48. package/dist/src/sshsig/sig_parser.js +83 -0
  49. package/dist/src/sshsig/verifier.d.ts +2 -0
  50. package/dist/src/sshsig/verifier.js +37 -0
  51. package/dist/src/sshsig/writer.d.ts +17 -0
  52. package/dist/src/sshsig/writer.js +51 -0
  53. package/dist/src/types.d.ts +42 -0
  54. package/dist/src/types.js +20 -0
  55. package/dist/src/utils.d.ts +76 -0
  56. package/dist/src/utils.js +370 -0
  57. package/dist/src/verify.d.ts +6 -0
  58. package/dist/src/verify.js +17 -0
  59. package/dist/sshsig/armor.d.ts +7 -0
  60. package/dist/sshsig/armor.js +18 -0
  61. package/dist/sshsig/formats.d.ts +52 -0
  62. package/dist/sshsig/formats.js +174 -0
  63. package/dist/sshsig/index.d.ts +15 -0
  64. package/dist/sshsig/index.js +34 -0
  65. package/dist/sshsig/reader.d.ts +20 -0
  66. package/dist/sshsig/reader.js +54 -0
  67. package/dist/sshsig/sig.d.ts +16 -0
  68. package/dist/sshsig/sig.js +1 -0
  69. package/dist/sshsig/sig_parser.d.ts +9 -0
  70. package/dist/sshsig/sig_parser.js +83 -0
  71. package/dist/sshsig/verifier.d.ts +2 -0
  72. package/dist/sshsig/verifier.js +37 -0
  73. package/dist/sshsig/writer.d.ts +17 -0
  74. package/dist/sshsig/writer.js +51 -0
  75. package/dist/types.d.ts +42 -0
  76. package/dist/types.js +1 -0
  77. package/dist/utils.d.ts +59 -0
  78. package/dist/utils.js +323 -0
  79. package/dist/verify.d.ts +7 -0
  80. package/dist/verify.js +16 -0
  81. package/package.json +58 -0
@@ -0,0 +1,263 @@
1
+ import { group } from "@andrewheberle/ts-slog";
2
+ import { ApiException, ConflictException, ForbiddenException, fromHono, InternalServerErrorException, NotFoundException, OpenAPIRoute, UnauthorizedException, UnprocessableEntityException, } from "chanfana";
3
+ import { Hono } from "hono";
4
+ import { BadIssuerError, createSignedCertificate, createSignedHostCertificate, } from "../../certificate";
5
+ import { getPublic, split, } from "../../utils";
6
+ import { KeyParseError, parsePrivateKey, } from "sshpk";
7
+ import { CertificateType, getRevocationList, recordCertificate, RevocationStatus, revocationStatus, revokeCertificate, } from "../../db";
8
+ import { KRLBuilder } from "../../krl";
9
+ import { CaPublicKeyEndpointSchema, HostCertificateRenewEndpointSchema, HostCertificateRequestEndpointSchema, RevocationListEndpointSchema, RevokeCertificateEndpointSchema, UserCertificateRequestEndpointSchema, } from "./schema";
10
+ import { logger } from "../../logger";
11
+ export const api = fromHono(new Hono());
12
+ class CaPublicKeyEndpoint extends OpenAPIRoute {
13
+ schema = CaPublicKeyEndpointSchema;
14
+ async handle(c) {
15
+ const l = logger.with(...group("request", "action", "ca"));
16
+ try {
17
+ const pub = await getPublic();
18
+ return c.text(`${pub.toString("ssh").trim()}\n`);
19
+ }
20
+ catch (err) {
21
+ switch (true) {
22
+ case (err instanceof KeyParseError):
23
+ l.error("error parsing private key");
24
+ throw new InternalServerErrorException("error parsing private key");
25
+ case (err instanceof ApiException):
26
+ // re-throw any exisiting chanfana error
27
+ throw err;
28
+ default:
29
+ // otherwise throw as InternalServerErrorException
30
+ l.error("unhandled error", "error", err);
31
+ throw new InternalServerErrorException("internal server error");
32
+ }
33
+ }
34
+ }
35
+ }
36
+ api.get("/ca", CaPublicKeyEndpoint);
37
+ class UserCertificateRequestEndpoint extends OpenAPIRoute {
38
+ schema = UserCertificateRequestEndpointSchema;
39
+ async handle(c) {
40
+ const data = await this.getValidatedData();
41
+ const l = logger.with(...group("request", "type", "user", "action", "request"), ...group("auth_token", "email", data.headers.Authorization.email, "sub", data.headers.Authorization.sub), ...group("id_token", "sub", data.body.identity.sub));
42
+ if (data.body.identity.sub !== data.headers["Authorization"].sub) {
43
+ l.error("token subjects did not match");
44
+ throw new ForbiddenException("Possible token substitution as subjects for authentication and identity tokens did not match");
45
+ }
46
+ const opts = {
47
+ lifetime: data.body.lifetime,
48
+ principals: data.body.identity.principals,
49
+ extensions: data.body.extensions,
50
+ };
51
+ try {
52
+ const certificate = await createSignedCertificate(data.headers.Authorization.email, data.body.public_key, opts);
53
+ const response = {
54
+ certificate: btoa(certificate.toString("openssh"))
55
+ };
56
+ try {
57
+ await recordCertificate(c.env, certificate, data.headers.Authorization.email);
58
+ }
59
+ catch (err) {
60
+ l.error("there was a problem adding issued certificate to database", "error", err);
61
+ }
62
+ l.info("completed issuing certificate", ...group("certificate", "principals", certificate.subjects.map(v => v.toString()).join(","), "serial", certificate.serial.readBigUInt64BE(0)));
63
+ return c.json(response);
64
+ }
65
+ catch (err) {
66
+ switch (true) {
67
+ case (err instanceof ApiException):
68
+ // re-throw any exisiting chanfana error
69
+ throw err;
70
+ default:
71
+ // otherwise throw as InternalServerErrorException
72
+ l.error("unhandled error", "error", err);
73
+ throw new InternalServerErrorException("internal server error");
74
+ }
75
+ }
76
+ }
77
+ }
78
+ api.post("/user/certificate", UserCertificateRequestEndpoint);
79
+ class RevocationListEndpoint extends OpenAPIRoute {
80
+ schema = RevocationListEndpointSchema;
81
+ async handle(c) {
82
+ const data = await this.getValidatedData();
83
+ const l = logger.with(...group("request", "type", data.params.certificateType, "action", "krl"));
84
+ const certificateType = data.params.certificateType === "user" ? CertificateType.User : CertificateType.Host;
85
+ try {
86
+ // get revoked serials
87
+ const serials = (await getRevocationList(c.env, certificateType)).map(v => BigInt(v));
88
+ // grab private key from secret store
89
+ const secret = await c.env.PRIVATE_KEY.get();
90
+ // parse key
91
+ const key = parsePrivateKey(secret);
92
+ // generate KRL
93
+ const krl = new KRLBuilder(key)
94
+ .addSerials(serials);
95
+ const krlBytes = krl.generate();
96
+ // convert to base64
97
+ const bytes = new Uint8Array(krlBytes);
98
+ let binary = "";
99
+ for (const byte of bytes) {
100
+ binary += String.fromCharCode(byte);
101
+ }
102
+ const base64Krl = btoa(binary);
103
+ const signature = await krl.signature();
104
+ return c.json({
105
+ krl: base64Krl,
106
+ signature: signature,
107
+ });
108
+ }
109
+ catch (err) {
110
+ switch (true) {
111
+ case (err instanceof KeyParseError):
112
+ l.error("error parsing private key");
113
+ throw new InternalServerErrorException("error parsing private key");
114
+ case (err instanceof ApiException):
115
+ // re-throw any exisiting chanfana error
116
+ throw err;
117
+ default:
118
+ // otherwise throw as InternalServerErrorException
119
+ l.error("unhandled error", "error", err);
120
+ throw new InternalServerErrorException("internal server error");
121
+ }
122
+ }
123
+ }
124
+ }
125
+ api.get("/:certificateType/krl", RevocationListEndpoint);
126
+ class HostCertificateRequestEndpoint extends OpenAPIRoute {
127
+ schema = HostCertificateRequestEndpointSchema;
128
+ async handle(c) {
129
+ const data = await this.getValidatedData();
130
+ const l = logger.with(...group("request", "type", "host", "action", "request"), ...group("auth_token", "email", data.headers.Authorization.email, "sub", data.headers.Authorization.sub), ...group("id_token", "sub", data.body.identity.sub));
131
+ // check id and access token are for the same user
132
+ if (data.body.identity.sub !== data.headers.Authorization.sub) {
133
+ l.error("token subjects did not match");
134
+ throw new ForbiddenException("possible token substitution as subjects for authentication and identity tokens did not match");
135
+ }
136
+ // check user can issue host certificates
137
+ if (!split(c.env.SSH_HOST_CERTIFICATE_ALLOWED_EMAILS).includes(data.headers.Authorization.email) && !data.body.identity.principals.some((p) => split(c.env.SSH_HOST_CERTIFICATE_ALLOWED_ROLES).includes(p))) {
138
+ l.error("unauthorized host certificate request");
139
+ throw new UnauthorizedException("User not allowed to issue host certificates");
140
+ }
141
+ const opts = {
142
+ lifetime: data.body.lifetime,
143
+ principals: data.body.principals,
144
+ };
145
+ try {
146
+ const certificate = await createSignedHostCertificate(data.body.public_key, opts);
147
+ const response = {
148
+ certificate: btoa(certificate.toString("openssh"))
149
+ };
150
+ try {
151
+ await recordCertificate(c.env, certificate, `host_${certificate.subjects[0].hostname}`, CertificateType.Host);
152
+ }
153
+ catch (err) {
154
+ l.error("there was a problem adding issued certificate to database", "error", err);
155
+ }
156
+ l.info("completed issuing certificate", ...group("certificate", "principals", certificate.subjects.map(v => v.toString()).join(","), "serial", certificate.serial.readBigUInt64BE(0)));
157
+ return c.json(response);
158
+ }
159
+ catch (err) {
160
+ switch (true) {
161
+ case (err instanceof ApiException):
162
+ // re-throw any exisiting chanfana error
163
+ throw err;
164
+ default:
165
+ // otherwise throw as InternalServerErrorException
166
+ l.error("unhandled error", "error", err);
167
+ throw new InternalServerErrorException("internal server error");
168
+ }
169
+ }
170
+ }
171
+ }
172
+ api.post("/host/certificate", HostCertificateRequestEndpoint);
173
+ class HostCertificateRenewEndpoint extends OpenAPIRoute {
174
+ schema = HostCertificateRenewEndpointSchema;
175
+ async handle(c) {
176
+ const data = await this.getValidatedData();
177
+ const serial = data.body.certificate.serial.readBigUInt64BE(0);
178
+ const l = logger.with(...group("request", "type", "host", "action", "renewal", "serial", serial));
179
+ // use smaller of the current certificate lifetime and the requested lifetime
180
+ const originalLifetime = (data.body.certificate.validUntil.getTime() - data.body.certificate.validFrom.getTime()) / 1000;
181
+ const lifetime = originalLifetime > data.body.lifetime
182
+ ? data.body.lifetime
183
+ : originalLifetime;
184
+ const opts = {
185
+ lifetime: lifetime,
186
+ subjects: data.body.certificate.subjects
187
+ };
188
+ try {
189
+ const certificate = await createSignedHostCertificate(data.body.public_key, opts);
190
+ const response = {
191
+ certificate: btoa(certificate.toString("openssh"))
192
+ };
193
+ try {
194
+ await recordCertificate(c.env, certificate, `host_${certificate.subjects[0].hostname}`, CertificateType.Host);
195
+ }
196
+ catch (err) {
197
+ l.error("there was a problem adding issued certificate to database", "error", err);
198
+ }
199
+ l.info("completed issuing certificate", ...group("certificate", "principals", certificate.subjects.map(v => v.toString()).join(","), "serial", certificate.serial.readBigUInt64BE(0)));
200
+ return c.json(response);
201
+ }
202
+ catch (err) {
203
+ switch (true) {
204
+ case (err instanceof ApiException):
205
+ // re-throw any exisiting chanfana error
206
+ throw err;
207
+ case (err instanceof BadIssuerError):
208
+ throw new UnprocessableEntityException(err.message);
209
+ default:
210
+ // otherwise throw as InternalServerErrorException
211
+ l.error("unhandled error", "error", err);
212
+ throw new InternalServerErrorException("internal server error");
213
+ }
214
+ }
215
+ }
216
+ }
217
+ api.post("/host/renew", HostCertificateRenewEndpoint);
218
+ class RevokeCertificateEndpoint extends OpenAPIRoute {
219
+ schema = RevokeCertificateEndpointSchema;
220
+ async handle(c) {
221
+ const data = await this.getValidatedData();
222
+ const l = logger.with(...group("request", "type", data.params.certificateType, "action", "revoke", "serial", data.body.serial));
223
+ const certificatetype = data.params.certificateType === "user" ? CertificateType.User : CertificateType.Host;
224
+ // ensure certificate can be revoked
225
+ const status = await revocationStatus(c.env, data.body.serial, data.body.public_key.toString("ssh"), certificatetype);
226
+ switch (status) {
227
+ case RevocationStatus.Revoked:
228
+ l.warn("certificate is already revoked");
229
+ throw new ConflictException("certificate is already revoked");
230
+ case RevocationStatus.Unrevokable:
231
+ l.warn("cannot revoke this certificate");
232
+ throw new ForbiddenException("cannot revoke this certificate");
233
+ case RevocationStatus.NotFound:
234
+ l.warn("certificate not found");
235
+ throw new NotFoundException("certificate not found");
236
+ }
237
+ try {
238
+ const res = await revokeCertificate(c.env, data.body.serial);
239
+ if (res.length === 0) {
240
+ throw new InternalServerErrorException("no results were returned for revocation");
241
+ }
242
+ if (res.length > 1) {
243
+ logger.warn("more than one revocation result was returned", "count", res.length);
244
+ }
245
+ logger.info("completed revocation");
246
+ return c.json({
247
+ revoked_at: res[0].revoked_at
248
+ });
249
+ }
250
+ catch (err) {
251
+ switch (true) {
252
+ case (err instanceof ApiException):
253
+ // re-throw any exisiting chanfana error
254
+ throw err;
255
+ default:
256
+ // otherwise throw as InternalServerErrorException
257
+ l.error("unhandled error", "error", err);
258
+ throw new InternalServerErrorException("internal server error");
259
+ }
260
+ }
261
+ }
262
+ }
263
+ api.post("/:certificateType/revoke", RevokeCertificateEndpoint);
@@ -0,0 +1,217 @@
1
+ import z from "zod";
2
+ export declare const CaPublicKeyEndpointSchema: {
3
+ responses: {
4
+ "200": {
5
+ content: {
6
+ "text/plain": {
7
+ schema: z.ZodString;
8
+ };
9
+ };
10
+ description: string;
11
+ };
12
+ };
13
+ };
14
+ export declare const UserCertificateRequestEndpointSchema: {
15
+ security: {
16
+ oidcAuth: never[];
17
+ }[];
18
+ request: {
19
+ headers: z.ZodObject<{
20
+ Authorization: z.ZodPipe<z.ZodString, z.ZodTransform<{
21
+ email: string;
22
+ sub: string;
23
+ }, string>>;
24
+ }, z.core.$strip>;
25
+ body: {
26
+ content: {
27
+ "application/json": {
28
+ schema: z.ZodType<{
29
+ public_key: import("sshpk").Key;
30
+ proof: import("../../proof").ProofOfPossession;
31
+ identity: {
32
+ sub: string;
33
+ principals: string[];
34
+ };
35
+ extensions: string[];
36
+ lifetime: number;
37
+ }, unknown, z.core.$ZodTypeInternals<{
38
+ public_key: import("sshpk").Key;
39
+ proof: import("../../proof").ProofOfPossession;
40
+ identity: {
41
+ sub: string;
42
+ principals: string[];
43
+ };
44
+ extensions: string[];
45
+ lifetime: number;
46
+ }, unknown>>;
47
+ };
48
+ };
49
+ };
50
+ };
51
+ responses: {
52
+ "200": {
53
+ content: {
54
+ "application/json": {
55
+ schema: z.ZodType<{
56
+ certificate: Buffer<ArrayBuffer>;
57
+ }, unknown, z.core.$ZodTypeInternals<{
58
+ certificate: Buffer<ArrayBuffer>;
59
+ }, unknown>>;
60
+ };
61
+ };
62
+ description: string;
63
+ };
64
+ };
65
+ };
66
+ export declare const RevocationListEndpointSchema: {
67
+ request: {
68
+ params: z.ZodObject<{
69
+ certificateType: z.ZodEnum<{
70
+ user: "user";
71
+ host: "host";
72
+ }>;
73
+ }, z.core.$strip>;
74
+ };
75
+ responses: {
76
+ "200": {
77
+ content: {
78
+ "application/json": {
79
+ schema: z.ZodType<{
80
+ krl: Buffer<ArrayBuffer>;
81
+ signature: string;
82
+ }, unknown, z.core.$ZodTypeInternals<{
83
+ krl: Buffer<ArrayBuffer>;
84
+ signature: string;
85
+ }, unknown>>;
86
+ };
87
+ };
88
+ description: string;
89
+ };
90
+ };
91
+ };
92
+ export declare const HostCertificateRequestEndpointSchema: {
93
+ security: {
94
+ oidcAuth: never[];
95
+ }[];
96
+ request: {
97
+ headers: z.ZodObject<{
98
+ Authorization: z.ZodPipe<z.ZodString, z.ZodTransform<{
99
+ email: string;
100
+ sub: string;
101
+ }, string>>;
102
+ }, z.core.$strip>;
103
+ body: {
104
+ content: {
105
+ "application/json": {
106
+ schema: z.ZodType<{
107
+ public_key: import("sshpk").Key;
108
+ proof: import("../../proof").ProofOfPossession;
109
+ identity: {
110
+ sub: string;
111
+ principals: string[];
112
+ };
113
+ principals: string[];
114
+ lifetime: number;
115
+ }, unknown, z.core.$ZodTypeInternals<{
116
+ public_key: import("sshpk").Key;
117
+ proof: import("../../proof").ProofOfPossession;
118
+ identity: {
119
+ sub: string;
120
+ principals: string[];
121
+ };
122
+ principals: string[];
123
+ lifetime: number;
124
+ }, unknown>>;
125
+ };
126
+ };
127
+ };
128
+ };
129
+ responses: {
130
+ "200": {
131
+ content: {
132
+ "application/json": {
133
+ schema: z.ZodType<{
134
+ certificate: Buffer<ArrayBuffer>;
135
+ }, unknown, z.core.$ZodTypeInternals<{
136
+ certificate: Buffer<ArrayBuffer>;
137
+ }, unknown>>;
138
+ };
139
+ };
140
+ description: string;
141
+ };
142
+ };
143
+ };
144
+ export declare const HostCertificateRenewEndpointSchema: {
145
+ request: {
146
+ body: {
147
+ content: {
148
+ "application/json": {
149
+ schema: z.ZodType<{
150
+ certificate: import("sshpk").Certificate;
151
+ public_key: import("sshpk").Key;
152
+ proof: import("../../proof").RenewalProofOfPossession;
153
+ lifetime: number;
154
+ }, unknown, z.core.$ZodTypeInternals<{
155
+ certificate: import("sshpk").Certificate;
156
+ public_key: import("sshpk").Key;
157
+ proof: import("../../proof").RenewalProofOfPossession;
158
+ lifetime: number;
159
+ }, unknown>>;
160
+ };
161
+ };
162
+ };
163
+ };
164
+ responses: {
165
+ "200": {
166
+ content: {
167
+ "application/json": {
168
+ schema: z.ZodType<{
169
+ certificate: Buffer<ArrayBuffer>;
170
+ }, unknown, z.core.$ZodTypeInternals<{
171
+ certificate: Buffer<ArrayBuffer>;
172
+ }, unknown>>;
173
+ };
174
+ };
175
+ description: string;
176
+ };
177
+ };
178
+ };
179
+ export declare const RevokeCertificateEndpointSchema: {
180
+ request: {
181
+ params: z.ZodObject<{
182
+ certificateType: z.ZodEnum<{
183
+ user: "user";
184
+ host: "host";
185
+ }>;
186
+ }, z.core.$strip>;
187
+ body: {
188
+ content: {
189
+ "application/json": {
190
+ schema: z.ZodType<{
191
+ serial: bigint;
192
+ public_key: import("sshpk").Key;
193
+ proof: import("../../proof").ProofOfPossession;
194
+ }, unknown, z.core.$ZodTypeInternals<{
195
+ serial: bigint;
196
+ public_key: import("sshpk").Key;
197
+ proof: import("../../proof").ProofOfPossession;
198
+ }, unknown>>;
199
+ };
200
+ };
201
+ };
202
+ };
203
+ responses: {
204
+ "200": {
205
+ content: {
206
+ "application/json": {
207
+ schema: z.ZodType<{
208
+ revoked_at: string;
209
+ }, unknown, z.core.$ZodTypeInternals<{
210
+ revoked_at: string;
211
+ }, unknown>>;
212
+ };
213
+ };
214
+ description: string;
215
+ };
216
+ };
217
+ };