Skip to content

Commit 4bcb061

Browse files
committed
Add OpenSSF security hardening: pinned deps, SAST, CI tests, auto-versioning
- Pin all GitHub Actions to SHA (checkout, setup-node, setup-python, pypi-publish) - Add Dependabot config for github-actions, npm, pip (weekly) - Add CodeQL SAST workflow for JS and Python - Add auto version-bump workflow (PATCH increment on merge to main) - Add VERSION file as single source of truth (1.0.0) - Sync Python SDK version to 1.0.0 - Add permissions: read-all to CI workflow - Add 25 unit tests for JS SDK (canonicalize, hash, signature, chain, merkle, bundle) - Add 25 unit tests for Python SDK (matching JS test coverage) - Wire tests into CI pipeline
1 parent 7b0d52f commit 4bcb061

10 files changed

Lines changed: 512 additions & 12 deletions

File tree

.github/dependabot.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: "github-actions"
4+
directory: "/"
5+
schedule:
6+
interval: "weekly"
7+
- package-ecosystem: "npm"
8+
directory: "/sdk/js"
9+
schedule:
10+
interval: "weekly"
11+
- package-ecosystem: "pip"
12+
directory: "/sdk/python"
13+
schedule:
14+
interval: "weekly"

.github/workflows/ci.yml

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,21 @@ on:
66
pull_request:
77
branches: [main]
88

9+
permissions: read-all
10+
911
jobs:
1012
sdk-js-check:
1113
name: JS SDK Checks
1214
runs-on: ubuntu-latest
1315
steps:
14-
- uses: actions/checkout@v4
15-
- uses: actions/setup-node@v4
16+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
17+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
1618
with:
1719
node-version: '20'
1820
- name: Verify SDK loads
1921
run: node -e "require('./sdk/js/src/verify.js')"
22+
- name: Run unit tests
23+
run: node sdk/js/test/verify.test.js
2024
- name: Verify types exist
2125
run: test -f sdk/js/src/types.d.ts
2226

@@ -27,14 +31,16 @@ jobs:
2731
matrix:
2832
python-version: ['3.8', '3.10', '3.12']
2933
steps:
30-
- uses: actions/checkout@v4
31-
- uses: actions/setup-python@v5
34+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
35+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
3236
with:
3337
python-version: ${{ matrix.python-version }}
3438
- name: Install SDK
3539
run: cd sdk/python && pip install -e .
3640
- name: Verify import
3741
run: python -c "from openexecution_verify import OpenExecutionVerifier; print('SDK loaded successfully')"
42+
- name: Run unit tests
43+
run: cd sdk/python && python test_verify.py
3844

3945
schema-validate:
4046
name: Schema Validation
@@ -54,7 +60,7 @@ jobs:
5460
--health-timeout 5s
5561
--health-retries 5
5662
steps:
57-
- uses: actions/checkout@v4
63+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
5864
- name: Enable uuid-ossp extension
5965
run: PGPASSWORD=test_password psql -h localhost -U oe_test -d spec_test -c 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp";'
6066
- name: Validate schema-spec.sql

.github/workflows/codeql.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: CodeQL
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
schedule:
9+
- cron: '0 6 * * 1'
10+
11+
permissions: read-all
12+
13+
jobs:
14+
analyze:
15+
name: Analyze
16+
runs-on: ubuntu-latest
17+
permissions:
18+
security-events: write
19+
strategy:
20+
matrix:
21+
language: ['javascript', 'python']
22+
steps:
23+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
24+
- name: Initialize CodeQL
25+
uses: github/codeql-action/init@v3
26+
with:
27+
languages: ${{ matrix.language }}
28+
- name: Autobuild
29+
uses: github/codeql-action/autobuild@v3
30+
- name: Perform CodeQL Analysis
31+
uses: github/codeql-action/analyze@v3

