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,87 @@
|
||||
package com.cameleer3.server.core.license;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.security.*;
|
||||
import java.security.spec.NamedParameterSpec;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Base64;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class LicenseValidatorTest {
|
||||
|
||||
private KeyPair generateKeyPair() throws Exception {
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance("Ed25519");
|
||||
return kpg.generateKeyPair();
|
||||
}
|
||||
|
||||
private String sign(PrivateKey key, String payload) throws Exception {
|
||||
Signature signer = Signature.getInstance("Ed25519");
|
||||
signer.initSign(key);
|
||||
signer.update(payload.getBytes());
|
||||
return Base64.getEncoder().encodeToString(signer.sign());
|
||||
}
|
||||
|
||||
@Test
|
||||
void validate_validLicense_returnsLicenseInfo() throws Exception {
|
||||
KeyPair kp = generateKeyPair();
|
||||
String publicKeyBase64 = Base64.getEncoder().encodeToString(kp.getPublic().getEncoded());
|
||||
LicenseValidator validator = new LicenseValidator(publicKeyBase64);
|
||||
|
||||
Instant expires = Instant.now().plus(365, ChronoUnit.DAYS);
|
||||
String payload = """
|
||||
{"tier":"HIGH","features":["topology","lineage","debugger"],"limits":{"max_agents":50,"retention_days":90},"iat":%d,"exp":%d}
|
||||
""".formatted(Instant.now().getEpochSecond(), expires.getEpochSecond()).trim();
|
||||
String signature = sign(kp.getPrivate(), payload);
|
||||
String token = Base64.getEncoder().encodeToString(payload.getBytes()) + "." + signature;
|
||||
|
||||
LicenseInfo info = validator.validate(token);
|
||||
|
||||
assertThat(info.tier()).isEqualTo("HIGH");
|
||||
assertThat(info.hasFeature(Feature.debugger)).isTrue();
|
||||
assertThat(info.hasFeature(Feature.replay)).isFalse();
|
||||
assertThat(info.getLimit("max_agents", 0)).isEqualTo(50);
|
||||
assertThat(info.isExpired()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void validate_expiredLicense_throwsException() throws Exception {
|
||||
KeyPair kp = generateKeyPair();
|
||||
String publicKeyBase64 = Base64.getEncoder().encodeToString(kp.getPublic().getEncoded());
|
||||
LicenseValidator validator = new LicenseValidator(publicKeyBase64);
|
||||
|
||||
Instant past = Instant.now().minus(1, ChronoUnit.DAYS);
|
||||
String payload = """
|
||||
{"tier":"LOW","features":["topology"],"limits":{},"iat":%d,"exp":%d}
|
||||
""".formatted(past.minus(30, ChronoUnit.DAYS).getEpochSecond(), past.getEpochSecond()).trim();
|
||||
String signature = sign(kp.getPrivate(), payload);
|
||||
String token = Base64.getEncoder().encodeToString(payload.getBytes()) + "." + signature;
|
||||
|
||||
assertThatThrownBy(() -> validator.validate(token))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("expired");
|
||||
}
|
||||
|
||||
@Test
|
||||
void validate_tamperedPayload_throwsException() throws Exception {
|
||||
KeyPair kp = generateKeyPair();
|
||||
String publicKeyBase64 = Base64.getEncoder().encodeToString(kp.getPublic().getEncoded());
|
||||
LicenseValidator validator = new LicenseValidator(publicKeyBase64);
|
||||
|
||||
String payload = """
|
||||
{"tier":"LOW","features":["topology"],"limits":{},"iat":0,"exp":9999999999}
|
||||
""".trim();
|
||||
String signature = sign(kp.getPrivate(), payload);
|
||||
|
||||
// Tamper with payload
|
||||
String tampered = payload.replace("LOW", "BUSINESS");
|
||||
String token = Base64.getEncoder().encodeToString(tampered.getBytes()) + "." + signature;
|
||||
|
||||
assertThatThrownBy(() -> validator.validate(token))
|
||||
.isInstanceOf(SecurityException.class)
|
||||
.hasMessageContaining("signature");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user