-
Notifications
You must be signed in to change notification settings - Fork 36
WIP: ARCs, Relays, and Hubs #183
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 5 commits
92b5dc8
67d6edf
91f0342
3a4d87a
b984c1f
3fc4b66
a97e752
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| # npm 12+ defaults allow-git=none. | ||
| # @fabric/core itself has no Fabric git dependencies, but consumers that install | ||
| # core/http/hub from GitHub need `allow-git=all` so nested git dep preparation | ||
| # (commit-SHA fetches) is not refused. Keep this file so the monorepo default | ||
| # matches Hub / http / app packages. | ||
| allow-git=all | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Medium — Supply-chain: re-enables npm 12 git-dependency RCE path
Attack path: Compromised or malicious transitive Same relaxation remains in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Medium — Supply-chain: re-enables npm 12 git-dependency RCE path
Attack path: Compromised or malicious transitive Same relaxation remains in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Medium — Supply-chain: re-enables npm 12 git-dependency RCE path
Attack path: Compromised or malicious transitive Same relaxation remains in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Medium — Supply-chain: re-enables npm 12 git-dependency RCE path
Attack path: Compromised or malicious transitive Same relaxation remains in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Medium — Supply-chain: re-enables npm 12 git-dependency RCE path
Attack path: Compromised or malicious transitive Same relaxation remains in |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| 'use strict'; | ||
|
|
||
| /** | ||
| * Contract-scoped Token capabilities (read-only member vs signer). | ||
| */ | ||
|
|
||
| const Key = require('../types/key'); | ||
| const Token = require('../types/token'); | ||
|
|
||
| const OP_CONTRACT_READ = 'OP_CONTRACT_READ'; | ||
| const OP_CONTRACT_SIGN = 'OP_CONTRACT_SIGN'; | ||
|
|
||
| /** | ||
| * @param {object} opts | ||
| * @param {object|Key} opts.issuerKey Fabric Key (or settings) of issuer | ||
| * @param {string} opts.subject Subject compressed pubkey | ||
| * @param {string} opts.contractId Contract / namespace id | ||
| * @param {string} [opts.capability=OP_CONTRACT_READ] | ||
| * @param {number} [opts.expiresInSeconds] | ||
| * @returns {string} Token.toSignedString() | ||
| */ | ||
| function issueContractCapability (opts = {}) { | ||
| const capability = opts.capability || OP_CONTRACT_READ; | ||
| if (capability !== OP_CONTRACT_READ && capability !== OP_CONTRACT_SIGN) { | ||
| throw new Error(`unsupported contract capability: ${capability}`); | ||
| } | ||
| const contractId = String(opts.contractId || '').trim().toLowerCase(); | ||
| if (!contractId) throw new Error('contractId required'); | ||
| const subject = String(opts.subject || '').trim().toLowerCase(); | ||
| if (!subject) throw new Error('subject required'); | ||
| const issuer = opts.issuerKey && typeof opts.issuerKey.sign === 'function' | ||
| ? opts.issuerKey | ||
| : new Key(opts.issuerKey || {}); | ||
| const token = new Token({ | ||
| capability, | ||
| issuer, | ||
| subject, | ||
| ctx: { contractId } | ||
| }); | ||
| return token.toSignedString({ | ||
| expiresInSeconds: opts.expiresInSeconds, | ||
| ctx: { contractId } | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * @param {string} tokenString | ||
| * @param {object} expect | ||
| * @param {string} expect.contractId | ||
| * @param {string} [expect.expectedCap] | ||
| * @param {string} [expect.subject] | ||
| * @param {Key|object} [expect.issuerKey] Verify Schnorr against this key (auth path) | ||
| * @param {boolean} [expect.allowUnverified] Opt-in parse-only (no sig check); result has verified:false | ||
| * @returns {{ cap: string, iss: string, sub: string, iat: number, exp: number, ctx?: object, verified?: boolean }|null} | ||
| */ | ||
| function verifyContractCapability (tokenString, expect = {}) { | ||
| const contractId = String(expect.contractId || '').trim().toLowerCase(); | ||
| if (!contractId) return null; | ||
| let payload = null; | ||
| let verified = false; | ||
| if (expect.issuerKey) { | ||
| const key = expect.issuerKey && typeof expect.issuerKey.verify === 'function' | ||
| ? expect.issuerKey | ||
| : new Key(expect.issuerKey); | ||
| payload = Token.verifySigned(tokenString, key); | ||
| verified = !!payload; | ||
| } else if (expect.allowUnverified === true) { | ||
| // Parse-only: not authorization. Callers must not treat this as a verified grant. | ||
| if (!tokenString || typeof tokenString !== 'string') return null; | ||
| const parts = tokenString.split('.'); | ||
| if (parts.length !== 2) return null; | ||
| try { | ||
| const { tryParseWireJson } = require('./wireJson'); | ||
| const payloadStr = Token.base64UrlDecode(parts[0]); | ||
| const pr = tryParseWireJson(payloadStr); | ||
| if (!pr.ok) return null; | ||
| payload = pr.value; | ||
| if (!payload || payload.exp == null || Date.now() / 1000 > payload.exp) return null; | ||
| verified = false; | ||
| } catch (_) { | ||
| return null; | ||
| } | ||
| } else { | ||
| return null; | ||
| } | ||
| if (!payload) return null; | ||
| const ctxId = payload.ctx && payload.ctx.contractId | ||
| ? String(payload.ctx.contractId).trim().toLowerCase() | ||
| : ''; | ||
| if (ctxId !== contractId) return null; | ||
| if (expect.expectedCap && payload.cap !== expect.expectedCap) return null; | ||
| if (expect.subject) { | ||
| const sub = String(payload.sub || '').trim().toLowerCase(); | ||
| if (sub !== String(expect.subject).trim().toLowerCase()) return null; | ||
| } | ||
| return Object.assign({}, payload, { verified }); | ||
| } | ||
|
|
||
| function roleToCapability (role) { | ||
| const r = String(role || '').toLowerCase(); | ||
| if (r === 'signer' || r === 'sign') return OP_CONTRACT_SIGN; | ||
| return OP_CONTRACT_READ; | ||
| } | ||
|
|
||
| function capabilityToRole (cap) { | ||
| if (cap === OP_CONTRACT_SIGN) return 'signer'; | ||
| if (cap === OP_CONTRACT_READ) return 'reader'; | ||
| return null; | ||
| } | ||
|
|
||
| module.exports = { | ||
| OP_CONTRACT_READ, | ||
| OP_CONTRACT_SIGN, | ||
| issueContractCapability, | ||
| verifyContractCapability, | ||
| roleToCapability, | ||
| capabilityToRole | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| 'use strict'; | ||
|
|
||
| /** | ||
| * Contract-namespace tip attestation (k-of-n Schnorr). | ||
| * | ||
| * Same witness shape as {@link beaconFederationSigning}: members / validators | ||
| * Schnorr-sign a canonical tip string. Used by GoonCitizen Group Statechain | ||
| * journals today; Hub contract sidechains SHOULD reuse this when sealing | ||
| * per-namespace tips (keep Hub docs / RPC in sync if the tip kind changes). | ||
| * | ||
| * @see functions/beaconFederationSigning.js | ||
| * @see docs/APPLICATION_NAMESPACES.md | ||
| * @see docs/DISTRIBUTED_EXECUTION.md | ||
| */ | ||
|
|
||
| const crypto = require('crypto'); | ||
| const Key = require('../types/key'); | ||
| const fabricCanonicalJson = require('./fabricCanonicalJson'); | ||
| const { verifyFederationWitnessOnMessage } = require('./beaconFederationSigning'); | ||
|
|
||
| /** Canonical tip kind — Hub and apps must agree; bump only with protocol note. */ | ||
| const CONTRACT_STATE_TIP_KIND = 'ContractStateTip'; | ||
|
|
||
| /** | ||
| * UTF-8 string members Schnorr-sign for a contract-namespace tip. | ||
| * @param {object} fields | ||
| * @param {string} fields.contractId | ||
| * @param {number} fields.clock | ||
| * @param {string} fields.stateDigest | ||
| * @returns {string} | ||
| */ | ||
| function signingStringForContractStateTip (fields = {}) { | ||
| return fabricCanonicalJson({ | ||
| version: 1, | ||
| kind: CONTRACT_STATE_TIP_KIND, | ||
| contractId: String(fields.contractId || '').trim().toLowerCase(), | ||
| clock: Number(fields.clock) || 0, | ||
| stateDigest: String(fields.stateDigest || '').trim().toLowerCase() | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * @param {string} contractId | ||
| * @param {number} clock | ||
| * @param {string} stateDigest | ||
| * @returns {Buffer} | ||
| */ | ||
| function tipMessageBuffer (contractId, clock, stateDigest) { | ||
| return Buffer.from(signingStringForContractStateTip({ contractId, clock, stateDigest }), 'utf8'); | ||
| } | ||
|
|
||
| /** | ||
| * Sign a tip with a Fabric Key (or `{ xprv }` / Key-like). | ||
| * @param {object} keyOrSettings Key instance or settings for `new Key(...)` | ||
| * @param {string} contractId | ||
| * @param {number} clock | ||
| * @param {string} stateDigest | ||
| * @returns {{ pubkey: string, signature: string, message: string }} | ||
| */ | ||
| function signContractStateTip (keyOrSettings, contractId, clock, stateDigest) { | ||
| const key = keyOrSettings && typeof keyOrSettings.signSchnorr === 'function' | ||
| ? keyOrSettings | ||
| : new Key(keyOrSettings || {}); | ||
| const message = signingStringForContractStateTip({ contractId, clock, stateDigest }); | ||
| const signature = Buffer.from(key.signSchnorr(Buffer.from(message, 'utf8'))).toString('hex'); | ||
| return { pubkey: key.pubkey, signature, message }; | ||
| } | ||
|
|
||
| /** | ||
| * Verify k-of-n tip signatures (Federation witness shape). | ||
| * @param {string[]} validatorPubkeys | ||
| * @param {number} threshold | ||
| * @param {string} contractId | ||
| * @param {number} clock | ||
| * @param {string} stateDigest | ||
| * @param {{ [pubkey: string]: string }|{ signatures: object }} signaturesOrWitness | ||
| * @returns {boolean} | ||
| */ | ||
| function verifyContractStateTip ( | ||
| validatorPubkeys, | ||
| threshold, | ||
| contractId, | ||
| clock, | ||
| stateDigest, | ||
| signaturesOrWitness | ||
| ) { | ||
| const thr = Number(threshold); | ||
| if (!Number.isInteger(thr) || thr < 1) { | ||
| throw new Error('threshold must be a positive integer'); | ||
| } | ||
| const witness = signaturesOrWitness && signaturesOrWitness.signatures | ||
| && typeof signaturesOrWitness.signatures === 'object' | ||
| ? signaturesOrWitness | ||
| : { signatures: signaturesOrWitness || {} }; | ||
| return verifyFederationWitnessOnMessage( | ||
| tipMessageBuffer(contractId, clock, stateDigest), | ||
| witness, | ||
| validatorPubkeys, | ||
| thr | ||
| ); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| /** | ||
| * Merge signature maps (pubkey → sig hex), last write wins per key. | ||
| * @param {...object} maps | ||
| * @returns {{ [pubkey: string]: string }} | ||
| */ | ||
| function mergeTipSignatures (...maps) { | ||
| const out = {}; | ||
| for (const m of maps) { | ||
| if (!m || typeof m !== 'object') continue; | ||
| const src = m.signatures && typeof m.signatures === 'object' ? m.signatures : m; | ||
| for (const [pk, sig] of Object.entries(src)) { | ||
| if (typeof pk === 'string' && pk && typeof sig === 'string' && sig) { | ||
| out[pk] = sig; | ||
| } | ||
| } | ||
| } | ||
| return out; | ||
| } | ||
|
|
||
| /** | ||
| * Stable digest helper for tests / logging. | ||
| * @param {string} contractId | ||
| * @param {number} clock | ||
| * @param {string} stateDigest | ||
| * @returns {string} hex sha256 of tip message | ||
| */ | ||
| function tipDigestHex (contractId, clock, stateDigest) { | ||
| return crypto.createHash('sha256') | ||
| .update(tipMessageBuffer(contractId, clock, stateDigest)) | ||
| .digest('hex'); | ||
| } | ||
|
|
||
| module.exports = { | ||
| CONTRACT_STATE_TIP_KIND, | ||
| signingStringForContractStateTip, | ||
| tipMessageBuffer, | ||
| signContractStateTip, | ||
| verifyContractStateTip, | ||
| mergeTipSignatures, | ||
| tipDigestHex | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Medium — Supply-chain: re-enables npm 12 git-dependency RCE path
allow-git=allopts this repo (and GitHub installs of it) out of npm 12’s defaultallow-git=none, which was added specifically to block install-time code execution via a git dependency’s.npmrcoverriding the git binary (works even with--ignore-scripts).Attack path: A compromised or malicious transitive dependency introduces a
git+/github:URL → npm fetches it underall→ that git checkout’s.npmrccan redirectgit→ arbitrary code at install time.@fabric/corecurrently declares no git deps, soallis broader than needed;root(or keeping the default and scoping the opt-in to Hub/http only) would preserve the boundary.Same relaxation is also forced in
package.jsonreport:install(npm i --allow-git=allafter wiping the lockfile), which further widens install-time trust during that script.