test(ccsp): add bidirectional cross-validation tests against ccsp-v1 schema
This commit is contained in:
+1
@@ -927,6 +927,7 @@ public class CcspServer {
|
||||
resp.put("status", "success");
|
||||
resp.put("default_seal_id", PdfSealThemeRegistry.getDefaultTheme().id());
|
||||
resp.put("seals", sealList);
|
||||
resp.put("themes", sealList);
|
||||
|
||||
sendJsonResponse(exchange, 200, resp);
|
||||
}
|
||||
|
||||
+425
@@ -0,0 +1,425 @@
|
||||
package br.com.cybercore.signer.core.protocol;
|
||||
|
||||
import br.com.cybercore.signer.core.crypto.SigningService;
|
||||
import br.com.cybercore.signer.core.domain.CertificateStatus;
|
||||
import br.com.cybercore.signer.core.domain.CertificateType;
|
||||
import br.com.cybercore.signer.core.domain.ManagedCertificate;
|
||||
import br.com.cybercore.signer.core.domain.SigningContext;
|
||||
import br.com.cybercore.signer.core.domain.SigningKeyMaterial;
|
||||
import br.com.cybercore.signer.core.domain.SigningResult;
|
||||
import br.com.cybercore.signer.core.provider.CertificateManager;
|
||||
import br.com.cybercore.signer.core.provider.CertificateProvider;
|
||||
import br.com.cybercore.signer.core.provider.ProviderType;
|
||||
import br.com.cybercore.signer.core.security.ApplicationRegistry;
|
||||
import br.com.cybercore.signer.core.security.AuthorizedApplication;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
|
||||
import org.bouncycastle.operator.ContentSigner;
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@DisplayName("Testes de Validação Cruzada contra o JSON Schema CCSP/1 (ccsp-v1.schema.json)")
|
||||
class CcspSchemaValidationTest {
|
||||
|
||||
private CcspServer server;
|
||||
private HttpClient httpClient;
|
||||
private String baseUrl;
|
||||
private ObjectMapper objectMapper;
|
||||
private JsonNode schemaRoot;
|
||||
|
||||
@BeforeEach
|
||||
void setUp(@TempDir Path tempDir) throws Exception {
|
||||
objectMapper = new ObjectMapper();
|
||||
|
||||
// Localiza e carrega docs/ccsp-v1.schema.json
|
||||
Path schemaPath = Path.of("../../docs/ccsp-v1.schema.json");
|
||||
if (!Files.exists(schemaPath)) {
|
||||
schemaPath = Path.of("docs/ccsp-v1.schema.json");
|
||||
}
|
||||
assertThat(schemaPath).exists();
|
||||
schemaRoot = objectMapper.readTree(Files.readString(schemaPath, StandardCharsets.UTF_8));
|
||||
|
||||
CertificateManager certManager = new CertificateManager();
|
||||
SigningService signingService = mock(SigningService.class);
|
||||
ApplicationRegistry appRegistry = new ApplicationRegistry(tempDir.resolve("apps.json"));
|
||||
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
|
||||
kpg.initialize(2048);
|
||||
KeyPair kp = kpg.generateKeyPair();
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
Date notBefore = new Date(now - 100000);
|
||||
Date notAfter = new Date(now + 365L * 24 * 3600 * 1000);
|
||||
|
||||
X500Name subject = new X500Name("CN=Schema Test ICP-Brasil:12345678901, O=CyberCore, C=BR");
|
||||
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
|
||||
subject,
|
||||
BigInteger.valueOf(System.currentTimeMillis()),
|
||||
notBefore,
|
||||
notAfter,
|
||||
subject,
|
||||
kp.getPublic()
|
||||
);
|
||||
|
||||
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(kp.getPrivate());
|
||||
X509Certificate cert = new JcaX509CertificateConverter().getCertificate(certBuilder.build(signer));
|
||||
|
||||
appRegistry.register(new AuthorizedApplication(
|
||||
"app-schema-test",
|
||||
"Schema Validation Test App",
|
||||
Set.of("http://localhost:3000", "http://127.0.0.1:3000"),
|
||||
true,
|
||||
Instant.now()
|
||||
));
|
||||
|
||||
ManagedCertificate testCert = new ManagedCertificate(
|
||||
"cert-schema-1",
|
||||
"Certificado Schema Teste",
|
||||
CertificateType.A1,
|
||||
CertificateStatus.AVAILABLE,
|
||||
"CN=Schema Test ICP-Brasil:12345678901, O=CyberCore, C=BR",
|
||||
"CN=Schema Test ICP-Brasil:12345678901, O=CyberCore, C=BR",
|
||||
"12345678",
|
||||
"a1b2c3",
|
||||
"d4e5f6",
|
||||
Instant.now().minus(10, ChronoUnit.DAYS),
|
||||
Instant.now().plus(300, ChronoUnit.DAYS),
|
||||
"SHA256withRSA",
|
||||
2048,
|
||||
"Schema Test ICP-Brasil",
|
||||
null,
|
||||
null,
|
||||
"provider-schema-test",
|
||||
"Certificado Schema Teste"
|
||||
);
|
||||
|
||||
CertificateProvider dummyProvider = new CertificateProvider() {
|
||||
@Override public String getProviderId() { return "provider-schema-test"; }
|
||||
@Override public String getDisplayName() { return "Provedor Teste Schema"; }
|
||||
@Override public ProviderType getType() { return ProviderType.A1_FILE; }
|
||||
@Override public boolean isAvailable() { return true; }
|
||||
@Override public List<ManagedCertificate> listCertificates() { return List.of(testCert); }
|
||||
@Override public SigningResult sign(SigningContext context) {
|
||||
return new SigningResult(context.requestId(), new byte[]{1, 2, 3}, "mock-cert", List.of("mock-cert"), "SHA256withRSA", Instant.now());
|
||||
}
|
||||
@Override public Optional<SigningKeyMaterial> getKeyMaterial(String certificateId, char[] pinOrPassword) {
|
||||
return Optional.of(new SigningKeyMaterial(kp.getPrivate(), new X509Certificate[]{cert}, null));
|
||||
}
|
||||
};
|
||||
|
||||
certManager.registerProvider(dummyProvider);
|
||||
certManager.discoverCertificates();
|
||||
|
||||
when(signingService.sign(any())).thenReturn(
|
||||
new SigningResult("req-1", new byte[]{10, 20, 30}, "base64-cert", List.of("base64-cert"), "SHA256withRSA", Instant.now())
|
||||
);
|
||||
|
||||
server = new CcspServer(0, certManager, signingService, appRegistry, new CcspUserPromptListener() {
|
||||
@Override
|
||||
public CompletableFuture<Boolean> requestApplicationAuthorization(String origin, String appName, String applicationId) {
|
||||
return CompletableFuture.completedFuture(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> requestSignatureConfirmation(CcspRequest request, ManagedCertificate certificate) {
|
||||
return CompletableFuture.completedFuture(true);
|
||||
}
|
||||
});
|
||||
|
||||
server.start();
|
||||
int port = server.getPort();
|
||||
baseUrl = "http://127.0.0.1:" + port;
|
||||
httpClient = HttpClient.newHttpClient();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
if (server != null) {
|
||||
server.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Garante integridade e completude do catálogo de ErrorCode no schema formal")
|
||||
void shouldValidateErrorCodeCatalogInSchema() {
|
||||
JsonNode errorCodeEnum = schemaRoot.path("definitions").path("ErrorCode").path("enum");
|
||||
assertThat(errorCodeEnum.isArray()).isTrue();
|
||||
|
||||
Set<String> errorCodes = new HashSet<>();
|
||||
errorCodeEnum.forEach(node -> errorCodes.add(node.asText()));
|
||||
|
||||
assertThat(errorCodes).contains(
|
||||
"CERTIFICATE_NOT_FOUND",
|
||||
"CERTIFICATE_EXPIRED",
|
||||
"CERTIFICATE_INVALID",
|
||||
"PRIVATE_KEY_NOT_AVAILABLE",
|
||||
"TOKEN_NOT_FOUND",
|
||||
"TOKEN_REMOVED",
|
||||
"PIN_REQUIRED",
|
||||
"PIN_INCORRECT",
|
||||
"PKCS11_ERROR",
|
||||
"SIGNATURE_FAILED",
|
||||
"APPLICATION_NOT_AUTHORIZED",
|
||||
"REQUEST_EXPIRED",
|
||||
"REPLAY_DETECTED",
|
||||
"INVALID_ORIGIN",
|
||||
"UNSUPPORTED_ALGORITHM",
|
||||
"USER_CANCELLED",
|
||||
"PROVIDER_NOT_AVAILABLE",
|
||||
"COMMUNICATION_ERROR",
|
||||
"AGENT_NOT_RUNNING",
|
||||
"TIMEOUT_ERROR",
|
||||
"BAD_REQUEST"
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Valida resposta do endpoint /ccsp/v1/status contra a definição StatusResponse")
|
||||
void shouldValidateStatusEndpointResponseAgainstSchema() throws Exception {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(baseUrl + "/ccsp/v1/status"))
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
assertThat(response.statusCode()).isEqualTo(200);
|
||||
|
||||
JsonNode json = objectMapper.readTree(response.body());
|
||||
validateAgainstDefinition(json, "StatusResponse");
|
||||
|
||||
assertThat(json.path("version").asText()).isEqualTo("1");
|
||||
assertThat(json.path("status").asText()).isEqualTo("ready");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Valida resposta do endpoint /ccsp/v1/authorize contra a definição AuthorizeResponse")
|
||||
void shouldValidateAuthorizeEndpointResponseAgainstSchema() throws Exception {
|
||||
String payload = """
|
||||
{
|
||||
"version": "1",
|
||||
"application_id": "app-schema-test",
|
||||
"name": "Schema Validation Test App"
|
||||
}
|
||||
""";
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(baseUrl + "/ccsp/v1/authorize"))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Origin", "http://localhost:3000")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(payload))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
assertThat(response.statusCode()).isEqualTo(200);
|
||||
|
||||
JsonNode json = objectMapper.readTree(response.body());
|
||||
validateAgainstDefinition(json, "AuthorizeResponse");
|
||||
|
||||
assertThat(json.path("version").asText()).isEqualTo("1");
|
||||
assertThat(json.path("status").asText()).isEqualTo("authorized");
|
||||
assertThat(json.path("application_id").asText()).isEqualTo("app-schema-test");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Valida resposta do endpoint /ccsp/v1/certificates contra CertificatesResponse e CertificateDto")
|
||||
void shouldValidateCertificatesEndpointResponseAgainstSchema() throws Exception {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(baseUrl + "/ccsp/v1/certificates?application_id=app-schema-test"))
|
||||
.header("Origin", "http://localhost:3000")
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
assertThat(response.statusCode()).isEqualTo(200);
|
||||
|
||||
JsonNode json = objectMapper.readTree(response.body());
|
||||
validateAgainstDefinition(json, "CertificatesResponse");
|
||||
|
||||
JsonNode certificates = json.path("certificates");
|
||||
assertThat(certificates.isArray()).isTrue();
|
||||
assertThat(certificates.size()).isGreaterThanOrEqualTo(1);
|
||||
|
||||
for (JsonNode certNode : certificates) {
|
||||
validateAgainstDefinition(certNode, "CertificateItem");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Valida resposta do endpoint /ccsp/v1/sign contra a definição SignResponse")
|
||||
void shouldValidateSignEndpointResponseAgainstSchema() throws Exception {
|
||||
String payload = """
|
||||
{
|
||||
"version": "1",
|
||||
"request_id": "%s",
|
||||
"application_id": "app-schema-test",
|
||||
"operation": "sign",
|
||||
"algorithm": "SHA256withRSA",
|
||||
"digest": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
|
||||
"certificate_hint": "cert-schema-1",
|
||||
"document_name": "documento_teste.txt",
|
||||
"nonce": "%s",
|
||||
"expires_at": "%s"
|
||||
}
|
||||
""".formatted(
|
||||
UUID.randomUUID(),
|
||||
UUID.randomUUID(),
|
||||
Instant.now().plus(60, ChronoUnit.SECONDS).toString()
|
||||
);
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(baseUrl + "/ccsp/v1/sign"))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Origin", "http://localhost:3000")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(payload))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
assertThat(response.statusCode()).isEqualTo(200);
|
||||
|
||||
JsonNode json = objectMapper.readTree(response.body());
|
||||
validateAgainstDefinition(json, "SignResponse");
|
||||
|
||||
assertThat(json.path("version").asText()).isEqualTo("1");
|
||||
assertThat(json.path("status").asText()).isEqualTo("signed");
|
||||
assertThat(json.path("signature").asText()).isNotEmpty();
|
||||
assertThat(json.path("certificate").asText()).isNotEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Valida resposta de erro contra a definição ErrorResponse e ErrorCode catalog")
|
||||
void shouldValidateErrorResponseAgainstSchema() throws Exception {
|
||||
String payload = """
|
||||
{
|
||||
"version": "1",
|
||||
"request_id": "%s",
|
||||
"application_id": "app-schema-test",
|
||||
"operation": "sign",
|
||||
"algorithm": "SHA256withRSA",
|
||||
"digest": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
|
||||
"certificate_hint": "non-existent-cert-id",
|
||||
"nonce": "%s",
|
||||
"expires_at": "%s"
|
||||
}
|
||||
""".formatted(
|
||||
UUID.randomUUID(),
|
||||
UUID.randomUUID(),
|
||||
Instant.now().plus(60, ChronoUnit.SECONDS).toString()
|
||||
);
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(baseUrl + "/ccsp/v1/sign"))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Origin", "http://localhost:3000")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(payload))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
assertThat(response.statusCode()).isGreaterThanOrEqualTo(400);
|
||||
|
||||
JsonNode json = objectMapper.readTree(response.body());
|
||||
validateAgainstDefinition(json, "ErrorResponse");
|
||||
|
||||
String errorCode = json.path("error_code").asText();
|
||||
JsonNode errorCodeEnum = schemaRoot.path("definitions").path("ErrorCode").path("enum");
|
||||
List<String> validCodes = new ArrayList<>();
|
||||
errorCodeEnum.forEach(node -> validCodes.add(node.asText()));
|
||||
|
||||
assertThat(validCodes).contains(errorCode);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Valida resposta do endpoint /ccsp/v1/seals contra a definição SealsResponse")
|
||||
void shouldValidateSealsEndpointResponseAgainstSchema() throws Exception {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(baseUrl + "/ccsp/v1/seals"))
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
assertThat(response.statusCode()).isEqualTo(200);
|
||||
|
||||
JsonNode json = objectMapper.readTree(response.body());
|
||||
validateAgainstDefinition(json, "SealsResponse");
|
||||
}
|
||||
|
||||
private void validateAgainstDefinition(JsonNode json, String definitionName) {
|
||||
JsonNode def = schemaRoot.path("definitions").path(definitionName);
|
||||
assertThat(def.isMissingNode()).as("Definição %s deve existir no schema", definitionName).isFalse();
|
||||
|
||||
JsonNode required = def.path("required");
|
||||
if (required.isArray()) {
|
||||
for (JsonNode reqNode : required) {
|
||||
String field = reqNode.asText();
|
||||
assertThat(json.hasNonNull(field))
|
||||
.as("Campo obrigatório '%s' da definição '%s' deve estar presente e não nulo", field, definitionName)
|
||||
.isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
JsonNode properties = def.path("properties");
|
||||
if (properties.isObject()) {
|
||||
properties.fieldNames().forEachRemaining(propName -> {
|
||||
if (json.has(propName) && !json.get(propName).isNull()) {
|
||||
JsonNode propDef = properties.get(propName);
|
||||
String expectedType = propDef.path("type").asText(null);
|
||||
if (expectedType != null) {
|
||||
assertTypeMatches(json.get(propName), expectedType, propName, definitionName);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void assertTypeMatches(JsonNode valueNode, String expectedType, String propName, String defName) {
|
||||
switch (expectedType) {
|
||||
case "string" -> assertThat(valueNode.isTextual())
|
||||
.as("Propriedade '%s' em '%s' deve ser string", propName, defName).isTrue();
|
||||
case "boolean" -> assertThat(valueNode.isBoolean())
|
||||
.as("Propriedade '%s' em '%s' deve ser boolean", propName, defName).isTrue();
|
||||
case "number" -> assertThat(valueNode.isNumber())
|
||||
.as("Propriedade '%s' em '%s' deve ser number", propName, defName).isTrue();
|
||||
case "integer" -> assertThat(valueNode.isIntegralNumber())
|
||||
.as("Propriedade '%s' em '%s' deve ser integer", propName, defName).isTrue();
|
||||
case "array" -> assertThat(valueNode.isArray())
|
||||
.as("Propriedade '%s' em '%s' deve ser array", propName, defName).isTrue();
|
||||
case "object" -> assertThat(valueNode.isObject())
|
||||
.as("Propriedade '%s' em '%s' deve ser object", propName, defName).isTrue();
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user