nc_program_manager/.paul/phases/09-license-generator/09-01-PLAN.md
dtrentin 15644cfdf8 feat(09-license-generator): standalone LicenseGenerator tool + real RSA key pair
Phase 09 complete — v0.2 NcProgramManager milestone done:
- LicenseGenerator standalone console EXE (separate repo: license-generator.git)
  - keygen: RSA 2048-bit key pair generation
  - fingerprint: WMI machine fingerprint (Windows)
  - sign: RSA-SHA256 sign fingerprint -> .lic file
- LicenseValidator.cs: PublicKeyXml PLACEHOLDER replaced with real vendor public key
- Offline license system now production-ready end-to-end

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 23:37:49 +02:00

15 KiB

phase plan type wave depends_on files_modified autonomous
09-license-generator 01 execute 1
07-01
~/Documents/mine/c#/LicenseGenerator/LicenseGenerator.csproj
~/Documents/mine/c#/LicenseGenerator/LicenseGenerator.sln
~/Documents/mine/c#/LicenseGenerator/Program.cs
~/Documents/mine/c#/LicenseGenerator/InputArgs.cs
~/Documents/mine/c#/LicenseGenerator/Commands/KeygenCommand.cs
~/Documents/mine/c#/LicenseGenerator/Commands/FingerprintCommand.cs
~/Documents/mine/c#/LicenseGenerator/Commands/SignCommand.cs
~/Documents/mine/c#/LicenseGenerator/Properties/AssemblyInfo.cs
Licensing/LicenseValidator.cs
false
## Goal Build a standalone vendor console tool (`LicenseGenerator.exe`) in a separate repository at `~/Documents/mine/c#/LicenseGenerator`. Three commands: `keygen` (RSA pair), `fingerprint` (WMI), `sign` (produce `.lic`). After running keygen, replace the PLACEHOLDER in `LicenseValidator.cs` with the real public key.

Purpose

Completes the offline license system. Without a real key pair, all production licenses are rejected. This tool gives the vendor the ability to issue machine-bound licenses and embeds the matching public key into the product binary.

Output

  • ~/Documents/mine/c#/LicenseGenerator/ — new standalone .NET 4.7.2 console project and solution
  • Git repo initialized, remote: ssh://git@3nt-git.duckdns.org:222/davide.trentin/license-generator.git
  • Licensing/LicenseValidator.cs (in NcProgramManager) — PLACEHOLDER replaced with real RSA 2048-bit public key XML
## Project Context @.paul/PROJECT.md @.paul/STATE.md

Prior Work

@.paul/phases/07-license-core/07-01-SUMMARY.md

Source Files

@Licensing/LicenseValidator.cs @Licensing/MachineFingerprint.cs @Licensing/LicenseFile.cs

<acceptance_criteria>

AC-1: keygen produces RSA 2048-bit key pair

Given LicenseGenerator.exe is present
When vendor runs `LicenseGenerator.exe keygen`
Then private.key.xml written to current dir, public key XML printed to stdout
And stdout message instructs vendor to paste public key into LicenseValidator.cs

AC-2: fingerprint command prints machine fingerprint

Given LicenseGenerator.exe running on Windows with WMI access
When vendor runs `LicenseGenerator.exe fingerprint`
Then machine fingerprint string printed to stdout (CPU ProcessorID + BaseBoard SerialNumber, same logic as MachineFingerprint.GetFingerprint())

AC-3: sign command produces valid .lic file

Given private.key.xml exists and fingerprint string is known
When vendor runs `LicenseGenerator.exe sign -key=private.key.xml -fingerprint=<fp> -out=license.lic`
Then license.lic written containing base64 RSA-SHA256 signature
And LicenseValidator(publicKeyXml).Validate(fp, File.ReadAllText("license.lic").Trim()) returns true

AC-4: LicenseValidator.cs uses real public key