.github/workflows/publish.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ jobs:
1212
contents: read
1313
id-token: write
1414
steps:
15-
- uses: actions/checkout@v4
16-
- uses: actions/setup-node@v4
15+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
16+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
1717
with:
1818
node-version: '20'
1919
registry-url: 'https://registry.npmjs.org'
@@ -28,15 +28,15 @@ jobs:
2828
permissions:
2929
id-token: write
3030
steps:
31-
- uses: actions/checkout@v4
32-
- uses: actions/setup-python@v5
31+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
32+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
3333
with:
3434
python-version: '3.12'
3535
- name: Install build tools
3636
run: pip install build twine
3737
- name: Build
3838
run: cd sdk/python && python -m build
3939
- name: Publish to PyPI
40-
uses: pypa/gh-action-pypi-publish@release/v1
40+
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1
4141
with:
4242
packages-dir: sdk/python/dist/

.github/workflows/version-bump.yml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Auto Version Bump
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths-ignore:
7+
- 'VERSION'
8+
- 'sdk/js/package.json'
9+
- 'sdk/python/setup.py'
10+
11+
permissions:
12+
contents: write
13+
14+
jobs:
15+
bump:
16+
name: Bump Patch Version
17+
runs-on: ubuntu-latest
18+
steps:
19+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
20+
with:
21+
token: ${{ secrets.GITHUB_TOKEN }}
22+
23+
- name: Bump VERSION file
24+
id: bump
25+
run: |
26+
CURRENT=$(cat VERSION | tr -d '[:space:]')
27+
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
28+
PATCH=$((PATCH + 1))
29+
NEW="${MAJOR}.${MINOR}.${PATCH}"
30+
echo "$NEW" > VERSION
31+
echo "version=$NEW" >> "$GITHUB_OUTPUT"
32+
echo "Bumped $CURRENT -> $NEW"
33+
34+
- name: Sync JS SDK version
35+
run: |
36+
cd sdk/js
37+
jq --arg v "${{ steps.bump.outputs.version }}" '.version = $v' package.json > tmp.json && mv tmp.json package.json
38+
39+
- name: Sync Python SDK version
40+
run: |
41+
sed -i "s/version=\"[^\"]*\"/version=\"${{ steps.bump.outputs.version }}\"/" sdk/python/setup.py
42+
43+
- name: Commit and tag
44+
run: |
45+
git config user.name "github-actions[bot]"
46+
git config user.email "github-actions[bot]@users.noreply.github.com"
47+
git add VERSION sdk/js/package.json sdk/python/setup.py
48+
git commit -m "chore: bump version to v${{ steps.bump.outputs.version }}"
49+
git tag "v${{ steps.bump.outputs.version }}"
50+
git push origin main --tags

VERSION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1.0.0

