57 lines
5.0 KiB
TypeScript
57 lines
5.0 KiB
TypeScript
import {createPublicKey,verify,type KeyObject} from 'node:crypto'
|
|
import {z} from 'zod'
|
|
import {bytes,canonical,sha} from './application-writer-profile-canonical.js'
|
|
export {bytes,canonical,sha}
|
|
|
|
// Internal schema/crypto only: no key loader, DB/CLI/runtime, installation issuer,
|
|
// caller-JSON pin parser, provider, source/current-head proof or writer capability.
|
|
// A verified signature under a supplied public key is DATA, never authorization.
|
|
export const KEY=8723110,PUBLISHER='barrier_profile_publisher',OPERATOR='barrier_operator'
|
|
export const POLICY_DOMAIN='mmapp:application-writer-policy:ed25519:v1\n',HEAD_DOMAIN='mmapp:application-writer-head:ed25519:v1\n'
|
|
export const MAX_EPOCH=9223372036854775807n
|
|
export function epoch(value:unknown):string {
|
|
if(typeof value!=='string'||!(/^[1-9][0-9]{0,18}$/).test(value)||BigInt(value)>MAX_EPOCH)throw Error('Canonical PG bigint epoch required')
|
|
return value
|
|
}
|
|
export function nextEpoch(value:unknown):string {
|
|
const next=BigInt(epoch(value))+1n
|
|
if(next>MAX_EPOCH)throw Error('PG bigint epoch exhausted')
|
|
return next.toString()
|
|
}
|
|
const hex=z.string().regex(/^[a-f0-9]{64}$/),uuid=z.string().uuid(),decimal=z.string().refine(x=>{try{return epoch(x)===x}catch{return false}})
|
|
const oid=z.string().regex(/^[1-9][0-9]{0,9}$/).refine(value=>BigInt(value)<=4294967295n)
|
|
const roles=z.object({runtime:oid,operator:oid,capture:oid,publisher:oid,control:oid}).strict()
|
|
const source=z.object({systemIdentifier:z.string().regex(/^[1-9][0-9]{0,19}$/).refine(value=>BigInt(value)<=18446744073709551615n),databaseOid:oid,databaseName:z.string().min(1).max(63),serverVersionNum:z.number().int().min(180000).max(189999),inRecovery:z.literal(false)}).strict()
|
|
export const policySchema=z.object({format:z.literal('application-writer-policy-v1'),installationId:uuid,epoch:decimal,issuedAt:z.number().int().nonnegative(),notBefore:z.number().int().nonnegative(),notAfter:z.number().int().nonnegative(),source,sourceObservationSha256:hex,roles,manifestSha256:hex,definitionSha256:hex,catalogSha256:hex,futureLeaves:z.literal('fixed12-native-policy-v1')}).strict().refine(p=>p.notBefore===p.issuedAt&&p.notAfter>p.notBefore&&p.notAfter-p.notBefore<=86400000)
|
|
export const headSchema=z.object({format:z.literal('application-writer-head-v1'),installationId:uuid,epoch:decimal,policySha256:hex,previousHeadSha256:hex.nullable()}).strict().refine(p=>(p.epoch==='1')===(p.previousHeadSha256===null))
|
|
const envelope=z.object({body:z.unknown(),signature:z.string().regex(/^[A-Za-z0-9_-]{86}$/)}).strict()
|
|
export type ApplicationWriterPolicy=z.infer<typeof policySchema>
|
|
export type ApplicationWriterHead=z.infer<typeof headSchema>
|
|
type ArtifactKind='policy'|'head'
|
|
// Not exported as a provisioning/config API. Future private host must obtain
|
|
// this subject from protected installation state, never a request or provider.
|
|
interface PublicVerificationSubject {readonly installationId:string;readonly publicKeyPem:string;readonly publicKeySpkiSha256:string}
|
|
type ParsedData<T>={body:T;sha256:string}
|
|
export function publicKey(installation:PublicVerificationSubject):KeyObject {
|
|
const key=createPublicKey(installation.publicKeyPem)
|
|
if(key.asymmetricKeyType!=='ed25519'||sha(key.export({format:'der',type:'spki'}))!==installation.publicKeySpkiSha256)throw Error('Fixed installation SPKI required')
|
|
return key
|
|
}
|
|
export function signedEnvelope(raw:Uint8Array|string,kind:'policy',installation:PublicVerificationSubject):ParsedData<ApplicationWriterPolicy>
|
|
export function signedEnvelope(raw:Uint8Array|string,kind:'head',installation:PublicVerificationSubject):ParsedData<ApplicationWriterHead>
|
|
export function signedEnvelope(raw:Uint8Array|string,kind:ArtifactKind,installation:PublicVerificationSubject):ParsedData<ApplicationWriterPolicy|ApplicationWriterHead>
|
|
export function signedEnvelope(raw:Uint8Array|string,kind:ArtifactKind,installation:PublicVerificationSubject):ParsedData<ApplicationWriterPolicy|ApplicationWriterHead> {
|
|
const data=Buffer.from(raw)
|
|
if(data.length<1||data.length>(kind==='policy'?1048576:8192))throw Error('Signed artifact bound')
|
|
const value=envelope.parse(JSON.parse(new TextDecoder('utf8',{fatal:true}).decode(data)))
|
|
if(!bytes(value).equals(data))throw Error('Canonical signed artifact required')
|
|
if(Buffer.from(value.signature,'base64url').toString('base64url')!==value.signature)throw Error('Canonical signature encoding required')
|
|
const body=(kind==='policy'?policySchema:headSchema).parse(value.body)
|
|
if(body.installationId!==installation.installationId||!verify(null,Buffer.from((kind==='policy'?POLICY_DOMAIN:HEAD_DOMAIN)+canonical(body)),publicKey(installation),Buffer.from(value.signature,'base64url')))throw Error('Installation signature refused')
|
|
return {body,sha256:sha(data)}
|
|
}
|
|
export function liveValidity(policy:ApplicationWriterPolicy,serverNow:number):void {
|
|
const now=Date.now()
|
|
if(!Number.isSafeInteger(serverNow)||Math.abs(serverNow-now)>5000||now<policy.notBefore||now>=policy.notAfter||serverNow<policy.notBefore||serverNow>=policy.notAfter)throw Error('Current signed policy interval/clock refused')
|
|
}
|