Phase 07 complete: - IMachineFingerprint + ILicenseValidator interfaces - MachineFingerprint: WMI CPU ProcessorID + BaseBoard SerialNumber - LicenseValidator: RSACryptoServiceProvider + SHA256, injectable public key - LicenseFile.TryRead: out-param bool, no exceptions from file I/O - 6 NUnit tests (valid/wrong-machine/tampered/empty/not-base64/missing-file) - 118 tests total, 116 pass, 2 Fanuc FOCAS ignored (Windows DLL) - Fix: missing 'using System' in CncMachineFactoryTests.cs - Fix: Dockerfile.test updated for NcProgramManager rename PublicKeyXml = PLACEHOLDER until Phase 09 generates real key pair. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
15 KiB
15 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 07-license-core | 01 | execute | 1 |
|
true |
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 filesLicenseValidatorverifies RSA-SHA256 signature of machine fingerprint against embedded public keyMachineFingerprintreads CPU ProcessorID + Motherboard SerialNumber via WMI (same data as currentcheckLicense)LicenseFilereads base64 signature from a.licfile on disk- Unit tests: valid license passes, tampered/wrong-machine license fails
- 5 Compile entries added to main csproj, 1 to tests csproj
Source Files (existing license logic to extract from)
@Program.cs @NcProgramManager.csproj
<acceptance_criteria>
AC-1: Machine fingerprint readable via interface
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
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
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
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)
</acceptance_criteria>
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 =
"<RSAKeyValue>" +
"<Modulus>PLACEHOLDER</Modulus>" +
"<Exponent>AQAB</Exponent>" +
"</RSAKeyValue>";
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 `<Compile>` in the
`Licensing` group (create new ItemGroup if none exists):
```xml
<Compile Include="Licensing\IMachineFingerprint.cs" />
<Compile Include="Licensing\ILicenseValidator.cs" />
<Compile Include="Licensing\LicenseFile.cs" />
<Compile Include="Licensing\MachineFingerprint.cs" />
<Compile Include="Licensing\LicenseValidator.cs" />
```
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
<Compile Include="Unit\LicenseValidatorTests.cs" />
```
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 08InputArgs.cs—-chiave=field stays until Phase 08AesCrypt.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
MachineFingerprintis new class; do NOT delete WMI code from Program.cs yet
<success_criteria>
- 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 </success_criteria>