@andrewheberle/serverless-ssh-ca 0.17.12 → 0.17.15
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/README.md +6 -17
- package/dist/api/v3/index.d.ts +1 -0
- package/dist/api/v3/index.d.ts.map +1 -0
- package/dist/api/v3/index.js +1 -0
- package/dist/api/v3/index.js.map +1 -0
- package/dist/api/v3/schema.d.ts +9 -0
- package/dist/api/v3/schema.d.ts.map +1 -0
- package/dist/api/v3/schema.js +4 -4
- package/dist/api/v3/schema.js.map +1 -0
- package/dist/certificate.d.ts +1 -0
- package/dist/certificate.d.ts.map +1 -0
- package/dist/certificate.js +1 -0
- package/dist/certificate.js.map +1 -0
- package/dist/{db.d.ts → db/index.d.ts} +6 -6
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +151 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/migrations/0001_initial_schema.d.ts +3 -0
- package/dist/db/migrations/0001_initial_schema.d.ts.map +1 -0
- package/dist/db/migrations/0001_initial_schema.js +24 -0
- package/dist/db/migrations/0001_initial_schema.js.map +1 -0
- package/dist/db/migrations/index.d.ts +3 -0
- package/dist/db/migrations/index.d.ts.map +1 -0
- package/dist/db/migrations/index.js +5 -0
- package/dist/db/migrations/index.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -0
- package/dist/krl.d.ts +1 -0
- package/dist/krl.d.ts.map +1 -0
- package/dist/krl.js +1 -0
- package/dist/krl.js.map +1 -0
- package/dist/logger.d.ts +1 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +1 -0
- package/dist/logger.js.map +1 -0
- package/dist/proof.d.ts +2 -0
- package/dist/proof.d.ts.map +1 -0
- package/dist/proof.js +2 -1
- package/dist/proof.js.map +1 -0
- package/dist/router.d.ts +1 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +1 -0
- package/dist/router.js.map +1 -0
- package/dist/sshsig/armor.d.ts +1 -0
- package/dist/sshsig/armor.d.ts.map +1 -0
- package/dist/sshsig/armor.js +1 -0
- package/dist/sshsig/armor.js.map +1 -0
- package/dist/sshsig/formats.d.ts +1 -0
- package/dist/sshsig/formats.d.ts.map +1 -0
- package/dist/sshsig/formats.js +1 -0
- package/dist/sshsig/formats.js.map +1 -0
- package/dist/sshsig/index.d.ts +1 -0
- package/dist/sshsig/index.d.ts.map +1 -0
- package/dist/sshsig/index.js +1 -0
- package/dist/sshsig/index.js.map +1 -0
- package/dist/sshsig/reader.d.ts +1 -0
- package/dist/sshsig/reader.d.ts.map +1 -0
- package/dist/sshsig/reader.js +1 -0
- package/dist/sshsig/reader.js.map +1 -0
- package/dist/sshsig/sig.d.ts +1 -0
- package/dist/sshsig/sig.d.ts.map +1 -0
- package/dist/sshsig/sig.js +1 -0
- package/dist/sshsig/sig.js.map +1 -0
- package/dist/sshsig/sig_parser.d.ts +1 -0
- package/dist/sshsig/sig_parser.d.ts.map +1 -0
- package/dist/sshsig/sig_parser.js +1 -0
- package/dist/sshsig/sig_parser.js.map +1 -0
- package/dist/sshsig/verifier.d.ts +1 -0
- package/dist/sshsig/verifier.d.ts.map +1 -0
- package/dist/sshsig/verifier.js +1 -0
- package/dist/sshsig/verifier.js.map +1 -0
- package/dist/sshsig/writer.d.ts +1 -0
- package/dist/sshsig/writer.d.ts.map +1 -0
- package/dist/sshsig/writer.js +1 -0
- package/dist/sshsig/writer.js.map +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +4 -0
- package/dist/utils.js.map +1 -0
- package/dist/verify.d.ts +1 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +1 -0
- package/dist/verify.js.map +1 -0
- package/package.json +16 -8
- package/src/api/v3/index.ts +419 -0
- package/src/api/v3/schema.ts +265 -0
- package/src/certificate.ts +214 -0
- package/src/db/index.ts +194 -0
- package/src/db/migrations/0001_initial_schema.ts +25 -0
- package/src/db/migrations/index.ts +6 -0
- package/src/index.ts +18 -0
- package/src/krl.ts +414 -0
- package/src/logger.ts +6 -0
- package/src/proof.ts +187 -0
- package/src/router.ts +35 -0
- package/src/sshsig/armor.ts +24 -0
- package/src/sshsig/formats.ts +226 -0
- package/src/sshsig/index.ts +47 -0
- package/src/sshsig/reader.ts +59 -0
- package/src/sshsig/sig.ts +17 -0
- package/src/sshsig/sig_parser.ts +90 -0
- package/src/sshsig/verifier.ts +85 -0
- package/src/sshsig/writer.ts +56 -0
- package/src/types.ts +49 -0
- package/src/utils.ts +414 -0
- package/src/verify.ts +25 -0
- package/LICENSE +0 -201
- package/dist/db.js +0 -111
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { seconds } from "itty-time"
|
|
2
|
+
import { Certificate, createCertificate, Identity, identityForHost, identityForUser, identityFromDN, Key, parsePrivateKey, PrivateKey } from "sshpk"
|
|
3
|
+
import { SSHExtension } from "./types"
|
|
4
|
+
import { split } from "./utils"
|
|
5
|
+
import type { SshCaBindings } from "./types"
|
|
6
|
+
|
|
7
|
+
// const sshCertificateExtensions = split(env.SSH_CERTIFICATE_EXTENSIONS)
|
|
8
|
+
|
|
9
|
+
export type CreateCertificateOptions = {
|
|
10
|
+
lifetime?: number
|
|
11
|
+
principals?: string[]
|
|
12
|
+
extensions?: string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* const DefaultCreateCertificateOptions: CreateCertificateOptions = {
|
|
16
|
+
lifetime: seconds(env.SSH_CERTIFICATE_LIFETIME),
|
|
17
|
+
principals: [],
|
|
18
|
+
extensions: sshCertificateExtensions
|
|
19
|
+
} */
|
|
20
|
+
|
|
21
|
+
export class CertificateError extends Error {
|
|
22
|
+
constructor(message: string) {
|
|
23
|
+
super(message)
|
|
24
|
+
this.name = "CertificateError"
|
|
25
|
+
|
|
26
|
+
// This is necessary for proper stack trace in TypeScript
|
|
27
|
+
Object.setPrototypeOf(this, CertificateError.prototype)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type GenerateCertificateOptions = {
|
|
32
|
+
/**
|
|
33
|
+
* @internal
|
|
34
|
+
* Override the certificate serial number.
|
|
35
|
+
* Should only be set in tests.
|
|
36
|
+
*/
|
|
37
|
+
serial?: bigint
|
|
38
|
+
} & CreateCertificateOptions
|
|
39
|
+
|
|
40
|
+
export const generateCertificate = (env: SshCaBindings, email: string, key: PrivateKey, public_key: Key, options?: GenerateCertificateOptions): Certificate => {
|
|
41
|
+
let { lifetime, principals, extensions, serial } = options ?? {}
|
|
42
|
+
|
|
43
|
+
// add identities
|
|
44
|
+
const identity = env.SSH_CERTIFICATE_INCLUDE_SELF as string === "true"
|
|
45
|
+
? [identityForUser(email.split("@")[0]!)]
|
|
46
|
+
: []
|
|
47
|
+
if (principals !== undefined) {
|
|
48
|
+
for (const p of principals) {
|
|
49
|
+
identity.push(identityForUser(p))
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (env.SSH_CERTIFICATE_PRINCIPALS as string !== "") {
|
|
53
|
+
for (const p of split(env.SSH_CERTIFICATE_PRINCIPALS)) {
|
|
54
|
+
identity.push(identityForUser(p))
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// lifetime is the smaller of what was provided in the options or the default
|
|
59
|
+
lifetime = lifetime !== undefined
|
|
60
|
+
? Math.min(lifetime, seconds(env.SSH_CERTIFICATE_LIFETIME))
|
|
61
|
+
: seconds(env.SSH_CERTIFICATE_LIFETIME)
|
|
62
|
+
|
|
63
|
+
// generate value for serial of certificate
|
|
64
|
+
const s = generateSerial(serial)
|
|
65
|
+
|
|
66
|
+
// set issuer of certificate based on ISSUER_DN
|
|
67
|
+
const issuer = identityFromDN(env.ISSUER_DN)
|
|
68
|
+
|
|
69
|
+
// create certificate
|
|
70
|
+
const certificate = createCertificate(identity, public_key, issuer, key, { lifetime: lifetime, serial: s })
|
|
71
|
+
|
|
72
|
+
// ensure openssh info is included in certificate (should not occur)
|
|
73
|
+
if (certificate.signatures.openssh === undefined) {
|
|
74
|
+
throw new CertificateError("missing openssh information in certificate")
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const sshCertificateExtensions = split(env.SSH_CERTIFICATE_EXTENSIONS)
|
|
78
|
+
|
|
79
|
+
// add usage extensions
|
|
80
|
+
const sshextensions: SSHExtension[] = []
|
|
81
|
+
if (extensions !== undefined) {
|
|
82
|
+
// if extensions are provided in request, ensure they do not include extra extensions beyond the defaults
|
|
83
|
+
for (const ext of extensions) {
|
|
84
|
+
if (!sshCertificateExtensions.includes(ext)) {
|
|
85
|
+
throw new CertificateError(`${ext} is not allowed`)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// add to list of allowed extensions
|
|
89
|
+
sshextensions.push({
|
|
90
|
+
critical: false,
|
|
91
|
+
name: ext,
|
|
92
|
+
data: Buffer.alloc(0)
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
// use defaults if not provided
|
|
97
|
+
sshCertificateExtensions.forEach((ext: string) => {
|
|
98
|
+
sshextensions.push({
|
|
99
|
+
critical: false,
|
|
100
|
+
name: ext,
|
|
101
|
+
data: Buffer.alloc(0)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// add info to certificate
|
|
108
|
+
certificate.signatures = {
|
|
109
|
+
openssh: {
|
|
110
|
+
nonce: certificate.signatures.openssh.nonce,
|
|
111
|
+
keyId: email,
|
|
112
|
+
signature: certificate.signatures.openssh.signature,
|
|
113
|
+
exts: sshextensions
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// re-sign after changes
|
|
118
|
+
certificate.signWith(key)
|
|
119
|
+
|
|
120
|
+
return certificate
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function createSignedCertificate(env: SshCaBindings, email: string, public_key: Key, options?: CreateCertificateOptions): Promise<Certificate> {
|
|
124
|
+
// grab private key from secret store
|
|
125
|
+
const secret = await env.PRIVATE_KEY.get()
|
|
126
|
+
|
|
127
|
+
// parse key
|
|
128
|
+
const key = parsePrivateKey(secret)
|
|
129
|
+
|
|
130
|
+
if (options === undefined) {
|
|
131
|
+
options = {
|
|
132
|
+
lifetime: seconds(env.SSH_CERTIFICATE_LIFETIME),
|
|
133
|
+
principals: [],
|
|
134
|
+
extensions: split(env.SSH_CERTIFICATE_EXTENSIONS)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return generateCertificate(env, email, key, public_key, options)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export type CreateHostCertificateOptions = {
|
|
142
|
+
lifetime?: number
|
|
143
|
+
principals?: string[]
|
|
144
|
+
subjects?: Identity[]
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* const DefaultCreateHostertificateOptions: CreateHostCertificateOptions = {
|
|
148
|
+
lifetime: seconds(env.SSH_HOST_CERTIFICATE_LIFETIME),
|
|
149
|
+
principals: [],
|
|
150
|
+
} */
|
|
151
|
+
|
|
152
|
+
export class BadIssuerError extends Error {
|
|
153
|
+
constructor(message: string) {
|
|
154
|
+
super(message)
|
|
155
|
+
this.name = "BadIssuerError"
|
|
156
|
+
|
|
157
|
+
Object.setPrototypeOf(this, BadIssuerError.prototype)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function createSignedHostCertificate(env: SshCaBindings, public_key: Key, options?: CreateHostCertificateOptions): Promise<Certificate> {
|
|
162
|
+
if (options === undefined) {
|
|
163
|
+
options = {
|
|
164
|
+
lifetime: seconds(env.SSH_HOST_CERTIFICATE_LIFETIME),
|
|
165
|
+
principals: []
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// generate list of identities for host key
|
|
169
|
+
const identity: Identity[] = []
|
|
170
|
+
if (options.principals !== undefined) {
|
|
171
|
+
for (const p of options.principals) {
|
|
172
|
+
identity.push(identityForHost(p))
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// add any already provided subjects (for renewals only)
|
|
177
|
+
if (options.subjects !== undefined) {
|
|
178
|
+
identity.push(...options.subjects)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// grab private key from secret store
|
|
182
|
+
const secret = await env.PRIVATE_KEY.get()
|
|
183
|
+
|
|
184
|
+
// parse private key
|
|
185
|
+
const key = parsePrivateKey(secret)
|
|
186
|
+
|
|
187
|
+
// lifetime is the smaller of what was provided in the options or the default
|
|
188
|
+
const lifetime = options.lifetime !== undefined
|
|
189
|
+
? Math.min(options.lifetime, seconds(env.SSH_HOST_CERTIFICATE_LIFETIME))
|
|
190
|
+
: seconds(env.SSH_HOST_CERTIFICATE_LIFETIME)
|
|
191
|
+
|
|
192
|
+
// generate value for serial of certificate
|
|
193
|
+
const serial = generateSerial()
|
|
194
|
+
|
|
195
|
+
// set issuer of certificate based on ISSUER_DN
|
|
196
|
+
const issuer = identityFromDN(env.ISSUER_DN)
|
|
197
|
+
|
|
198
|
+
// create certificate
|
|
199
|
+
const certificate = createCertificate(identity, public_key, issuer, key, { lifetime: lifetime, serial: serial })
|
|
200
|
+
|
|
201
|
+
return certificate
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export const generateSerial = (serial?: bigint): Buffer<ArrayBuffer> => {
|
|
205
|
+
if (serial !== undefined) {
|
|
206
|
+
const s = Buffer.alloc(8)
|
|
207
|
+
s.writeBigUInt64BE(serial)
|
|
208
|
+
|
|
209
|
+
return s
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const randomBytes = crypto.getRandomValues(new Uint8Array(8))
|
|
213
|
+
return Buffer.from(randomBytes)
|
|
214
|
+
}
|
package/src/db/index.ts
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { Certificate, Format, Identity } from "sshpk"
|
|
2
|
+
import { logger } from "../logger"
|
|
3
|
+
import type { SshCaBindings } from "../types"
|
|
4
|
+
import { D1QB } from "workers-qb"
|
|
5
|
+
import { migrations } from "./migrations"
|
|
6
|
+
|
|
7
|
+
export enum CertificateType {
|
|
8
|
+
User,
|
|
9
|
+
Host
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type CertificateRecord = {
|
|
13
|
+
id: number
|
|
14
|
+
serial: string
|
|
15
|
+
key_id: string
|
|
16
|
+
principals: string
|
|
17
|
+
extensions: string | null
|
|
18
|
+
valid_after: Date
|
|
19
|
+
valid_before: Date
|
|
20
|
+
revoked_at: Date | null
|
|
21
|
+
certificate_type: CertificateType
|
|
22
|
+
public_key: string | null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type DatabaseSchema = {
|
|
26
|
+
certificates: CertificateRecord
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const connect = async (env: SshCaBindings): Promise<D1QB<DatabaseSchema>> => {
|
|
30
|
+
const l = logger(env)
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
l.info("connecting to database")
|
|
34
|
+
|
|
35
|
+
const qb = new D1QB<DatabaseSchema>(env.DB)
|
|
36
|
+
const migrationBuilder = qb.migrations({ migrations })
|
|
37
|
+
const appliedMigrations = await migrationBuilder.apply()
|
|
38
|
+
|
|
39
|
+
l.info("applied database migrations", "migrations", appliedMigrations.length, "migration_names", appliedMigrations.map(m => m.name))
|
|
40
|
+
|
|
41
|
+
return qb
|
|
42
|
+
} catch (err) {
|
|
43
|
+
l.error("error connecting to database", "error", err)
|
|
44
|
+
throw err
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const dbCleanup = async (env: SshCaBindings) => {
|
|
49
|
+
const l = logger(env).with("retention", env.DB_CERTIFICATE_RETENTION)
|
|
50
|
+
|
|
51
|
+
if (env.DB_CERTIFICATE_RETENTION === "infinite") {
|
|
52
|
+
l.info("skipping database cleanup")
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
l.info("starting database cleanup")
|
|
56
|
+
try {
|
|
57
|
+
const qb = await connect(env)
|
|
58
|
+
const res = await qb.delete({
|
|
59
|
+
tableName: "certificates",
|
|
60
|
+
where: {
|
|
61
|
+
conditions: "unixepoch('subsec') > unixepoch(valid_before,'subsec',?)",
|
|
62
|
+
params: env.DB_CERTIFICATE_RETENTION
|
|
63
|
+
}
|
|
64
|
+
}).execute()
|
|
65
|
+
l.info("completed database cleanup", "changes", res.meta?.changes)
|
|
66
|
+
} catch (err) {
|
|
67
|
+
l.error("error during database cleanup", "retention", env.DB_CERTIFICATE_RETENTION)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const recordCertificate = async (env: SshCaBindings, certificate: Certificate, keyid: string, certificateType: CertificateType = CertificateType.User) => {
|
|
72
|
+
const serial = certificate.serial.readBigUInt64BE(0)
|
|
73
|
+
const subjects = certificate.subjects.map((v: Identity): string => {
|
|
74
|
+
return v.toString()
|
|
75
|
+
}).join(",")
|
|
76
|
+
const extensions = certificate.getExtensions().map((v: Format.OpenSshSignatureExt | Format.x509SignatureExt): string => {
|
|
77
|
+
// @ts-ignore: the name property does exist
|
|
78
|
+
return v.name as string
|
|
79
|
+
}).join(",")
|
|
80
|
+
const qb = await connect(env)
|
|
81
|
+
const res = await qb.insert({
|
|
82
|
+
tableName: "certificates",
|
|
83
|
+
data: {
|
|
84
|
+
serial: `${serial}`,
|
|
85
|
+
key_id: keyid,
|
|
86
|
+
principals: subjects,
|
|
87
|
+
extensions: extensions,
|
|
88
|
+
valid_after: certificate.validFrom.toISOString(),
|
|
89
|
+
valid_before: certificate.validUntil.toISOString(),
|
|
90
|
+
certificate_type: certificateType,
|
|
91
|
+
public_key: certificate.subjectKey.toString("ssh")
|
|
92
|
+
},
|
|
93
|
+
}).execute()
|
|
94
|
+
|
|
95
|
+
if (!res.success) {
|
|
96
|
+
throw new Error("error during query")
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const isRevoked = async (env: SshCaBindings, serial: bigint): Promise<boolean> => {
|
|
101
|
+
const qb = await connect(env)
|
|
102
|
+
const res = await qb.fetchOne({
|
|
103
|
+
tableName: "certificates",
|
|
104
|
+
fields: ["serial"],
|
|
105
|
+
where: {
|
|
106
|
+
conditions: "serial = ? AND revoked_at NOTNULL",
|
|
107
|
+
params: `${serial}`
|
|
108
|
+
},
|
|
109
|
+
}).execute()
|
|
110
|
+
|
|
111
|
+
return res.results !== undefined
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export enum RevocationStatus {
|
|
115
|
+
NotFound,
|
|
116
|
+
Revoked,
|
|
117
|
+
Revokable,
|
|
118
|
+
Unrevokable
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const revocationStatus = async (env: SshCaBindings, serial: bigint, publickey: string, certificateType: CertificateType): Promise<RevocationStatus> => {
|
|
122
|
+
const qb = await connect(env)
|
|
123
|
+
const res = await qb.fetchOne({
|
|
124
|
+
tableName: "certificates",
|
|
125
|
+
fields: ["public_key", "revoked_at"],
|
|
126
|
+
where: {
|
|
127
|
+
conditions: "serial = ? AND public_key = ? AND certificate_type = ?",
|
|
128
|
+
params: [`${serial}`, publickey, certificateType]
|
|
129
|
+
}
|
|
130
|
+
}).execute()
|
|
131
|
+
|
|
132
|
+
if (res.results === undefined) {
|
|
133
|
+
return RevocationStatus.NotFound
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (res.results.revoked_at !== null) {
|
|
137
|
+
return RevocationStatus.Revoked
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (publickey === res.results.public_key) {
|
|
141
|
+
return RevocationStatus.Revokable
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return RevocationStatus.Unrevokable
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export const getRevocationList = async (env: SshCaBindings, certificateType: CertificateType): Promise<string[]> => {
|
|
148
|
+
const qb = await connect(env)
|
|
149
|
+
const res = await qb.fetchAll({
|
|
150
|
+
tableName: "certificates",
|
|
151
|
+
fields: ["serial"],
|
|
152
|
+
where: {
|
|
153
|
+
conditions: "revoked_at NOTNULL AND certificate_type = ? AND unixepoch('subsec') < unixepoch(valid_before,'subsec','24 hours')",
|
|
154
|
+
params: certificateType
|
|
155
|
+
}
|
|
156
|
+
}).execute()
|
|
157
|
+
|
|
158
|
+
if (!res.success) {
|
|
159
|
+
throw new Error("error during query")
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (res.results === undefined) {
|
|
163
|
+
return []
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return res.results.map(item => item.serial)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
type RevokedCertificate = Pick<CertificateRecord, "revoked_at">
|
|
170
|
+
|
|
171
|
+
export const revokeCertificate = async (env: SshCaBindings, serial: bigint): Promise<RevokedCertificate[]> => {
|
|
172
|
+
const qb = await connect(env)
|
|
173
|
+
const res = await qb.update<RevokedCertificate>({
|
|
174
|
+
tableName: "certificates",
|
|
175
|
+
data: {
|
|
176
|
+
revoked_at: (new Date).toISOString()
|
|
177
|
+
},
|
|
178
|
+
where: {
|
|
179
|
+
conditions: "serial = ?",
|
|
180
|
+
params: `${serial}`
|
|
181
|
+
},
|
|
182
|
+
returning: ["revoked_at"]
|
|
183
|
+
}).execute()
|
|
184
|
+
|
|
185
|
+
if (!res.success) {
|
|
186
|
+
throw new Error("error during query")
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (res.results === undefined) {
|
|
190
|
+
return []
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return res.results
|
|
194
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type Migration } from "workers-qb";
|
|
2
|
+
|
|
3
|
+
export const migration: Migration = {
|
|
4
|
+
name: "0001_initial_schema",
|
|
5
|
+
sql: `
|
|
6
|
+
CREATE TABLE IF NOT EXISTS certificates (
|
|
7
|
+
id INTEGER PRIMARY KEY,
|
|
8
|
+
serial TEXT UNIQUE NOT NULL,
|
|
9
|
+
key_id TEXT NOT NULL,
|
|
10
|
+
principals TEXT NOT NULL,
|
|
11
|
+
extensions TEXT,
|
|
12
|
+
valid_after TIMESTAMP NOT NULL,
|
|
13
|
+
valid_before TIMESTAMP NOT NULL,
|
|
14
|
+
revoked_at TIMESTAMP,
|
|
15
|
+
certificate_type INTEGER NOT NULL DEFAULT 0,
|
|
16
|
+
public_key TEXT
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
CREATE INDEX IF NOT EXISTS idx_certificates_serial ON certificates(serial);
|
|
20
|
+
CREATE INDEX IF NOT EXISTS idx_certificates_valid_after ON certificates(valid_after);
|
|
21
|
+
CREATE INDEX IF NOT EXISTS idx_certificates_valid_before ON certificates(valid_before);
|
|
22
|
+
CREATE INDEX IF NOT EXISTS idx_certificates_revoked_at ON certificates(revoked_at);
|
|
23
|
+
CREATE INDEX IF NOT EXISTS idx_certificates_certificate_type ON certificates(certificate_type);
|
|
24
|
+
`
|
|
25
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { dbCleanup } from "./db"
|
|
2
|
+
import { createApp } from "./router"
|
|
3
|
+
import type { SshCaBindings } from "./types"
|
|
4
|
+
export type { SshCaBindings } from "./types"
|
|
5
|
+
|
|
6
|
+
let app: ReturnType<typeof createApp> | undefined
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
fetch(req: Request, env: SshCaBindings, ctx: ExecutionContext) {
|
|
10
|
+
if (app === undefined) {
|
|
11
|
+
app = createApp(env)
|
|
12
|
+
}
|
|
13
|
+
return app.fetch(req, env, ctx)
|
|
14
|
+
},
|
|
15
|
+
async scheduled(controller: ScheduledController, env: SshCaBindings, ctx: ExecutionContext) {
|
|
16
|
+
ctx.waitUntil(dbCleanup(env))
|
|
17
|
+
},
|
|
18
|
+
} satisfies ExportedHandler<SshCaBindings>
|