feat: implement LicenseValidator with Ed25519 signature verification
- Validates payload.signature license tokens using Ed25519 public key - Parses tier, features, limits, timestamps from JSON payload - Rejects expired and tampered tokens - Unit tests for valid, expired, and tampered license scenarios
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
package com.cameleer3.server.core.license;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.security.*;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
|
||||
public class LicenseValidator {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(LicenseValidator.class);
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
private final PublicKey publicKey;
|
||||
|
||||
public LicenseValidator(String publicKeyBase64) {
|
||||
try {
|
||||
byte[] keyBytes = Base64.getDecoder().decode(publicKeyBase64);
|
||||
KeyFactory kf = KeyFactory.getInstance("Ed25519");
|
||||
this.publicKey = kf.generatePublic(new X509EncodedKeySpec(keyBytes));
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to load license public key", e);
|
||||
}
|
||||
}
|
||||
|
||||
public LicenseInfo validate(String token) {
|
||||
String[] parts = token.split("\\.", 2);
|
||||
if (parts.length != 2) {
|
||||
throw new IllegalArgumentException("Invalid license token format: expected payload.signature");
|
||||
}
|
||||
|
||||
byte[] payloadBytes = Base64.getDecoder().decode(parts[0]);
|
||||
byte[] signatureBytes = Base64.getDecoder().decode(parts[1]);
|
||||
|
||||
// Verify signature
|
||||
try {
|
||||
Signature verifier = Signature.getInstance("Ed25519");
|
||||
verifier.initVerify(publicKey);
|
||||
verifier.update(payloadBytes);
|
||||
if (!verifier.verify(signatureBytes)) {
|
||||
throw new SecurityException("License signature verification failed");
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new SecurityException("License signature verification failed", e);
|
||||
}
|
||||
|
||||
// Parse payload
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(payloadBytes);
|
||||
|
||||
String tier = root.get("tier").asText();
|
||||
|
||||
Set<Feature> features = new HashSet<>();
|
||||
if (root.has("features")) {
|
||||
for (JsonNode f : root.get("features")) {
|
||||
try {
|
||||
features.add(Feature.valueOf(f.asText()));
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Unknown feature in license: {}", f.asText());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Integer> limits = new HashMap<>();
|
||||
if (root.has("limits")) {
|
||||
root.get("limits").fields().forEachRemaining(entry ->
|
||||
limits.put(entry.getKey(), entry.getValue().asInt()));
|
||||
}
|
||||
|
||||
Instant issuedAt = root.has("iat") ? Instant.ofEpochSecond(root.get("iat").asLong()) : Instant.now();
|
||||
Instant expiresAt = root.has("exp") ? Instant.ofEpochSecond(root.get("exp").asLong()) : null;
|
||||
|
||||
LicenseInfo info = new LicenseInfo(tier, features, limits, issuedAt, expiresAt);
|
||||
|
||||
if (info.isExpired()) {
|
||||
throw new IllegalArgumentException("License expired at " + expiresAt);
|
||||
}
|
||||
|
||||
return info;
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("Failed to parse license payload", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user