Skip to content

Commit aeb8881

Browse files
committed
php: add kduma/pcf-sig as a sibling Composer package
Mirrors the Rust reference at reference/PCF-SIG-v1.0/ field-for-field as a second Composer package under implementations/php/pcf-sig/. Follows the recipe documented in implementations/README.md: a self-contained composer.json with a path repository on ../pcf for local dev resolution. Source structure (one PHP class per Rust module): - Consts: TYPE_PCFSIG_KEY/SIG, magic strings, sizes, version - ErrorKind + PcfSigException: exception hierarchy - SigAlgo + KeyFormat: registry enums; only Ed25519 implemented, other ids recognised so verify returns Unverifiable rather than Malformed for them - KeyRecord + KeyMetadata: serialisation + fingerprint cross-check - Manifest + SignedEntry: 60 + 218*N byte layout - SignaturePartition: manifest || sig_length || sig || trailer - SigningMaterial::ed25519FromSeed: libsodium-backed signer - SignPartitions::run, ensureKeyPartition: high-level Writer API - Verify::all, Verify::allWithRecheck + verdict/reason enums Tests (34 total): roundtrip, canonical-vector (byte-exact match with b158e2f5...1307), tamper, relocation, multi-signer, spec-compliance. PHPUnit 11 with strict-warnings/strict-output. Crypto deps: PHP's bundled ext-sodium (sodium_crypto_sign_detached / sodium_crypto_sign_verify_detached) and ext-hash. No Composer crypto dependencies. CI: - php.yml: matrix expanded to (php X package); both tests and test-vector jobs run pcf and pcf-sig in parallel; sodium added to setup-php extensions list - php-split.yml: matrix expanded to publish both kduma/pcf and kduma/pcf-sig (to kduma-OSS-splits/PHP-PCF-SIG-lib) - release-prepare.yml: bumps the kduma/pcf constraint in pcf-sig's composer.json alongside the workspace version bump The canonical 966-byte test vector regenerates byte-for-byte from this PHP writer with sha256 = b158e2f5b160d72cea3226af2041f8d18aa75b3db6cb85faeca5df7879871307, matching the Rust reference and the TypeScript port. https://claude.ai/code/session_01ST4PcjqvobURus32WuyEi5
1 parent ea4b1bd commit aeb8881

29 files changed

Lines changed: 2293 additions & 19 deletions

.github/workflows/php-split.yml

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
branches: [master]
66
paths:
77
- 'implementations/php/pcf/**'
8+
- 'implementations/php/pcf-sig/**'
89
- '.github/workflows/php-split.yml'
910
workflow_dispatch:
1011
inputs:
@@ -22,8 +23,14 @@ on:
2223

2324
jobs:
2425
split:
25-
name: split implementations/php/pcf → kduma-OSS-splits/PHP-PCF-lib
26+
name: split ${{ matrix.package.dir }} → kduma-OSS-splits/${{ matrix.package.repo }}
2627
runs-on: ubuntu-latest
28+
strategy:
29+
fail-fast: false
30+
matrix:
31+
package:
32+
- { dir: 'implementations/php/pcf', repo: 'PHP-PCF-lib' }
33+
- { dir: 'implementations/php/pcf-sig', repo: 'PHP-PCF-SIG-lib' }
2734
steps:
2835
- uses: actions/checkout@v4
2936
with:
@@ -42,7 +49,7 @@ jobs:
4249
GH_TOKEN: ${{ steps.app-token.outputs.token }}
4350
run: |
4451
set -euo pipefail
45-
REPO="kduma-OSS-splits/PHP-PCF-lib"
52+
REPO="kduma-OSS-splits/${{ matrix.package.repo }}"
4653
TMPDIR=$(mktemp -d)
4754
git clone "https://x-access-token:${GH_TOKEN}@github.com/$REPO.git" "$TMPDIR" 2>&1 || true
4855
if ! git -C "$TMPDIR" rev-parse HEAD >/dev/null 2>&1; then
@@ -78,9 +85,9 @@ jobs:
7885
env:
7986
PAT: x-access-token:${{ steps.app-token.outputs.token }}
8087
with:
81-
package_directory: 'implementations/php/pcf'
88+
package_directory: ${{ matrix.package.dir }}
8289
repository_organization: 'kduma-OSS-splits'
83-
repository_name: 'PHP-PCF-lib'
90+
repository_name: ${{ matrix.package.repo }}
8491
branch: 'master'
8592
user_name: 'github-actions[bot]'
8693
user_email: '41898282+github-actions[bot]@users.noreply.github.com'
@@ -92,9 +99,9 @@ jobs:
9299
PAT: x-access-token:${{ steps.app-token.outputs.token }}
93100
with:
94101
tag: ${{ steps.resolve-tag.outputs.tag }}
95-
package_directory: 'implementations/php/pcf'
102+
package_directory: ${{ matrix.package.dir }}
96103
repository_organization: 'kduma-OSS-splits'
97-
repository_name: 'PHP-PCF-lib'
104+
repository_name: ${{ matrix.package.repo }}
98105
branch: 'master'
99106
user_name: 'github-actions[bot]'
100107
user_email: '41898282+github-actions[bot]@users.noreply.github.com'

