feat(07-license-core): RSA offline license core
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>
This commit is contained in:
parent
a5d73db69a
commit
39a9566858
16 changed files with 851 additions and 42 deletions
|
|
@ -13,8 +13,8 @@ Machinists and operators transfer CNC programs to/from any controller without ma
|
||||||
| Attribute | Value |
|
| Attribute | Value |
|
||||||
|-----------|-------|
|
|-----------|-------|
|
||||||
| Type | Application |
|
| Type | Application |
|
||||||
| Version | 0.1.0 |
|
| Version | 0.2.0-dev |
|
||||||
| Status | v0.1 complete — pending hardware testing |
|
| Status | v0.2 in progress — Phase 07 complete |
|
||||||
| Last Updated | 2026-05-17 |
|
| Last Updated | 2026-05-17 |
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
@ -36,8 +36,13 @@ Machinists and operators transfer CNC programs to/from any controller without ma
|
||||||
- [x] NC program validation before upload (all 4 manufacturers) — Phase 5
|
- [x] NC program validation before upload (all 4 manufacturers) — Phase 5
|
||||||
- [x] Project renamed FanucProgramManager → NcProgramManager — Phase 6
|
- [x] Project renamed FanucProgramManager → NcProgramManager — Phase 6
|
||||||
|
|
||||||
|
### Validated (Shipped) — continued
|
||||||
|
- [x] RSA offline license core: IMachineFingerprint, ILicenseValidator, LicenseValidator, LicenseFile — Phase 7
|
||||||
|
|
||||||
### Planned (Next)
|
### Planned (Next)
|
||||||
- [ ] Hardware fingerprint license system (CPU ID + disk serial + encrypted file)
|
- [ ] Wire license into Program.cs — replace checkLicense, replace -chiave= with -licfile= (Phase 08)
|
||||||
|
- [ ] License generator tool — sign fingerprint with RSA private key, write .lic file (Phase 09)
|
||||||
|
- [ ] Replace PublicKeyXml PLACEHOLDER with real vendor key pair (Phase 09 output)
|
||||||
- [ ] ReadActiveProgramAsync / SelectMainProgram for Heidenhain and Siemens
|
- [ ] ReadActiveProgramAsync / SelectMainProgram for Heidenhain and Siemens
|
||||||
- [ ] Integration test against real hardware (Heidenhain, Siemens, Mitsubishi)
|
- [ ] Integration test against real hardware (Heidenhain, Siemens, Mitsubishi)
|
||||||
|
|
||||||
|
|
@ -68,6 +73,8 @@ Machinists and operators transfer CNC programs to/from any controller without ma
|
||||||
| Offline license only | Operators may have no internet on shop floor | 2026-05-13 | Active |
|
| Offline license only | Operators may have no internet on shop floor | 2026-05-13 | Active |
|
||||||
| `#` = Warning in Fanuc/Mitsubishi | Macro variable indicator — valid in context | 2026-05-13 | Active |
|
| `#` = Warning in Fanuc/Mitsubishi | Macro variable indicator — valid in context | 2026-05-13 | Active |
|
||||||
| Constructor injection for validator | Internal ctor accepts mock — enables unit testing without Windows DLL | 2026-05-13 | Active |
|
| Constructor injection for validator | Internal ctor accepts mock — enables unit testing without Windows DLL | 2026-05-13 | Active |
|
||||||
|
| RSA asymmetric licensing | Private key vendor-only; public key embedded in binary — decompiler can't forge licenses | 2026-05-17 | Active |
|
||||||
|
| PublicKeyXml PLACEHOLDER until Phase 09 | Real key pair not yet generated; prevents blocking Phase 07 on Phase 09 | 2026-05-17 | Active |
|
||||||
| Heidenhain LSV2 port 19000 | Standard LSV2 port; needs real hardware verification | 2026-05-13 | Active |
|
| Heidenhain LSV2 port 19000 | Standard LSV2 port; needs real hardware verification | 2026-05-13 | Active |
|
||||||
| Siemens FTP dir `/_N_MPF_DIR/` | Sinumerik standard MPF directory; FTP option must be enabled | 2026-05-13 | Active |
|
| Siemens FTP dir `/_N_MPF_DIR/` | Sinumerik standard MPF directory; FTP option must be enabled | 2026-05-13 | Active |
|
||||||
|
|
||||||
|
|
@ -76,7 +83,7 @@ Machinists and operators transfer CNC programs to/from any controller without ma
|
||||||
| Metric | Target | Current | Status |
|
| Metric | Target | Current | Status |
|
||||||
|--------|--------|---------|--------|
|
|--------|--------|---------|--------|
|
||||||
| Program transfer correctness | 100% (byte-verified) | Unknown | Pending hardware test |
|
| Program transfer correctness | 100% (byte-verified) | Unknown | Pending hardware test |
|
||||||
| License validation (offline) | Works without network | Hardcoded | At risk — v0.2 target |
|
| License validation (offline) | Works without network | RSA core built, wiring Phase 08 | In progress |
|
||||||
| Multi-CN support | ≥2 manufacturers | 4 (Fanuc/Heidenhain/Siemens/Mitsubishi) | Done |
|
| Multi-CN support | ≥2 manufacturers | 4 (Fanuc/Heidenhain/Siemens/Mitsubishi) | Done |
|
||||||
| NC validation coverage | All manufacturers | 4 manufacturers, 30+ tests | Done |
|
| NC validation coverage | All manufacturers | 4 manufacturers, 30+ tests | Done |
|
||||||
|
|
||||||
|
|
@ -92,9 +99,9 @@ Machinists and operators transfer CNC programs to/from any controller without ma
|
||||||
| CN Protocol (Mitsubishi) | FTP (FtpWebRequest) | Port 21, `/PRG/`, no extensions |
|
| CN Protocol (Mitsubishi) | FTP (FtpWebRequest) | Port 21, `/PRG/`, no extensions |
|
||||||
| Validation | NcProgramValidator | Universal + per-manufacturer rules |
|
| Validation | NcProgramValidator | Universal + per-manufacturer rules |
|
||||||
| Encryption | AES (AesCrypt.cs) | Program file protection |
|
| Encryption | AES (AesCrypt.cs) | Program file protection |
|
||||||
| Licensing | Machine fingerprint | Hardcoded now, v0.2 target |
|
| Licensing | RSA-SHA256 asymmetric | IMachineFingerprint + ILicenseValidator; wiring in Phase 08 |
|
||||||
| Testing | NUnit 3.13.3 + Moq 4.18.4 | .NET 4.7.2 test project |
|
| Testing | NUnit 3.13.3 + Moq 4.18.4 | .NET 4.7.2 test project |
|
||||||
|
|
||||||
---
|
---
|
||||||
*PROJECT.md — Updated when requirements or context change*
|
*PROJECT.md — Updated when requirements or context change*
|
||||||
*Last updated: 2026-05-17 after Phase 6 (Rename Cleanup)*
|
*Last updated: 2026-05-17 after Phase 7 (License Core)*
|
||||||
|
|
|
||||||
|
|
@ -85,13 +85,13 @@ Completed: 2026-05-13
|
||||||
## Next Milestone
|
## Next Milestone
|
||||||
|
|
||||||
**v0.2 NcProgramManager** (in progress)
|
**v0.2 NcProgramManager** (in progress)
|
||||||
Status: Planning
|
Status: In Progress
|
||||||
Phases: 0 of 4 complete
|
Phases: 2 of 4 complete
|
||||||
|
|
||||||
| Phase | Name | Plans | Status |
|
| Phase | Name | Plans | Status |
|
||||||
|-------|------|-------|--------|
|
|-------|------|-------|--------|
|
||||||
| 06 | rename-cleanup | 1 | ✅ Complete |
|
| 06 | rename-cleanup | 1 | ✅ Complete |
|
||||||
| 07 | license-core | 1 | Not started |
|
| 07 | license-core | 1 | ✅ Complete |
|
||||||
| 08 | license-integration | 1 | Not started |
|
| 08 | license-integration | 1 | Not started |
|
||||||
| 09 | license-generator | 1 | Not started |
|
| 09 | license-generator | 1 | Not started |
|
||||||
|
|
||||||
|
|
@ -100,9 +100,10 @@ Phases: 0 of 4 complete
|
||||||
**Completed:** 2026-05-17
|
**Completed:** 2026-05-17
|
||||||
**Plans:** [x] 06-01: Namespace + file rename
|
**Plans:** [x] 06-01: Namespace + file rename
|
||||||
|
|
||||||
### Phase 07: license-core (proposed)
|
### Phase 07: license-core ✅
|
||||||
**Goal:** `IMachineFingerprint`, `ILicenseValidator`, RSA signature verification, `.lic` file model
|
**Goal:** `IMachineFingerprint`, `ILicenseValidator`, RSA signature verification, `.lic` file model
|
||||||
**Depends on:** Phase 06
|
**Completed:** 2026-05-17
|
||||||
|
**Plans:** [x] 07-01: RSA core + interfaces + 6 unit tests
|
||||||
|
|
||||||
### Phase 08: license-integration (proposed)
|
### Phase 08: license-integration (proposed)
|
||||||
**Goal:** Wire license system into Program.cs, replace `checkLicense`, replace `-chiave=` with `-licfile=`
|
**Goal:** Wire license system into Program.cs, replace `checkLicense`, replace `-chiave=` with `-licfile=`
|
||||||
|
|
@ -113,4 +114,4 @@ Phases: 0 of 4 complete
|
||||||
**Depends on:** Phase 07
|
**Depends on:** Phase 07
|
||||||
|
|
||||||
---
|
---
|
||||||
*Roadmap updated: 2026-05-17 — v0.1 milestone complete*
|
*Roadmap updated: 2026-05-17 — Phase 07 license-core complete*
|
||||||
|
|
|
||||||
|
|
@ -10,15 +10,15 @@ See: .paul/PROJECT.md (updated 2026-05-17)
|
||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Milestone: v0.2 NcProgramManager
|
Milestone: v0.2 NcProgramManager
|
||||||
Phase: 1 of 4 (06-rename-cleanup) — Complete
|
Phase: 3 of 4 (08-license-integration) — Not started
|
||||||
Plan: 06-01 unified
|
Plan: Not started
|
||||||
Status: Phase 06 complete, ready to plan Phase 07
|
Status: Ready to plan Phase 08
|
||||||
Last activity: 2026-05-17 — Phase 06 rename-cleanup unified
|
Last activity: 2026-05-17 — Phase 07 complete, transitioned to Phase 08
|
||||||
|
|
||||||
Progress:
|
Progress:
|
||||||
- Milestone v0.1: [██████████] 100% (complete)
|
- Milestone v0.1: [██████████] 100% (complete)
|
||||||
- Milestone v0.2: [██░░░░░░░░] 25%
|
- Milestone v0.2: [█████░░░░░] 50%
|
||||||
- Phase 06: [██████████] 100%
|
- Phase 08: [░░░░░░░░░░] 0%
|
||||||
|
|
||||||
## Loop Position
|
## Loop Position
|
||||||
|
|
||||||
|
|
@ -71,27 +71,32 @@ PLAN ──▶ APPLY ──▶ UNIFY
|
||||||
|
|
||||||
### Decisions
|
### Decisions
|
||||||
- .NET 4.7.2 locked — max compatibility
|
- .NET 4.7.2 locked — max compatibility
|
||||||
- Offline-only license check (AES, unchanged)
|
- RSA asymmetric offline licensing: private key vendor-only, public key embedded in binary
|
||||||
|
- PublicKeyXml = PLACEHOLDER until Phase 09 generates real key pair
|
||||||
|
- `RSACryptoServiceProvider` + `SHA256CryptoServiceProvider` (not HashAlgorithmName) — .NET 4.7.2 compat
|
||||||
|
- Constructor injection pattern: internal ctor accepts key XML — testable without Windows WMI
|
||||||
- Heidenhain: LSV2 port 19000, needs real hardware testing
|
- Heidenhain: LSV2 port 19000, needs real hardware testing
|
||||||
- Siemens: FTP `/_N_MPF_DIR/`, needs Sinumerik FTP option enabled on machine
|
- Siemens: FTP `/_N_MPF_DIR/`, needs Sinumerik FTP option enabled on machine
|
||||||
- `#` = Warning in Fanuc/Mitsubishi — macro variable indicator
|
- `#` = Warning in Fanuc/Mitsubishi — macro variable indicator
|
||||||
- Constructor injection for validator: internal ctor accepts mock — testability without Windows DLL
|
|
||||||
|
|
||||||
### Deferred Issues
|
### Deferred Issues
|
||||||
- License system: hardcoded AES key/IV — v0.2 target (hardware fingerprint + encrypted file)
|
- License wiring into Program.cs — Phase 08 target
|
||||||
|
- License generator tool — Phase 09 target
|
||||||
|
- Real RSA key pair — Phase 09 output; copy PublicKeyXml into LicenseValidator.cs
|
||||||
- Heidenhain: `ReadActiveProgramAsync` / `SelectMainProgram` not implemented
|
- Heidenhain: `ReadActiveProgramAsync` / `SelectMainProgram` not implemented
|
||||||
- Siemens: same as Heidenhain
|
- Siemens: same as Heidenhain
|
||||||
- Build + integration not verifiable on Linux (fwlib32 Windows-only)
|
- Build + integration not verifiable on Linux (fwlib32 Windows-only)
|
||||||
|
- MachineFingerprint WMI — code-review verified, needs real Windows hardware test
|
||||||
|
|
||||||
### Blockers/Concerns
|
### Blockers/Concerns
|
||||||
None — code complete, pending hardware testing on real machines.
|
None — Phase 08 can start.
|
||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-05-17
|
Last session: 2026-05-17
|
||||||
Stopped at: Phase 06 complete — rename-cleanup unified
|
Stopped at: Phase 07 complete (UNIFY done)
|
||||||
Next action: /paul:plan for Phase 07 (license-core)
|
Next action: /paul:plan for Phase 08 (license-integration)
|
||||||
Resume file: .paul/phases/06-rename-cleanup/06-01-SUMMARY.md
|
Resume file: .paul/ROADMAP.md
|
||||||
|
|
||||||
---
|
---
|
||||||
*STATE.md — Updated after every significant action*
|
*STATE.md — Updated after every significant action*
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
{
|
{
|
||||||
"name": "FanucProgramManager",
|
"name": "NcProgramManager",
|
||||||
"version": "0.0.0",
|
"version": "0.2.0-dev",
|
||||||
"milestone": {
|
"milestone": {
|
||||||
"name": "None",
|
"name": "v0.2 NcProgramManager",
|
||||||
"version": "0.0.0",
|
"version": "0.2.0",
|
||||||
"status": "not_started"
|
"status": "in_progress"
|
||||||
},
|
},
|
||||||
"phase": {
|
"phase": {
|
||||||
"number": 0,
|
"number": 7,
|
||||||
"name": "None",
|
"name": "license-core",
|
||||||
"status": "not_started"
|
"status": "complete"
|
||||||
},
|
},
|
||||||
"loop": {
|
"loop": {
|
||||||
"plan": null,
|
"plan": null,
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
},
|
},
|
||||||
"timestamps": {
|
"timestamps": {
|
||||||
"created_at": "2026-05-13T00:00:00Z",
|
"created_at": "2026-05-13T00:00:00Z",
|
||||||
"updated_at": "2026-05-13T00:00:00Z"
|
"updated_at": "2026-05-17T21:00:00Z"
|
||||||
},
|
},
|
||||||
"satellite": {
|
"satellite": {
|
||||||
"groom": true
|
"groom": true
|
||||||
|
|
|
||||||
441
.paul/phases/07-license-core/07-01-PLAN.md
Normal file
441
.paul/phases/07-license-core/07-01-PLAN.md
Normal file
|
|
@ -0,0 +1,441 @@
|
||||||
|
---
|
||||||
|
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
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
## 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
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
## Project Context
|
||||||
|
@.paul/PROJECT.md
|
||||||
|
@.paul/STATE.md
|
||||||
|
|
||||||
|
## Source Files (existing license logic to extract from)
|
||||||
|
@Program.cs
|
||||||
|
@NcProgramManager.csproj
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<acceptance_criteria>
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
```
|
||||||
|
|
||||||
|
</acceptance_criteria>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Create Licensing interfaces and LicenseFile</name>
|
||||||
|
<files>
|
||||||
|
Licensing/IMachineFingerprint.cs,
|
||||||
|
Licensing/ILicenseValidator.cs,
|
||||||
|
Licensing/LicenseFile.cs
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
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.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
```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.
|
||||||
|
</verify>
|
||||||
|
<done>AC-4 satisfied: LicenseFile.TryRead handles missing/empty file without exception</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Implement MachineFingerprint and LicenseValidator</name>
|
||||||
|
<files>
|
||||||
|
Licensing/MachineFingerprint.cs,
|
||||||
|
Licensing/LicenseValidator.cs,
|
||||||
|
NcProgramManager.csproj
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
**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.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
```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.
|
||||||
|
</verify>
|
||||||
|
<done>AC-1 satisfied: MachineFingerprint implements IMachineFingerprint with WMI logic. AC-2/AC-3 groundwork: LicenseValidator implements ILicenseValidator with RSA verify.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Unit tests for LicenseValidator and LicenseFile</name>
|
||||||
|
<files>
|
||||||
|
NcProgramManager.Tests/Unit/LicenseValidatorTests.cs,
|
||||||
|
NcProgramManager.Tests/NcProgramManager.Tests.csproj
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
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.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
```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.
|
||||||
|
</verify>
|
||||||
|
<done>AC-2 satisfied: valid license returns true. AC-3 satisfied: wrong machine returns false. AC-4 satisfied: missing file returns false.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<boundaries>
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
</boundaries>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
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
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.paul/phases/07-license-core/07-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
148
.paul/phases/07-license-core/07-01-SUMMARY.md
Normal file
148
.paul/phases/07-license-core/07-01-SUMMARY.md
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
---
|
||||||
|
phase: 07-license-core
|
||||||
|
plan: 01
|
||||||
|
subsystem: licensing
|
||||||
|
tags: [rsa, cryptography, wmi, fingerprint, unit-tests, net472]
|
||||||
|
|
||||||
|
requires:
|
||||||
|
- phase: 06-rename-cleanup
|
||||||
|
provides: NcProgramManager namespace + renamed csproj files
|
||||||
|
|
||||||
|
provides:
|
||||||
|
- IMachineFingerprint interface + MachineFingerprint WMI implementation
|
||||||
|
- ILicenseValidator interface + LicenseValidator RSA-SHA256 verifier
|
||||||
|
- LicenseFile.TryRead helper
|
||||||
|
- 6 unit tests covering valid/invalid/tampered/missing license scenarios
|
||||||
|
|
||||||
|
affects:
|
||||||
|
- 08-license-integration (wires these into Program.cs)
|
||||||
|
- 09-license-generator (produces real key pair; PublicKeyXml PLACEHOLDER replaced here)
|
||||||
|
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- Constructor injection for testability (internal ctor accepts public key XML)
|
||||||
|
- RSACryptoServiceProvider + SHA256CryptoServiceProvider for .NET 4.7.2 compat
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- Licensing/IMachineFingerprint.cs
|
||||||
|
- Licensing/ILicenseValidator.cs
|
||||||
|
- Licensing/LicenseFile.cs
|
||||||
|
- Licensing/MachineFingerprint.cs
|
||||||
|
- Licensing/LicenseValidator.cs
|
||||||
|
- NcProgramManager.Tests/Unit/LicenseValidatorTests.cs
|
||||||
|
modified:
|
||||||
|
- NcProgramManager.csproj
|
||||||
|
- NcProgramManager.Tests/NcProgramManager.Tests.csproj
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "RSA asymmetric: private key vendor-only, public key embedded in binary"
|
||||||
|
- "PublicKeyXml is PLACEHOLDER until Phase 09 generates real key pair"
|
||||||
|
- "Constructor injection: internal LicenseValidator(string publicKeyXml) for test isolation"
|
||||||
|
- "SHA256CryptoServiceProvider (not HashAlgorithmName) — .NET 4.7.2 RSACryptoServiceProvider overload"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "LicenseValidator(string publicKeyXml) internal ctor mirrors NcProgramValidator injection pattern"
|
||||||
|
- "TryRead out-param bool pattern: no exceptions escape from file I/O"
|
||||||
|
|
||||||
|
duration: ~2h (split across sessions)
|
||||||
|
started: 2026-05-17T00:00:00Z
|
||||||
|
completed: 2026-05-17T21:00:00Z
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 07 Plan 01: license-core Summary
|
||||||
|
|
||||||
|
**RSA offline license core shipped: IMachineFingerprint + ILicenseValidator interfaces, WMI fingerprint reader, RSA-SHA256 verifier with injectable key, LicenseFile TryRead helper — 118 tests pass (116 run, 2 Fanuc FOCAS ignored).**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|--------|-------|
|
||||||
|
| Duration | ~2h |
|
||||||
|
| Started | 2026-05-17 |
|
||||||
|
| Completed | 2026-05-17 |
|
||||||
|
| Tasks | 3 completed |
|
||||||
|
| Files modified | 8 |
|
||||||
|
|
||||||
|
## Acceptance Criteria Results
|
||||||
|
|
||||||
|
| Criterion | Status | Notes |
|
||||||
|
|-----------|--------|-------|
|
||||||
|
| AC-1: Machine fingerprint readable via interface | Pass | MachineFingerprint : IMachineFingerprint — WMI logic extracted (not testable on Linux; verified via code review) |
|
||||||
|
| AC-2: Valid license passes validation | Pass | Validate_ValidLicense_ReturnsTrue — in-memory RSA sign+verify |
|
||||||
|
| AC-3: Invalid license rejected | Pass | Validate_WrongMachineFingerprint_ReturnsFalse, Validate_TamperedLicense_ReturnsFalse |
|
||||||
|
| AC-4: Corrupted/missing license file handled | Pass | LicenseFile_TryRead_NonExistentPath_ReturnsFalse |
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- 5 new files in `Licensing/` — interfaces, implementations, file helper — clean DI-ready layer
|
||||||
|
- 6 LicenseValidator tests all pass in Docker (Mono 6.12 / .NET 4.7.2)
|
||||||
|
- PublicKeyXml PLACEHOLDER documented — Phase 09 replaces with real key pair
|
||||||
|
- Full test suite: 118 total, 116 pass, 2 ignored (Fanuc FOCAS, Windows-only DLL, expected)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
| File | Change | Purpose |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `Licensing/IMachineFingerprint.cs` | Created | Interface for fingerprint provider |
|
||||||
|
| `Licensing/ILicenseValidator.cs` | Created | Interface for RSA validator |
|
||||||
|
| `Licensing/LicenseFile.cs` | Created | TryRead: reads .lic base64, returns false on missing/empty |
|
||||||
|
| `Licensing/MachineFingerprint.cs` | Created | WMI: CPU ProcessorID + BaseBoard SerialNumber |
|
||||||
|
| `Licensing/LicenseValidator.cs` | Created | RSACryptoServiceProvider VerifyData with SHA256 |
|
||||||
|
| `NcProgramManager.Tests/Unit/LicenseValidatorTests.cs` | Created | 6 NUnit tests; generates ephemeral key pair in SetUp |
|
||||||
|
| `NcProgramManager.csproj` | Modified | +5 Compile entries for Licensing/*.cs |
|
||||||
|
| `NcProgramManager.Tests/NcProgramManager.Tests.csproj` | Modified | +1 Compile entry for LicenseValidatorTests.cs |
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
| Decision | Rationale | Impact |
|
||||||
|
|----------|-----------|--------|
|
||||||
|
| RSA asymmetric (not AES) | Private key vendor-only; embedded public key safe to decompile | Phase 09 must generate real 2048-bit key pair |
|
||||||
|
| `RSACryptoServiceProvider` + `SHA256CryptoServiceProvider` | .NET 4.7.2: `VerifyData(byte[], object, byte[])` overload — `HashAlgorithmName` variant requires RSA base class | Locked to this API; no upgrade path issue |
|
||||||
|
| PublicKeyXml = PLACEHOLDER | Real key not yet generated; prevents Phase 07 from blocking on Phase 09 | Phase 09 outputs real XML; copy-paste into LicenseValidator.cs |
|
||||||
|
| Internal constructor injection | Testable without Windows WMI DLL | Same pattern as NcProgramValidator — consistent across codebase |
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
| Type | Count | Impact |
|
||||||
|
|------|-------|--------|
|
||||||
|
| Auto-fixed | 1 | Pre-existing bug, zero scope creep |
|
||||||
|
| Scope additions | 0 | — |
|
||||||
|
| Deferred | 0 | — |
|
||||||
|
|
||||||
|
### Auto-fixed Issues
|
||||||
|
|
||||||
|
**1. Build — Missing `using System;` in CncMachineFactoryTests.cs**
|
||||||
|
- **Found during:** Docker build (msbuild step)
|
||||||
|
- **Issue:** `InvalidOperationException` unresolved — `using System;` absent from `CncMachineFactoryTests.cs`
|
||||||
|
- **Fix:** Added `using System;` at top of file
|
||||||
|
- **Files:** `NcProgramManager.Tests/Unit/CncMachineFactoryTests.cs`
|
||||||
|
- **Verification:** Docker build passed; all 118 tests ran
|
||||||
|
|
||||||
|
**2. Dockerfile.test — Stale FanucProgramManager paths**
|
||||||
|
- **Found during:** First Docker build attempt
|
||||||
|
- **Issue:** Dockerfile.test still referenced `FanucProgramManager.*` paths from before Phase 06 rename
|
||||||
|
- **Fix:** Updated all paths to `NcProgramManager.*`
|
||||||
|
- **Files:** `Dockerfile.test`
|
||||||
|
- **Verification:** Docker build and test run succeeded
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
**Ready:**
|
||||||
|
- `IMachineFingerprint` / `ILicenseValidator` injectable into any caller
|
||||||
|
- `LicenseValidator` verified correct with real RSA operations
|
||||||
|
- `LicenseFile.TryRead` ready for `-licfile=` arg wiring
|
||||||
|
|
||||||
|
**Concerns:**
|
||||||
|
- PublicKeyXml is PLACEHOLDER — production build will silently reject all licenses until Phase 09 provides real key and it's copied in
|
||||||
|
- `MachineFingerprint` uses WMI — only tested via code review on Linux; validate on Windows hardware
|
||||||
|
|
||||||
|
**Blockers:**
|
||||||
|
- None — Phase 08 can proceed
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 07-license-core, Plan: 01*
|
||||||
|
*Completed: 2026-05-17*
|
||||||
|
|
@ -6,16 +6,16 @@ WORKDIR /build
|
||||||
RUN cert-sync /etc/ssl/certs/ca-certificates.crt
|
RUN cert-sync /etc/ssl/certs/ca-certificates.crt
|
||||||
|
|
||||||
# Copy solution + projects first for layer caching
|
# Copy solution + projects first for layer caching
|
||||||
COPY FanucProgramManager.csproj ./
|
COPY NcProgramManager.csproj ./
|
||||||
COPY packages.config ./
|
COPY packages.config ./
|
||||||
COPY FanucProgramManager.Tests/FanucProgramManager.Tests.csproj ./FanucProgramManager.Tests/
|
COPY NcProgramManager.Tests/NcProgramManager.Tests.csproj ./NcProgramManager.Tests/
|
||||||
COPY FanucProgramManager.Tests/packages.config ./FanucProgramManager.Tests/
|
COPY NcProgramManager.Tests/packages.config ./NcProgramManager.Tests/
|
||||||
COPY FanucProgramManager.sln ./
|
COPY NcProgramManager.sln ./
|
||||||
|
|
||||||
# Restore all packages (sln now includes test project)
|
# Restore all packages (sln now includes test project)
|
||||||
RUN nuget restore FanucProgramManager.sln -NonInteractive
|
RUN nuget restore NcProgramManager.sln -NonInteractive
|
||||||
# Explicit fallback: restore test packages.config directly to same packages dir
|
# Explicit fallback: restore test packages.config directly to same packages dir
|
||||||
RUN nuget restore FanucProgramManager.Tests/packages.config \
|
RUN nuget restore NcProgramManager.Tests/packages.config \
|
||||||
-OutputDirectory packages -NonInteractive
|
-OutputDirectory packages -NonInteractive
|
||||||
# Grab NUnit console runner
|
# Grab NUnit console runner
|
||||||
RUN nuget install NUnit.ConsoleRunner -Version 3.16.3 -OutputDirectory packages -NonInteractive
|
RUN nuget install NUnit.ConsoleRunner -Version 3.16.3 -OutputDirectory packages -NonInteractive
|
||||||
|
|
@ -27,11 +27,11 @@ COPY . .
|
||||||
RUN ls Cnc/Fanuc/libs/ || true
|
RUN ls Cnc/Fanuc/libs/ || true
|
||||||
|
|
||||||
# Build test project (Debug)
|
# Build test project (Debug)
|
||||||
RUN msbuild FanucProgramManager.Tests/FanucProgramManager.Tests.csproj \
|
RUN msbuild NcProgramManager.Tests/NcProgramManager.Tests.csproj \
|
||||||
/p:Configuration=Debug /p:Platform=AnyCPU \
|
/p:Configuration=Debug /p:Platform=AnyCPU \
|
||||||
/v:minimal /nologo
|
/v:minimal /nologo
|
||||||
|
|
||||||
# Run tests — skip Fanuc FOCAS tests (marked [Ignore] — need fwlib32.dll)
|
# Run tests — skip Fanuc FOCAS tests (marked [Ignore] — need fwlib32.dll)
|
||||||
CMD mono packages/NUnit.ConsoleRunner.3.16.3/tools/nunit3-console.exe \
|
CMD mono packages/NUnit.ConsoleRunner.3.16.3/tools/nunit3-console.exe \
|
||||||
FanucProgramManager.Tests/bin/Debug/FanucProgramManager.Tests.dll \
|
NcProgramManager.Tests/bin/Debug/NcProgramManager.Tests.dll \
|
||||||
--labels=All
|
--labels=All
|
||||||
|
|
|
||||||
7
Licensing/ILicenseValidator.cs
Normal file
7
Licensing/ILicenseValidator.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
namespace NcProgramManager.Licensing
|
||||||
|
{
|
||||||
|
internal interface ILicenseValidator
|
||||||
|
{
|
||||||
|
bool Validate(string fingerprint, string licenseBase64);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
Licensing/IMachineFingerprint.cs
Normal file
7
Licensing/IMachineFingerprint.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
namespace NcProgramManager.Licensing
|
||||||
|
{
|
||||||
|
internal interface IMachineFingerprint
|
||||||
|
{
|
||||||
|
string GetFingerprint();
|
||||||
|
}
|
||||||
|
}
|
||||||
26
Licensing/LicenseFile.cs
Normal file
26
Licensing/LicenseFile.cs
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
46
Licensing/LicenseValidator.cs
Normal file
46
Licensing/LicenseValidator.cs
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
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 key pair with the LicenseGenerator tool, then copy the public XML here.
|
||||||
|
private const string PublicKeyXml =
|
||||||
|
"<RSAKeyValue>" +
|
||||||
|
"<Modulus>PLACEHOLDER</Modulus>" +
|
||||||
|
"<Exponent>AQAB</Exponent>" +
|
||||||
|
"</RSAKeyValue>";
|
||||||
|
|
||||||
|
private readonly string _publicKeyXml;
|
||||||
|
|
||||||
|
// Uses embedded vendor public key.
|
||||||
|
internal LicenseValidator() : this(PublicKeyXml) { }
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
Licensing/MachineFingerprint.cs
Normal file
34
Licensing/MachineFingerprint.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -52,6 +52,7 @@
|
||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Compile Include="Unit\LicenseValidatorTests.cs" />
|
||||||
<Compile Include="Unit\NcProgramValidatorTests.cs" />
|
<Compile Include="Unit\NcProgramValidatorTests.cs" />
|
||||||
<Compile Include="Unit\FanucProgramParsingTests.cs" />
|
<Compile Include="Unit\FanucProgramParsingTests.cs" />
|
||||||
<Compile Include="Unit\IsoProgramFormatterTests.cs" />
|
<Compile Include="Unit\IsoProgramFormatterTests.cs" />
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NcProgramManager.Cnc;
|
using NcProgramManager.Cnc;
|
||||||
using NcProgramManager.Cnc.Fanuc;
|
using NcProgramManager.Cnc.Fanuc;
|
||||||
|
|
|
||||||
80
NcProgramManager.Tests/Unit/LicenseValidatorTests.cs
Normal file
80
NcProgramManager.Tests/Unit/LicenseValidatorTests.cs
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
using System;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NcProgramManager.Licensing;
|
||||||
|
|
||||||
|
namespace NcProgramManager.Tests.Unit
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
internal class LicenseValidatorTests
|
||||||
|
{
|
||||||
|
private RSACryptoServiceProvider _rsa;
|
||||||
|
private string _publicKeyXml;
|
||||||
|
private LicenseValidator _validator;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp()
|
||||||
|
{
|
||||||
|
_rsa = new RSACryptoServiceProvider(2048);
|
||||||
|
_publicKeyXml = _rsa.ToXmlString(false);
|
||||||
|
_validator = new LicenseValidator(_publicKeyXml);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TearDown]
|
||||||
|
public void TearDown()
|
||||||
|
{
|
||||||
|
_rsa.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string Sign(string fingerprint)
|
||||||
|
{
|
||||||
|
byte[] data = Encoding.UTF8.GetBytes(fingerprint);
|
||||||
|
byte[] sig = _rsa.SignData(data, new SHA256CryptoServiceProvider());
|
||||||
|
return Convert.ToBase64String(sig);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_ValidLicense_ReturnsTrue()
|
||||||
|
{
|
||||||
|
string fingerprint = "CPU123BOARD456";
|
||||||
|
string license = Sign(fingerprint);
|
||||||
|
Assert.IsTrue(_validator.Validate(fingerprint, license));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_WrongMachineFingerprint_ReturnsFalse()
|
||||||
|
{
|
||||||
|
string license = Sign("CPU123BOARD456");
|
||||||
|
Assert.IsFalse(_validator.Validate("DIFFERENT_MACHINE", license));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_TamperedLicense_ReturnsFalse()
|
||||||
|
{
|
||||||
|
string license = Sign("CPU123BOARD456");
|
||||||
|
string tampered = license.Substring(0, license.Length - 4) + "AAAA";
|
||||||
|
Assert.IsFalse(_validator.Validate("CPU123BOARD456", tampered));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_EmptyLicense_ReturnsFalse()
|
||||||
|
{
|
||||||
|
Assert.IsFalse(_validator.Validate("CPU123BOARD456", string.Empty));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_NotBase64_ReturnsFalse()
|
||||||
|
{
|
||||||
|
Assert.IsFalse(_validator.Validate("CPU123BOARD456", "not-base64!!!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void LicenseFile_TryRead_NonExistentPath_ReturnsFalse()
|
||||||
|
{
|
||||||
|
bool result = LicenseFile.TryRead(@"C:\nonexistent\path\license.lic", out string base64);
|
||||||
|
Assert.IsFalse(result);
|
||||||
|
Assert.IsNull(base64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -118,6 +118,11 @@
|
||||||
<Compile Include="Cnc\Mitsubishi\IMitsubishiFtpClient.cs" />
|
<Compile Include="Cnc\Mitsubishi\IMitsubishiFtpClient.cs" />
|
||||||
<Compile Include="Cnc\Mitsubishi\MitsubishiFtpClient.cs" />
|
<Compile Include="Cnc\Mitsubishi\MitsubishiFtpClient.cs" />
|
||||||
<Compile Include="Cnc\Mitsubishi\MitsubishiMachine.cs" />
|
<Compile Include="Cnc\Mitsubishi\MitsubishiMachine.cs" />
|
||||||
|
<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" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="App.config" />
|
<None Include="App.config" />
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue