From fdc71874247d997a68683571aeff74915722090d Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sun, 26 Apr 2026 17:22:05 +0200 Subject: [PATCH] feat: rewrite LicenseService to mint Ed25519-signed tokens Replaces UUID token generation with LicenseMinter.mint() from cameleer-license-minter. Adds full-control generateLicense() overload accepting custom limits, expiry, grace period, and label. Adds verifyToken() and verifyTokenSignature() using LicenseValidator. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../cameleer/saas/license/LicenseService.java | 102 ++++++++++++++---- 1 file changed, 83 insertions(+), 19 deletions(-) diff --git a/src/main/java/net/siegeln/cameleer/saas/license/LicenseService.java b/src/main/java/net/siegeln/cameleer/saas/license/LicenseService.java index 5d22dd6..f856c71 100644 --- a/src/main/java/net/siegeln/cameleer/saas/license/LicenseService.java +++ b/src/main/java/net/siegeln/cameleer/saas/license/LicenseService.java @@ -1,12 +1,18 @@ package net.siegeln.cameleer.saas.license; +import com.cameleer.license.minter.LicenseMinter; +import com.cameleer.server.core.license.LicenseInfo; +import com.cameleer.server.core.license.LicenseValidator; import net.siegeln.cameleer.saas.audit.AuditAction; import net.siegeln.cameleer.saas.audit.AuditService; import net.siegeln.cameleer.saas.tenant.TenantEntity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.time.Duration; import java.time.Instant; +import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -14,28 +20,52 @@ import java.util.UUID; @Service public class LicenseService { + private static final Logger log = LoggerFactory.getLogger(LicenseService.class); + private final LicenseRepository licenseRepository; private final AuditService auditService; + private final SigningKeyService signingKeyService; - public LicenseService(LicenseRepository licenseRepository, AuditService auditService) { + public LicenseService(LicenseRepository licenseRepository, + AuditService auditService, + SigningKeyService signingKeyService) { this.licenseRepository = licenseRepository; this.auditService = auditService; + this.signingKeyService = signingKeyService; } - public LicenseEntity generateLicense(TenantEntity tenant, Duration validity, UUID actorId) { - var limits = LicenseDefaults.limitsForTier(tenant.getTier()); + /** + * Mint an Ed25519-signed license with full control over limits. + */ + public LicenseEntity generateLicense(TenantEntity tenant, + Map limits, + Instant expiresAt, + int gracePeriodDays, + String label, + UUID actorId) { Instant now = Instant.now(); - Instant expiresAt = now.plus(validity); + UUID licenseId = UUID.randomUUID(); - String token = UUID.randomUUID().toString(); + LicenseInfo info = new LicenseInfo( + licenseId, + tenant.getSlug(), + label, + limits, + now, + expiresAt, + gracePeriodDays + ); + + String token = LicenseMinter.mint(info, signingKeyService.getPrivateKey()); var entity = new LicenseEntity(); entity.setTenantId(tenant.getId()); entity.setTier(tenant.getTier().name()); - entity.setLimits(new java.util.HashMap<>(limits)); + entity.setLabel(label); + entity.setLimits(new HashMap<>(limits)); + entity.setGracePeriodDays(gracePeriodDays); entity.setIssuedAt(now); entity.setExpiresAt(expiresAt); - entity.setGracePeriodDays(LicenseDefaults.DEFAULT_GRACE_PERIOD_DAYS); entity.setToken(token); var saved = licenseRepository.save(entity); @@ -47,6 +77,17 @@ public class LicenseService { return saved; } + /** + * Convenience overload using tier presets and default validity. + */ + public LicenseEntity generateLicense(TenantEntity tenant, Duration validity, UUID actorId) { + var limits = LicenseDefaults.limitsForTier(tenant.getTier()); + Instant expiresAt = Instant.now().plus(validity); + String label = tenant.getName() + " (" + tenant.getTier().name() + ")"; + return generateLicense(tenant, limits, expiresAt, + LicenseDefaults.DEFAULT_GRACE_PERIOD_DAYS, label, actorId); + } + public Optional getActiveLicense(UUID tenantId) { return licenseRepository.findFirstByTenantIdAndRevokedAtIsNullOrderByCreatedAtDesc(tenantId); } @@ -63,18 +104,41 @@ public class LicenseService { } /** - * Verifies a license token by checking its existence and validity in the database. - * Returns the license entity's metadata as a map if found and not expired/revoked, - * or empty if the token is unknown or invalid. + * Verify a signed license token using the stored public key. + * Returns the parsed LicenseInfo if valid, or empty if invalid. */ - public Optional> verifyLicenseToken(String token) { - return licenseRepository.findByToken(token) - .filter(e -> e.getRevokedAt() == null) - .filter(e -> e.getExpiresAt() == null || Instant.now().isBefore(e.getExpiresAt())) - .map(e -> Map.of( - "tenant_id", e.getTenantId().toString(), - "tier", e.getTier(), - "limits", e.getLimits() - )); + public Optional verifyToken(String token, String expectedTenantId) { + try { + String publicKeyB64 = signingKeyService.getPublicKeyBase64(); + LicenseValidator validator = new LicenseValidator(publicKeyB64, expectedTenantId); + LicenseInfo info = validator.validate(token); + return Optional.of(info); + } catch (Exception e) { + log.debug("License token verification failed: {}", e.getMessage()); + return Optional.empty(); + } + } + + /** + * Verify a signed license token without tenant ID check (for vendor verify tool). + * Decodes the payload and validates the signature only. + */ + public Optional verifyTokenSignature(String token) { + try { + // Decode the payload portion to extract tenantId, then validate + String payloadB64 = token.split("\\.", 2)[0]; + String payloadJson = new String(java.util.Base64.getDecoder().decode(payloadB64)); + var mapper = new com.fasterxml.jackson.databind.ObjectMapper(); + var tree = mapper.readTree(payloadJson); + String tenantId = tree.has("tid") ? tree.get("tid").asText() : tree.get("tenantId").asText(); + + String publicKeyB64 = signingKeyService.getPublicKeyBase64(); + LicenseValidator validator = new LicenseValidator(publicKeyB64, tenantId); + LicenseInfo info = validator.validate(token); + return Optional.of(info); + } catch (Exception e) { + log.debug("License token signature verification failed: {}", e.getMessage()); + return Optional.empty(); + } } }