diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md
index 4af744f..4fe9d9d 100644
--- a/.paul/ROADMAP.md
+++ b/.paul/ROADMAP.md
@@ -86,13 +86,13 @@ Completed: 2026-05-13
**v0.2 NcProgramManager** (in progress)
Status: In Progress
-Phases: 2 of 4 complete
+Phases: 3 of 4 complete
| Phase | Name | Plans | Status |
|-------|------|-------|--------|
| 06 | rename-cleanup | 1 | ✅ Complete |
| 07 | license-core | 1 | ✅ Complete |
-| 08 | license-integration | 1 | Not started |
+| 08 | license-integration | 1 | ✅ Complete | 2026-05-17 |
| 09 | license-generator | 1 | Not started |
### Phase 06: rename-cleanup ✅
@@ -105,13 +105,14 @@ Phases: 2 of 4 complete
**Completed:** 2026-05-17
**Plans:** [x] 07-01: RSA core + interfaces + 6 unit tests
-### Phase 08: license-integration (proposed)
-**Goal:** Wire license system into Program.cs, replace `checkLicense`, replace `-chiave=` with `-licfile=`
-**Depends on:** Phase 07
+### Phase 08: license-integration ✅
+**Goal:** Wire license system into Program.cs, delete `checkLicense`, remove `-chiave=`, hardcode `license.lic` next to EXE
+**Completed:** 2026-05-17
+**Plans:** [x] 08-01: Program.cs wiring
### Phase 09: license-generator (proposed)
**Goal:** Separate generator console app — reads machine fingerprint, signs, writes `.lic` file
**Depends on:** Phase 07
---
-*Roadmap updated: 2026-05-17 — Phase 07 license-core complete*
+*Roadmap updated: 2026-05-17 — Phase 08 license-integration complete*
diff --git a/.paul/STATE.md b/.paul/STATE.md
index ed5d67f..6a1340a 100644
--- a/.paul/STATE.md
+++ b/.paul/STATE.md
@@ -5,27 +5,25 @@
See: .paul/PROJECT.md (updated 2026-05-17)
**Core value:** Machinists transfer CNC programs to/from any controller without manual steps or internet
-**Current focus:** v0.1 complete — ready for next milestone planning
+**Current focus:** v0.2 — Phase 08 complete, Phase 09 (license-generator) next
## Current Position
Milestone: v0.2 NcProgramManager
-Phase: 3 of 4 (08-license-integration) — Not started
-Plan: Not started
-Status: Ready to plan Phase 08
-Last activity: 2026-05-17 — Phase 07 complete, transitioned to Phase 08
+Phase: 4 of 4 (09-license-generator) — Not started
+Status: Phase 08 complete, ready to plan Phase 09
+Last activity: 2026-05-17 — Phase 08 license-integration complete (UNIFY done)
Progress:
- Milestone v0.1: [██████████] 100% (complete)
-- Milestone v0.2: [█████░░░░░] 50%
-- Phase 08: [░░░░░░░░░░] 0%
+- Milestone v0.2: [███████░░░] 75%
+- Phase 09: [░░░░░░░░░░] 0%
## Loop Position
-Current loop state:
```
PLAN ──▶ APPLY ──▶ UNIFY
- ✓ ✓ ✓ [Loop complete — ready for next PLAN]
+ ✓ ✓ ✓ [Phase 08 closed]
```
## What Was Built (v0.1 Multi-CN Release)
@@ -58,44 +56,72 @@ PLAN ──▶ APPLY ──▶ UNIFY
- `NcProgramValidatorTests` — 30+ NUnit tests
### Test Infrastructure
-- `FanucProgramManager.Tests/` — NUnit 3.13.3 + Moq 4.18.4, .NET 4.7.2
+- `NcProgramManager.Tests/` — NUnit 3.13.3 + Moq 4.18.4, .NET 4.7.2
- `FtpServerStub` + `Lsv2ServerStub` — in-process TCP stubs
- Unit + integration tests; Fanuc FOCAS tests `[Ignore]`d (Windows-only DLL)
-## CLI Args (v0.1)
+## What Was Built (v0.2 NcProgramManager — in progress)
+
+### Phase 06 — Rename Cleanup
+- Namespace: `FanucProgramManager.*` → `NcProgramManager.*` (all files)
+- Project renamed, test folder renamed, dead `FANUCMachine.cs` deleted
+
+### Phase 07 — License Core
+- `IMachineFingerprint` / `ILicenseValidator` interfaces
+- `MachineFingerprint` — WMI CPU/BIOS fingerprint, SHA256 hash
+- `LicenseFile.TryRead` — reads `.lic` file, returns base64 signature
+- `LicenseValidator.Validate` — RSA-SHA256 verify; PLACEHOLDER public key until Phase 09
+- `LicenseValidatorTests` — 6 unit tests (testable via internal ctor injection)
+
+### Phase 08 — License Integration
+- `Program.cs` — `checkLicense` deleted; RSA check wired at startup:
+ ```csharp
+ string _licPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "license.lic");
+ if (!LicenseFile.TryRead(_licPath, out _licBase64)
+ || !new LicenseValidator().Validate(_licFingerprint, _licBase64)) { ... }
+ ```
+- `InputArgs.cs` — `chiave` field removed
+- `-chiave=` CLI arg removed; license file hardcoded next to EXE as `license.lic`
+- `MostraAiuto` updated: instructs user to place `license.lic` next to executable
+
+## CLI Args (current)
- `-manufacturer=fanuc|heidenhain|siemens|mitsubishi` (default: fanuc)
+- `-azione=` — action (Scarica/Invia/etc.)
+- `-ip=` — CNC IP
+- `-porta=` — port
- `-username=` (Heidenhain/Siemens/Mitsubishi)
- `-password=` (Heidenhain/Siemens/Mitsubishi)
+- License: place `license.lic` next to the EXE (no CLI arg)
## Accumulated Context
### Decisions
- .NET 4.7.2 locked — max compatibility
- RSA asymmetric offline licensing: private key vendor-only, public key embedded in binary
-- PublicKeyXml = PLACEHOLDER until Phase 09 generates real key pair
+- `license.lic` always at `AppDomain.CurrentDomain.BaseDirectory` — no CLI override
- `RSACryptoServiceProvider` + `SHA256CryptoServiceProvider` (not HashAlgorithmName) — .NET 4.7.2 compat
- Constructor injection pattern: internal ctor accepts key XML — testable without Windows WMI
+- PublicKeyXml = PLACEHOLDER until Phase 09 generates real key pair
- Heidenhain: LSV2 port 19000, needs real hardware testing
- Siemens: FTP `/_N_MPF_DIR/`, needs Sinumerik FTP option enabled on machine
- `#` = Warning in Fanuc/Mitsubishi — macro variable indicator
### Deferred Issues
-- 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
+- `AesCrypt.cs` dead code — remove in future cleanup phase
- Heidenhain: `ReadActiveProgramAsync` / `SelectMainProgram` not implemented
- Siemens: same as Heidenhain
- Build + integration not verifiable on Linux (fwlib32 Windows-only)
- MachineFingerprint WMI — code-review verified, needs real Windows hardware test
### Blockers/Concerns
-None — Phase 08 can start.
+None — Phase 09 can start.
## Session Continuity
Last session: 2026-05-17
-Stopped at: Phase 07 complete (UNIFY done)
-Next action: /paul:plan for Phase 08 (license-integration)
+Stopped at: Phase 08 UNIFY complete
+Next action: /paul:plan phase 09 license-generator
Resume file: .paul/ROADMAP.md
---
diff --git a/.paul/paul.json b/.paul/paul.json
index 3d3c378..386d245 100644
--- a/.paul/paul.json
+++ b/.paul/paul.json
@@ -7,8 +7,8 @@
"status": "in_progress"
},
"phase": {
- "number": 7,
- "name": "license-core",
+ "number": 8,
+ "name": "license-integration",
"status": "complete"
},
"loop": {
@@ -17,7 +17,7 @@
},
"timestamps": {
"created_at": "2026-05-13T00:00:00Z",
- "updated_at": "2026-05-17T21:00:00Z"
+ "updated_at": "2026-05-17T22:00:00Z"
},
"satellite": {
"groom": true
diff --git a/.paul/phases/08-license-integration/08-01-PLAN.md b/.paul/phases/08-license-integration/08-01-PLAN.md
new file mode 100644
index 0000000..2881e5e
--- /dev/null
+++ b/.paul/phases/08-license-integration/08-01-PLAN.md
@@ -0,0 +1,199 @@
+---
+phase: 08-license-integration
+plan: 01
+type: execute
+wave: 1
+depends_on: ["07-01"]
+files_modified:
+ - Program.cs
+autonomous: true
+---
+
+
+## Goal
+Wire the Phase 07 RSA license core into Program.cs: replace the hardcoded AES `checkLicense` call with `MachineFingerprint` + `LicenseFile` + `LicenseValidator`. Keep `-chiave=` as the CLI arg — it now carries a path to a `.lic` file instead of the AES key string.
+
+## Purpose
+The current `checkLicense` embeds a hardcoded AES key/IV in source — anyone with the source or a decompiler can generate licenses for any machine. The Phase 07 RSA core is built but not yet wired in. This phase closes the gap: the app will reject any run where the `.lic` file (passed via `-chiave=`) doesn't contain a valid RSA-SHA256 signature matching the current machine's fingerprint.
+
+## Output
+- `Program.cs` only — `checkLicense` method deleted; license check replaced with `MachineFingerprint` + `LicenseFile.TryRead` + `LicenseValidator.Validate`; `inputArgs.chiave` now treated as `.lic` file path; help text for `-chiave=` updated to describe the file path; dead `using System.Management` and `using System.Security.Cryptography` removed; `using NcProgramManager.Licensing` added
+- `InputArgs.cs` — NO CHANGE (`chiave` field stays as-is)
+- Docker build + all 118 tests still pass
+
+
+
+## Project Context
+@.paul/PROJECT.md
+@.paul/STATE.md
+
+## Phase 07 Output (direct inputs)
+@Licensing/IMachineFingerprint.cs
+@Licensing/ILicenseValidator.cs
+@Licensing/LicenseFile.cs
+@Licensing/MachineFingerprint.cs
+@Licensing/LicenseValidator.cs
+
+## File Being Modified
+@Program.cs
+
+
+
+
+## AC-1: -chiave= arg still parsed, value treated as .lic file path
+```gherkin
+Given Program.cs parseArgomenti is called with args containing "-chiave=C:\license.lic"
+When the arg is parsed
+Then inputArgs.chiave equals "C:\license.lic"
+And the parsing code is unchanged from the current implementation
+```
+
+## AC-2: RSA license check used at startup
+```gherkin
+Given Program.cs Main is called
+When the license check runs
+Then it calls MachineFingerprint().GetFingerprint(), LicenseFile.TryRead(inputArgs.chiave), and LicenseValidator.Validate(fingerprint, base64)
+And the old checkLicense method no longer exists
+```
+
+## AC-3: License failure exits with -100
+```gherkin
+Given the .lic file is missing OR the signature does not match the machine fingerprint
+When Main runs
+Then it prints "Licenza ERRATA", calls vediErrore(-100), and returns -1
+```
+
+## AC-4: Help text updated for -chiave=
+```gherkin
+Given MostraAiuto is called
+When its output is inspected
+Then -chiave= is documented as the path to the .lic license file
+```
+
+
+
+
+
+
+ Task 1: Wire RSA license check into Program.cs
+ Program.cs
+
+ **1. Replace usings at top of file:**
+
+ Remove these two lines:
+ ```csharp
+ using System.Management;
+ using System.Security.Cryptography;
+ ```
+ Add:
+ ```csharp
+ using NcProgramManager.Licensing;
+ ```
+ Keep all other existing usings unchanged.
+
+ **2. Replace the license check call in Main:**
+
+ Remove:
+ ```csharp
+ if (!checkLicense(inputArgs.chiave))
+ {
+ Console.WriteLine("Licenza ERRATA");
+ vediErrore(-100);
+ return -1;
+ }
+ ```
+ Replace with:
+ ```csharp
+ string _licFingerprint = new MachineFingerprint().GetFingerprint();
+ string _licBase64;
+ if (!LicenseFile.TryRead(inputArgs.chiave, out _licBase64)
+ || !new LicenseValidator().Validate(_licFingerprint, _licBase64))
+ {
+ Console.WriteLine("Licenza ERRATA");
+ vediErrore(-100);
+ return -1;
+ }
+ ```
+ `inputArgs.chiave` carries the `.lic` file path — parsing in `parseArgomenti` is unchanged.
+
+ **3. Delete the entire `checkLicense` static method:**
+ ```csharp
+ static Boolean checkLicense(string chiave)
+ {
+ ...
+ }
+ ```
+ Remove from first line to closing brace entirely.
+
+ **4. Update MostraAiuto — update the description for `-chiave=`:**
+
+ Remove:
+ ```csharp
+ Console.WriteLine("-chiave=");
+ Console.WriteLine(" la stringa chiave per eseguire il software.");
+ ```
+ Replace with:
+ ```csharp
+ Console.WriteLine("-chiave=");
+ Console.WriteLine(" Path del file di licenza (.lic) per autenticare il software.");
+ ```
+
+ Avoid: touching `parseArgomenti` chiave parsing, `Scarica`, `Invia`, `vediErrore`, or any other part of the file.
+
+
+ ```bash
+ grep -n "checkLicense\|System\.Management\|System\.Security\.Cryptography" Program.cs
+ grep -n "MachineFingerprint\|LicenseFile\|LicenseValidator\|NcProgramManager\.Licensing" Program.cs
+ grep -n "chiave" Program.cs InputArgs.cs
+ ```
+ First grep: zero hits.
+ Second grep: 4+ hits.
+ Third grep: `chiave` present in both files (parsing in Program.cs unchanged, field in InputArgs.cs unchanged).
+
+ AC-2 satisfied: RSA check wired. AC-3 satisfied: failure path unchanged (-1 return). AC-4 satisfied: help text updated. AC-1 satisfied: -chiave= parsing untouched.
+
+
+
+
+
+
+## DO NOT CHANGE
+- `InputArgs.cs` — `chiave` field stays as-is; no rename
+- `-chiave=` parsing in `parseArgomenti` — keep exactly as-is
+- `AesCrypt.cs` — now dead code but removal is out of scope; leave for a cleanup phase
+- `Licensing/*.cs` — Phase 07 output, must not be modified
+- All `Cnc/` files
+- All test files
+
+## SCOPE LIMITS
+- No changes to `Scarica`, `Invia`, `vediErrore` logic
+- No changes to any other arg parsing in `parseArgomenti`
+- No new test files (license core already tested in Phase 07)
+- `AesCrypt.cs` stays in csproj and on disk
+
+
+
+
+Before declaring plan complete:
+- [ ] `grep "checkLicense" Program.cs` — zero hits
+- [ ] `grep "System.Management\|System.Security.Cryptography" Program.cs` — zero hits
+- [ ] `grep "MachineFingerprint\|LicenseFile\|LicenseValidator" Program.cs` — 3 hits
+- [ ] `grep "NcProgramManager.Licensing" Program.cs` — 1 hit
+- [ ] `grep "chiave" InputArgs.cs` — still present (field unchanged)
+- [ ] `grep "\-chiave=" Program.cs` — still present in parseArgomenti AND in MostraAiuto
+- [ ] Docker build passes, all 118 tests pass
+
+
+
+- All tasks completed
+- All verification checks pass
+- `checkLicense` method gone
+- `-chiave=` CLI arg and `inputArgs.chiave` field unchanged
+- RSA license check wired at startup using `inputArgs.chiave` as .lic path
+- Help text describes `-chiave=` as the .lic file path
+- No test regressions
+
+
+
diff --git a/.paul/phases/08-license-integration/08-01-SUMMARY.md b/.paul/phases/08-license-integration/08-01-SUMMARY.md
new file mode 100644
index 0000000..559e4f3
--- /dev/null
+++ b/.paul/phases/08-license-integration/08-01-SUMMARY.md
@@ -0,0 +1,63 @@
+---
+phase: 08-license-integration
+plan: 01
+type: summary
+status: complete
+completed: 2026-05-17
+---
+
+# Phase 08 Summary: license-integration
+
+## What Was Built
+
+RSA license check wired into `Program.cs`. Old AES-based `checkLicense` deleted. License now validated at startup against `license.lic` in the EXE directory.
+
+**Files modified:**
+- `Program.cs` — license check replaced, `checkLicense` deleted, `-chiave=` parsing removed, `MostraAiuto` updated
+- `InputArgs.cs` — `chiave` field removed
+
+## Acceptance Criteria Results
+
+| AC | Result | Notes |
+|----|--------|-------|
+| AC-1: -chiave= arg still parsed | **SUPERSEDED** | User directed removal mid-APPLY (see Deviations) |
+| AC-2: RSA license check at startup | PASS | `MachineFingerprint` + `LicenseFile.TryRead` + `LicenseValidator.Validate` wired |
+| AC-3: License failure exits -100 | PASS | `Console.WriteLine("Licenza ERRATA")` + `vediErrore(-100)` + `return -1` |
+| AC-4: Help text updated | PASS | `-chiave=` block replaced with `license.lic` location note |
+
+## Deviations
+
+**Plan AC-1 superseded by user decision.**
+Plan stated: keep `-chiave=` as the CLI arg carrying the `.lic` file path.
+User directed mid-APPLY: remove `-chiave=` entirely; hardcode `license.lic` next to the EXE.
+
+Actual license path:
+```csharp
+string _licPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "license.lic");
+```
+
+`inputArgs.chiave` field removed from `InputArgs.cs`. All `-chiave=` parsing removed from `parseArgomenti`.
+
+## Verification (post-apply grep)
+
+```
+grep "checkLicense|chiave|System.Management|System.Security.Cryptography" Program.cs → 0 hits
+grep "MachineFingerprint|LicenseFile|LicenseValidator|NcProgramManager.Licensing" Program.cs → 5 hits
+grep "chiave" InputArgs.cs → 0 hits
+```
+
+## Decisions Made
+
+- License file always at `AppDomain.CurrentDomain.BaseDirectory + "license.lic"` — no CLI override
+- `InputArgs.chiave` field deleted (not just unused) — cleaner than leaving dead field
+- `AesCrypt.cs` left on disk (still in csproj, no cleanup in scope)
+
+## Deferred Issues
+
+- Real RSA key pair — Phase 09 generates; copy `PublicKeyXml` into `LicenseValidator.cs`
+- Phase 09: separate `LicenseGenerator` console app — reads WMI fingerprint, signs, writes `.lic`
+- `AesCrypt.cs` dead code — remove in future cleanup phase
+
+## Tests
+
+No new tests (license core tested in Phase 07). Docker build + 116 tests pass (2 Fanuc FOCAS ignored, Windows-only DLL).
diff --git a/InputArgs.cs b/InputArgs.cs
index 8edcc06..628bd7b 100755
--- a/InputArgs.cs
+++ b/InputArgs.cs
@@ -11,7 +11,6 @@ namespace NcProgramManager
public string porta = "";
public short pathCNC = 1;
public int nodoHssb = -1;
- public string chiave = "";
public string username = "";
public string password = "";
public string commentoProgramma = "";
diff --git a/Program.cs b/Program.cs
index 483d97d..b2a80f9 100755
--- a/Program.cs
+++ b/Program.cs
@@ -1,11 +1,10 @@
using System;
using System.Collections.Generic;
using System.IO;
-using System.Management;
-using System.Security.Cryptography;
using System.Text.RegularExpressions;
using NcProgramManager.Cnc;
using NcProgramManager.Cnc.Models;
+using NcProgramManager.Licensing;
namespace NcProgramManager
{
@@ -35,7 +34,11 @@ namespace NcProgramManager
return 0;
}
- if (!checkLicense(inputArgs.chiave))
+ string _licFingerprint = new MachineFingerprint().GetFingerprint();
+ string _licBase64;
+ string _licPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "license.lic");
+ if (!LicenseFile.TryRead(_licPath, out _licBase64)
+ || !new LicenseValidator().Validate(_licFingerprint, _licBase64))
{
Console.WriteLine("Licenza ERRATA");
vediErrore(-100);
@@ -275,47 +278,6 @@ namespace NcProgramManager
Console.ReadLine();
}
- static Boolean checkLicense(string chiave)
- {
- string pcInfo = string.Empty;
- ManagementClass mc = new ManagementClass("win32_processor");
- ManagementObjectCollection moc = mc.GetInstances();
-
- foreach (ManagementObject mo in moc)
- {
- //Get only the first CPU's ID
- pcInfo = mo.Properties["processorID"].Value.ToString();
- }
-
- ManagementObjectSearcher baseboardSearcher = new ManagementObjectSearcher("root\\CIMV2", "SELECT * FROM Win32_BaseBoard");
- foreach (ManagementObject queryObj in baseboardSearcher.Get())
- {
- //Get motherboardSerialNumber
- pcInfo += queryObj["SerialNumber"].ToString();
- break;
- }
-
- using (Aes myAes = Aes.Create())
- {
- if (chiave == "")
- return false;
- myAes.IV = Convert.FromBase64String("qqxNM8Li99Cwx61VX/QoWA==");
- myAes.Key = Convert.FromBase64String("7/12mP3dFLGqPGjujlwBCVblURxBaBcGGQ2c2FatfP4=");
- byte[] encrypted = AesCrypt.EncryptStringToBytes_Aes(pcInfo, myAes.Key, myAes.IV);
- string roundtrip = AesCrypt.DecryptStringFromBytes_Aes(encrypted, myAes.Key, myAes.IV);
- string decoded = "";
- try
- {
- decoded = AesCrypt.DecryptStringFromBytes_Aes(Convert.FromBase64String(chiave), myAes.Key, myAes.IV);
- }
- catch (Exception ex)
- {
- Console.WriteLine(ex.ToString());
- }
- return pcInfo == decoded;
- }
- }
-
static void MostraAiuto()
{
Console.WriteLine("Utilizzo del Software gestore Programmi FANUC");
@@ -326,8 +288,7 @@ namespace NcProgramManager
Console.WriteLine();
Console.WriteLine("Lista dei parametri da fornire all'eseguibile\nDevono essere separati da uno spazio tra l'uno e l'altro,\nnon andare a capo e non lasciare spazi dopo l'= ");
Console.WriteLine();
- Console.WriteLine("-chiave=");
- Console.WriteLine(" la stringa chiave per eseguire il software.");
+ Console.WriteLine("Il file 'license.lic' deve essere posizionato nella stessa cartella dell'eseguibile.");
Console.WriteLine("-azione=");
Console.WriteLine(" l'{AZIONE} da eseguire.");
Console.WriteLine("-manufacturer=");
@@ -385,8 +346,6 @@ namespace NcProgramManager
inputArgs.mostraHelp = true;
return;
}
- var chiave = arguments.Find(it => new Regex("-chiave=.*", RegexOptions.IgnoreCase).IsMatch(it));
- inputArgs.chiave = chiave.Substring(8);
var azione = arguments.Find(it => new Regex("-azione=.*", RegexOptions.IgnoreCase).IsMatch(it));
inputArgs.azione = azione.Substring(8);
var manufacturer = arguments.Find(it => new Regex("-manufacturer=.*", RegexOptions.IgnoreCase).IsMatch(it));