Skip to content

Commit e609879

Browse files
committed
dotnet: add KDuma.Pcf.Sig sibling project
Mirrors the Rust reference at reference/PCF-SIG-v1.0/ field-for-field as a second sibling project tree under implementations/dotnet/pcf-sig/. Follows the recipe documented in implementations/README.md: a new Pcf.Sig.sln next to the existing Pcf.sln, with the source library declaring a ProjectReference to ../../pcf/src/Pcf/Pcf.csproj for local dev resolution. Lockstep version is provided by the shared Directory.Build.props at implementations/dotnet/. Source structure (one C# file per Rust module): - Constants: TYPE_PCFSIG_KEY/SIG, magic strings, sizes, version - PcfSigException + PcfSigErrorKind: exception hierarchy - SigAlgo + KeyFormat enums + extensions; 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: BouncyCastle-backed signer - SignPartitions.Run, EnsureKeyPartition: high-level Writer API - Verify.All, Verify.AllWithRecheck + verdict/reason enums Tests (32 total) mirroring TS and PHP ports: Roundtrip, CanonicalVector (byte-exact match with b158e2f5...1307), Tamper, Relocation, SpecCompliance. xUnit on net8.0; library targets netstandard2.0. Crypto deps: - BouncyCastle.Cryptography v2.4 (Org.BouncyCastle.Math.EC.Rfc8032.Ed25519) - System.Security.Cryptography.SHA256 for fingerprints CI: - dotnet-ci.yml: matrix expanded to (os X package); pcf and pcf-sig each build+test on Linux/macOS/Windows; path filter also covers new project tree - release.yml: new publish-nuget-sig job runs after publish-nuget, pack from src/Pcf.Sig/Pcf.Sig.csproj, push as KDuma.Pcf.Sig The library has not been built locally (no dotnet SDK on this container); CI will validate. Code is structurally identical to the byte-exact- verified TS and PHP ports, so any compile failures would be local syntax issues, not algorithmic. https://claude.ai/code/session_01ST4PcjqvobURus32WuyEi5
1 parent aeb8881 commit e609879

24 files changed

Lines changed: 2137 additions & 5 deletions

.github/workflows/dotnet-ci.yml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,29 @@ on:
55
branches: [master]
66
paths:
77
- 'implementations/dotnet/pcf/**'
8+
- 'implementations/dotnet/pcf-sig/**'
89
- 'implementations/dotnet/Directory.Build.props'
910
- '.github/workflows/dotnet-ci.yml'
1011
pull_request:
1112
branches: [master]
1213
paths:
1314
- 'implementations/dotnet/pcf/**'
15+
- 'implementations/dotnet/pcf-sig/**'
1416
- 'implementations/dotnet/Directory.Build.props'
1517
- '.github/workflows/dotnet-ci.yml'
1618

17-
defaults:
18-
run:
19-
working-directory: implementations/dotnet/pcf
20-
2119
jobs:
2220
test:
23-
name: build & test (${{ matrix.os }})
21+
name: build & test ${{ matrix.package }} (${{ matrix.os }})
2422
runs-on: ${{ matrix.os }}
2523
strategy:
2624
fail-fast: false
2725
matrix:
2826
os: [ubuntu-latest, macos-latest, windows-latest]
27+
package: [pcf, pcf-sig]
28+
defaults:
29+
run:
30+
working-directory: implementations/dotnet/${{ matrix.package }}
2931
steps:
3032
- uses: actions/checkout@v4
3133
- uses: actions/setup-dotnet@v4

.github/workflows/release.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,49 @@ jobs:
235235
name: nuget-package
236236
path: implementations/dotnet/pcf/out/*.nupkg
237237

238+
publish-nuget-sig:
239+
name: Publish KDuma.Pcf.Sig to NuGet
240+
needs: [resolve, publish-nuget]
241+
runs-on: ubuntu-latest
242+
permissions:
243+
id-token: write
244+
contents: read
245+
defaults:
246+
run:
247+
working-directory: implementations/dotnet/pcf-sig
248+
steps:
249+
- uses: actions/checkout@v4
250+
- uses: actions/setup-dotnet@v4
251+
with:
252+
dotnet-version: '8.0.x'
253+
- run: dotnet restore
254+
- name: dotnet pack
255+
run: |
256+
dotnet pack src/Pcf.Sig/Pcf.Sig.csproj \
257+
-c Release \
258+
-p:Version='${{ needs.resolve.outputs.version }}' \
259+
-o out
260+
- name: NuGet login (OIDC trusted publishing)
261+
id: nuget-login
262+
if: needs.resolve.outputs.dry_run != 'true'
263+
uses: NuGet/login@v1
264+
with:
265+
user: krystianduma
266+
- name: dotnet nuget push
267+
if: needs.resolve.outputs.dry_run != 'true'
268+
run: |
269+
dotnet nuget push out/*.nupkg \
270+
--source https://api.nuget.org/v3/index.json \
271+
--api-key '${{ steps.nuget-login.outputs.NUGET_API_KEY }}' \
272+
--skip-duplicate
273+
- name: Dry-run note
274+
if: needs.resolve.outputs.dry_run == 'true'
275+
run: 'echo "Dry-run - skipping dotnet nuget push. Package would be out/*.nupkg."'
276+
- uses: actions/upload-artifact@v4
277+
with:
278+
name: nuget-package-sig
279+
path: implementations/dotnet/pcf-sig/out/*.nupkg
280+
238281
split-php:
239282
name: Split PHP to packagist source repo
240283
needs: resolve
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# --- .NET build artefacts ---
2+
bin/
3+
obj/
4+
out/
5+
*.user
6+
7+
# --- Generated test vectors ---
8+
*.bin
9+
!testdata/canonical.bin
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
Microsoft Visual Studio Solution File, Format Version 12.00
2+
# Visual Studio Version 17
3+
VisualStudioVersion = 17.0.31903.59
4+
MinimumVisualStudioVersion = 10.0.40219.1
5+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pcf.Sig", "src\Pcf.Sig\Pcf.Sig.csproj", "{B0000001-0000-0000-0000-000000000001}"
6+
EndProject
7+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pcf.Sig.Tests", "tests\Pcf.Sig.Tests\Pcf.Sig.Tests.csproj", "{B0000001-0000-0000-0000-000000000002}"
8+
EndProject
9+
Global
10+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
11+
Debug|Any CPU = Debug|Any CPU
12+
Release|Any CPU = Release|Any CPU
13+
EndGlobalSection
14+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
15+
{B0000001-0000-0000-0000-000000000001}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
16+
{B0000001-0000-0000-0000-000000000001}.Debug|Any CPU.Build.0 = Debug|Any CPU
17+
{B0000001-0000-0000-0000-000000000001}.Release|Any CPU.ActiveCfg = Release|Any CPU
18+
{B0000001-0000-0000-0000-000000000001}.Release|Any CPU.Build.0 = Release|Any CPU
19+
{B0000001-0000-0000-0000-000000000002}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
20+
{B0000001-0000-0000-0000-000000000002}.Debug|Any CPU.Build.0 = Debug|Any CPU
21+
{B0000001-0000-0000-0000-000000000002}.Release|Any CPU.ActiveCfg = Release|Any CPU
22+
{B0000001-0000-0000-0000-000000000002}.Release|Any CPU.Build.0 = Release|Any CPU
23+
EndGlobalSection
24+
EndGlobal
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# KDuma.Pcf.Sig
2+
3+
.NET 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+
dotnet add package KDuma.Pcf
14+
dotnet add package KDuma.Pcf.Sig
15+
```
16+
17+
## What it adds
18+
19+
Two new PCF partition types layered on top of the [`KDuma.Pcf`](../pcf/)
20+
container, without changing the PCF byte format:
21+
22+
| Type | Name | Holds |
23+
|--------------|--------------|------------------------------------------------------|
24+
| `0xAAAB0001` | `PCFSIG_KEY` | One signer's public key, identified by SHA-256 fingerprint of the key bytes |
25+
| `0xAAAB0002` | `PCFSIG_SIG` | One Manifest enumerating signed partitions + the signature over the Manifest |
26+
27+
A **Manifest** binds the *protected fields* of each covered partition:
28+
`Uid`, `PartitionType`, `Label`, `UsedBytes`, `DataHashAlgo`, `DataHash`. It
29+
does NOT bind `StartOffset` or `MaxLength`, so PCF compaction and other
30+
relocations preserve signature validity as long as partition bytes do not
31+
change.
32+
33+
## Algorithm support
34+
35+
| `sig_algo_id` | Algorithm | This release |
36+
|---------------|---------------------|--------------|
37+
| 1 | Ed25519 (RFC 8032) | implemented (MUST) |
38+
| 2, 4, 5, 7 | RSA-PSS / PKCS1v15 | registry only |
39+
| 16, 18 | ECDSA P-256 / P-521 | registry only |
40+
| 32 | X.509 chain | registry only |
41+
42+
Algorithms marked *registry only* are recognised at parse time and reported as
43+
`ManifestVerdict.Unverifiable` (with `UnverifiableReason.UnsupportedSigAlgo`)
44+
rather than `Malformed`. Adding a full implementation for any of them is a
45+
pure addition that does not touch the on-disk format.
46+
47+
Hash algorithm constraint: signed partitions MUST use a cryptographic
48+
`DataHashAlgo` (SHA-256, SHA-512, BLAKE3). The Writer refuses to sign
49+
weakly-hashed partitions; the Verifier rejects them per entry.
50+
51+
## Usage
52+
53+
```csharp
54+
using Pcf;
55+
using Pcf.Sig;
56+
using System.IO;
57+
58+
var c = Container.Create(new MemoryStream());
59+
var alpha = new byte[16] { 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
60+
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11 };
61+
c.AddPartition(0x10, alpha, "alpha",
62+
System.Text.Encoding.UTF8.GetBytes("Hello, PCF-SIG!"), 0, HashAlgo.Sha256);
63+
64+
var seed = new byte[32]; for (int i = 0; i < 32; i++) seed[i] = 0x42;
65+
var signer = SigningMaterial.Ed25519FromSeed(seed);
66+
SignPartitions.Run(
67+
c, signer, new[] { alpha },
68+
/* sigPartitionUid: */ new byte[16] { 0x33,0x33,0x33,0x33,0x33,0x33,0x33,0x33, 0x33,0x33,0x33,0x33,0x33,0x33,0x33,0x33 },
69+
/* keyPartitionUid: */ new byte[16] { 0x22,0x22,0x22,0x22,0x22,0x22,0x22,0x22, 0x22,0x22,0x22,0x22,0x22,0x22,0x22,0x22 },
70+
signedAtUnixSeconds: 0,
71+
sigLabel: "pcfsig",
72+
keyLabel: "pcfkey");
73+
74+
foreach (var report in Verify.AllWithRecheck(c))
75+
{
76+
if (report.Verdict == ManifestVerdict.Valid)
77+
{
78+
System.Console.WriteLine(
79+
$"signature valid; {report.Entries.Count} entries covered");
80+
}
81+
}
82+
```
83+
84+
## Cross-port test vector parity
85+
86+
The shipped `testdata/canonical.bin` is byte-identical to the canonical vector
87+
produced by the Rust reference, the TypeScript port and the PHP port. SHA-256:
88+
`b158e2f5b160d72cea3226af2041f8d18aa75b3db6cb85faeca5df7879871307`.
89+
90+
## Dependencies
91+
92+
- `KDuma.Pcf` — the PCF base container library (same version as pcf-sig).
93+
- `BouncyCastle.Cryptography` v2.4+ — actively maintained main BouncyCastle
94+
fork; ships RFC 8032 Ed25519 (`Org.BouncyCastle.Math.EC.Rfc8032.Ed25519`)
95+
and targets `netstandard2.0`.
96+
- `System.Security.Cryptography` (BCL) — SHA-256 for fingerprints.
97+
98+
The library targets `netstandard2.0` to match the PCF base; tests target
99+
`net8.0`.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
namespace Pcf.Sig;
2+
3+
/// <summary>
4+
/// On-disk constants defined by PCF-SIG v1.0. Every value here is normative
5+
/// and corresponds directly to a figure in the specification
6+
/// (`specs/PCF-SIG-spec-v1.0.txt`, Appendix A).
7+
/// </summary>
8+
public static class Constants
9+
{
10+
/// <summary>PCF partition type carrying one Key Record (spec Section 5).</summary>
11+
public const uint TypePcfsigKey = 0xAAAB_0001;
12+
13+
/// <summary>PCF partition type carrying one Signature Partition (spec Section 5).</summary>
14+
public const uint TypePcfsigSig = 0xAAAB_0002;
15+
16+
/// <summary>8-byte magic at the start of a Key Record (spec Section 6.1).</summary>
17+
public static readonly byte[] KeyMagic =
18+
{ (byte)'P', (byte)'C', (byte)'F', (byte)'K', (byte)'E', (byte)'Y', 0x00, 0x00 };
19+
20+
/// <summary>8-byte magic at the start of a Signature Partition Manifest (spec Section 7.1).</summary>
21+
public static readonly byte[] SigMagic =
22+
{ (byte)'P', (byte)'C', (byte)'F', (byte)'S', (byte)'I', (byte)'G', 0x00, 0x00 };
23+
24+
/// <summary>Profile version implemented by this library (major).</summary>
25+
public const ushort ProfileVersionMajor = 1;
26+
27+
/// <summary>Profile version implemented by this library (minor).</summary>
28+
public const ushort ProfileVersionMinor = 0;
29+
30+
/// <summary>Length of the Key Record fixed prefix that precedes key_data (spec 6.1).</summary>
31+
public const int KeyPrefixSize = 52;
32+
33+
/// <summary>Length of the Manifest fixed prefix that precedes signed_entries (spec 7.1).</summary>
34+
public const int ManifestPrefixSize = 60;
35+
36+
/// <summary>Length of one Signed Entry (spec Section 7.2).</summary>
37+
public const int SignedEntrySize = 218;
38+
39+
/// <summary>Length of a SHA-256 key fingerprint (spec Section 6.3).</summary>
40+
public const int FingerprintSize = 32;
41+
42+
/// <summary>Length of the Ed25519 raw public key (spec Section 6.2, key_format_id = 1).</summary>
43+
public const int Ed25519PublicKeyLen = 32;
44+
45+
/// <summary>Length of an Ed25519 signature (spec Section 8, sig_algo_id = 1).</summary>
46+
public const int Ed25519SignatureLen = 64;
47+
}

0 commit comments

Comments
 (0)