---
phase: 07-license-core
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- Licensing/IMachineFingerprint.cs
- Licensing/ILicenseValidator.cs
- Licensing/LicenseFile.cs
- Licensing/MachineFingerprint.cs
- Licensing/LicenseValidator.cs
- NcProgramManager.Tests/Unit/LicenseValidatorTests.cs
- NcProgramManager.csproj
- NcProgramManager.Tests/NcProgramManager.Tests.csproj
autonomous: true
---
## Goal
Create RSA-based offline license core: machine fingerprint reader, license file model, RSA signature validator — all injectable via interfaces for testability.
## Purpose
Current `checkLicense` in Program.cs uses hardcoded AES key — anyone with source or decompiler can generate licenses for any machine. RSA asymmetric crypto means only the holder of the private key (vendor) can issue valid licenses. Public key is safe to embed in binary.
## Output
- `Licensing/` folder with 5 new files
- `LicenseValidator` verifies RSA-SHA256 signature of machine fingerprint against embedded public key
- `MachineFingerprint` reads CPU ProcessorID + Motherboard SerialNumber via WMI (same data as current `checkLicense`)
- `LicenseFile` reads base64 signature from a `.lic` file on disk
- Unit tests: valid license passes, tampered/wrong-machine license fails
- 5 Compile entries added to main csproj, 1 to tests csproj
## Project Context
@.paul/PROJECT.md
@.paul/STATE.md
## Source Files (existing license logic to extract from)
@Program.cs
@NcProgramManager.csproj
## AC-1: Machine fingerprint readable via interface
```gherkin
Given a Windows machine with WMI available
When MachineFingerprint.GetFingerprint() is called
Then it returns a non-empty string concatenating CPU ProcessorID and Motherboard SerialNumber
```
## AC-2: Valid license passes validation
```gherkin
Given a fingerprint string and a license file containing the RSA-SHA256 signature of that fingerprint
When LicenseValidator.Validate(fingerprint, licenseBase64) is called with the matching public key
Then it returns true
```
## AC-3: Invalid license rejected
```gherkin
Given a fingerprint string and a license signed for a different fingerprint (wrong machine)
When LicenseValidator.Validate(fingerprint, licenseBase64) is called
Then it returns false
```
## AC-4: Corrupted/missing license file handled
```gherkin
Given a path to a non-existent or empty .lic file
When LicenseFile.TryRead(path, out string b64) is called
Then it returns false and b64 is null (no exception thrown)
```
Task 1: Create Licensing interfaces and LicenseFile
Licensing/IMachineFingerprint.cs,
Licensing/ILicenseValidator.cs,
Licensing/LicenseFile.cs
Create folder `Licensing/` at project root.
**Licensing/IMachineFingerprint.cs**
```csharp
namespace NcProgramManager.Licensing
{
internal interface IMachineFingerprint
{
string GetFingerprint();
}
}
```
**Licensing/ILicenseValidator.cs**
```csharp
namespace NcProgramManager.Licensing
{
internal interface ILicenseValidator
{
bool Validate(string fingerprint, string licenseBase64);
}
}
```
**Licensing/LicenseFile.cs**
```csharp
using System;
using System.IO;
namespace NcProgramManager.Licensing
{
internal static class LicenseFile
{
internal static bool TryRead(string path, out string base64)
{
base64 = null;
try
{
if (!File.Exists(path))
return false;
string content = File.ReadAllText(path).Trim();
if (string.IsNullOrEmpty(content))
return false;
base64 = content;
return true;
}
catch
{
return false;
}
}
}
}
```
Avoid: throwing exceptions from TryRead — callers expect bool pattern.
```bash
ls Licensing/IMachineFingerprint.cs Licensing/ILicenseValidator.cs Licensing/LicenseFile.cs
grep -n "namespace NcProgramManager.Licensing" Licensing/IMachineFingerprint.cs Licensing/ILicenseValidator.cs Licensing/LicenseFile.cs
```
All three files exist with correct namespace.
AC-4 satisfied: LicenseFile.TryRead handles missing/empty file without exception
Task 2: Implement MachineFingerprint and LicenseValidator
Licensing/MachineFingerprint.cs,
Licensing/LicenseValidator.cs,
NcProgramManager.csproj
**Licensing/MachineFingerprint.cs**
Extract the WMI fingerprint logic from `Program.checkLicense`. Do NOT modify Program.cs yet — that is Phase 08.
```csharp
using System.Management;
namespace NcProgramManager.Licensing
{
internal class MachineFingerprint : IMachineFingerprint
{
public string GetFingerprint()
{
string pcInfo = string.Empty;
using (var mc = new ManagementClass("win32_processor"))
using (ManagementObjectCollection moc = mc.GetInstances())
{
foreach (ManagementObject mo in moc)
{
pcInfo = mo.Properties["processorID"].Value.ToString();
break;
}
}
using (var searcher = new ManagementObjectSearcher(
"root\\CIMV2", "SELECT * FROM Win32_BaseBoard"))
{
foreach (ManagementObject obj in searcher.Get())
{
pcInfo += obj["SerialNumber"].ToString();
break;
}
}
return pcInfo;
}
}
}
```
**Licensing/LicenseValidator.cs**
RSA-SHA256 verify. Public key is a placeholder XML constant — Phase 09 (generator) produces the real key pair; replace `PublicKeyXml` at that point.
```csharp
using System;
using System.Security.Cryptography;
using System.Text;
namespace NcProgramManager.Licensing
{
internal class LicenseValidator : ILicenseValidator
{
// Placeholder — replace with real vendor public key XML after Phase 09 generates the key pair.
// Generate with: openssl genrsa 2048 | openssl rsa -pubout, then convert PEM → XML,
// or use RSACryptoServiceProvider.ToXmlString(false) from the generator tool.
private const string PublicKeyXml =
"" +
"PLACEHOLDER" +
"AQAB" +
"";
private readonly string _publicKeyXml;
// Public constructor: uses embedded vendor public key.
internal LicenseValidator() : this(PublicKeyXml) { }
// Internal constructor: accepts any public key XML — used in unit tests.
internal LicenseValidator(string publicKeyXml)
{
_publicKeyXml = publicKeyXml;
}
public bool Validate(string fingerprint, string licenseBase64)
{
try
{
byte[] signature = Convert.FromBase64String(licenseBase64);
byte[] data = Encoding.UTF8.GetBytes(fingerprint);
using (var rsa = new RSACryptoServiceProvider())
{
rsa.FromXmlString(_publicKeyXml);
return rsa.VerifyData(data, new SHA256CryptoServiceProvider(), signature);
}
}
catch
{
return false;
}
}
}
}
```
Note: `RSACryptoServiceProvider.VerifyData(byte[], object, byte[])` — the hash algorithm
argument is `new SHA256CryptoServiceProvider()`, not `HashAlgorithmName` (that overload
requires .NET 4.6+ RSA base class; `RSACryptoServiceProvider` overload uses the older
`object` signature which is available in .NET 4.7.2).
**NcProgramManager.csproj** — add 5 Compile entries after last existing `` in the
`Licensing` group (create new ItemGroup if none exists):
```xml
```
System.Management reference already present — no new reference needed.
```bash
ls Licensing/MachineFingerprint.cs Licensing/LicenseValidator.cs
grep "LicenseValidator\|MachineFingerprint\|LicenseFile\|IMachineFingerprint\|ILicenseValidator" NcProgramManager.csproj | grep "Compile"
```
Both files exist. Five Compile entries present in csproj.
AC-1 satisfied: MachineFingerprint implements IMachineFingerprint with WMI logic. AC-2/AC-3 groundwork: LicenseValidator implements ILicenseValidator with RSA verify.
Task 3: Unit tests for LicenseValidator and LicenseFile
NcProgramManager.Tests/Unit/LicenseValidatorTests.cs,
NcProgramManager.Tests/NcProgramManager.Tests.csproj
Tests use the internal `LicenseValidator(string publicKeyXml)` constructor to inject a
test-generated key pair — same pattern as NcProgramValidatorTests.
```csharp
using System;
using System.Security.Cryptography;
using System.Text;
using NUnit.Framework;
using NcProgramManager.Licensing;
namespace NcProgramManager.Tests.Unit
{
[TestFixture]
public class LicenseValidatorTests
{
private string _publicKeyXml;
private string _privateKeyXml;
private const string TestFingerprint = "BFEBFBFF000906EAM80-80001234";
[SetUp]
public void SetUp()
{
using (var rsa = new RSACryptoServiceProvider(2048))
{
_publicKeyXml = rsa.ToXmlString(false); // public only
_privateKeyXml = rsa.ToXmlString(true); // includes private
}
}
private string Sign(string fingerprint)
{
using (var rsa = new RSACryptoServiceProvider())
{
rsa.FromXmlString(_privateKeyXml);
byte[] sig = rsa.SignData(
Encoding.UTF8.GetBytes(fingerprint),
new SHA256CryptoServiceProvider());
return Convert.ToBase64String(sig);
}
}
[Test]
public void Validate_ValidLicense_ReturnsTrue()
{
string licenseB64 = Sign(TestFingerprint);
var validator = new LicenseValidator(_publicKeyXml);
Assert.IsTrue(validator.Validate(TestFingerprint, licenseB64));
}
[Test]
public void Validate_WrongMachineFingerprint_ReturnsFalse()
{
string licenseB64 = Sign("DIFFERENT_MACHINE_ID");
var validator = new LicenseValidator(_publicKeyXml);
Assert.IsFalse(validator.Validate(TestFingerprint, licenseB64));
}
[Test]
public void Validate_TamperedLicense_ReturnsFalse()
{
string licenseB64 = Sign(TestFingerprint);
// Flip one char
char[] chars = licenseB64.ToCharArray();
chars[10] = chars[10] == 'A' ? 'B' : 'A';
var validator = new LicenseValidator(_publicKeyXml);
Assert.IsFalse(validator.Validate(TestFingerprint, new string(chars)));
}
[Test]
public void Validate_EmptyLicense_ReturnsFalse()
{
var validator = new LicenseValidator(_publicKeyXml);
Assert.IsFalse(validator.Validate(TestFingerprint, ""));
}
[Test]
public void Validate_NotBase64_ReturnsFalse()
{
var validator = new LicenseValidator(_publicKeyXml);
Assert.IsFalse(validator.Validate(TestFingerprint, "not-valid-base64!!!"));
}
[Test]
public void LicenseFile_TryRead_NonExistentPath_ReturnsFalse()
{
bool result = LicenseFile.TryRead(@"C:\does\not\exist.lic", out string b64);
Assert.IsFalse(result);
Assert.IsNull(b64);
}
}
}
```
**NcProgramManager.Tests/NcProgramManager.Tests.csproj** — add 1 Compile entry:
```xml
```
Avoid: creating real .lic files on disk in tests — use in-memory signing only.
Note: `InternalsVisibleTo("NcProgramManager.Tests")` already present in AssemblyInfo.cs
from Phase 5 — no change needed.
```bash
ls NcProgramManager.Tests/Unit/LicenseValidatorTests.cs
grep "LicenseValidatorTests" NcProgramManager.Tests/NcProgramManager.Tests.csproj
grep "LicenseValidatorTests\|LicenseFile" NcProgramManager.Tests/Unit/LicenseValidatorTests.cs | head -5
```
File exists. Compile entry present. Class contains both LicenseValidator and LicenseFile tests.
AC-2 satisfied: valid license returns true. AC-3 satisfied: wrong machine returns false. AC-4 satisfied: missing file returns false.
## DO NOT CHANGE
- `Program.cs` — checkLicense stays untouched until Phase 08
- `InputArgs.cs` — `-chiave=` field stays until Phase 08
- `AesCrypt.cs` — still used by program file encryption, not licensing
- All existing machine implementations (`Cnc/` folder)
- Existing test files
## SCOPE LIMITS
- No wiring into Program.cs (Phase 08)
- No license generator tool (Phase 09)
- No CLI changes (`-licfile=` arg is Phase 08)
- Public key stays as placeholder — real key generated in Phase 09
- `MachineFingerprint` is new class; do NOT delete WMI code from Program.cs yet
Before declaring plan complete:
- [ ] `ls Licensing/*.cs` shows 5 files
- [ ] `grep "Compile" NcProgramManager.csproj | grep Licensing` shows 5 entries
- [ ] `ls NcProgramManager.Tests/Unit/LicenseValidatorTests.cs` exists
- [ ] `grep "LicenseValidatorTests" NcProgramManager.Tests/NcProgramManager.Tests.csproj` finds entry
- [ ] `grep "NcProgramManager.Licensing" Licensing/*.cs` — all files correct namespace
- [ ] All acceptance criteria met
- All tasks completed
- All verification checks pass
- No changes to Program.cs, InputArgs.cs, or any existing .cs file outside Licensing/
- 6 new files created, 2 csproj files updated