Reference source · 0.1.0-dev.11
rotation.ts
src/rotation.ts · Download original file
import { sign, verify } from 'node:crypto';
import type { Credential, CredentialBody, Identity } from './identity.ts';
import { publicKey, TrustStore } from './identity.ts';
import { b64, unb64, jsonBytes, parseObject, exact, id, integer, newId, requireThat } from './core.ts';
export type RotationProof = { payload: string; old_credential: Credential; new_credential: Credential; old_signature: string; new_signature: string };
export function makeRotationProof(oldIdentity: Identity, oldCredential: Credential, newIdentity: Identity, newCredential: Credential, host: string, now: number, rotationId = newId()): RotationProof {
requireThat(oldIdentity.public.subject === newIdentity.public.subject, 'wrong_identity');
const payload = b64(jsonBytes({ rotation_id: rotationId, host, subject: oldIdentity.public.subject,
old_signing_kid: oldIdentity.public.signingKid, old_encryption_kid: oldIdentity.public.encryptionKid,
new_signing_kid: newIdentity.public.signingKid, new_encryption_kid: newIdentity.public.encryptionKid, issued_at: now, expires_at: now + 120 }));
const input = Buffer.from(`OPRP-ROTATION-0.1.${payload}`);
return { payload, old_credential: oldCredential, new_credential: newCredential, old_signature: b64(sign(null, input, oldIdentity.signingPrivate)), new_signature: b64(sign(null, input, newIdentity.signingPrivate)) };
}
export function rotationBody(proof: RotationProof): Record<string, any> {
exact(proof, ['payload', 'old_credential', 'new_credential', 'old_signature', 'new_signature']);
requireThat(typeof proof.payload === 'string' && proof.payload.length <= 4000, 'invalid_rotation');
const body = parseObject(unb64(proof.payload));
exact(body, ['rotation_id', 'host', 'subject', 'old_signing_kid', 'old_encryption_kid', 'new_signing_kid', 'new_encryption_kid', 'issued_at', 'expires_at']);
['rotation_id', 'host', 'subject', 'old_signing_kid', 'old_encryption_kid', 'new_signing_kid', 'new_encryption_kid'].forEach(key => id(body[key]));
integer(body.issued_at); integer(body.expires_at); return body;
}
export function validateRotation(proof: RotationProof, trust: TrustStore, host: string, caller: CredentialBody, now: number): { oldIdentity: CredentialBody; body: Record<string, any> } {
const body = rotationBody(proof);
requireThat(body.host === host && body.subject === caller.subject && body.issued_at <= now && now < body.expires_at && body.expires_at - body.issued_at <= 120, 'invalid_rotation');
const oldIdentity = trust.validate(proof.old_credential, now, caller.subject, 'application');
const next = trust.validate(proof.new_credential, now, caller.subject, 'application');
requireThat(oldIdentity.signingKid === body.old_signing_kid && oldIdentity.encryptionKid === body.old_encryption_kid && next.signingKid === body.new_signing_kid && next.encryptionKid === body.new_encryption_kid && caller.signingKid === next.signingKid && caller.encryptionKid === next.encryptionKid && next.signingKid !== oldIdentity.signingKid && next.encryptionKid !== oldIdentity.encryptionKid, 'invalid_rotation');
const input = Buffer.from(`OPRP-ROTATION-0.1.${proof.payload}`);
requireThat(verify(null, input, publicKey(oldIdentity.signingKey, 'signing'), unb64(proof.old_signature, 3309)) && verify(null, input, publicKey(next.signingKey, 'signing'), unb64(proof.new_signature, 3309)), 'invalid_rotation');
return { oldIdentity, body };
}