ca groundwork

This commit is contained in:
DecDuck
2024-10-07 22:35:54 +11:00
parent 1bd19ad917
commit bfafd2a044
44 changed files with 628 additions and 130 deletions
+26
View File
@@ -0,0 +1,26 @@
# Client Handshake process
Drop clients need to complete a handshake in order to connect to a Drop server. It also trades certificates for encrypted P2P connections.
## 1. Client requests a handshake
Client makes request: `POST /api/v1/client/initiate` with information about the client.
Server responds with a URL to send the user to. It generates a device ID, which has all the metadata attached.
## 2. User signs in
Client sends user to the provided URL (in external browser). User signs in using the existing authentication stack.
Server sends redirect to drop://handshake/[id]/[token], where the token is an authentication token to generate the necessary certificates, and the ID is the client ID as generated by the server.
## 3. Client requests certificates
Client makes request: `POST /api/v1/client/handshake` with the token recieved in the previous step.
The server uses it's CA to generate a public-private key pair, the CN of the client ID. It then sends that pair, plus the CA's public key, to the client, which stores it all.
The certificate lasts for a year, and is rotated when it has 3 months or less left on it's expiry.
## 4.a Client requests one-time device endpoint
The client generates a nonce and signs it with their private key. This is then attached to any device-related request.
## 4.b Client wants a long-lived session
The client does the same as above, but instead makes the request to `POST /api/v1/client/session`, which generates a session token that lasts for a day. This can then be used in the request to provide authentication.
+34
View File
@@ -0,0 +1,34 @@
import path from "path";
import droplet from "@drop/droplet";
import { CertificateStore } from "./store";
export type CertificateBundle = {
priv: string;
pub: string;
cert: string;
};
/*
This is designed to handle client certificates, as described in the README.md
*/
export class CertificateAuthority {
private certificateStore: CertificateStore;
private root: CertificateBundle;
constructor(store: CertificateStore, root: CertificateBundle) {
this.certificateStore = store;
this.root = root;
}
static async new(store: CertificateStore) {
const root = await store.fetch("ca");
if (root === undefined) {
const [priv, pub, cert] = droplet.generateRootCa();
const bundle: CertificateBundle = { priv, pub, cert };
await store.store("ca", bundle);
return new CertificateAuthority(store, bundle);
}
return new CertificateAuthority(store, root);
}
}
+23
View File
@@ -0,0 +1,23 @@
import path from "path";
import fs from "fs";
import { CertificateBundle } from "./ca";
export type CertificateStore = {
store(name: string, data: CertificateBundle): Promise<void>;
fetch(name: string): Promise<CertificateBundle | undefined>;
};
export const fsCertificateStore = (base: string) => {
const store: CertificateStore = {
async store(name: string, data: CertificateBundle) {
const filepath = path.join(base, name);
fs.writeFileSync(filepath, JSON.stringify(data));
},
async fetch(name: string) {
const filepath = path.join(base, name);
if (!fs.existsSync(filepath)) return undefined;
return JSON.parse(fs.readFileSync(filepath, "utf-8"));
},
};
return store;
};