.github/workflows/php.yml

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,26 @@ on:
66
pull_request:
77
branches: [master]
88

9-
defaults:
10-
run:
11-
working-directory: implementations/php/pcf
12-
139
jobs:
1410
test:
15-
name: test (PHP ${{ matrix.php }})
11+
name: test ${{ matrix.package }} (PHP ${{ matrix.php }})
1612
runs-on: ubuntu-latest
1713
strategy:
1814
fail-fast: false
1915
matrix:
2016
php: ['8.1', '8.2', '8.3', '8.4']
17+
package: [pcf, pcf-sig]
18+
defaults:
19+
run:
20+
working-directory: implementations/php/${{ matrix.package }}
2121
steps:
2222
- uses: actions/checkout@v4
2323

2424
- name: Set up PHP
2525
uses: shivammathur/setup-php@v2
2626
with:
2727
php-version: ${{ matrix.php }}
28-
extensions: hash, mbstring
28+
extensions: hash, mbstring, sodium
2929
coverage: none
3030
tools: composer:v2
3131

@@ -39,31 +39,40 @@ jobs:
3939
run: vendor/bin/phpunit
4040

4141
test-vector:
42-
name: regenerate spec test vector
42+
name: regenerate spec test vector (${{ matrix.package }})
4343
runs-on: ubuntu-latest
44+
strategy:
45+
fail-fast: false
46+
matrix:
47+
include:
48+
- { package: pcf, output: pcf_testvector.bin, expected: 395 }
49+
- { package: pcf-sig, output: pcfsig_testvector.bin, expected: 966 }
50+
defaults:
51+
run:
52+
working-directory: implementations/php/${{ matrix.package }}
4453
steps:
4554
- uses: actions/checkout@v4
4655

4756
- name: Set up PHP
4857
uses: shivammathur/setup-php@v2
4958
with:
5059
php-version: '8.3'
51-
extensions: hash, mbstring
60+
extensions: hash, mbstring, sodium
5261
coverage: none
5362
tools: composer:v2
5463

5564
- name: Install dependencies
5665
run: composer install --prefer-dist --no-progress --no-interaction
5766

5867
- name: Build the test-vector example
59-
run: php examples/gen_testvector.php pcf_testvector.bin
68+
run: php examples/gen_testvector.php ${{ matrix.output }}
6069

6170
- name: Inspect generated test vector
6271
run: |
63-
ls -l pcf_testvector.bin
64-
test "$(wc -c < pcf_testvector.bin)" = "395"
72+
ls -l ${{ matrix.output }}
73+
test "$(wc -c < ${{ matrix.output }})" = "${{ matrix.expected }}"
6574
6675
- uses: actions/upload-artifact@v4
6776
with:
68-
name: pcf-testvector-php
69-
path: implementations/php/pcf/pcf_testvector.bin
77+
name: ${{ matrix.package }}-testvector-php
78+
path: implementations/php/${{ matrix.package }}/${{ matrix.output }}

.github/workflows/release-prepare.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ jobs:
9090
working-directory: implementations/ts
9191
run: npm version '${{ steps.version.outputs.version }}' -ws --no-git-tag-version --allow-same-version
9292

