browsec/trustlab/sdk/plugin-api.ts

207 lines
5.7 KiB
TypeScript

/** Version of the first TLS-only trust-plugin API. */
export const TRUST_API_VERSION = "0.1" as const;
export type TrustApiVersion = typeof TRUST_API_VERSION;
export type PluginMode =
| "observer"
| "advisor"
| "decision-authority"
| "veto-authority";
export type TlsValidation = "success" | "failure";
export interface CertificateFacts {
readonly subject: string;
readonly issuer?: string;
readonly sha256: string;
readonly spkiSha256?: string;
readonly serialNumber?: string;
readonly dnsNames?: readonly string[];
readonly validFrom?: string;
readonly validUntil?: string;
readonly isCa: boolean;
readonly keyUsages: readonly CertificateKeyUsage[];
readonly selfSigned: boolean;
}
export type CertificateKeyUsage =
| "digitalSignature"
| "keyEncipherment"
| "keyCertSign"
| "crlSign";
export interface TlsFailure {
readonly code: string;
readonly certificateSha256?: string;
readonly check: "identity" | "validity" | "signature" | "trust-anchor" | "local-policy";
readonly summary: string;
}
export interface TlsFacts {
readonly schemaVersion: 0;
readonly connectionId: string;
readonly hostname: string;
readonly port: number;
readonly validation: TlsValidation;
readonly errors: readonly string[];
readonly failure?: TlsFailure;
readonly presentedChain: readonly CertificateFacts[];
readonly constructedChain: readonly CertificateFacts[];
readonly tls: Readonly<Record<string, unknown>>;
}
export type PluginEntry = EvidenceEntry | WarningEntry | VoteEntry;
interface EntryBase {
readonly code: string;
readonly message: string;
readonly data?: Readonly<Record<string, unknown>>;
}
export interface EvidenceEntry extends EntryBase {
readonly kind: "evidence";
}
export interface WarningEntry extends EntryBase {
readonly kind: "warning";
}
export interface VoteEntry extends EntryBase {
readonly kind: "vote";
readonly data: Readonly<{ trusted: boolean } & Record<string, unknown>>;
}
export interface JournalEntry extends EntryBase {
readonly id: string;
readonly pluginId: string;
readonly pluginName: string;
readonly kind: PluginEntry["kind"] | "resolution" | "error" | "abstention";
}
export interface ReadonlyDecisionJournal {
readonly entries: readonly JournalEntry[];
}
export interface PluginContribution {
readonly entries: readonly PluginEntry[];
}
export type TrustScope = CertificateForHostScope | AuthorityForHostScope;
export interface CertificateForHostScope {
readonly kind: "certificate-for-host";
readonly hostname: string;
readonly port: number;
readonly certificateSha256: string;
}
export interface AuthorityForHostScope {
readonly kind: "authority-for-host";
readonly hostname: string;
readonly port: number | "any";
readonly authorityCertificateSha256: string;
readonly includeSubdomains: boolean;
}
export type TrustLifetime =
| { readonly kind: "connection" }
| { readonly kind: "session" }
| { readonly kind: "until"; readonly expiresAt: string }
| { readonly kind: "persistent" };
export interface TrustedVerdict {
readonly trusted: true;
readonly scope: TrustScope;
readonly lifetime: TrustLifetime;
readonly reasonEntryIds: readonly string[];
readonly overriddenErrors: readonly string[];
}
export interface NotTrustedVerdict {
readonly trusted: false;
readonly scope: TrustScope;
readonly lifetime: TrustLifetime;
readonly reasonEntryIds: readonly string[];
readonly overriddenErrors: readonly [];
}
export type TrustVerdict = TrustedVerdict | NotTrustedVerdict;
export interface EvidenceContext {
readonly facts: TlsFacts;
readonly signal: AbortSignal;
}
export interface DecisionContext {
readonly facts: TlsFacts;
readonly journal: ReadonlyDecisionJournal;
}
export interface FailureDecisionContext extends DecisionContext {
readonly ui: TrustDecisionUi;
readonly signal: AbortSignal;
}
export interface TrustDecisionRequest {
readonly title: string;
readonly summary: string;
readonly proposedScope: TrustScope;
readonly evidenceEntryIds: readonly string[];
}
export interface TrustDecisionUi {
requestDecision(request: TrustDecisionRequest): Promise<boolean | undefined>;
}
export interface TrustPluginHooks {
collectEvidence?(
context: EvidenceContext,
): PluginContribution | undefined | Promise<PluginContribution | undefined>;
/** Fast and deliberately synchronous. Network and UI are unavailable. */
onBeforeTlsAccept?(context: DecisionContext): TrustVerdict | undefined;
/** Failed navigation is investigated asynchronously before a fresh retry. */
onTlsFailure?(
context: FailureDecisionContext,
): Promise<TrustVerdict | undefined>;
}
export interface TrustPluginManifest {
readonly manifestVersion: 1;
readonly trustApiVersion: TrustApiVersion;
readonly id: string;
readonly name: string;
readonly version: string;
readonly supportedModes: readonly PluginMode[];
readonly capabilities: {
readonly interactiveUi?: boolean;
};
}
export interface TrustPluginRegistration {
readonly manifest: TrustPluginManifest;
readonly hooks: TrustPluginHooks;
}
/**
* Defines the module's single registration object. Browsec still validates the
* installed package manifest and local capability grants before activation.
*/
export function defineTrustPlugin<const T extends TrustPluginRegistration>(
registration: T,
): T {
return Object.freeze(registration);
}
export function exactCertificateScope(facts: TlsFacts): CertificateForHostScope {
const leaf = facts.presentedChain[0];
if (!leaf) throw new TypeError("TLS facts do not contain a leaf certificate");
return Object.freeze({
kind: "certificate-for-host",
hostname: facts.hostname,
port: facts.port,
certificateSha256: leaf.sha256,
});
}