-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
2a972f0
commit 94beb72
Showing
9 changed files
with
177 additions
and
223 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,52 +1,107 @@ | ||
import { getObject } from "~/aws/s3"; | ||
import { env } from "~/env"; | ||
import { identityCertificate, identityPrivateKey } from "~/win/common"; | ||
import { gt, lt } from "drizzle-orm"; | ||
import type { pki } from "node-forge"; | ||
import { db, deviceAuthorities } from "~/db"; | ||
|
||
// TODO: For better self-hosting we are probs gonna wanna have R2 as our main storage and replicate to S3 where required. | ||
// Why S3? Because API Gateway handles TLS terminations and it requires the certificate pool to be in S3. | ||
// We cache between invocations of the code | ||
let certCache: | ||
| { | ||
publicKey: string; | ||
privateKey: string; | ||
cachedAt: Date; | ||
} | ||
| undefined = undefined; | ||
let truststores: | ||
| { | ||
certs: pki.Certificate[]; | ||
cachedAt: Date; | ||
} | ||
| undefined = undefined; | ||
|
||
export const TRUSTSTORE_BUCKET_REGION = "us-east-1"; | ||
export const TRUSTSTORE_ACTIVE_AUTHORITY = "authority"; | ||
const cacheValidityPeriod = 15 * 60 * 1000; // 15 minutes | ||
|
||
// Get the public and private keypair for the active MDM authority certificates used for issuing new client certificates. | ||
export async function getMDMAuthority() { | ||
// if (!env.TRUSTSTORE_BUCKET) return undefined; | ||
|
||
// const activeAuthority = await getObject( | ||
// env.TRUSTSTORE_BUCKET, | ||
// TRUSTSTORE_BUCKET_REGION, | ||
// TRUSTSTORE_ACTIVE_AUTHORITY, | ||
// { | ||
// // This is okay. Search for the `REF[0]` comment for explanation. | ||
// // @ts-expect-error // TODO: Fix this type error | ||
// cf: { | ||
// // Cache for 1 day | ||
// cacheTtl: 24 * 60 * 60, | ||
// cacheEverything: true, | ||
// }, | ||
// }, | ||
// ); | ||
// let activeAuthorityRaw: string; | ||
// if (activeAuthority.status === 404) { | ||
// activeAuthorityRaw = await (await import("./issue")).issueAuthority(""); | ||
// } else if (!activeAuthority.ok) | ||
// throw new Error( | ||
// `Failed to get '${TRUSTSTORE_ACTIVE_AUTHORITY}' from bucket '${env.TRUSTSTORE_BUCKET}' with status ${activeAuthority.statusText}: ${await activeAuthority.text()}`, | ||
// ); | ||
// else activeAuthorityRaw = await activeAuthority.text(); | ||
|
||
// const parts = activeAuthorityRaw.split("\n---\n"); | ||
// if (parts.length !== 2) throw new Error("Authority file is malformed"); | ||
|
||
export async function getActiveAuthority(shouldRenew = false) { | ||
const { pki } = (await import("node-forge")).default; | ||
|
||
// return [ | ||
// pki.certificateFromPem(parts[0]!), | ||
// pki.privateKeyFromPem(parts[1]!), | ||
// ] as const; | ||
if ( | ||
!certCache || | ||
// Refresh the local cache if it's older than 15 minutes | ||
(certCache.cachedAt.getTime() - new Date().getTime()) / 1000 / 60 > 15 | ||
) { | ||
let [result] = await db | ||
.select({ | ||
publicKey: deviceAuthorities.publicKey, | ||
privateKey: deviceAuthorities.privateKey, | ||
expiresAt: deviceAuthorities.expiresAt, | ||
}) | ||
.from(deviceAuthorities) | ||
// We want to grab the latest certificate | ||
.orderBy(deviceAuthorities.createdAt) | ||
// but we avoid using any certs issued for a while to ensure it has time to propagate | ||
// because we don't wanna sign the device cert and then the device talks with a server without the new cert yet. | ||
.where( | ||
gt( | ||
deviceAuthorities.expiresAt, | ||
new Date(new Date().getTime() - cacheValidityPeriod * 2), | ||
), | ||
) | ||
.limit(1); // TODO: Filter out new certs for a while to ensure they can progate once issued | ||
|
||
if (!result) result = await (await import("./issue")).issueAuthority(); | ||
|
||
// If the authority is 15 days from expiring and asked renew it | ||
if ( | ||
// We explicitly require this so that we don't run into race conditions if many devices are trying to enroll at once. | ||
// The expectation is this is only set in a cron job. | ||
shouldRenew && | ||
(result.expiresAt.getTime() - new Date().getTime()) / | ||
1000 / | ||
60 / | ||
60 / | ||
24 < | ||
15 | ||
) { | ||
console.log( | ||
`Detected authority certificate is about to expire at ${result.expiresAt.toString()}, renewing`, | ||
); | ||
result = await (await import("./issue")).issueAuthority(); | ||
} | ||
|
||
certCache = { | ||
publicKey: result.publicKey, | ||
privateKey: result.privateKey, | ||
cachedAt: new Date(), | ||
}; | ||
} | ||
|
||
return [ | ||
pki.certificateFromPem(identityCertificate), | ||
pki.privateKeyFromPem(identityPrivateKey), | ||
]; | ||
pki.certificateFromPem(certCache.publicKey), | ||
pki.privateKeyFromPem(certCache.privateKey), | ||
] as const; | ||
} | ||
|
||
// Get all of the public keys for active MDM authority certificates | ||
export async function getAuthorityTruststore() { | ||
if ( | ||
!truststores || | ||
// Refresh the local cache if it's older than 15 minutes | ||
truststores.cachedAt.getTime() - new Date().getTime() > cacheValidityPeriod | ||
) { | ||
const { pki } = (await import("node-forge")).default; | ||
|
||
const result = await db | ||
.select({ | ||
publicKey: deviceAuthorities.publicKey, | ||
}) | ||
.from(deviceAuthorities) | ||
// We only want to trust certificates that are still valid | ||
.where(gt(deviceAuthorities.expiresAt, new Date())); | ||
|
||
truststores = { | ||
certs: result.map((v) => pki.certificateFromPem(v.publicKey)), | ||
cachedAt: new Date(), | ||
}; | ||
} | ||
|
||
return truststores.certs; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.