Given keygen was run and public key XML captured
When PublicKeyXml constant in LicenseValidator.cs is updated from PLACEHOLDER to real XML
Then a license signed by the matching private key passes Validate()
And a license signed by any other key fails Validate()

</acceptance_criteria>

Task 1: Create LicenseGenerator project and git repo ~/Documents/mine/c#/LicenseGenerator/LicenseGenerator.sln, ~/Documents/mine/c#/LicenseGenerator/LicenseGenerator.csproj, ~/Documents/mine/c#/LicenseGenerator/Program.cs, ~/Documents/mine/c#/LicenseGenerator/InputArgs.cs, ~/Documents/mine/c#/LicenseGenerator/Properties/AssemblyInfo.cs, ~/Documents/mine/c#/LicenseGenerator/.gitignore Create `~/Documents/mine/c#/LicenseGenerator/` as a fully standalone project (NOT inside NcProgramManager).
**Directory layout:**
```
LicenseGenerator/
  LicenseGenerator.sln
  LicenseGenerator.csproj
  Program.cs
  InputArgs.cs
  Commands/          (empty dir, populated in Task 2+3)
  Properties/
    AssemblyInfo.cs
  .gitignore
```

**LicenseGenerator.csproj:**
- OutputType: Exe, TargetFrameworkVersion: v4.7.2, Platform: AnyCPU
- References: System, System.Core, System.Management
- Compile includes: Program.cs, InputArgs.cs, Properties/AssemblyInfo.cs, Commands\KeygenCommand.cs, Commands\FingerprintCommand.cs, Commands\SignCommand.cs

**LicenseGenerator.sln:** minimal VS2019-compatible solution referencing LicenseGenerator.csproj

**InputArgs.cs** (namespace LicenseGenerator):
- Field: `string Command` — args[0] if present, else ""
- Field: `string KeyPath` — value of `-key=`
- Field: `string Fingerprint` — value of `-fingerprint=`
- Field: `string OutPath` — value of `-out=`, default "license.lic"
- Static factory: `InputArgs Parse(string[] args)` — split each arg on first '='

**Program.cs** (namespace LicenseGenerator):
- Call `InputArgs.Parse(args)`
- Switch Command: "keygen" → KeygenCommand.Execute(), "fingerprint" → FingerprintCommand.Execute(), "sign" → SignCommand.Execute(args), default → print usage + Environment.Exit(1)
- Usage text lists all three commands

**AssemblyInfo.cs:** standard .NET 4.7.2 template, AssemblyVersion "1.0.0.0", product "LicenseGenerator"

**.gitignore:** standard C# gitignore (bin/, obj/, *.user, .vs/, packages/)

**Git setup (run after creating files):**
```bash
cd ~/Documents/mine/c#/LicenseGenerator
git init
git remote add origin ssh://git@3nt-git.duckdns.org:222/davide.trentin/license-generator.git
```

Avoid: any reference to NcProgramManager.csproj or NcProgramManager.sln
`ls ~/Documents/mine/c#/LicenseGenerator/` shows .sln, .csproj, Program.cs, InputArgs.cs, Properties/, Commands/, .gitignore `git -C ~/Documents/mine/c#/LicenseGenerator remote -v` shows origin pointing to ssh://git@3nt-git.duckdns.org:222/davide.trentin/license-generator.git Project structure created; git repo initialized with correct remote. Task 2: Implement keygen and fingerprint commands ~/Documents/mine/c#/LicenseGenerator/Commands/KeygenCommand.cs, ~/Documents/mine/c#/LicenseGenerator/Commands/FingerprintCommand.cs **KeygenCommand.cs** (namespace LicenseGenerator.Commands): ```csharp internal static class KeygenCommand { internal static void Execute() { using (var rsa = new RSACryptoServiceProvider(2048)) { string privateXml = rsa.ToXmlString(true); string publicXml = rsa.ToXmlString(false); File.WriteAllText("private.key.xml", privateXml); Console.WriteLine("=== PUBLIC KEY XML — paste into LicenseValidator.cs ==="); Console.WriteLine(publicXml); Console.WriteLine("========================================================"); Console.WriteLine("Private key saved: private.key.xml"); Console.WriteLine("KEEP private.key.xml SECRET — never distribute it."); } } } ``` Usings: System, System.IO, System.Security.Cryptography
**FingerprintCommand.cs** (namespace LicenseGenerator.Commands):
- Duplicate WMI logic from MachineFingerprint.GetFingerprint():
  1. ManagementClass("win32_processor") → processorID property
  2. ManagementObjectSearcher("root\\CIMV2", "SELECT * FROM Win32_BaseBoard") → SerialNumber
  3. Concatenate → print to Console.WriteLine
- Wrap in try/catch: on exception print "Error reading fingerprint (Windows + WMI required): " + ex.Message, Environment.Exit(1)
- Usings: System, System.Management

Avoid: referencing MachineFingerprint class from NcProgramManager — keep standalone
On Linux/Mono: `LicenseGenerator.exe keygen` writes private.key.xml and prints RSAKeyValue XML to stdout. On Windows: `LicenseGenerator.exe fingerprint` prints non-empty CPU+board string. AC-1 satisfied: keygen generates 2048-bit RSA pair. AC-2 satisfied: fingerprint reads WMI. Task 3: Implement sign command ~/Documents/mine/c#/LicenseGenerator/Commands/SignCommand.cs **SignCommand.cs** (namespace LicenseGenerator.Commands): ```csharp internal static class SignCommand { internal static void Execute(InputArgs args) { if (string.IsNullOrEmpty(args.KeyPath)) { Console.Error.WriteLine("Missing: -key="); Environment.Exit(1); } if (string.IsNullOrEmpty(args.Fingerprint)) { Console.Error.WriteLine("Missing: -fingerprint="); Environment.Exit(1); }
        string privateXml = File.ReadAllText(args.KeyPath);
        byte[] data = Encoding.UTF8.GetBytes(args.Fingerprint);

        using (var rsa = new RSACryptoServiceProvider())
        {
            rsa.FromXmlString(privateXml);
            byte[] signature = rsa.SignData(data, new SHA256CryptoServiceProvider());
            string base64 = Convert.ToBase64String(signature);
            File.WriteAllText(args.OutPath, base64);
            Console.WriteLine("License written: " + args.OutPath);
        }
    }
}
```
Usings: System, System.IO, System.Security.Cryptography, System.Text

SHA256CryptoServiceProvider (not HashAlgorithmName) — .NET 4.7.2 RSACryptoServiceProvider constraint.
SignData here MUST pair with VerifyData in LicenseValidator — same provider type on both sides.

Avoid: HashAlgorithmName overloads — not available in .NET 4.7.2 RSACryptoServiceProvider
`LicenseGenerator.exe sign -key=private.key.xml -fingerprint=TEST_FP -out=test.lic` → test.lic contains non-empty base64. Cross-check: `new LicenseValidator(publicKeyXml).Validate("TEST_FP", File.ReadAllText("test.lic").Trim())` returns true. AC-3 satisfied: sign command produces .lic file verifiable by LicenseValidator. LicenseGenerator project complete — keygen, fingerprint, sign commands implemented. 1. Build: `msbuild ~/Documents/mine/c#/LicenseGenerator/LicenseGenerator.sln /p:Configuration=Release` (or on Windows: build via VS or msbuild) 2. Navigate to output: `LicenseGenerator/bin/Release/` 3. Run: `LicenseGenerator.exe keygen` 4. Capture the PUBLIC KEY XML printed between the `===` banners 5. Paste the captured XML in the resume signal below
Note: keygen works on Linux (Mono) and Windows — no WMI needed for key generation.
Paste captured public key XML (the full RSAKeyValue string) to continue Task 4: Replace PublicKeyXml PLACEHOLDER in LicenseValidator.cs Licensing/LicenseValidator.cs In NcProgramManager project, replace the PLACEHOLDER in `Licensing/LicenseValidator.cs`.
Current (lines 11-15):
```csharp
private const string PublicKeyXml =
    "<RSAKeyValue>" +
    "<Modulus>PLACEHOLDER</Modulus>" +
    "<Exponent>AQAB</Exponent>" +
    "</RSAKeyValue>";
```

