@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
package/dist/utils.d.ts CHANGED
@@ -1,17 +1,73 @@
1
1
  import type { SshCaBindings } from "./types.js";
2
- import type { Certificate, Key, PrivateKey } from "sshpk";
2
+ import { SshCertificate } from "./ssh/certificate.js";
3
+ import { PublicKey, UnsupportedKeyError, type PrivateKey } from "./ssh/keys.js";
3
4
  import z from "zod";
4
5
  import { CertificateRequestJWTPayload } from "./types.js";
5
6
  import { RenewalProofOfPossession, ProofOfPossession } from "./proof.js";
6
7
  import type { isRevoked as IsRevokedFn } from "./db/index.js";
8
+ export { UnsupportedKeyError };
7
9
  export declare const fatalIssue: (ctx: z.RefinementCtx, message: string, val: unknown) => never;
8
10
  export declare const transformProofOfPossession: (env: SshCaBindings, val: string, ctx: z.core.$RefinementCtx<string>) => ProofOfPossession | never;
9
11
  type AccessToken = {
10
12
  email: string;
11
13
  sub: string;
12
14
  };
15
+ /**
16
+ * Thrown when a claim of a verified token is missing or is not of the
17
+ * expected type.
18
+ */
19
+ export declare class TokenClaimError extends Error {
20
+ constructor(message: string);
21
+ }
22
+ /**
23
+ * Returns the subject of a verified token, which is compared between the
24
+ * access and identity tokens so must always be present.
25
+ *
26
+ * @param payload The payload of a verified token
27
+ * @returns The `sub` claim of the token
28
+ * @throws {@link TokenClaimError} when the `sub` claim is missing, empty or not a string
29
+ */
30
+ export declare const tokenSubject: (payload: {
31
+ sub?: unknown;
32
+ }) => string;
33
+ /**
34
+ * Returns the email address of a verified access token, or why it cannot be
35
+ * used.
36
+ *
37
+ * @param payload The payload of a verified access token
38
+ * @returns The email address, or an error message if the claims are not acceptable
39
+ */
40
+ export declare const tokenEmail: (payload: Record<string, unknown>) => {
41
+ ok: true;
42
+ email: string;
43
+ } | {
44
+ ok: false;
45
+ error: string;
46
+ };
13
47
  export declare const transformAuthorizationHeader: (env: SshCaBindings, val: string, ctx: z.RefinementCtx) => Promise<AccessToken | never>;
14
- export declare const transformPublicKey: (val: string | Buffer<ArrayBufferLike>, ctx: z.RefinementCtx) => Key | never;
48
+ /**
49
+ * Parses a public key in OpenSSH format, `${type} ${base64}[ ${comment}]`
50
+ *
51
+ * @param val The public key, either base64 encoded or as bytes
52
+ * @param ctx The Zod context for any errors
53
+ */
54
+ export declare const transformPublicKey: (val: string | Buffer<ArrayBufferLike>, ctx: z.RefinementCtx) => PublicKey | never;
55
+ /**
56
+ * Returns why a public key cannot be issued a certificate, or undefined if it
57
+ * can be.
58
+ *
59
+ * RSA keys must be at least {@link MinimumRsaKeyBits} bits, as smaller keys
60
+ * can be factored and are refused by current OpenSSH releases.
61
+ *
62
+ * @param key The public key a certificate would be issued for
63
+ * @returns The reason the key cannot be used, or undefined if it can be
64
+ */
65
+ export declare const subjectKeyIssue: (key: PublicKey) => string | undefined;
66
+ /**
67
+ * Refines a public key so only one allowed by {@link subjectKeyIssue} is
68
+ * accepted for a certificate request.
69
+ */
70
+ export declare const refineSubjectKey: (val: PublicKey, ctx: z.RefinementCtx) => void;
15
71
  /**
16
72
  * Thrown by {@link identityPrincipals} when the principals claim of an
17
73
  * identity token is not a string or an array of strings.
@@ -32,10 +88,10 @@ export declare const transformIdentityToken: (env: SshCaBindings, val: string, c
32
88
  * @param ctx The Zod context for any errors
33
89
  * @returns
34
90
  */