93+
- name: Bump PHP pcf-sig dependency on pcf
94+
shell: bash
95+
run: |
96+
NEW='${{ steps.version.outputs.version }}'
97+
sed -i 's|"kduma/pcf": "[^"]*"|"kduma/pcf": "^'"$NEW"' || dev-master"|' implementations/php/pcf-sig/composer.json
98+
9399
- name: Bump .NET Directory.Build.props
94100
shell: bash
95101
run: |
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# --- Composer ---
2+
/vendor/
3+
composer.lock
4+
5+
# --- PHPUnit ---
6+
/.phpunit.cache/
7+
.phpunit.result.cache
8+
9+
# --- Generated artifacts ---
10+
*.bin
11+
!testdata/canonical.bin
12+
13+
# --- Editors ---
14+
.idea/
15+
.vscode/
16+
*.swp
17+
*~
18+
19+
# --- macOS ---
20+
.DS_Store
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# kduma/pcf-sig
2+
3+
PHP implementation of **PCF-SIG v1.0**, the PCF Cryptographic Signatures
4+
profile. Mirrors the [normative specification][spec] and the [Rust reference
5+
implementation][rust] field-for-field.
6+
7+
[spec]: ../../../specs/PCF-SIG-spec-v1.0.txt
8+
[rust]: ../../../reference/PCF-SIG-v1.0/
9+
10+
## Install
11+
12+
```sh
13+
composer require kduma/pcf kduma/pcf-sig
14+
```
15+
16+
## What it adds
17+
18+
Two new PCF partition types layered on top of the [`kduma/pcf`](../pcf/)
19+
container, without changing the PCF byte format:
20+
21+
| Type | Name | Holds |
22+
|--------------|--------------|------------------------------------------------------|
23+
| `0xAAAB0001` | `PCFSIG_KEY` | One signer's public key, identified by SHA-256 fingerprint of the key bytes |
24+
| `0xAAAB0002` | `PCFSIG_SIG` | One Manifest enumerating signed partitions + the signature over the Manifest |
25+
26+
A **Manifest** binds the *protected fields* of each covered partition:
27+
`uid`, `partitionType`, `label`, `usedBytes`, `dataHashAlgo`, `dataHash`. It
28+
does NOT bind `startOffset` or `maxLength`, so PCF compaction and other
29+
relocations preserve signature validity as long as partition bytes do not
30+
change.
31+
32+
## Algorithm support
33+
34+
| `sig_algo_id` | Algorithm | This release |
35+
|---------------|---------------------|--------------|
36+
| 1 | Ed25519 (RFC 8032) | implemented (MUST) |
37+
| 2, 4, 5, 7 | RSA-PSS / PKCS1v15 | registry only |
38+
| 16, 18 | ECDSA P-256 / P-521 | registry only |
39+
| 32 | X.509 chain | registry only |
40+
41+
Algorithms marked *registry only* are recognised at parse time and reported as
42+
`ManifestVerdict::Unverifiable` (with `UnverifiableReason::UnsupportedSigAlgo`)
43+
rather than `Malformed`. Adding a full implementation for any of them is a
44+
pure addition that does not touch the on-disk format.
45+
46+
Hash algorithm constraint: signed partitions MUST use a cryptographic
47+
`dataHashAlgo` (SHA-256, SHA-512, BLAKE3). The Writer refuses to sign
48+
weakly-hashed partitions; the Verifier rejects them per entry.
49+
50+
## Usage
51+
52+
```php
53+
use Kduma\PCF\Container;
54+
use Kduma\PCF\HashAlgo;
55+
use Kduma\PCFSIG\ManifestVerdict;
56+
use Kduma\PCFSIG\SignPartitions;
57+
use Kduma\PCFSIG\SigningMaterial;
58+
use Kduma\PCFSIG\Verify;
59+
60+
$c = Container::create();
61+
$alpha = str_repeat("\x11", 16);
62+
$c->addPartition(0x10, $alpha, 'alpha', 'Hello, PCF-SIG!', 0, HashAlgo::Sha256);
63+
64+
$signer = SigningMaterial::ed25519FromSeed(str_repeat("\x42", 32));
65+
SignPartitions::run(
66+
$c, $signer, [$alpha],
67+
str_repeat("\x33", 16),
68+
str_repeat("\x22", 16),
69+
0, 'pcfsig', 'pcfkey',
70+
);
71+
72+
foreach (Verify::allWithRecheck($c) as $report) {
73+
if ($report->verdict === ManifestVerdict::Valid) {
74+
printf("signature valid; %d entries covered\n", count($report->entries));
75+
}
76+
}
77+
```
78+
79+
## Cross-port test vector parity
80+
81+
The shipped `testdata/canonical.bin` is byte-identical to the canonical vector
82+
produced by the Rust reference and the TypeScript port. SHA-256:
83+
`b158e2f5b160d72cea3226af2041f8d18aa75b3db6cb85faeca5df7879871307`.
84+
85+
```sh
86+
composer gen-testvector -- /tmp/php.bin
87+
```
88+
89+
The test suite asserts byte-exact equality on every CI run.
90+
91+
## Dependencies
92+
93+
- `kduma/pcf` — the PCF base container library (same version as pcf-sig).
94+
- `ext-sodium` — PHP's bundled libsodium, used for Ed25519 sign/verify
95+
(`sodium_crypto_sign_detached` / `sodium_crypto_sign_verify_detached`).
96+
Available in PHP 7.2+ without external dependencies.
97+
- `ext-hash` — PHP's bundled hash extension, used for SHA-256 fingerprints.
98+
99+
No Composer crypto dependencies; all signing/hashing runs through built-in
100+
PHP extensions.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"name": "kduma/pcf-sig",
3+
"description": "PHP implementation of PCF-SIG v1.0, the PCF Cryptographic Signatures profile",
4+
"type": "library",
5+
"license": "MIT",
6+
"keywords": ["pcf", "pcf-sig", "signature", "ed25519", "cryptography", "container"],
7+
"homepage": "https://github.com/kduma-OSS/Partitioned-Container-Format",
8+
"support": {
9+
"issues": "https://github.com/kduma-OSS/Partitioned-Container-Format/issues",
10+
"source": "https://github.com/kduma-OSS-splits/PHP-PCF-SIG-lib"
11+
},
12+
"require": {
13+
"php": ">=8.1",
14+
"ext-hash": "*",
15+
"ext-sodium": "*",
16+
"kduma/pcf": "^0.0.6 || dev-master"
17+
},
18+
"require-dev": {
19+
"phpunit/phpunit": "^10.5 || ^11.0"
20+
},
21+
"repositories": [
22+
{
23+
"type": "path",
24+
"url": "../pcf",
25+
"options": { "symlink": true }
26+
}
27+
],
28+
"autoload": {
29+
"psr-4": {
30+
"Kduma\\PCFSIG\\": "src/"
31+
}
32+
},
33+
"autoload-dev": {
34+
"psr-4": {
35+
"Kduma\\PCFSIG\\Tests\\": "tests/"
36+
}
37+
},
38+
"scripts": {
39+
"test": "phpunit",
40+
"gen-testvector": "php examples/gen_testvector.php"
41+
},
42+
"config": {
43+
"sort-packages": true
44+
}
45+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* Generates the canonical PCF-SIG v1.0 test-vector file. Run with
7+
* `php examples/gen_testvector.php <output-path>` (defaults to
8+
* ./pcfsig_testvector.bin).
9+
*
10+
* The Ed25519 keypair is generated deterministically from a fixed 32-byte seed
11+
* of 0x00..0x1F, so independent implementations can reproduce the file
12+
* byte-for-byte.
13+
*/
14+
15+
require __DIR__ . '/../vendor/autoload.php';
16+
17+
use Kduma\PCF\Container;
18+
use Kduma\PCF\HashAlgo;
19+
use Kduma\PCF\Storage\MemoryStorage;
20+
use Kduma\PCFSIG\ManifestVerdict;
21+
use Kduma\PCFSIG\SignPartitions;
22+
use Kduma\PCFSIG\SigningMaterial;
23+
use Kduma\PCFSIG\Verify;
24+
25+
$path = $argv[1] ?? 'pcfsig_testvector.bin';
26+
27+
$seed = '';
28+
for ($i = 0; $i < 32; ++$i) {
29+
$seed .= chr($i);
30+
}
31+
$signer = SigningMaterial::ed25519FromSeed($seed);
32+
33+
$c = Container::createWith(new MemoryStorage(), 8, HashAlgo::Sha256);
34+
$c->addPartition(
35+
0x10,
36+
str_repeat("\x11", 16),
37+
'alpha',
38+
'Hello, PCF-SIG!',
39+
0,
40+
HashAlgo::Sha256,
41+
);
42+
SignPartitions::run(
43+
$c,
44+
$signer,
45+
[str_repeat("\x11", 16)],
46+
str_repeat("\x33", 16),
47+
str_repeat("\x22", 16),
48+
0,
49+
'pcfsig',
50+
'pcfkey',
51+
);
52+
53+
$image = $c->compactedImage();
54+
file_put_contents($path, $image);
55+
56+
$verifier = Container::open(new MemoryStorage($image));
57+
$verifier->verify();
58+
$reports = Verify::allWithRecheck($verifier);
59+
if (count($reports) !== 1 || $reports[0]->verdict !== ManifestVerdict::Valid) {
60+
fwrite(STDERR, "generated vector does not self-verify\n");
61+
exit(1);
62+
}
63+
64+
fprintf(STDERR, "wrote %s (%d bytes)\n", $path, strlen($image));
65+
fprintf(STDERR, "sha256 = %s\n", bin2hex(hash('sha256', $image, true)));
66+
fprintf(STDERR, "signer fingerprint = %s\n", bin2hex($signer->fingerprint()));

0 commit comments

Comments
 (0)