sdk/js/test/verify.test.js

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
'use strict';
2+
3+
const crypto = require('crypto');
4+
const {
5+
OpenExecutionVerifier,
6+
canonicalize,
7+
hash,
8+
verifySignature,
9+
verifyContentIntegrity,
10+
verifyTimestamp,
11+
verifyBlockchain,
12+
HASH_MAP,
13+
SIGNATURE_ALGORITHMS,
14+
} = require('../src/verify.js');
15+
16+
const assert = require('assert');
17+
18+
// ── Canonicalize (JCS / RFC 8785) ──
19+
20+
assert.strictEqual(canonicalize(null), 'null');
21+
assert.strictEqual(canonicalize(42), '42');
22+
assert.strictEqual(canonicalize('hello'), '"hello"');
23+
assert.strictEqual(canonicalize([1, 2]), '[1,2]');
24+
assert.strictEqual(
25+
canonicalize({ b: 2, a: 1 }),
26+
'{"a":1,"b":2}',
27+
'Keys must be sorted'
28+
);
29+
assert.strictEqual(
30+
canonicalize({ b: undefined, a: 1 }),
31+
'{"a":1}',
32+
'Undefined values omitted per RFC 8785'
33+
);
34+
35+
// ── Hash ──
36+
37+
const sha256Hello = hash('hello', 'sha256');
38+
assert.strictEqual(sha256Hello.length, 64);
39+
assert.strictEqual(
40+
sha256Hello,
41+
crypto.createHash('sha256').update('hello').digest('hex')
42+
);
43+
44+
for (const algo of Object.keys(HASH_MAP)) {
45+
const h = hash('test', algo);
46+
assert.ok(h.length > 0, `hash(${algo}) should produce output`);
47+
}
48+
49+
assert.throws(() => hash('x', 'md5'), /Unsupported hash algorithm/);
50+
51+
// ── Signature ──
52+
53+
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
54+
const data = Buffer.from('test-payload');
55+
const sig = crypto.sign(null, data, privateKey).toString('hex');
56+
57+
assert.strictEqual(verifySignature(data, sig, publicKey, 'ed25519'), true);
58+
assert.strictEqual(verifySignature(Buffer.from('wrong'), sig, publicKey, 'ed25519'), false);
59+
assert.strictEqual(verifySignature(data, 'bad', publicKey, 'ed25519'), false);
60+
61+
// ── Chain Integrity ──
62+
63+
function buildChain(n, hashAlgorithm = 'sha256') {
64+
const genesisLen = hashAlgorithm === 'sha256' ? 64 : 128;
65+
let prevHash = '0'.repeat(genesisLen);
66+
const events = [];
67+
for (let i = 1; i <= n; i++) {
68+
const eventData = {
69+
seq: i,
70+
event_type: 'test',
71+
actor_id: 'system',
72+
timestamp: new Date(1700000000000 + i * 1000).toISOString(),
73+
payload: { value: i },
74+
prev_hash: prevHash,
75+
};
76+
const eventHash = hash(canonicalize(eventData), hashAlgorithm);
77+
events.push({
78+
seq: i,
79+
event_type: 'test',
80+
actor_id: 'system',
81+
created_at: eventData.timestamp,
82+
payload: { value: i },
83+
prev_hash: prevHash,
84+
event_hash: eventHash,
85+
});
86+
prevHash = eventHash;
87+
}
88+
return events;
89+
}
90+
91+
const chain3 = buildChain(3);
92+
const result3 = OpenExecutionVerifier.verifyChainIntegrity(chain3);
93+
assert.strictEqual(result3.is_valid, true);
94+
assert.strictEqual(result3.event_count, 3);
95+
assert.strictEqual(result3.errors.length, 0);
96+
97+
// Tampered chain
98+
const tampered = JSON.parse(JSON.stringify(chain3));
99+
tampered[1].event_hash = 'deadbeef'.repeat(8);
100+
const resultTampered = OpenExecutionVerifier.verifyChainIntegrity(tampered);
101+
assert.strictEqual(resultTampered.is_valid, false);
102+
assert.ok(resultTampered.errors.length > 0);
103+
104+
// Empty chain
105+
const resultEmpty = OpenExecutionVerifier.verifyChainIntegrity([]);
106+
assert.strictEqual(resultEmpty.is_valid, true);
107+
assert.strictEqual(resultEmpty.event_count, 0);
108+
109+
// ── Chain Hash ──
110+
111+
const hashes = chain3.map(e => e.event_hash);
112+
const chainHash = OpenExecutionVerifier.computeChainHash(hashes);
113+
assert.strictEqual(chainHash.length, 64);
114+
assert.strictEqual(chainHash, hash(hashes.join(''), 'sha256'));
115+
116+
// ── Content Integrity (Merkle) ──
117+
118+
const leaf1 = hash('data1', 'sha256');
119+
const leaf2 = hash('data2', 'sha256');
120+
const LEAF_PREFIX = Buffer.from([0x00]);
121+
const NODE_PREFIX = Buffer.from([0x01]);
122+
const h1 = crypto.createHash('sha256').update(Buffer.concat([LEAF_PREFIX, Buffer.from(leaf1, 'hex')])).digest();
123+
const h2 = crypto.createHash('sha256').update(Buffer.concat([LEAF_PREFIX, Buffer.from(leaf2, 'hex')])).digest();
124+
const root = crypto.createHash('sha256').update(Buffer.concat([NODE_PREFIX, h1, h2])).digest('hex');
125+
126+
const ciResult = verifyContentIntegrity({
127+
type: 'ContentIntegrity',
128+
root_hash: root,
129+
leaves: [leaf1, leaf2],
130+
});
131+
assert.strictEqual(ciResult.valid, true);
132+
133+
const ciBadRoot = verifyContentIntegrity({
134+
type: 'ContentIntegrity',
135+
root_hash: 'wrong',
136+
leaves: [leaf1, leaf2],
137+
});
138+
assert.strictEqual(ciBadRoot.valid, false);
139+
140+
// Empty leaves
141+
const ciEmpty = verifyContentIntegrity({
142+
type: 'ContentIntegrity',
143+
root_hash: '0'.repeat(64),
144+
leaves: [],
145+
});
146+
assert.strictEqual(ciEmpty.valid, true);
147+
148+
// ── Timestamp ──
149+
150+
const ts = new Date().toISOString();
151+
const tsHash = hash(ts, 'sha256');
152+
const tsResult = verifyTimestamp({ type: 'Timestamp', timestamp: ts, hash: tsHash });
153+
assert.strictEqual(tsResult.valid, true);
154+
assert.strictEqual(tsResult.hash_valid, true);
155+
156+
const tsBad = verifyTimestamp({ type: 'Timestamp', timestamp: ts, hash: 'wrong' });
157+
assert.strictEqual(tsBad.valid, false);
158+
159+
// ── Blockchain ──
160+
161+
const bcResult = verifyBlockchain(
162+
{ type: 'Blockchain', chain_hash: 'abc', tx_hash: '0x123', network: 'ethereum' },
163+
'abc'
164+
);
165+
assert.strictEqual(bcResult.valid, true);
166+
167+
const bcMismatch = verifyBlockchain(
168+
{ type: 'Blockchain', chain_hash: 'abc', tx_hash: '0x123' },
169+
'xyz'
170+
);
171+
assert.strictEqual(bcMismatch.valid, false);
172+
173+
// ── Bundle ──
174+
175+
const chainEvents = buildChain(2);
176+
const eventHashes = chainEvents.map(e => e.event_hash);
177+
const bundleChainHash = OpenExecutionVerifier.computeChainHash(eventHashes);
178+
const cert = { chain_hash: bundleChainHash, scope: 'test' };
179+
const certCanonical = canonicalize(cert);
180+
const certSig = crypto.sign(null, Buffer.from(certCanonical), privateKey).toString('hex');
181+
const pemKey = publicKey.export({ type: 'spki', format: 'pem' });
182+
183+
const bundle = {
184+
certificate: cert,
185+
certificate_signature: certSig,
186+
chain: { events: chainEvents, hash_algorithm: 'sha256', signature_algorithm: 'ed25519', canonicalization: 'jcs' },
187+
public_key: pemKey,
188+
};
189+
const bundleResult = OpenExecutionVerifier.verifyBundle(bundle);
190+
assert.strictEqual(bundleResult.valid, true);
191+
assert.strictEqual(bundleResult.certificate_signature_valid, true);
192+
assert.strictEqual(bundleResult.chain_integrity.is_valid, true);
193+
assert.strictEqual(bundleResult.chain_hash_valid, true);
194+
195+
console.log('All 25 tests passed.');

sdk/python/openexecution_verify/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
SUPPORTED_SIG_ALGORITHMS,
1111
)
1212

13-
__version__ = "2.1.0"
13+
__version__ = "1.0.0"
1414
__all__ = [
1515
"OpenExecutionVerifier",
1616
"canonicalize",

sdk/python/setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
setup(
44
name="openexecution-verify",
5-
version="2.1.0",
5+
version="1.0.0",
66
description="OpenExecution Provenance Certificate Verification SDK",
77
packages=find_packages(),
88
python_requires=">=3.8",

0 commit comments

Comments
 (0)