deadbyte-mcp 0.15.68 → 0.15.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MANIFEST.SHA256 +81 -38
- package/PROVENANCE.json +35 -23
- package/README.txt +44 -31
- package/RELEASE.json +30 -17
- package/SBOM.cdx.json +2 -2
- package/TEST-MATRIX.json +25 -2
- package/bridge/package.json +1 -1
- package/controller/deadbyte-controller.ps1 +65 -1
- package/controller/deadbyte-process-policy.json +4 -4
- package/docs/R184-package-lock.json +97 -0
- package/docs/R186-OWNER-CONSENT-HANDOFF.txt +87 -0
- package/package.json +1 -1
- package/scripts/install-r186-owner-trust.ps1 +59 -0
- package/scripts/r175-focused-runner.mjs +38 -0
- package/scripts/r176-log-preview.mjs +18 -0
- package/scripts/r177-owner-trust-inventory.ps1 +36 -0
- package/scripts/r178-full-regression.mjs +21 -0
- package/scripts/r178-npm-ci.mjs +10 -0
- package/scripts/r178-npm-preflight.mjs +16 -0
- package/scripts/r178-release-gates-readonly.mjs +9 -0
- package/scripts/r178-shard-regression.mjs +25 -0
- package/scripts/r179-predecessor-registry.mjs +24 -0
- package/scripts/r179-protected-inventory.ps1 +21 -0
- package/scripts/r186-focused-runner.mjs +22 -0
- package/scripts/r186-owner-anchor-inspect.mjs +6 -0
- package/scripts/r186-owner-consent-check.mjs +86 -0
- package/scripts/r186-owner-grant-cli.mjs +45 -0
- package/scripts/r186-owner-key-local.mjs +22 -0
- package/scripts/r186-owner-review-export.mjs +34 -0
- package/src/deadbyte-cli.mjs +69 -56
- package/src/r174-owner-webauthn.mjs +176 -0
- package/src/r175-foreground-boundary.mjs +28 -0
- package/src/r186-owner-anchor.mjs +12 -0
- package/src/r186-owner-consent-preflight.mjs +63 -0
- package/src/r186-owner-grant-broker.mjs +96 -0
- package/src/r186-owner-local-signing.mjs +25 -0
- package/src/r186-owner-request.mjs +40 -0
- package/src/r186-owner-review-export.mjs +49 -0
- package/src/remote-live-log.mjs +81 -5
- package/src/remote-session-guardian.mjs +23 -0
- package/src/version.mjs +1 -1
- package/test/helpers/r175-active-pointer.mjs +28 -0
- package/test/helpers/release-lock.mjs +12 -0
- package/test/r129-sdk-security-baseline.test.mjs +2 -1
- package/test/r130-release-security-contract.test.mjs +11 -10
- package/test/r132-release-identity.test.mjs +12 -11
- package/test/r153-prepublish-identity.test.mjs +1 -1
- package/test/r155-default-live-log.test.mjs +17 -16
- package/test/r160-package-decision-manifest.test.mjs +1 -1
- package/test/r160-release-contract.test.mjs +4 -5
- package/test/r163-isolated-slot-rollback-restore.test.mjs +6 -6
- package/test/r165-live-cli-skew.test.mjs +6 -6
- package/test/r167-release-contract.test.mjs +15 -13
- package/test/r168-protected-entrypoint.test.mjs +5 -2
- package/test/r168-release-contract.test.mjs +4 -3
- package/test/r168-remote-one-shot-coherence.test.mjs +2 -1
- package/test/r172-release-contract.test.mjs +7 -7
- package/test/r173-release-contract.test.mjs +18 -17
- package/test/r174-owner-webauthn.test.mjs +144 -0
- package/test/r175-cli-flags.test.mjs +23 -0
- package/test/r175-foreground-boundary.test.mjs +87 -0
- package/test/r175-guardian-owner-boundary.test.mjs +56 -0
- package/test/r176-pretty-live-log.test.mjs +54 -0
- package/test/r177-windows-absolute-live-path.test.mjs +37 -0
- package/test/r179-absolute-path-live-log.test.mjs +40 -0
- package/test/r179-release-contract.test.mjs +62 -0
- package/test/r186-controller-owner-grant.test.mjs +20 -0
- package/test/r186-owner-consent-preflight.test.mjs +98 -0
- package/test/r186-owner-enrollment.test.mjs +20 -0
- package/test/r186-owner-grant-broker.test.mjs +85 -0
- package/test/r186-owner-local-signing.test.mjs +30 -0
- package/test/r186-owner-request.test.mjs +28 -0
- package/test/r186-owner-review-export.test.mjs +52 -0
- package/test/r48-release-contract.test.mjs +2 -1
- package/test/r51-runtime-coherence.test.mjs +4 -2
- package/test/r58-cleanup-retention.test.mjs +5 -12
- package/test/r58-concurrent-recovery.test.mjs +5 -1
- package/test/r58-stale-owner.test.mjs +9 -3
- package/test/r59-tool-surface-compat.test.mjs +1 -1
- package/test/remote-live-log-r37.test.mjs +2 -2
- package/test/remote-live-log-recovery-r36.test.mjs +1 -1
- package/test/remote-session-cli.test.mjs +92 -126
- package/node_modules/which/CHANGELOG.md +0 -166
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// R174 pure WebAuthn owner assertion verifier.
|
|
2
|
+
//
|
|
3
|
+
// This module does not enroll a credential, solicit owner consent, consume a
|
|
4
|
+
// challenge, write an authority epoch, or grant autonomous access. Those are
|
|
5
|
+
// separate protected-host boundaries and MUST NOT be supplied by an agent.
|
|
6
|
+
import {createHash,createPublicKey,verify,timingSafeEqual} from 'node:crypto';
|
|
7
|
+
import {lstat,realpath,open} from 'node:fs/promises';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import {canonicalJson} from './canonical-json.mjs';
|
|
10
|
+
|
|
11
|
+
const HEX64=/^[0-9a-f]{64}$/;
|
|
12
|
+
const B64URL=/^[A-Za-z0-9_-]+$/;
|
|
13
|
+
const sha256=x=>createHash('sha256').update(x).digest();
|
|
14
|
+
const canonicalHash=x=>sha256(Buffer.from(canonicalJson(x),'utf8')).toString('hex');
|
|
15
|
+
function requireRecord(x,label){
|
|
16
|
+
if(!x||typeof x!=='object'||Array.isArray(x))throw new Error('R174 '+label+' invalid');
|
|
17
|
+
return x;
|
|
18
|
+
}
|
|
19
|
+
function keys(x,names,label){
|
|
20
|
+
requireRecord(x,label);
|
|
21
|
+
if(Object.keys(x).length!==names.length||names.some(n=>!Object.hasOwn(x,n)))
|
|
22
|
+
throw new Error('R174 '+label+' fields invalid');
|
|
23
|
+
}
|
|
24
|
+
function base64url(value,label,minimum=1,maximum=16384){
|
|
25
|
+
if(typeof value!=='string'||value.length>maximum*2||!B64URL.test(value))
|
|
26
|
+
throw new Error('R174 '+label+' encoding invalid');
|
|
27
|
+
const decoded=Buffer.from(value,'base64url');
|
|
28
|
+
if(decoded.length<minimum||decoded.length>maximum||decoded.toString('base64url')!==value)
|
|
29
|
+
throw new Error('R174 '+label+' encoding invalid');
|
|
30
|
+
return decoded;
|
|
31
|
+
}
|
|
32
|
+
function matches(a,b){return a.length===b.length&&timingSafeEqual(a,b);}
|
|
33
|
+
function instant(value){
|
|
34
|
+
if(typeof value!=='string'||!/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/.test(value))
|
|
35
|
+
throw new Error('R174 transaction UTC invalid');
|
|
36
|
+
const timestamp=Date.parse(value);
|
|
37
|
+
if(!Number.isFinite(timestamp)||new Date(timestamp).toISOString()!==value)
|
|
38
|
+
throw new Error('R174 transaction UTC invalid');
|
|
39
|
+
return timestamp;
|
|
40
|
+
}
|
|
41
|
+
function validateSubject(s){
|
|
42
|
+
keys(s,['schema','release_manifest_sha256','coding_policy_sha256',
|
|
43
|
+
'autonomous_policy_sha256','root_id','project_root','effect','max_seconds'],'subject');
|
|
44
|
+
if(s.schema!=='deadbyte.r174-autonomous-owner-subject.v1'||
|
|
45
|
+
[s.release_manifest_sha256,s.coding_policy_sha256,s.autonomous_policy_sha256]
|
|
46
|
+
.some(x=>typeof x!=='string'||!HEX64.test(x))||
|
|
47
|
+
s.root_id!=='core'||typeof s.project_root!=='string'||
|
|
48
|
+
!path.isAbsolute(s.project_root)||s.effect!=='autonomous_goal_run'||
|
|
49
|
+
!Number.isSafeInteger(s.max_seconds)||s.max_seconds<1||s.max_seconds>3600)
|
|
50
|
+
throw new Error('R174 subject scope invalid');
|
|
51
|
+
}
|
|
52
|
+
export async function verifyR174OwnerAssertion({credential,subject,transaction,assertion,now}={}){
|
|
53
|
+
validateSubject(subject);
|
|
54
|
+
keys(transaction,['schema','challenge_b64url','subject_sha256','issued_at_utc','expires_at_utc'],'challenge');
|
|
55
|
+
if(transaction.schema!=='deadbyte.r174-owner-challenge.v1'||!HEX64.test(transaction.subject_sha256))
|
|
56
|
+
throw new Error('R174 challenge schema invalid');
|
|
57
|
+
const challenge=base64url(transaction.challenge_b64url,'challenge',32,32);
|
|
58
|
+
if(canonicalHash(subject)!==transaction.subject_sha256)
|
|
59
|
+
throw new Error('R174 subject hash mismatch');
|
|
60
|
+
const issued=instant(transaction.issued_at_utc),expires=instant(transaction.expires_at_utc);
|
|
61
|
+
if(expires<=issued||expires-issued>300000)
|
|
62
|
+
throw new Error('R174 challenge lifetime exceeds five minutes');
|
|
63
|
+
if(!(now instanceof Date)||!Number.isFinite(now.getTime())||
|
|
64
|
+
now.getTime()<issued||now.getTime()>=expires)
|
|
65
|
+
throw new Error('R174 challenge expired or not yet valid');
|
|
66
|
+
keys(credential,['schema','credential_id_b64url','rp_id','origin',
|
|
67
|
+
'public_key_spki_pem','algorithm','sign_count'],'credential');
|
|
68
|
+
if(credential.schema!=='deadbyte.r174-owner-credential.v1'||credential.algorithm!=='ES256'||
|
|
69
|
+
typeof credential.rp_id!=='string'||!credential.rp_id||typeof credential.origin!=='string'||
|
|
70
|
+
!Number.isSafeInteger(credential.sign_count)||credential.sign_count<0)
|
|
71
|
+
throw new Error('R174 credential identity invalid');
|
|
72
|
+
if(credential.origin!=='https://'+credential.rp_id)
|
|
73
|
+
throw new Error('R174 RP ID and origin binding mismatch');
|
|
74
|
+
const credentialId=base64url(credential.credential_id_b64url,'credential id',16,1024);
|
|
75
|
+
keys(assertion,['credential_id_b64url','client_data_json_b64url',
|
|
76
|
+
'authenticator_data_b64url','signature_b64url'],'assertion');
|
|
77
|
+
if(!matches(credentialId,base64url(assertion.credential_id_b64url,'credential id',16,1024)))
|
|
78
|
+
throw new Error('R174 credential id mismatch');
|
|
79
|
+
const rawClient=base64url(assertion.client_data_json_b64url,'client data',16,16384);
|
|
80
|
+
let client;
|
|
81
|
+
try{client=JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(rawClient));}
|
|
82
|
+
catch{throw new Error('R174 client data JSON invalid');}
|
|
83
|
+
if(!client||typeof client!=='object'||Array.isArray(client)||client.type!=='webauthn.get')
|
|
84
|
+
throw new Error('R174 client data type invalid');
|
|
85
|
+
if(client.challenge!==transaction.challenge_b64url||
|
|
86
|
+
!matches(base64url(client.challenge,'challenge',32,32),challenge))
|
|
87
|
+
throw new Error('R174 client challenge mismatch');
|
|
88
|
+
if(client.origin!==credential.origin)throw new Error('R174 client origin mismatch');
|
|
89
|
+
if(client.crossOrigin===true||client.topOrigin!==undefined)
|
|
90
|
+
throw new Error('R174 cross-origin assertion denied');
|
|
91
|
+
const authenticator=base64url(assertion.authenticator_data_b64url,'authenticator data',37,16384);
|
|
92
|
+
const rpIdHash=sha256(Buffer.from(credential.rp_id,'utf8'));
|
|
93
|
+
if(!matches(authenticator.subarray(0,32),rpIdHash))
|
|
94
|
+
throw new Error('R174 RP ID hash mismatch');
|
|
95
|
+
const flags=authenticator[32];
|
|
96
|
+
if((flags&0x01)!==0x01)throw new Error('R174 user presence required');
|
|
97
|
+
if((flags&0x04)!==0x04)throw new Error('R174 user verification required');
|
|
98
|
+
const signCount=authenticator.readUInt32BE(33);
|
|
99
|
+
// Zero counters are common with platform authenticators. Replay resistance
|
|
100
|
+
// for such devices MUST be enforced by a one-time protected challenge journal.
|
|
101
|
+
if(credential.sign_count>0&&signCount<=credential.sign_count)
|
|
102
|
+
throw new Error('R174 sign counter rollback');
|
|
103
|
+
let key;
|
|
104
|
+
try{key=createPublicKey(credential.public_key_spki_pem);}
|
|
105
|
+
catch{throw new Error('R174 owner key invalid');}
|
|
106
|
+
if(key.asymmetricKeyType!=='ec'||key.asymmetricKeyDetails?.namedCurve!=='prime256v1')
|
|
107
|
+
throw new Error('R174 owner key must be ES256 P-256');
|
|
108
|
+
const signed=Buffer.concat([authenticator,sha256(rawClient)]);
|
|
109
|
+
const signature=base64url(assertion.signature_b64url,'signature',8,256);
|
|
110
|
+
if(!verify('sha256',signed,key,signature))
|
|
111
|
+
throw new Error('R174 signature verification failed');
|
|
112
|
+
return Object.freeze({status:'verified',subject_sha256:transaction.subject_sha256,
|
|
113
|
+
challenge_b64url:transaction.challenge_b64url,credential_id_b64url:credential.credential_id_b64url,
|
|
114
|
+
sign_count:signCount,user_verified:true,
|
|
115
|
+
// NOT an authority grant. Protected challenge consumption still mandatory.
|
|
116
|
+
authority_granted:false});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// The protected-host use-once boundary is intentionally separate from signature verification.
|
|
120
|
+
export async function acceptR174OwnerProof({ledgerRoot,ownerTrust,...inputs}={}){
|
|
121
|
+
// A trusted caller MUST load ownerTrust from an independently pinned
|
|
122
|
+
// owner-controlled trust store, not copy it from request JSON. This
|
|
123
|
+
// primitive deliberately does not authenticate its caller or issue grants.
|
|
124
|
+
keys(ownerTrust,['schema','credential_id_b64url','public_key_spki_sha256',
|
|
125
|
+
'rp_id','origin','subject_sha256'],'owner trust');
|
|
126
|
+
if(ownerTrust.schema!=='deadbyte.r174-owner-trust-binding.v1'||
|
|
127
|
+
typeof ownerTrust.public_key_spki_sha256!=='string'||
|
|
128
|
+
!HEX64.test(ownerTrust.public_key_spki_sha256)||
|
|
129
|
+
typeof ownerTrust.subject_sha256!=='string'||
|
|
130
|
+
!HEX64.test(ownerTrust.subject_sha256))
|
|
131
|
+
throw new Error('R174 owner trust metadata invalid');
|
|
132
|
+
const {credential,transaction,subject}=inputs;
|
|
133
|
+
const credentialKey=createPublicKey(credential?.public_key_spki_pem??'');
|
|
134
|
+
const keySha=sha256(credentialKey.export({type:'spki',format:'der'})).toString('hex');
|
|
135
|
+
if(keySha!==ownerTrust.public_key_spki_sha256 ||
|
|
136
|
+
ownerTrust.credential_id_b64url!==credential?.credential_id_b64url ||
|
|
137
|
+
ownerTrust.rp_id!==credential?.rp_id ||
|
|
138
|
+
ownerTrust.origin!==credential?.origin ||
|
|
139
|
+
ownerTrust.subject_sha256!==transaction?.subject_sha256 ||
|
|
140
|
+
ownerTrust.subject_sha256!==canonicalHash(subject))
|
|
141
|
+
throw new Error('R174 owner trust credential or reviewed subject binding mismatch');
|
|
142
|
+
// Then check signed owner assertion. Invalid assertions must never
|
|
143
|
+
// consume a challenge or produce a receipt.
|
|
144
|
+
const proof=await verifyR174OwnerAssertion(inputs);
|
|
145
|
+
if(typeof ledgerRoot!=='string'||!path.isAbsolute(ledgerRoot))
|
|
146
|
+
throw new Error('R174 replay ledger root must be absolute');
|
|
147
|
+
const dir=await lstat(ledgerRoot);
|
|
148
|
+
if(!dir.isDirectory()||dir.isSymbolicLink()||
|
|
149
|
+
path.resolve(await realpath(ledgerRoot)).toLowerCase()!==path.resolve(ledgerRoot).toLowerCase())
|
|
150
|
+
throw new Error('R174 replay ledger must be a real directory');
|
|
151
|
+
// Caller must independently attest protected ACLs and owner-only trust.
|
|
152
|
+
// This primitive does NOT establish those conditions or grant authority.
|
|
153
|
+
// A challenge is globally one-use within this protected journal regardless
|
|
154
|
+
// of owner credential or scoped subject. Identity is checked by the signature
|
|
155
|
+
// verifier; it must not widen the replay key.
|
|
156
|
+
const id=sha256(Buffer.concat([
|
|
157
|
+
Buffer.from('DEADBYTE-R174-OWNER-CHALLENGE-CONSUMPTION-v1','utf8'),
|
|
158
|
+
base64url(proof.challenge_b64url,'challenge',32,32)
|
|
159
|
+
])).toString('hex');
|
|
160
|
+
const file=path.join(ledgerRoot,'used-'+id+'.json');
|
|
161
|
+
const receipt={schema:'deadbyte.r174-consumed-owner-challenge.v1',
|
|
162
|
+
id,subject_sha256:proof.subject_sha256,credential_id_b64url:proof.credential_id_b64url,
|
|
163
|
+
challenge_b64url:proof.challenge_b64url,sign_count:proof.sign_count,
|
|
164
|
+
authority_granted:false,consumed_at_utc:new Date().toISOString()};
|
|
165
|
+
let handle;
|
|
166
|
+
try{handle=await open(file,'wx',0o600);}
|
|
167
|
+
catch(error){
|
|
168
|
+
if(error?.code==='EEXIST')throw new Error('R174 owner challenge replay denied');
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
try{
|
|
172
|
+
await handle.writeFile(JSON.stringify(receipt)+'\n');
|
|
173
|
+
await handle.sync();
|
|
174
|
+
}finally{await handle.close();}
|
|
175
|
+
return Object.freeze({...proof,status:'consumed',id,authority_granted:false});
|
|
176
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// R175: foreground log sessions never own operator standing grants.
|
|
2
|
+
import {AUTHORITY_PLANES} from './remote-authority.mjs';
|
|
3
|
+
export function observeOnlyAuthority(snapshot){
|
|
4
|
+
const expected={};
|
|
5
|
+
if(snapshot?.state!=='ONLINE')throw new Error('R175 foreground requires ONLINE runtime');
|
|
6
|
+
for(const plane of AUTHORITY_PLANES){
|
|
7
|
+
const observed=snapshot.authority?.[plane];
|
|
8
|
+
if(!observed||typeof observed.armed!=='boolean'||typeof observed.configured!=='boolean')
|
|
9
|
+
throw new Error('R175 foreground authority observation invalid: '+plane);
|
|
10
|
+
expected[plane]=observed.armed;
|
|
11
|
+
}
|
|
12
|
+
return Object.freeze({authority_scope:'observe-only',expected:Object.freeze(expected),
|
|
13
|
+
commands:Object.freeze([])});
|
|
14
|
+
}
|
|
15
|
+
export function assertObserveOnlyMarker(marker){
|
|
16
|
+
return marker?.schema==='deadbyte.remote-session.v2'&&
|
|
17
|
+
marker.authority_scope==='observe-only'&&
|
|
18
|
+
Number.isInteger(marker.owner_pid)&&marker.owner_pid>0&&
|
|
19
|
+
typeof marker.session_nonce==='string'&&/^[0-9a-f]{64}$/.test(marker.session_nonce);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function assertMatchingForegroundRelease(active,{version,manifest_sha256}={}){
|
|
23
|
+
if(!active||typeof version!=='string'||!/^[0-9]+\.[0-9]+\.[0-9]+$/.test(version)||
|
|
24
|
+
typeof manifest_sha256!=='string'||!/^[0-9a-f]{64}$/.test(manifest_sha256)||
|
|
25
|
+
active.version!==version||active.manifest_sha256!==manifest_sha256)
|
|
26
|
+
throw new Error('R175_EXACT_ACTIVE_RELEASE_REQUIRED: observation session will not activate, downgrade or upgrade runtime');
|
|
27
|
+
return active;
|
|
28
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {createHash,createPublicKey} from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export function createR186OwnerAnchor(publicKeyPem){
|
|
4
|
+
if(typeof publicKeyPem!=='string'||publicKeyPem.length<32||publicKeyPem.length>8192||/PRIVATE KEY/.test(publicKeyPem))
|
|
5
|
+
throw new Error('R186 owner enrollment requires a public PEM only');
|
|
6
|
+
let key;try{key=createPublicKey(publicKeyPem);}catch{throw new Error('R186 owner public key invalid');}
|
|
7
|
+
if(key.asymmetricKeyType!=='ed25519')throw new Error('R186 owner public key must be Ed25519');
|
|
8
|
+
const canonicalPem=key.export({type:'spki',format:'pem'}).toString();
|
|
9
|
+
const owner_key_id=createHash('sha256').update(key.export({type:'spki',format:'der'})).digest('hex');
|
|
10
|
+
return Object.freeze({anchor:Object.freeze({schema:'deadbyte.r186-owner-anchor.v1',owner_key_id}),
|
|
11
|
+
public_key_pem:canonicalPem});
|
|
12
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// R186: only a read-only verifier for independently enrolled owner consent.
|
|
2
|
+
// A successful result MUST NOT be treated as an Autonomous grant. The caller
|
|
3
|
+
// must separately prove protected enrollment, single-use consumption and epoch.
|
|
4
|
+
import {createHash,createPublicKey} from 'node:crypto';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {verifyProjectOwnerApproval} from './r70-owner-approval.mjs';
|
|
7
|
+
|
|
8
|
+
const SHA=/^[0-9a-f]{64}$/;
|
|
9
|
+
function exact(value,fields,label){
|
|
10
|
+
if(!value||typeof value!=='object'||Array.isArray(value)||
|
|
11
|
+
Object.keys(value).length!==fields.length||
|
|
12
|
+
fields.some(field=>!Object.hasOwn(value,field)))
|
|
13
|
+
throw new Error('R186 '+label+' invalid');
|
|
14
|
+
}
|
|
15
|
+
export async function verifyR186OwnerConsent({ownerTrust,ownerPublicKeyPem,approval,expected,now=new Date()}={}){
|
|
16
|
+
exact(ownerTrust,['schema','owner_key_id'],'owner anchor');
|
|
17
|
+
if(ownerTrust.schema!=='deadbyte.r186-owner-anchor.v1'||
|
|
18
|
+
typeof ownerTrust.owner_key_id!=='string'||!SHA.test(ownerTrust.owner_key_id))
|
|
19
|
+
throw new Error('R186 owner anchor schema or fingerprint invalid');
|
|
20
|
+
if(typeof ownerPublicKeyPem!=='string'||ownerPublicKeyPem.length>8192)
|
|
21
|
+
throw new Error('R186 owner anchor public key invalid');
|
|
22
|
+
let pub;
|
|
23
|
+
try{pub=createPublicKey(ownerPublicKeyPem);}catch{throw new Error('R186 owner anchor public key invalid');}
|
|
24
|
+
if(pub.asymmetricKeyType!=='ed25519')throw new Error('R186 owner anchor must be Ed25519');
|
|
25
|
+
const keySha=createHash('sha256').update(pub.export({type:'spki',format:'der'})).digest('hex');
|
|
26
|
+
if(ownerTrust.owner_key_id!==keySha)
|
|
27
|
+
throw new Error('R186 owner anchor public key mismatch');
|
|
28
|
+
const approved=await verifyProjectOwnerApproval({approval,ownerPublicKey:pub,expected,now});
|
|
29
|
+
return Object.freeze({status:'verified_readonly',authority_granted:false,
|
|
30
|
+
owner_key_id:keySha,subject_sha256:approved.subject_sha256,
|
|
31
|
+
root_id:approved.root_id,project_root:approved.project_root,
|
|
32
|
+
expires_at_utc:approved.expires_at_utc,
|
|
33
|
+
next_gate:'PROTECTED_OWNER_ENROLLMENT_AND_SINGLE_USE_EPOCH_ISSUANCE_REQUIRED'});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function verifyR186PolicyRootBinding({
|
|
37
|
+
codingPolicy,autonomousPolicy,rootId,projectRoot,codingPolicySha256
|
|
38
|
+
}={}){
|
|
39
|
+
if(!codingPolicy||typeof codingPolicy!=='object'||
|
|
40
|
+
codingPolicy.schema!=='deadbyte.coding-policy.v1'||codingPolicy.enabled!==true)
|
|
41
|
+
throw new Error('R186 coding policy schema invalid');
|
|
42
|
+
if(!autonomousPolicy||typeof autonomousPolicy!=='object'||
|
|
43
|
+
autonomousPolicy.schema!=='deadbyte.autonomous-policy.v1'||autonomousPolicy.enabled!==true)
|
|
44
|
+
throw new Error('R186 autonomous policy schema invalid');
|
|
45
|
+
if(typeof rootId!=='string'||!/^[A-Za-z0-9._-]{1,64}$/.test(rootId)||
|
|
46
|
+
!Array.isArray(autonomousPolicy.root_ids)||
|
|
47
|
+
autonomousPolicy.root_ids.length!==1||autonomousPolicy.root_ids[0]!==rootId)
|
|
48
|
+
throw new Error('R186 Autonomous root vector exceeds owner-reviewed scope');
|
|
49
|
+
if(typeof codingPolicySha256!=='string'||!SHA.test(codingPolicySha256)||
|
|
50
|
+
autonomousPolicy.coding_policy_sha256!==codingPolicySha256)
|
|
51
|
+
throw new Error('R186 coding-policy digest binding mismatch');
|
|
52
|
+
const bound=codingPolicy.roots?.[rootId]?.path;
|
|
53
|
+
if(typeof bound!=='string'||typeof projectRoot!=='string'||
|
|
54
|
+
!path.win32.isAbsolute(bound)||!path.win32.isAbsolute(projectRoot)||
|
|
55
|
+
bound.startsWith('\\\\')||projectRoot.startsWith('\\\\'))
|
|
56
|
+
throw new Error('R186 coding project root is not a real absolute drive path');
|
|
57
|
+
const actual=path.win32.normalize(bound).toLowerCase();
|
|
58
|
+
const requested=path.win32.normalize(projectRoot).toLowerCase();
|
|
59
|
+
if(actual!==requested||
|
|
60
|
+
actual===path.win32.parse(actual).root.toLowerCase())
|
|
61
|
+
throw new Error('R186 reviewed root must match selected Coding project root');
|
|
62
|
+
return Object.freeze({root_id:rootId,project_root:bound});
|
|
63
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Single-use R186 Autonomous grant issuance. This module does not enroll owner
|
|
2
|
+
// keys and accepts no caller-selected authority scope: the policy, reviewed
|
|
3
|
+
// manifest and independently protected owner anchor are the complete subject.
|
|
4
|
+
import {createHash,createPrivateKey,createPublicKey,randomBytes,sign,verify} from 'node:crypto';
|
|
5
|
+
import {mkdir,open,readFile,writeFile} from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import {canonicalJson} from './canonical-json.mjs';
|
|
8
|
+
import {createAuthorityEpochStore} from './authority-continuity-r65.mjs';
|
|
9
|
+
import {verifyR186OwnerConsent,verifyR186PolicyRootBinding} from './r186-owner-consent-preflight.mjs';
|
|
10
|
+
|
|
11
|
+
const SHA=/^[0-9a-f]{64}$/;
|
|
12
|
+
const TERMINAL_DOMAIN=Buffer.from('DEADBYTE-R186-OWNER-GRANT-TERMINAL\0','utf8');
|
|
13
|
+
const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
14
|
+
const bytes=value=>Buffer.from(canonicalJson(value),'utf8');
|
|
15
|
+
async function writeExclusive(file,data){
|
|
16
|
+
await mkdir(path.dirname(file),{recursive:true});
|
|
17
|
+
const handle=await open(file,'wx',0o600);
|
|
18
|
+
try{await handle.writeFile(data);await handle.sync();}
|
|
19
|
+
finally{await handle.close();}
|
|
20
|
+
}
|
|
21
|
+
function insist(condition,message){if(!condition)throw new Error(`R186 owner grant blocked: ${message}`);}
|
|
22
|
+
async function signTerminal(runtimeDir,body){
|
|
23
|
+
const trustDir=path.join(path.dirname(path.resolve(runtimeDir)),'trust');
|
|
24
|
+
const [privatePem,publicPem]=await Promise.all([
|
|
25
|
+
readFile(path.join(trustDir,'ed25519-private.pem')),readFile(path.join(trustDir,'ed25519-public.pem'))]);
|
|
26
|
+
const privateKey=createPrivateKey(privatePem),publicKey=createPublicKey(publicPem);
|
|
27
|
+
insist(privateKey.asymmetricKeyType==='ed25519'&&publicKey.asymmetricKeyType==='ed25519','machine receipt key type invalid');
|
|
28
|
+
const privateKeyId=sha(createPublicKey(privateKey).export({type:'spki',format:'der'}));
|
|
29
|
+
const publicKeyId=sha(publicKey.export({type:'spki',format:'der'}));
|
|
30
|
+
insist(privateKeyId===publicKeyId,'machine receipt key pair mismatch');
|
|
31
|
+
const signature=sign(null,Buffer.concat([TERMINAL_DOMAIN,bytes(body)]),privateKey);
|
|
32
|
+
insist(verify(null,Buffer.concat([TERMINAL_DOMAIN,bytes(body)]),publicKey,signature),'machine terminal signature self-check failed');
|
|
33
|
+
return Object.freeze({algorithm:'ed25519',domain:'DEADBYTE-R186-OWNER-GRANT-TERMINAL',key_id:publicKeyId,signature_base64:signature.toString('base64')});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function issueR186AutonomousGrant({
|
|
37
|
+
runtimeDir,grantFile,codingPolicy,codingPolicyBytes,autonomousPolicy,autonomousPolicyBytes,
|
|
38
|
+
releaseManifestBytes,ownerTrust,ownerPublicKeyPem,approval,now=new Date(),epochStore=null
|
|
39
|
+
}={}){
|
|
40
|
+
insist(typeof runtimeDir==='string'&&path.isAbsolute(runtimeDir),'runtime directory invalid');
|
|
41
|
+
insist(typeof grantFile==='string'&&path.isAbsolute(grantFile),'grant path invalid');
|
|
42
|
+
for(const [name,value] of [['Coding policy',codingPolicyBytes],['Autonomous policy',autonomousPolicyBytes],['release manifest',releaseManifestBytes]])
|
|
43
|
+
insist(Buffer.isBuffer(value)&&value.length>0,`${name} bytes invalid`);
|
|
44
|
+
insist(autonomousPolicy?.schema==='deadbyte.autonomous-policy.v1'&&autonomousPolicy.enabled===true,'Autonomous policy invalid');
|
|
45
|
+
insist(path.resolve(autonomousPolicy.lease_file)===path.resolve(grantFile),'policy grant path mismatch');
|
|
46
|
+
const runtimePrefix=path.resolve(runtimeDir)+path.sep;
|
|
47
|
+
insist(path.resolve(grantFile).startsWith(runtimePrefix),'grant path outside runtime');
|
|
48
|
+
const codingSha=sha(codingPolicyBytes),autonomousSha=sha(autonomousPolicyBytes),manifestSha=sha(releaseManifestBytes);
|
|
49
|
+
const rootId=Array.isArray(autonomousPolicy.root_ids)&&autonomousPolicy.root_ids.length===1?autonomousPolicy.root_ids[0]:null;
|
|
50
|
+
const projectRoot=rootId?codingPolicy?.roots?.[rootId]?.path:null;
|
|
51
|
+
const binding=verifyR186PolicyRootBinding({codingPolicy,autonomousPolicy,rootId,projectRoot,codingPolicySha256:codingSha});
|
|
52
|
+
const consent=await verifyR186OwnerConsent({ownerTrust,ownerPublicKeyPem,approval,now,expected:{
|
|
53
|
+
rootId:binding.root_id,projectRoot:binding.project_root,codingPolicySha256:codingSha,
|
|
54
|
+
autonomousPolicySha256:autonomousSha,reviewedArtifactSha256:manifestSha
|
|
55
|
+
}});
|
|
56
|
+
const evidenceDir=path.join(runtimeDir,'owner-consent','r186');
|
|
57
|
+
const claimPath=path.join(evidenceDir,`${consent.subject_sha256}.claim.json`);
|
|
58
|
+
const terminalPath=path.join(evidenceDir,`${consent.subject_sha256}.terminal.json`);
|
|
59
|
+
try{await readFile(terminalPath);throw new Error('R186 owner approval already consumed; replay denied');}
|
|
60
|
+
catch(error){if(error?.code!=='ENOENT')throw error;}
|
|
61
|
+
const nonce=randomBytes(32).toString('hex');
|
|
62
|
+
const claim={schema:'deadbyte.r186-owner-grant-claim.v1',subject_sha256:consent.subject_sha256,
|
|
63
|
+
owner_key_id:consent.owner_key_id,policy_sha256:autonomousSha,coding_policy_sha256:codingSha,
|
|
64
|
+
release_manifest_sha256:manifestSha,root_id:binding.root_id,project_root:binding.project_root,
|
|
65
|
+
grant_instance_nonce:nonce,claimed_at_utc:now.toISOString()};
|
|
66
|
+
try{await writeExclusive(claimPath,bytes(claim));}
|
|
67
|
+
catch(error){if(error?.code==='EEXIST')throw new Error('R186 owner approval already consumed or in progress; replay denied');throw error;}
|
|
68
|
+
const grant={schema:'deadbyte.autonomous-grant.v2',policy_sha256:autonomousSha,
|
|
69
|
+
coding_policy_sha256:codingSha,instance_nonce:nonce,root_ids:[binding.root_id],
|
|
70
|
+
granted_at_utc:now.toISOString(),expires_at_utc:consent.expires_at_utc};
|
|
71
|
+
const grantBytes=bytes(grant);
|
|
72
|
+
try{await writeExclusive(grantFile,grantBytes);}
|
|
73
|
+
catch(error){throw Object.assign(new Error(`R186 grant write failed after durable claim: ${error?.message??error}`),{outcome:'OUTCOME_UNKNOWN'});}
|
|
74
|
+
const store=epochStore??createAuthorityEpochStore({runtimeDir,policyBytes:autonomousPolicyBytes});
|
|
75
|
+
let head;
|
|
76
|
+
try{head=await store.append({plane:'autonomous',action:'grant',grantBytes,grantInstanceNonce:nonce,updateId:null});}
|
|
77
|
+
catch(error){throw Object.assign(new Error(`R186 epoch issuance failed after grant write: ${error?.message??error}`),{outcome:'OUTCOME_UNKNOWN'});}
|
|
78
|
+
insist(SHA.test(String(head?.event_sha256)),'epoch identity invalid after issuance');
|
|
79
|
+
const verified=await store.verifyGrant({plane:'autonomous',grantBytes,grantInstanceNonce:nonce,policyBytes:autonomousPolicyBytes});
|
|
80
|
+
insist(verified?.trusted===true&&verified.event_sha256===head.event_sha256,'grant epoch read-back failed');
|
|
81
|
+
insist((await readFile(grantFile)).equals(grantBytes),'grant changed after epoch issuance');
|
|
82
|
+
const terminalBody={schema:'deadbyte.r186-owner-grant-terminal.v1',status:'GRANTED',
|
|
83
|
+
subject_sha256:consent.subject_sha256,owner_key_id:consent.owner_key_id,
|
|
84
|
+
claim_sha256:sha(bytes(claim)),grant_sha256:sha(grantBytes),grant_instance_nonce:nonce,
|
|
85
|
+
policy_sha256:autonomousSha,coding_policy_sha256:codingSha,release_manifest_sha256:manifestSha,
|
|
86
|
+
root_id:binding.root_id,project_root:binding.project_root,epoch_event_sha256:head.event_sha256,
|
|
87
|
+
epoch_seq:head.seq,expires_at_utc:consent.expires_at_utc,completed_at_utc:new Date().toISOString()};
|
|
88
|
+
const attestation=await signTerminal(runtimeDir,terminalBody);
|
|
89
|
+
const unsigned={body:terminalBody,attestation};
|
|
90
|
+
const terminal={...unsigned,receipt_sha256:sha(bytes(unsigned))};
|
|
91
|
+
try{await writeExclusive(terminalPath,bytes(terminal));}
|
|
92
|
+
catch(error){throw Object.assign(new Error(`R186 terminal receipt write failed: ${error?.message??error}`),{outcome:'OUTCOME_UNKNOWN'});}
|
|
93
|
+
return Object.freeze({status:'GRANTED',authority_granted:true,subject_sha256:consent.subject_sha256,
|
|
94
|
+
grant_sha256:terminalBody.grant_sha256,epoch_event_sha256:head.event_sha256,epoch_seq:head.seq,
|
|
95
|
+
terminal_path:terminalPath,receipt_sha256:terminal.receipt_sha256});
|
|
96
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Human-invoked local signer. Callers receive approval JSON only; the private
|
|
2
|
+
// key is never returned or embedded in a request/receipt.
|
|
3
|
+
import {createHash,createPrivateKey,createPublicKey,sign} from 'node:crypto';
|
|
4
|
+
import {canonicalJson} from './canonical-json.mjs';
|
|
5
|
+
|
|
6
|
+
const DOMAIN=Buffer.from('DEADBYTE-R70-PROJECT-OWNER-APPROVAL\0','utf8');
|
|
7
|
+
const SHA=/^[0-9a-f]{64}$/;
|
|
8
|
+
const fields=['schema','subject','subject_sha256','owner_signature_present','authority_granted'];
|
|
9
|
+
const hash=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
10
|
+
export function createR186OwnerApproval({request,privateKeyPem}={}){
|
|
11
|
+
if(!request||typeof request!=='object'||Array.isArray(request)||Object.keys(request).length!==fields.length||fields.some(k=>!Object.hasOwn(request,k))||
|
|
12
|
+
request.schema!=='deadbyte.r186-owner-review-request.v1'||request.owner_signature_present!==false||request.authority_granted!==false)
|
|
13
|
+
throw new Error('R186 owner review request invalid');
|
|
14
|
+
const subjectSha=hash(Buffer.from(canonicalJson(request.subject),'utf8'));
|
|
15
|
+
if(!SHA.test(String(request.subject_sha256))||request.subject_sha256!==subjectSha)
|
|
16
|
+
throw new Error('R186 owner review request subject mismatch');
|
|
17
|
+
if(typeof privateKeyPem!=='string'||!/PRIVATE KEY/.test(privateKeyPem))throw new Error('R186 owner private key invalid');
|
|
18
|
+
let privateKey;try{privateKey=createPrivateKey(privateKeyPem);}catch{throw new Error('R186 owner private key invalid');}
|
|
19
|
+
if(privateKey.asymmetricKeyType!=='ed25519')throw new Error('R186 owner private key must be Ed25519');
|
|
20
|
+
const publicKey=createPublicKey(privateKey);
|
|
21
|
+
const owner_key_id=hash(publicKey.export({type:'spki',format:'der'}));
|
|
22
|
+
const signed={schema:'deadbyte.project-owner-approval.r70.v1',subject_sha256:subjectSha,owner_key_id};
|
|
23
|
+
const signature_base64=sign(null,Buffer.concat([DOMAIN,Buffer.from(canonicalJson(signed),'utf8')]),privateKey).toString('base64');
|
|
24
|
+
return Object.freeze({...signed,subject:request.subject,signature_base64});
|
|
25
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Owner-review subject construction only. No keys, signatures or grants.
|
|
2
|
+
import {createHash} from 'node:crypto';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {canonicalJson} from './canonical-json.mjs';
|
|
5
|
+
|
|
6
|
+
const SHA=/^[0-9a-f]{64}$/;
|
|
7
|
+
const ROOT=/^[A-Za-z0-9._-]{1,64}$/;
|
|
8
|
+
export function createR186OwnerReviewRequest({
|
|
9
|
+
rootId,projectRoot,codingPolicySha256,autonomousPolicySha256,
|
|
10
|
+
reviewedArtifactSha256,now=new Date(),durationMs=600000
|
|
11
|
+
}={}){
|
|
12
|
+
if(typeof rootId!=='string'||!ROOT.test(rootId))
|
|
13
|
+
throw new Error('R186 invalid root ID');
|
|
14
|
+
if(typeof projectRoot!=='string'||!path.win32.isAbsolute(projectRoot)||
|
|
15
|
+
projectRoot.startsWith('\\\\'))
|
|
16
|
+
throw new Error('R186 invalid project root');
|
|
17
|
+
for(const [name,value] of [
|
|
18
|
+
['codingPolicySha256',codingPolicySha256],
|
|
19
|
+
['autonomousPolicySha256',autonomousPolicySha256],
|
|
20
|
+
['reviewedArtifactSha256',reviewedArtifactSha256]
|
|
21
|
+
])if(typeof value!=='string'||!SHA.test(value))
|
|
22
|
+
throw new Error('R186 invalid '+name);
|
|
23
|
+
if(!(now instanceof Date)||!Number.isFinite(now.getTime())||
|
|
24
|
+
!Number.isSafeInteger(durationMs)||durationMs<1000||durationMs>3600000)
|
|
25
|
+
throw new Error('R186 invalid bounded consent duration');
|
|
26
|
+
const issued_at_utc=now.toISOString();
|
|
27
|
+
const expires_at_utc=new Date(now.getTime()+durationMs).toISOString();
|
|
28
|
+
const subject=Object.freeze({
|
|
29
|
+
schema:'deadbyte.project-authority-subject.r70.v1',
|
|
30
|
+
scope:'coding_autonomous_native_diff',
|
|
31
|
+
root_id:rootId,project_root:path.win32.normalize(projectRoot),
|
|
32
|
+
coding_policy_sha256:codingPolicySha256,
|
|
33
|
+
autonomous_policy_sha256:autonomousPolicySha256,
|
|
34
|
+
reviewed_artifact_sha256:reviewedArtifactSha256,
|
|
35
|
+
issued_at_utc,expires_at_utc
|
|
36
|
+
});
|
|
37
|
+
const subject_sha256=createHash('sha256').update(Buffer.from(canonicalJson(subject))).digest('hex');
|
|
38
|
+
return Object.freeze({schema:'deadbyte.r186-owner-review-request.v1',
|
|
39
|
+
subject,subject_sha256,owner_signature_present:false,authority_granted:false});
|
|
40
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Read-only export of the exact subject a physical owner may review and sign.
|
|
2
|
+
// This boundary neither reads a private key nor creates/changes authority.
|
|
3
|
+
import {createHash} from 'node:crypto';
|
|
4
|
+
import {lstat,open,readFile} from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {canonicalJson} from './canonical-json.mjs';
|
|
7
|
+
import {verifyR186PolicyRootBinding} from './r186-owner-consent-preflight.mjs';
|
|
8
|
+
import {createR186OwnerReviewRequest} from './r186-owner-request.mjs';
|
|
9
|
+
|
|
10
|
+
const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
11
|
+
async function boundedRegular(file,limit,label){
|
|
12
|
+
const info=await lstat(file);
|
|
13
|
+
if(!info.isFile()||info.isSymbolicLink()||info.size<1||info.size>limit)
|
|
14
|
+
throw new Error(`R186 ${label} is a reparse, symlink or invalid file`);
|
|
15
|
+
return readFile(file);
|
|
16
|
+
}
|
|
17
|
+
async function writeExclusive(file,bytes){
|
|
18
|
+
const parent=await lstat(path.dirname(file));
|
|
19
|
+
if(!parent.isDirectory()||parent.isSymbolicLink())
|
|
20
|
+
throw new Error('R186 review output parent is a reparse or symlink');
|
|
21
|
+
const handle=await open(file,'wx',0o600);
|
|
22
|
+
try{await handle.writeFile(bytes);await handle.sync();}
|
|
23
|
+
finally{await handle.close();}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function writeR186OwnerReviewRequest({
|
|
27
|
+
rootId,projectRoot,codingPolicyPath,autonomousPolicyPath,manifestPath,outputPath,
|
|
28
|
+
now=new Date(),durationMs=600000
|
|
29
|
+
}={}){
|
|
30
|
+
for(const [name,value] of [['project root',projectRoot],['Coding policy',codingPolicyPath],
|
|
31
|
+
['Autonomous policy',autonomousPolicyPath],['manifest',manifestPath],['output',outputPath]]){
|
|
32
|
+
if(typeof value!=='string'||!path.isAbsolute(value))throw new Error(`R186 ${name} path must be absolute`);
|
|
33
|
+
}
|
|
34
|
+
const [codingBytes,autonomousBytes,manifestBytes]=await Promise.all([
|
|
35
|
+
boundedRegular(codingPolicyPath,1024*1024,'Coding policy'),
|
|
36
|
+
boundedRegular(autonomousPolicyPath,1024*1024,'Autonomous policy'),
|
|
37
|
+
boundedRegular(manifestPath,512*1024,'release manifest')]);
|
|
38
|
+
const codingPolicy=JSON.parse(codingBytes.toString('utf8'));
|
|
39
|
+
const autonomousPolicy=JSON.parse(autonomousBytes.toString('utf8'));
|
|
40
|
+
const codingPolicySha256=sha(codingBytes);
|
|
41
|
+
const binding=verifyR186PolicyRootBinding({codingPolicy,autonomousPolicy,rootId,
|
|
42
|
+
projectRoot,codingPolicySha256});
|
|
43
|
+
const request=createR186OwnerReviewRequest({rootId:binding.root_id,
|
|
44
|
+
projectRoot:binding.project_root,codingPolicySha256,
|
|
45
|
+
autonomousPolicySha256:sha(autonomousBytes),reviewedArtifactSha256:sha(manifestBytes),
|
|
46
|
+
now,durationMs});
|
|
47
|
+
await writeExclusive(outputPath,Buffer.from(`${canonicalJson(request)}\n`,'utf8'));
|
|
48
|
+
return request;
|
|
49
|
+
}
|