import { createHash, createPublicKey, verify } from "node:crypto" import { constants } from "node:fs" import { lstat, open, realpath } from "node:fs/promises" import path from "node:path" import { z } from "zod" import { assertDatabaseSourceObservationCurrent, type DatabaseSourceObservation, type DatabaseSourceObservationOptions } from "./database-source-observation.js" import type { ProbeStorageSql } from "../services/server-probe-journal-maintenance.js" // Public verification only. No private key, signing, enrollment, SQL mutation, // configured pool, provisioning or automatic current-head advancement here. export class DatabaseSourceBindingError extends Error { readonly code = "DATABASE_SOURCE_BINDING_REFUSED" } const fail = (): never => { throw new DatabaseSourceBindingError("Доверенная подпись и текущая привязка источника БД не подтверждены") } const sha = z.string().regex(/^[a-f0-9]{64}$/) const uuid = z.string().uuid() const text = z.string().min(1).max(200).regex(/^[^\x00-\x1f\x7f]+$/) const source = z.object({ systemIdentifier: z.string().regex(/^[1-9][0-9]{0,19}$/).refine(value => BigInt(value) <= 18446744073709551615n), databaseOid: z.number().int().positive().max(4294967295), databaseName: text, sessionUser: text, currentUser: text, inRecovery: z.literal(false), serverVersionNum: z.number().int().min(180000).max(189999) }).strict() export const sourceBindingBodySchema = z.object({ format: z.literal("database-source-binding-v1"), installationId: uuid, sourceIdentityId: uuid, generation: uuid, ordinal: z.number().int().positive().max(Number.MAX_SAFE_INTEGER), source }).strict() export const sourceRegistryHeadSchema = z.object({ format: z.literal("database-source-registry-head-v1"), installationId: uuid, sourceIdentityId: uuid, generation: uuid, ordinal: z.number().int().positive().max(Number.MAX_SAFE_INTEGER), bindingSha256: sha, previousHeadSha256: sha.nullable() }).strict().refine(value => (value.ordinal === 1) === (value.previousHeadSha256 === null)) const deployment = z.object({ registryDirectory: z.string().min(1).max(4096), installationId: uuid, publicKeySpkiSha256: sha, currentHeadSha256: sha }).strict() const envelope = z.object({ body: z.unknown(), signature: z.string().regex(/^[A-Za-z0-9_-]{86}$/) }).strict() export type DatabaseSourceBindingBody = z.infer export type DatabaseSourceRegistryHead = z.infer export type DatabaseSourceDeploymentPin = z.infer export const DATABASE_SOURCE_SIGNATURE_DOMAINS = Object.freeze({ binding: "mmapp:database-source-binding:ed25519:v1\n", head: "mmapp:database-source-head:ed25519:v1\n" }) export function sourceBindingCanonical(value: unknown): string { return JSON.stringify(value, (_key, item: unknown) => item && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) : item) } export const sourceBindingSha256 = (bytes: Uint8Array | string) => createHash("sha256").update(bytes).digest("hex") export const sourceBindingBytes = (value: unknown) => Buffer.from(sourceBindingCanonical(value) + "\n", "utf8") function parseExact(bytes: Buffer) { const value: unknown = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)) // Exact canonical bytes also reject duplicate JSON keys, alternate numeric // spellings, trailing content and ambiguous/noncanonical Unicode encodings. if (!sourceBindingBytes(value).equals(bytes)) fail() return envelope.parse(value) } async function boundedFile(filename: string, maximum = 8192): Promise { const before = await lstat(filename) if (!before.isFile() || before.isSymbolicLink() || before.size < 1 || before.size > maximum) fail() const handle = await open(filename, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)) try { const stat = await handle.stat() if (!stat.isFile() || stat.dev !== before.dev || stat.ino !== before.ino || stat.size !== before.size) fail() const bytes = Buffer.alloc(stat.size + 1); let length = 0 while (length < bytes.length) { const got = await handle.read(bytes, length, bytes.length - length, length); if (!got.bytesRead) break; length += got.bytesRead } const after = await handle.stat() if (length !== stat.size || after.size !== stat.size || after.mtimeMs !== stat.mtimeMs || after.ctimeMs !== stat.ctimeMs) fail() return bytes.subarray(0, length) } finally { await handle.close() } } function fence(options: DatabaseSourceObservationOptions) { if (!Number.isSafeInteger(options.deadlineAt) || options.deadlineAt <= Date.now() || options.signal?.aborted) fail() } /** Input is privileged deployment configuration, never request JSON. Actual * protection/ACL and current-process invalidation are separate deployment gates. * This deliberately grants signature/source evidence only, NOT generation/READY. */ export async function readDatabaseSourceBindingRegistry(raw: DatabaseSourceDeploymentPin) { try { const cfg = deployment.parse(raw), directory = path.resolve(cfg.registryDirectory) if (!path.isAbsolute(cfg.registryDirectory) || directory !== await realpath(directory)) fail() const stat = await lstat(directory); if (!stat.isDirectory() || stat.isSymbolicLink()) fail() const publicBytes = await boundedFile(path.join(directory, "operator-public.pem"), 4096) if (publicBytes.toString("ascii").includes("PRIVATE KEY")) fail() const publicKey = createPublicKey(publicBytes) if (publicKey.type !== "public" || publicKey.asymmetricKeyType !== "ed25519") fail() const spki = publicKey.export({ type: "spki", format: "der" }) if (sourceBindingSha256(spki) !== cfg.publicKeySpkiSha256) fail() const headBytes = await boundedFile(path.join(directory, "head-" + cfg.currentHeadSha256 + ".json")) if (sourceBindingSha256(headBytes) !== cfg.currentHeadSha256) fail() const signedHead = parseExact(headBytes), head = sourceRegistryHeadSchema.parse(signedHead.body) const check = (domain: string, body: unknown, signature: string) => verify(null, Buffer.from(domain + sourceBindingCanonical(body)), publicKey, Buffer.from(signature, "base64url")) if (head.installationId !== cfg.installationId || !check(DATABASE_SOURCE_SIGNATURE_DOMAINS.head, head, signedHead.signature)) fail() const bindingBytes = await boundedFile(path.join(directory, "binding-" + head.bindingSha256 + ".json")) if (sourceBindingSha256(bindingBytes) !== head.bindingSha256) fail() const signedBinding = parseExact(bindingBytes), body = sourceBindingBodySchema.parse(signedBinding.body) if (!check(DATABASE_SOURCE_SIGNATURE_DOMAINS.binding, body, signedBinding.signature) || body.installationId !== head.installationId || body.sourceIdentityId !== head.sourceIdentityId || body.generation !== head.generation || body.ordinal !== head.ordinal) fail() return Object.freeze({ body: Object.freeze({ ...body, source: Object.freeze({ ...body.source }) }), head: Object.freeze(head), publicKeySpkiSha256: cfg.publicKeySpkiSha256, headSha256: cfg.currentHeadSha256, platformProtectionVerified: false as const, generationAuthenticated: false as const, readyAuthorized: false as const }) } catch { return fail() } } /** Genuine same-facade observation is mandatory. Serialized source facts never * become a capability. Registry pin comes only from future privileged assembly. */ export function createDatabaseSourceBindingVerifier(raw: DatabaseSourceDeploymentPin) { const cfg = Object.freeze(deployment.parse(raw)) return Object.freeze({ async verify(sql: ProbeStorageSql, observation: DatabaseSourceObservation, options: DatabaseSourceObservationOptions) { try { fence(options); await assertDatabaseSourceObservationCurrent(sql, observation, options) const before = await readDatabaseSourceBindingRegistry(cfg); fence(options) if (sourceBindingCanonical(before.body.source) !== sourceBindingCanonical(observation.source)) fail() await assertDatabaseSourceObservationCurrent(sql, observation, options); fence(options) const after = await readDatabaseSourceBindingRegistry(cfg); fence(options) if (sourceBindingCanonical(before) !== sourceBindingCanonical(after)) fail() await assertDatabaseSourceObservationCurrent(sql, observation, options); fence(options) return Object.freeze({ format: "database-source-binding-evidence-v1" as const, installationId: before.body.installationId, sourceIdentityId: before.body.sourceIdentityId, generation: before.body.generation, ordinal: before.body.ordinal, headSha256: before.headSha256, bindingSha256: before.head.bindingSha256, operatorSignatureVerified: true as const, currentHeadPinVerified: true as const, genuineSourceFactsMatched: true as const, platformProtectionVerified: false as const, generationAuthenticated: false as const, allWriterDrainAuthenticated: false as const, readyAuthorized: false as const }) } catch { return fail() } } }) }