35
- export declare const transformCertificate: (val: string | Buffer<ArrayBufferLike>, ctx: z.RefinementCtx) => Certificate | never;
91
+ export declare const transformCertificate: (val: string | Buffer<ArrayBufferLike>, ctx: z.RefinementCtx) => SshCertificate | never;
36
92
  type ParsedCertificateRequest = {
37
93
  proof: ProofOfPossession;
38
- public_key: Key;
94
+ public_key: PublicKey;
39
95
  lifetime: number;
40
96
  identity: IdentityToken;
41
97
  extensions: string[];
@@ -43,7 +99,7 @@ type ParsedCertificateRequest = {
43
99
  export declare const refineCertificateRequest: (env: SshCaBindings, val: ParsedCertificateRequest, ctx: z.RefinementCtx) => Promise<never>;
44
100
  export declare const transformRenewalProofOfPossession: (env: SshCaBindings, val: string, ctx: z.RefinementCtx) => RenewalProofOfPossession | never;
45
101
  type HostCertificateRequest = {
46
- public_key: Key;
102
+ public_key: PublicKey;
47
103
  lifetime: number;
48
104
  };
49
105
  type ParsedHostCertificateRequest = {
@@ -51,37 +107,67 @@ type ParsedHostCertificateRequest = {
51
107
  proof: ProofOfPossession;
52
108
  } & HostCertificateRequest;
53
109
  export declare const refineHostCertificateRequest: (env: SshCaBindings, val: ParsedHostCertificateRequest, ctx: z.RefinementCtx) => Promise<never>;
110
+ /**
111
+ * Returns why a principal cannot be included on a host certificate, or
112
+ * undefined if it can be.
113
+ *
114
+ * OpenSSH matches the principals of a host certificate as patterns, so a
115
+ * principal such as `*` would make the certificate valid for every host that
116
+ * trusts the CA. Principals are otherwise not restricted, as hosts may be
117
+ * reached by IP address, bare host name or fully qualified domain name.
118
+ *
119
+ * @param principal The principal to check
120
+ * @returns The reason the principal cannot be used, or undefined if it can be
121
+ */
122
+ export declare const hostPrincipalIssue: (principal: string) => string | undefined;
123
+ /**
124
+ * Refines a host certificate principal so only one allowed by
125
+ * {@link hostPrincipalIssue} is accepted.
126
+ */
127
+ export declare const refineHostPrincipal: (val: string, ctx: z.RefinementCtx) => void;
54
128
  type ParsedHostCertificateRenewal = {
55
- certificate: Certificate;
129
+ certificate: SshCertificate;
56
130
  proof: RenewalProofOfPossession;
57
131
  } & HostCertificateRequest;
132
+ /**
133
+ * Checks a certificate presented for renewal was issued by this CA, is a host
134
+ * certificate whose principals are allowed (see {@link hostPrincipalIssue}),
135
+ * is within its validity period and has not been revoked.
136
+ *
137
+ * @param env The CA bindings
138
+ * @param isRevoked Returns whether a serial has been revoked
139
+ * @param certificate The certificate presented for renewal
140
+ * @returns The reason the certificate cannot be renewed, or undefined if it can be
141
+ * @throws {@link UnsupportedKeyError} when the CA key is not supported
142
+ */
143
+ export declare const checkRenewableCertificate: (env: SshCaBindings, isRevoked: typeof IsRevokedFn, certificate: SshCertificate) => Promise<string | undefined>;
144
+ /**
145
+ * Refines a host certificate renewal so only a certificate that can be
146
+ * renewed (see {@link checkRenewableCertificate}) is accepted.
147
+ */
148
+ export declare const refineRenewableCertificate: (env: SshCaBindings, isRevoked: typeof IsRevokedFn, val: {
149
+ certificate: SshCertificate;
150
+ }, ctx: z.RefinementCtx) => Promise<never>;
58
151
  export declare const refineHostCertificateRenewal: (env: SshCaBindings, isRevoked: typeof IsRevokedFn, val: ParsedHostCertificateRenewal, ctx: z.RefinementCtx) => Promise<never>;
59
152
  type ParsedRevokeCertificate = {
60
153
  serial: bigint;
61
- public_key: Key;
154
+ public_key: PublicKey;
62
155
  proof: ProofOfPossession;
63
156
  };
64
157
  export declare const refineRevokeCertificate: (env: SshCaBindings, val: ParsedRevokeCertificate, ctx: z.RefinementCtx) => Promise<never>;
65
158
  export declare const split: (v?: string | string[]) => string[];
66
- /**
67
- * Thrown by {@link getPrivateKey} when the CA private key is of a type that
68
- * cannot be used.
69
- */
70
- export declare class UnsupportedKeyError extends Error {
71
- constructor(message: string);
72
- }
73
159
  /**
74
160
  * Load and parse the CA private key from the `PRIVATE_KEY` secret.
75
161
  *
76
- * Only Ed25519 and ECDSA keys are supported. RSA keys are rejected because
77
- * sshpk signs RSA certificates with SHA-1 (`ssh-rsa`), which OpenSSH 8.2 and
78
- * later refuse by default, and because RSA keys in OpenSSH format are
79
- * converted with invalid CRT parameters that the Workers runtime rejects.
162
+ * Only Ed25519 and ECDSA keys are supported, as these are the key types that
163
+ * certificates and KRLs are signed with (see `createSigner` in `ssh/signer.ts`).
80
164
  *
81
- * @throws KeyParseError when the secret cannot be parsed.
165
+ * @throws {@link KeyParseError} when the secret cannot be parsed.
82
166
  * @throws {@link UnsupportedKeyError} when the key is not Ed25519 or ECDSA.
83
167
  */
84
168
  export declare const getPrivateKey: (env: SshCaBindings) => Promise<PrivateKey>;
85
- export declare const getPublic: (env: SshCaBindings, key?: PrivateKey) => Promise<Key>;
86
- export {};
169
+ /**
170
+ * Returns the public key of the CA, with `ISSUER_DN` as its comment
171
+ */
172
+ export declare const getPublic: (env: SshCaBindings, key?: PrivateKey) => Promise<PublicKey>;
87
173
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,OAAO,EAAyB,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAC5E,OAAO,EAAqD,SAAS,EAAc,mBAAmB,EAAE,KAAK,UAAU,EAAE,MAAM,eAAe,CAAA;AAC9I,OAAO,CAAC,MAAM,KAAK,CAAA;AAEnB,OAAO,EAAE,4BAA4B,EAAE,MAAM,YAAY,CAAA;AACzD,OAAO,EAAE,wBAAwB,EAAE,iBAAiB,EAAwB,MAAM,YAAY,CAAA;AAC9F,OAAO,KAAK,EAAE,SAAS,IAAI,WAAW,EAAE,MAAM,eAAe,CAAA;AAK7D,OAAO,EAAE,mBAAmB,EAAE,CAAA;AAE9B,eAAO,MAAM,UAAU,QAAS,CAAC,CAAC,aAAa,WAAW,MAAM,OAAO,OAAO,UAQ7E,CAAA;AAED,eAAO,MAAM,0BAA0B,QAAS,aAAa,OAAO,MAAM,OAAO,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,KAAG,iBAAiB,GAAG,KA0BpI,CAAA;AAED,KAAK,WAAW,GAAG;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;CACX,CAAA;AAED;;;GAGG;AACH,qBAAa,eAAgB,SAAQ,KAAK;IACzC,YAAY,OAAO,EAAE,MAAM,EAK1B;CACD;AAID;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,YAAa;IAAE,GAAG,CAAC,EAAE,OAAO,CAAA;CAAE,KAAG,MAOzD,CAAA;AAcD;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,YAAa,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAerH,CAAA;AAED,eAAO,MAAM,4BAA4B,QAAe,aAAa,OAAO,MAAM,OAAO,CAAC,CAAC,aAAa,KAAG,OAAO,CAAC,WAAW,GAAG,KAAK,CAsDrI,CAAA;AAOD;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,QAAS,MAAM,GAAG,MAAM,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,aAAa,KAAG,SAAS,GAAG,KA6B5G,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,QAAS,SAAS,KAAG,MAAM,GAAG,SAOzD,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,gBAAgB,QAAS,SAAS,OAAO,CAAC,CAAC,aAAa,KAAG,IAKvE,CAAA;AAED;;;GAGG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;IAC9C,YAAY,OAAO,EAAE,MAAM,EAK1B;CACD;AAID,eAAO,MAAM,kBAAkB,QAAS,aAAa,WAAW,4BAA4B,UAAU,MAAM,KAAG,MAAM,EAsCpH,CAAA;AAED,KAAK,aAAa,GAAG;IACpB,GAAG,EAAE,MAAM,CAAA;IACX,UAAU,EAAE,MAAM,EAAE,CAAA;CACpB,CAAA;AAED,eAAO,MAAM,aAAa,QAAe,aAAa,OAAO,MAAM,GAAG,SAAS,UAAU,MAAM,KAAG,OAAO,CAAC,aAAa,CActH,CAAA;AAED,eAAO,MAAM,sBAAsB,QAAe,aAAa,OAAO,MAAM,OAAO,CAAC,CAAC,aAAa,KAAG,OAAO,CAAC,aAAa,GAAG,KAAK,CAkBjI,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,QAAS,MAAM,GAAG,MAAM,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,aAAa,KAAG,cAAc,GAAG,KA6BnH,CAAA;AAED,KAAK,wBAAwB,GAAG;IAC/B,KAAK,EAAE,iBAAiB,CAAA;IACxB,UAAU,EAAE,SAAS,CAAA;IACrB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,aAAa,CAAA;IACvB,UAAU,EAAE,MAAM,EAAE,CAAA;CACpB,CAAA;AAED,eAAO,MAAM,wBAAwB,QAAe,aAAa,OAAO,wBAAwB,OAAO,CAAC,CAAC,aAAa,KAAG,OAAO,CAAC,KAAK,CAmBrI,CAAA;AAED,eAAO,MAAM,iCAAiC,QAAS,aAAa,OAAO,MAAM,OAAO,CAAC,CAAC,aAAa,KAAG,wBAAwB,GAAG,KAYpI,CAAA;AAED,KAAK,sBAAsB,GAAG;IAC7B,UAAU,EAAE,SAAS,CAAA;IACrB,QAAQ,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,KAAK,4BAA4B,GAAG;IACnC,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,KAAK,EAAE,iBAAiB,CAAA;CACxB,GAAG,sBAAsB,CAAA;AAE1B,eAAO,MAAM,4BAA4B,QAAe,aAAa,OAAO,4BAA4B,OAAO,CAAC,CAAC,aAAa,KAAG,OAAO,CAAC,KAAK,CA4B7I,CAAA;AAQD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,kBAAkB,cAAe,MAAM,KAAG,MAAM,GAAG,SAc/D,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,mBAAmB,QAAS,MAAM,OAAO,CAAC,CAAC,aAAa,KAAG,IAKvE,CAAA;AAED,KAAK,4BAA4B,GAAG;IACnC,WAAW,EAAE,cAAc,CAAA;IAC3B,KAAK,EAAE,wBAAwB,CAAA;CAC/B,GAAG,sBAAsB,CAAA;AAE1B;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,QAAe,aAAa,aAAa,OAAO,WAAW,eAAe,cAAc,KAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CA8C1J,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,0BAA0B,QAAe,aAAa,aAAa,OAAO,WAAW,OAAO;IAAE,WAAW,EAAE,cAAc,CAAA;CAAE,OAAO,CAAC,CAAC,aAAa,KAAG,OAAO,CAAC,KAAK,CAkB7K,CAAA;AAED,eAAO,MAAM,4BAA4B,QAAe,aAAa,aAAa,OAAO,WAAW,OAAO,4BAA4B,OAAO,CAAC,CAAC,aAAa,KAAG,OAAO,CAAC,KAAK,CA6B5K,CAAA;AAED,KAAK,uBAAuB,GAAG;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,SAAS,CAAA;IACrB,KAAK,EAAE,iBAAiB,CAAA;CACxB,CAAA;AAED,eAAO,MAAM,uBAAuB,QAAe,aAAa,OAAO,uBAAuB,OAAO,CAAC,CAAC,aAAa,KAAG,OAAO,CAAC,KAAK,CAmBnI,CAAA;AAED,eAAO,MAAM,KAAK,OAAQ,MAAM,GAAG,MAAM,EAAE,KAAG,MAAM,EAQnD,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,QAAe,aAAa,KAAG,OAAO,CAAC,UAAU,CAK1E,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,SAAS,QAAe,aAAa,QAAQ,UAAU,KAAG,OAAO,CAAC,SAAS,CAMvF,CAAA"}
package/dist/utils.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { JWKInvalid, JWKSInvalid, JWSInvalid, JWSSignatureVerificationFailed, JWTClaimValidationFailed, JWTExpired, JWTInvalid } from "jose/errors";
2
- import sshpk from "sshpk";
2
+ import { CertificateParseError, SshCertificate } from "./ssh/certificate.js";
3
+ import { KeyParseError, MinimumRsaKeyBits, parsePrivateKey, PublicKey, rsaKeyBits, UnsupportedKeyError } from "./ssh/keys.js";
3
4
  import z from "zod";
4
- import { verifyJWT } from "./verify.js";
5
+ import { MissingAudienceError, verifyJWT } from "./verify.js";
5
6
  import { RenewalProofOfPossession, ProofOfPossession, PossessionParseError } from "./proof.js";
6
7
  import { logger } from "./logger.js";
7
8
  import { ms } from "itty-time";
8
9
  import { InternalServerErrorException } from "chanfana";
9
- // sshpk is CommonJS and Node's ESM loader cannot detect most of its named exports, so values are taken from the default import
10
- const { KeyParseError, CertificateParseError, parseCertificate, parseKey, parsePrivateKey } = sshpk;
10
+ export { UnsupportedKeyError };
11
11
  export const fatalIssue = (ctx, message, val) => {
12
12
  ctx.issues.push({
13
13
  code: "custom",
@@ -42,6 +42,64 @@ export const transformProofOfPossession = (env, val, ctx) => {
42
42
  return z.NEVER;
43
43
  }
44
44
  };
45
+ /**
46
+ * Thrown when a claim of a verified token is missing or is not of the
47
+ * expected type.
48
+ */
49
+ export class TokenClaimError extends Error {
50
+ constructor(message) {
51
+ super(message);
52
+ this.name = "TokenClaimError";
53
+ Object.setPrototypeOf(this, TokenClaimError.prototype);
54
+ }
55
+ }
56
+ const SubjectClaimSchema = z.string().min(1);
57
+ /**
58
+ * Returns the subject of a verified token, which is compared between the
59
+ * access and identity tokens so must always be present.
60
+ *
61
+ * @param payload The payload of a verified token
62
+ * @returns The `sub` claim of the token
63
+ * @throws {@link TokenClaimError} when the `sub` claim is missing, empty or not a string
64
+ */
65
+ export const tokenSubject = (payload) => {
66
+ const parsed = SubjectClaimSchema.safeParse(payload.sub);
67
+ if (!parsed.success) {
68
+ throw new TokenClaimError("JWT was verified but was missing required sub claim");
69
+ }
70
+ return parsed.data;
71
+ };
72
+ /**
73
+ * The claims of an access token that identify the user's email address.
74
+ *
75
+ * `email_verified` is optional as not every IdP includes it, but when it is
76
+ * included it must be true, as an IdP may otherwise issue a token for an
77
+ * address the user has not shown they control.
78
+ */
79
+ const EmailClaimsSchema = z.object({
80
+ email: z.email(),
81
+ email_verified: z.boolean().optional(),
82
+ });
83
+ /**
84
+ * Returns the email address of a verified access token, or why it cannot be
85
+ * used.
86
+ *
87
+ * @param payload The payload of a verified access token
88
+ * @returns The email address, or an error message if the claims are not acceptable
89
+ */
90
+ export const tokenEmail = (payload) => {
91
+ if (payload.email === undefined) {
92
+ return { ok: false, error: "JWT was verified but was missing required email claim" };
93
+ }
94
+ const parsed = EmailClaimsSchema.safeParse(payload);
95
+ if (!parsed.success) {
96
+ return { ok: false, error: "JWT was verified but the email or email_verified claim was not valid" };
97
+ }
98
+ if (parsed.data.email_verified === false) {
99
+ return { ok: false, error: "JWT was verified but the email address has not been verified by the IdP" };
100
+ }
101
+ return { ok: true, email: parsed.data.email };
102
+ };
45
103
  export const transformAuthorizationHeader = async (env, val, ctx) => {
46
104
  if (!val.startsWith("Bearer ") || val.length <= 7) {
47
105
  return fatalIssue(ctx, "authorization header must be a Bearer token", val);
@@ -52,18 +110,27 @@ export const transformAuthorizationHeader = async (env, val, ctx) => {
52
110
  return fatalIssue(ctx, "request did not contain a JWT", val);
53
111
  }
54
112
  try {
55
- // verify jwt
56
- const { payload } = await verifyJWT(env, jwt);
57
- if (payload.email === undefined) {
58
- return fatalIssue(ctx, "JWT was verified but was missing required email claim", val);
113
+ // verify jwt, which must be issued for this CA
114
+ const aud = split(env.JWT_ACCESS_TOKEN_AUD).length > 0 ? env.JWT_ACCESS_TOKEN_AUD : env.JWT_AUD;
115
+ const { payload } = await verifyJWT(env, jwt, aud);
116
+ const email = tokenEmail(payload);
117
+ if (!email.ok) {
118
+ l.warn(email.error, "in", "transformAuthorizationHeader");
119
+ return fatalIssue(ctx, email.error, val);
59
120
  }
60
121
  return {
61
- email: payload.email,
62
- sub: payload.sub,
122
+ email: email.email,
123
+ sub: tokenSubject(payload),
63
124
  };
64
125
  }
65
126
  catch (err) {
66
127
  switch (true) {
128
+ case (err instanceof MissingAudienceError):
129
+ // a misconfigured CA is a server error, not a validation failure
130
+ l.error(err.message, "in", "transformAuthorizationHeader");
131
+ throw new InternalServerErrorException(err.message);
132
+ case (err instanceof TokenClaimError):
133
+ return fatalIssue(ctx, err.message, val);
67
134
  case (err instanceof JWSInvalid):
68
135
  return fatalIssue(ctx, "the access token was invalid", val);
69
136
  case (err instanceof JWTInvalid):
@@ -84,10 +151,19 @@ export const transformAuthorizationHeader = async (env, val, ctx) => {
84
151
  }
85
152
  }
86
153
  };
154
+ // decodes a value sent as base64, which may already have been decoded by the schema
155
+ const fromBase64 = (val) => {
156
+ return (typeof val === "string" ? Buffer.from(val, "base64") : Buffer.from(val)).toString("utf8");
157
+ };
158
+ /**
159
+ * Parses a public key in OpenSSH format, `${type} ${base64}[ ${comment}]`
160
+ *
161
+ * @param val The public key, either base64 encoded or as bytes
162
+ * @param ctx The Zod context for any errors
163
+ */
87
164
  export const transformPublicKey = (val, ctx) => {
88
165
  try {
89
- const key = typeof val === "string" ? parseKey(Buffer.from(val, "base64")) : parseKey(val);
90
- return key;
166
+ return PublicKey.parse(fromBase64(val));
91
167
  }
92
168
  catch (err) {
93
169
  switch (true) {
@@ -115,6 +191,33 @@ export const transformPublicKey = (val, ctx) => {
115
191
  return z.NEVER;
116
192
  }
117
193
  };
194
+ /**
195
+ * Returns why a public key cannot be issued a certificate, or undefined if it
196
+ * can be.
197
+ *
198
+ * RSA keys must be at least {@link MinimumRsaKeyBits} bits, as smaller keys
199
+ * can be factored and are refused by current OpenSSH releases.
200
+ *
201
+ * @param key The public key a certificate would be issued for
202
+ * @returns The reason the key cannot be used, or undefined if it can be
203
+ */
204
+ export const subjectKeyIssue = (key) => {
205
+ const bits = rsaKeyBits(key);
206
+ if (bits !== undefined && bits < MinimumRsaKeyBits) {
207
+ return `RSA keys must be at least ${MinimumRsaKeyBits} bits, not ${bits}`;
208
+ }
209
+ return undefined;
210
+ };
211
+ /**
212
+ * Refines a public key so only one allowed by {@link subjectKeyIssue} is
213
+ * accepted for a certificate request.
214
+ */
215
+ export const refineSubjectKey = (val, ctx) => {
216
+ const issue = subjectKeyIssue(val);
217
+ if (issue !== undefined) {
218
+ fatalIssue(ctx, issue, val.toString());
219
+ }
220
+ };
118
221
  /**
119
222
  * Thrown by {@link identityPrincipals} when the principals claim of an
120
223
  * identity token is not a string or an array of strings.
@@ -161,11 +264,11 @@ export const parseIdentity = async (env, jwt, claim) => {
161
264
  if (jwt === undefined) {
162
265
  throw new Error("missing identity token");
163
266
  }
164
- const aud = split(env.JWT_AUD);
165
- const { payload } = await verifyJWT(env, jwt, { aud: aud.length === 0 ? undefined : aud });
267
+ const { payload } = await verifyJWT(env, jwt, env.JWT_AUD);
268
+ const sub = tokenSubject(payload);
166
269
  const principals = identityPrincipals(env, payload, claim);
167
270
  return {
168
- sub: payload.sub,
271
+ sub: sub,
169
272
  principals: principals,
170
273
  };
171
274
  };
@@ -175,9 +278,14 @@ export const transformIdentityToken = async (env, val, ctx) => {
175
278
  return identity;
176
279
  }
177
280
  catch (err) {
281
+ // a misconfigured CA is a server error, not a validation failure
282
+ if (err instanceof MissingAudienceError) {
283
+ logger(env).error(err.message, "in", "transformIdentityToken");
284
+ throw new InternalServerErrorException(err.message);
285
+ }
178
286
  ctx.issues.push({
179
287
  code: "custom",
180
- message: err instanceof PrincipalsClaimError ? err.message : "problem parsing identity token",
288
+ message: err instanceof PrincipalsClaimError || err instanceof TokenClaimError ? err.message : "problem parsing identity token",
181
289
  input: val
182
290
  });
183
291
  return z.NEVER;
@@ -191,8 +299,7 @@ export const transformIdentityToken = async (env, val, ctx) => {
191
299
  */
192
300
  export const transformCertificate = (val, ctx) => {
193
301
  try {
194
- const cert = typeof val === "string" ? parseCertificate(Buffer.from(val, "base64"), "openssh") : parseCertificate(val, "openssh");
195
- return cert;
302
+ return SshCertificate.parse(fromBase64(val));
196
303
  }
197
304
  catch (err) {
198
305
  switch (true) {
@@ -257,7 +364,7 @@ export const refineHostCertificateRequest = async (env, val, ctx) => {
257
364
  l.debug("started");
258
365
  try {
259
366
  // check proof of possession fingerprint matches public key
260
- l.debug("checking val.proof.matches()", "public_key", val.public_key.toString("ssh"));
367
+ l.debug("checking val.proof.matches()", "public_key", val.public_key.toString());
261
368
  if (!val.proof.matches(val.public_key)) {
262
369
  l.warn("proof of possession fingerprint did not match public_key");
263
370
  return fatalIssue(ctx, "proof of possession fingerprint did not match public_key", val);
@@ -277,31 +384,129 @@ export const refineHostCertificateRequest = async (env, val, ctx) => {
277
384
  return fatalIssue(ctx, "proof of possession verification unhandled error", val);
278
385
  }
279
386
  };
280
- export const refineHostCertificateRenewal = async (env, isRevoked, val, ctx) => {
387
+ // characters that OpenSSH treats as pattern syntax when matching host
388
+ // certificate principals, or that separate entries in a pattern list
389
+ const HostPrincipalPatternChars = /[*?!,]/;
390
+ // whitespace and control characters, which no host name contains
391
+ const HostPrincipalInvalidChars = /[\s\p{Cc}]/u;
392
+ /**
393
+ * Returns why a principal cannot be included on a host certificate, or
394
+ * undefined if it can be.
395
+ *
396
+ * OpenSSH matches the principals of a host certificate as patterns, so a
397
+ * principal such as `*` would make the certificate valid for every host that
398
+ * trusts the CA. Principals are otherwise not restricted, as hosts may be
399
+ * reached by IP address, bare host name or fully qualified domain name.
400
+ *
401
+ * @param principal The principal to check
402
+ * @returns The reason the principal cannot be used, or undefined if it can be
403
+ */
404
+ export const hostPrincipalIssue = (principal) => {
405
+ if (principal === "") {
406
+ return "host certificate principals must not be empty";
407
+ }
408
+ if (HostPrincipalPatternChars.test(principal)) {
409
+ return `host certificate principal ${JSON.stringify(principal)} must not contain wildcard or pattern characters (*, ?, ! or ,)`;
410
+ }
411
+ if (HostPrincipalInvalidChars.test(principal)) {
412
+ return `host certificate principal ${JSON.stringify(principal)} must not contain whitespace or control characters`;
413
+ }
414
+ return undefined;
415
+ };
416
+ /**
417
+ * Refines a host certificate principal so only one allowed by
418
+ * {@link hostPrincipalIssue} is accepted.
419
+ */
420
+ export const refineHostPrincipal = (val, ctx) => {
421
+ const issue = hostPrincipalIssue(val);
422
+ if (issue !== undefined) {
423
+ fatalIssue(ctx, issue, val);
424
+ }
425
+ };
426
+ /**
427
+ * Checks a certificate presented for renewal was issued by this CA, is a host
428
+ * certificate whose principals are allowed (see {@link hostPrincipalIssue}),
429
+ * is within its validity period and has not been revoked.
430
+ *
431
+ * @param env The CA bindings
432
+ * @param isRevoked Returns whether a serial has been revoked
433
+ * @param certificate The certificate presented for renewal
434
+ * @returns The reason the certificate cannot be renewed, or undefined if it can be
435
+ * @throws {@link UnsupportedKeyError} when the CA key is not supported
436
+ */
437
+ export const checkRenewableCertificate = async (env, isRevoked, certificate) => {
438
+ // ensure certificate is signed by CA
439
+ const issuer = await getPublic(env);
440
+ if (!await certificate.isSignedBy(issuer)) {
441
+ return "the provided certificate was not signed by this CA";
442
+ }
443
+ // only host certificates can be renewed, as renewal does not need the
444
+ // OIDC authentication a user certificate is issued with
445
+ if (certificate.kind !== "host") {
446
+ return "the provided certificate is not a host certificate";
447
+ }
448
+ // a certificate issued before small RSA keys were refused must not be
449
+ // renewed, as the renewal would keep its key
450
+ const keyIssue = subjectKeyIssue(certificate.publicKey);
451
+ if (keyIssue !== undefined) {
452
+ return `the provided certificate cannot be renewed: ${keyIssue}`;
453
+ }
454
+ // a certificate issued before wildcard principals were refused must not
455
+ // be renewed, as the renewal would keep its principals
456
+ for (const principal of certificate.principals) {
457
+ const issue = hostPrincipalIssue(principal);
458
+ if (issue !== undefined) {
459
+ return `the provided certificate cannot be renewed: ${issue}`;
460
+ }
461
+ }
462
+ // check certificate is within its validity period, allowing for clock
463
+ // skew at the start as a certificate issued moments ago may have a
464
+ // validity start up to a second in the future
465
+ const now = Date.now();
466
+ if (now >= certificate.validBefore.getTime()) {
467
+ return "the provided certificate is expired";
468
+ }
469
+ if (certificate.validAfter.getTime() > now + ms(env.CERTIFICATE_REQUEST_TIME_SKEW_MAX)) {
470
+ return "the provided certificate is not yet valid";
471
+ }
472
+ // ensure certificate presented for renewal is not revoked
473
+ if (await isRevoked(env, certificate.serial)) {
474
+ return "the provided certificate is revoked";
475
+ }
476
+ return undefined;
477
+ };
478
+ /**
479
+ * Refines a host certificate renewal so only a certificate that can be
480
+ * renewed (see {@link checkRenewableCertificate}) is accepted.
481
+ */
482
+ export const refineRenewableCertificate = async (env, isRevoked, val, ctx) => {
281
483
  const l = logger(env);
282
484
  try {
283
- // ensure certificate is signed by CA
284
- const issuer = await getPublic(env);
285
- if (!val.certificate.isSignedByKey(issuer)) {
286
- return fatalIssue(ctx, "the provided certificate was not signed by this CA", val);
485
+ const reason = await checkRenewableCertificate(env, isRevoked, val.certificate);
486
+ if (reason !== undefined) {
487
+ return fatalIssue(ctx, reason, val);
287
488
  }
288
- // check certificate is within its validity period, allowing for clock
289
- // skew at the start as a certificate issued moments ago may have a
290
- // validity start up to a second in the future
291
- const now = Date.now();
292
- if (now >= val.certificate.validUntil.getTime()) {
293
- return fatalIssue(ctx, "the provided certificate is expired", val);
294
- }
295
- if (val.certificate.validFrom.getTime() > now + ms(env.CERTIFICATE_REQUEST_TIME_SKEW_MAX)) {
296
- return fatalIssue(ctx, "the provided certificate is not yet valid", val);
489
+ return z.NEVER;
490
+ }
491
+ catch (err) {
492
+ // a misconfigured CA key is a server error, not a validation failure
493
+ if (err instanceof UnsupportedKeyError) {
494
+ l.error(err.message, "in", "refineRenewableCertificate");
495
+ throw new InternalServerErrorException(err.message);
297
496
  }
298
- // ensure certificate presented for renewal is not revoked
299
- const serial = val.certificate.serial.readBigUInt64BE(0);
300
- if (await isRevoked(env, serial)) {
301
- return fatalIssue(ctx, "the provided certificate is revoked", val);
497
+ l.error("certificate renewal check unhandled error", "in", "refineRenewableCertificate", "error", err);
498
+ return fatalIssue(ctx, "certificate renewal check unhandled error", val);
499
+ }
500
+ };
501
+ export const refineHostCertificateRenewal = async (env, isRevoked, val, ctx) => {
502
+ const l = logger(env);
503
+ try {
504
+ const reason = await checkRenewableCertificate(env, isRevoked, val.certificate);
505
+ if (reason !== undefined) {
506
+ return fatalIssue(ctx, reason, val);
302
507
  }
303
508
  // check proof of possession fingerprint matches public key and certificate subject key
304
- if (!val.proof.matches(val.public_key, val.certificate.subjectKey)) {
509
+ if (!val.proof.matches(val.public_key, val.certificate.publicKey)) {
305
510
  return fatalIssue(ctx, "proof of possession fingerprints did not match public_key and/or certificate", val);
306
511
  }
307
512
  // verify proof of possession signature
@@ -349,46 +554,27 @@ export const split = (v) => {
349
554
  : v.split(",")
350
555
  : v;
351
556
  };
352
- /**
353
- * Thrown by {@link getPrivateKey} when the CA private key is of a type that
354
- * cannot be used.
355
- */
356
- export class UnsupportedKeyError extends Error {
357
- constructor(message) {
358
- super(message);
359
- this.name = "UnsupportedKeyError";
360
- Object.setPrototypeOf(this, UnsupportedKeyError.prototype);
361
- }
362
- }
363
557
  /**
364
558
  * Load and parse the CA private key from the `PRIVATE_KEY` secret.
365
559
  *
366
- * Only Ed25519 and ECDSA keys are supported. RSA keys are rejected because
367
- * sshpk signs RSA certificates with SHA-1 (`ssh-rsa`), which OpenSSH 8.2 and
368
- * later refuse by default, and because RSA keys in OpenSSH format are
369
- * converted with invalid CRT parameters that the Workers runtime rejects.
560
+ * Only Ed25519 and ECDSA keys are supported, as these are the key types that
561
+ * certificates and KRLs are signed with (see `createSigner` in `ssh/signer.ts`).
370
562
  *
371
- * @throws KeyParseError when the secret cannot be parsed.
563
+ * @throws {@link KeyParseError} when the secret cannot be parsed.
372
564
  * @throws {@link UnsupportedKeyError} when the key is not Ed25519 or ECDSA.
373
565
  */
374
566
  export const getPrivateKey = async (env) => {
375
567
  // grab private key from secret store
376
568
  const secret = await env.PRIVATE_KEY.get();
377
- // parse it
378
- const key = parsePrivateKey(secret);
379
- if (key.type !== "ed25519" && key.type !== "ecdsa") {
380
- throw new UnsupportedKeyError(`CA key type ${key.type} is not supported, the CA key must be Ed25519 or ECDSA`);
381
- }
382
- return key;
569
+ return parsePrivateKey(secret);
383
570
  };
571
+ /**
572
+ * Returns the public key of the CA, with `ISSUER_DN` as its comment
573
+ */
384
574
  export const getPublic = async (env, key) => {
385
575
  if (key === undefined) {
386
576
  key = await getPrivateKey(env);
387
577
  }
388
- // convert to a public key and add comment
389
- const pub = key.toPublic();
390
- pub.comment = env.ISSUER_DN;
391
- // return in ssh format trimmed of any whitespace
392
- return pub;
578
+ return key.publicKey.withComment(env.ISSUER_DN);
393
579
  };
394
580
  //# sourceMappingURL=utils.js.map