From f6657f811bfa27a56f94873af64b6700693a4430 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sun, 26 Apr 2026 10:57:05 +0200 Subject: [PATCH] feat(license-minter): --verify round-trips before shipping Adds --verify (requires --public-key) to LicenseMinterCli. After writing the output file the CLI parses the freshly-minted token through LicenseValidator against the supplied public key. On verify failure the output file is deleted (so the bad token is not accidentally shipped) and the CLI exits 3. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../license/minter/cli/LicenseMinterCli.java | 17 ++++++ .../minter/cli/LicenseMinterCliTest.java | 61 +++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/cameleer-license-minter/src/main/java/com/cameleer/license/minter/cli/LicenseMinterCli.java b/cameleer-license-minter/src/main/java/com/cameleer/license/minter/cli/LicenseMinterCli.java index 334f1fbd..927cd5e7 100644 --- a/cameleer-license-minter/src/main/java/com/cameleer/license/minter/cli/LicenseMinterCli.java +++ b/cameleer-license-minter/src/main/java/com/cameleer/license/minter/cli/LicenseMinterCli.java @@ -98,6 +98,23 @@ public final class LicenseMinterCli { } else { out.println(token); } + if (bool.contains("--verify")) { + String pubPath = flags.get("--public-key"); + if (pubPath == null) { + err.println("--verify requires --public-key"); + if (outPath != null) Files.deleteIfExists(Path.of(outPath)); + return 2; + } + try { + String pubB64 = Files.readString(Path.of(pubPath)).trim(); + new com.cameleer.server.core.license.LicenseValidator(pubB64, tenant).validate(token); + out.println("verified ok"); + } catch (Exception ve) { + err.println("VERIFY FAILED: " + ve.getMessage()); + if (outPath != null) Files.deleteIfExists(Path.of(outPath)); + return 3; + } + } return 0; } catch (Exception e) { err.println("ERROR: " + e.getMessage()); diff --git a/cameleer-license-minter/src/test/java/com/cameleer/license/minter/cli/LicenseMinterCliTest.java b/cameleer-license-minter/src/test/java/com/cameleer/license/minter/cli/LicenseMinterCliTest.java index 3978fa4e..36f3dd2f 100644 --- a/cameleer-license-minter/src/test/java/com/cameleer/license/minter/cli/LicenseMinterCliTest.java +++ b/cameleer-license-minter/src/test/java/com/cameleer/license/minter/cli/LicenseMinterCliTest.java @@ -48,4 +48,65 @@ class LicenseMinterCliTest { int code = LicenseMinterCli.run(new String[]{"--frobnicate=yes"}); assertThat(code).isNotZero(); } + + @Test + void verify_happyPath_succeeds() throws Exception { + KeyPair kp = KeyPairGenerator.getInstance("Ed25519").generateKeyPair(); + Path priv = tmp.resolve("priv.b64"); + Path pub = tmp.resolve("pub.b64"); + Files.writeString(priv, Base64.getEncoder().encodeToString(kp.getPrivate().getEncoded())); + Files.writeString(pub, Base64.getEncoder().encodeToString(kp.getPublic().getEncoded())); + Path out = tmp.resolve("license.tok"); + + int code = LicenseMinterCli.run(new String[]{ + "--private-key=" + priv, + "--public-key=" + pub, + "--tenant=acme", + "--expires=2099-12-31", + "--output=" + out, + "--verify" + }); + + assertThat(code).isEqualTo(0); + assertThat(out).exists(); + } + + @Test + void verify_wrongPublicKey_deletesOutputAndExitsNonZero() throws Exception { + KeyPair signing = KeyPairGenerator.getInstance("Ed25519").generateKeyPair(); + KeyPair other = KeyPairGenerator.getInstance("Ed25519").generateKeyPair(); + Path priv = tmp.resolve("priv.b64"); + Path pub = tmp.resolve("pub.b64"); + Files.writeString(priv, Base64.getEncoder().encodeToString(signing.getPrivate().getEncoded())); + Files.writeString(pub, Base64.getEncoder().encodeToString(other.getPublic().getEncoded())); + Path out = tmp.resolve("license.tok"); + + int code = LicenseMinterCli.run(new String[]{ + "--private-key=" + priv, + "--public-key=" + pub, + "--tenant=acme", + "--expires=2099-12-31", + "--output=" + out, + "--verify" + }); + + assertThat(code).isNotZero(); + assertThat(out).doesNotExist(); + } + + @Test + void verify_withoutPublicKey_fails() throws Exception { + KeyPair kp = KeyPairGenerator.getInstance("Ed25519").generateKeyPair(); + Path priv = tmp.resolve("priv.b64"); + Files.writeString(priv, Base64.getEncoder().encodeToString(kp.getPrivate().getEncoded())); + + int code = LicenseMinterCli.run(new String[]{ + "--private-key=" + priv, + "--tenant=acme", + "--expires=2099-12-31", + "--verify" + }); + + assertThat(code).isNotZero(); + } }