Replace with the real public key XML from keygen output as a single string literal:
```csharp
// Real vendor public key — generated YYYY-MM-DD with LicenseGenerator keygen.
private const string PublicKeyXml = "[full RSAKeyValue XML from keygen]";
```

Avoid: embedding private key — public key only (keygen ToXmlString(false) output)
Avoid: multi-line concatenation — single string literal is fine for RSA XML
`msbuild NcProgramManager.csproj` — no errors. All 116 existing tests pass (unit tests use ephemeral keys — unaffected). grep "PLACEHOLDER" Licensing/LicenseValidator.cs → no match. AC-4 satisfied: LicenseValidator.cs uses real 2048-bit public key; production licenses now verifiable. Task 5: Initial commit and push LicenseGenerator repo ~/Documents/mine/c#/LicenseGenerator/ (all files) ```bash cd ~/Documents/mine/c#/LicenseGenerator git add LicenseGenerator.sln LicenseGenerator.csproj Program.cs InputArgs.cs \ Commands/KeygenCommand.cs Commands/FingerprintCommand.cs Commands/SignCommand.cs \ Properties/AssemblyInfo.cs .gitignore git commit -m "feat: initial LicenseGenerator — keygen, fingerprint, sign commands" git push -u origin main ``` Do NOT commit private.key.xml or any generated .lic files. Verify .gitignore excludes bin/, obj/, *.user, .vs/. `git -C ~/Documents/mine/c#/LicenseGenerator log --oneline` shows initial commit. LicenseGenerator source pushed to ssh://git@3nt-git.duckdns.org:222/davide.trentin/license-generator.git

DO NOT CHANGE

  • Licensing/MachineFingerprint.cs — WMI logic duplicated in FingerprintCommand; no changes to original
  • Licensing/LicenseValidator.cs — touch only the PublicKeyXml constant (Task 4 only)
  • Program.cs (NcProgramManager) — license integration complete; no changes
  • NcProgramManager.Tests/ — no test changes in this plan
  • NcProgramManager.sln — do NOT add LicenseGenerator; it is a separate solution

SCOPE LIMITS

  • LicenseGenerator is a standalone separate repo — no cross-project reference to NcProgramManager
  • No GUI, no installer, no auto-update
  • No network calls — fully offline tool
  • fingerprint command: WMI logic only, Windows-only, no file output
  • Do NOT commit private.key.xml to git
Before declaring plan complete: - [ ] `~/Documents/mine/c#/LicenseGenerator/` exists as independent git repo - [ ] `git remote -v` shows ssh://git@3nt-git.duckdns.org:222/davide.trentin/license-generator.git - [ ] `LicenseGenerator.exe keygen` writes private.key.xml + prints RSAKeyValue XML - [ ] `LicenseGenerator.exe sign -key=private.key.xml -fingerprint=TEST -out=test.lic` writes base64 - [ ] `LicenseValidator.cs` PublicKeyXml has no "PLACEHOLDER" - [ ] `msbuild NcProgramManager.csproj` passes, all 116 tests pass - [ ] LicenseGenerator source committed and pushed to remote

<success_criteria>

  • LicenseGenerator builds as standalone .NET 4.7.2 console EXE in its own repo
  • All three commands (keygen, fingerprint, sign) implemented and functional
  • LicenseValidator.cs uses real RSA 2048-bit public key
  • Existing NcProgramManager test suite unaffected (116 pass, 2 ignored)
  • Source pushed to ssh://git@3nt-git.duckdns.org:222/davide.trentin/license-generator.git </success_criteria>
After completion, create `.paul/phases/09-license-generator/09-01-SUMMARY.md`