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>
357 lines
15 KiB
Markdown
357 lines
15 KiB
Markdown
---
|
|
phase: 09-license-generator
|
|
plan: 01
|
|
type: execute
|
|
wave: 1
|
|
depends_on: ["07-01"]
|
|
files_modified:
|
|
- ~/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
|
|
autonomous: false
|
|
---
|
|
|
|
<objective>
|
|
## 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
|
|
</objective>
|
|
|
|
<context>
|
|
## 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
|
|
</context>
|
|
|
|
<acceptance_criteria>
|
|
|
|
## AC-1: keygen produces RSA 2048-bit key pair
|
|
```gherkin
|
|
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
|
|
```gherkin
|
|
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
|
|
```gherkin
|
|
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
|
|
```gherkin
|
|
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>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Create LicenseGenerator project and git repo</name>
|
|
<files>
|
|
~/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
|
|
</files>
|
|
<action>
|
|
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
|
|
</action>
|
|
<verify>
|
|
`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
|
|
</verify>
|
|
<done>Project structure created; git repo initialized with correct remote.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Implement keygen and fingerprint commands</name>
|
|
<files>
|
|
~/Documents/mine/c#/LicenseGenerator/Commands/KeygenCommand.cs,
|
|
~/Documents/mine/c#/LicenseGenerator/Commands/FingerprintCommand.cs
|
|
</files>
|
|
<action>
|
|
**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
|
|
</action>
|
|
<verify>
|
|
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.
|
|
</verify>
|
|
<done>AC-1 satisfied: keygen generates 2048-bit RSA pair. AC-2 satisfied: fingerprint reads WMI.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 3: Implement sign command</name>
|
|
<files>
|
|
~/Documents/mine/c#/LicenseGenerator/Commands/SignCommand.cs
|
|
</files>
|
|
<action>
|
|
**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=<path-to-private.key.xml>");
|
|
Environment.Exit(1);
|
|
}
|
|
if (string.IsNullOrEmpty(args.Fingerprint))
|
|
{
|
|
Console.Error.WriteLine("Missing: -fingerprint=<machine-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
|
|
</action>
|
|
<verify>
|
|
`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.
|
|
</verify>
|
|
<done>AC-3 satisfied: sign command produces .lic file verifiable by LicenseValidator.</done>
|
|
</task>
|
|
|
|
<task type="checkpoint:human-action" gate="blocking">
|
|
<what-built>LicenseGenerator project complete — keygen, fingerprint, sign commands implemented.</what-built>
|
|
<how-to-verify>
|
|
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.
|
|
</how-to-verify>
|
|
<resume-signal>Paste captured public key XML (the full RSAKeyValue string) to continue</resume-signal>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 4: Replace PublicKeyXml PLACEHOLDER in LicenseValidator.cs</name>
|
|
<files>
|
|
Licensing/LicenseValidator.cs
|
|
</files>
|
|
<action>
|
|
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
|
|
</action>
|
|
<verify>
|
|
`msbuild NcProgramManager.csproj` — no errors.
|
|
All 116 existing tests pass (unit tests use ephemeral keys — unaffected).
|
|
grep "PLACEHOLDER" Licensing/LicenseValidator.cs → no match.
|
|
</verify>
|
|
<done>AC-4 satisfied: LicenseValidator.cs uses real 2048-bit public key; production licenses now verifiable.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 5: Initial commit and push LicenseGenerator repo</name>
|
|
<files>
|
|
~/Documents/mine/c#/LicenseGenerator/ (all files)
|
|
</files>
|
|
<action>
|
|
```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/.
|
|
</action>
|
|
<verify>`git -C ~/Documents/mine/c#/LicenseGenerator log --oneline` shows initial commit.</verify>
|
|
<done>LicenseGenerator source pushed to ssh://git@3nt-git.duckdns.org:222/davide.trentin/license-generator.git</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<boundaries>
|
|
|
|
## 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
|
|
|
|
</boundaries>
|
|
|
|
<verification>
|
|
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
|
|
</verification>
|
|
|
|
<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>
|
|
|
|
<output>
|
|
After completion, create `.paul/phases/09-license-generator/09-01-SUMMARY.md`
|
|
</output>
|