feat(ui): enhance signature confirmation dialog with wysiwys details and update ccsp spec

This commit is contained in:
Claudecio Martins
2026-10-20 14:56:08 -03:00
parent 1a727e6ff5
commit 66459bfae4
4 changed files with 82 additions and 11 deletions
+9
View File
@@ -152,11 +152,20 @@ Solicita a assinatura digital local do digest informado com o certificado escolh
"certificate_hint": "win-4340656a", "certificate_hint": "win-4340656a",
"document_name": "contrato_prestacao_servicos.pdf", "document_name": "contrato_prestacao_servicos.pdf",
"document_type": "PDF", "document_type": "PDF",
"document_size": 1474560,
"origin": "https://meusistema.empresa.com.br",
"nonce": "c4ca4238a0b923820dcc509a6f75849b", "nonce": "c4ca4238a0b923820dcc509a6f75849b",
"expires_at": "2026-10-11T20:50:00Z" "expires_at": "2026-10-11T20:50:00Z"
} }
``` ```
> **Garantia WYSIWYS (What You See Is What You Sign):**
> O agente CyberCore Signer apresenta ao usuário, antes de qualquer operação criptográfica:
> - **Aplicação Solicitante e Origem Web:** Exibe a URL de origem da página web chamadora para prevenir ataques de personificação (Spoofing).
> - **Nome e Tamanho do Documento:** Se informados via `document_name` e `document_size` (em bytes), são apresentados de forma legível (ex: `1.4 MB`).
> - **Hash do Documento (SHA-256 Digest):** Formatado em blocos legíveis de 4 caracteres (ex: `A1B2 C3D4 ...`) em tipografia monoespaçada para fácil conferência humana.
> - **Certificado e Dispositivo:** Exibe titular, CPF/CNPJ, tipo A1/A3 e algoritmo de assinatura.
#### Resposta de Sucesso (`200 OK`) #### Resposta de Sucesso (`200 OK`)
```json ```json
{ {
@@ -116,7 +116,13 @@ public record CcspRequest(
String targetElementTag, String targetElementTag,
@JsonProperty("target_element_id") @JsonProperty("target_element_id")
String targetElementId String targetElementId,
@JsonProperty("document_size")
Long documentSize,
@JsonProperty("origin")
String origin
) implements Serializable { ) implements Serializable {
public static final String PROTOCOL_VERSION = "1"; public static final String PROTOCOL_VERSION = "1";
@@ -130,7 +136,8 @@ public record CcspRequest(
certificateHint, expiresAt, timestamp, nonce, signatureType, certificateHint, expiresAt, timestamp, nonce, signatureType,
null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null); null, null, null, null, null, null, null, null, null, null,
null, null);
} }
public boolean isVersionSupported() { public boolean isVersionSupported() {
@@ -472,9 +472,27 @@ public class CyberCoreDesktopApplication extends Application {
.map(AuthorizedApplication::name) .map(AuthorizedApplication::name)
.orElse(request.applicationId()); .orElse(request.applicationId());
Long docSize = request.documentSize();
if (docSize == null && request.pdf() != null) {
try {
docSize = (long) Base64.getDecoder().decode(request.pdf()).length;
} catch (Exception ignored) {}
} else if (docSize == null && request.xmlBase64() != null) {
try {
docSize = (long) Base64.getDecoder().decode(request.xmlBase64()).length;
} catch (Exception ignored) {}
}
String requestOrigin = request.origin();
if (requestOrigin == null || requestOrigin.isBlank()) {
requestOrigin = ApplicationRegistry.getInstance().findById(request.applicationId())
.map(a -> String.join(", ", a.allowedOrigins()))
.orElse("Requisição Web Local");
}
SignaturePromptDialog dialog = new SignaturePromptDialog( SignaturePromptDialog dialog = new SignaturePromptDialog(
stage, appName, "Requisição Web CCSP", certificate, context, stage, appName, requestOrigin, certificate, context,
request.documentName(), request.documentType(), request.isAuthentication() request.documentName(), request.documentType(), docSize, request.isAuthentication()
); );
dialog.showAndAwait().whenComplete((approved, err) -> { dialog.showAndAwait().whenComplete((approved, err) -> {
if (err != null) { if (err != null) {
@@ -36,12 +36,18 @@ public class SignaturePromptDialog {
public SignaturePromptDialog(Stage owner, String applicationName, String origin, public SignaturePromptDialog(Stage owner, String applicationName, String origin,
ManagedCertificate certificate, SigningContext context) { ManagedCertificate certificate, SigningContext context) {
this(owner, applicationName, origin, certificate, context, null, null, false); this(owner, applicationName, origin, certificate, context, null, null, null, false);
} }
public SignaturePromptDialog(Stage owner, String applicationName, String origin, public SignaturePromptDialog(Stage owner, String applicationName, String origin,
ManagedCertificate certificate, SigningContext context, ManagedCertificate certificate, SigningContext context,
String documentName, String documentType, boolean isAuthentication) { String documentName, String documentType, boolean isAuthentication) {
this(owner, applicationName, origin, certificate, context, documentName, documentType, null, isAuthentication);
}
public SignaturePromptDialog(Stage owner, String applicationName, String origin,
ManagedCertificate certificate, SigningContext context,
String documentName, String documentType, Long documentSize, boolean isAuthentication) {
stage = new Stage(); stage = new Stage();
stage.initModality(Modality.APPLICATION_MODAL); stage.initModality(Modality.APPLICATION_MODAL);
if (owner != null) { if (owner != null) {
@@ -52,11 +58,11 @@ public class SignaturePromptDialog {
: "CyberCore Signer — Solicitação de Assinatura"); : "CyberCore Signer — Solicitação de Assinatura");
stage.setResizable(false); stage.setResizable(false);
initLayout(applicationName, origin, certificate, context, documentName, documentType, isAuthentication); initLayout(applicationName, origin, certificate, context, documentName, documentType, documentSize, isAuthentication);
} }
private void initLayout(String appName, String origin, ManagedCertificate cert, SigningContext context, private void initLayout(String appName, String origin, ManagedCertificate cert, SigningContext context,
String documentName, String documentType, boolean isAuthentication) { String documentName, String documentType, Long documentSize, boolean isAuthentication) {
VBox root = new VBox(16); VBox root = new VBox(16);
root.getStyleClass().add("dialog-container"); root.getStyleClass().add("dialog-container");
root.setPadding(new Insets(20, 24, 20, 24)); root.setPadding(new Insets(20, 24, 20, 24));
@@ -94,7 +100,8 @@ public class SignaturePromptDialog {
grid.setVgap(10); grid.setVgap(10);
int row = 0; int row = 0;
addField(grid, row++, "Sistema:", appName + " (" + (origin != null ? origin : "Local") + ")"); addField(grid, row++, "Aplicação Solicitante:", appName);
addField(grid, row++, "Origem Web:", (origin != null && !origin.isBlank() ? origin : "Local"));
if (isAuthentication) { if (isAuthentication) {
addField(grid, row++, "Operação:", "Autenticação de Usuário (Challenge-Response)"); addField(grid, row++, "Operação:", "Autenticação de Usuário (Challenge-Response)");
} else { } else {
@@ -102,7 +109,12 @@ public class SignaturePromptDialog {
String docLabel = (documentType != null && !documentType.isBlank()) String docLabel = (documentType != null && !documentType.isBlank())
? "[" + documentType.toUpperCase() + "] " + documentName ? "[" + documentType.toUpperCase() + "] " + documentName
: documentName; : documentName;
if (documentSize != null && documentSize > 0) {
docLabel += " (" + formatFileSize(documentSize) + ")";
}
addField(grid, row++, "Documento:", docLabel); addField(grid, row++, "Documento:", docLabel);
} else if (documentSize != null && documentSize > 0) {
addField(grid, row++, "Tamanho do Documento:", formatFileSize(documentSize));
} }
addField(grid, row++, "Operação:", "Assinatura Digital (Digest)"); addField(grid, row++, "Operação:", "Assinatura Digital (Digest)");
} }
@@ -128,14 +140,16 @@ public class SignaturePromptDialog {
detailsBox.getChildren().add(grid); detailsBox.getChildren().add(grid);
// Caixa do Hash / Digest // Caixa do Hash / Digest formatado em blocos legíveis (WYSIWYS)
VBox hashBox = new VBox(6); VBox hashBox = new VBox(6);
Label hashLabel = new Label("Hash do Documento (SHA-256 Digest):"); Label hashLabel = new Label("Hash do Documento (SHA-256 Digest formatado):");
hashLabel.getStyleClass().add("dialog-field-title"); hashLabel.getStyleClass().add("dialog-field-title");
String hashHex = HexFormat.of().formatHex(context.digest()); String hashHex = HexFormat.of().formatHex(context.digest());
Label hashValue = new Label(hashHex); String formattedHash = formatHashBlocks(hashHex);
Label hashValue = new Label(formattedHash);
hashValue.getStyleClass().add("dialog-hash-value"); hashValue.getStyleClass().add("dialog-hash-value");
hashValue.setStyle("-fx-font-family: 'Liberation Mono', 'Courier New', monospace; -fx-font-size: 11px;");
hashValue.setWrapText(true); hashValue.setWrapText(true);
hashBox.getChildren().addAll(hashLabel, hashValue); hashBox.getChildren().addAll(hashLabel, hashValue);
@@ -301,4 +315,27 @@ public class SignaturePromptDialog {
stage.toFront(); stage.toFront();
return approvalFuture; return approvalFuture;
} }
private static String formatFileSize(long bytes) {
if (bytes < 1024) {
return bytes + " B";
} else if (bytes < 1024 * 1024) {
return String.format(java.util.Locale.ROOT, "%.1f KB", bytes / 1024.0);
} else {
return String.format(java.util.Locale.ROOT, "%.1f MB", bytes / (1024.0 * 1024.0));
}
}
private static String formatHashBlocks(String hex) {
if (hex == null || hex.isBlank()) return "";
StringBuilder sb = new StringBuilder();
String upper = hex.toUpperCase(java.util.Locale.ROOT);
for (int i = 0; i < upper.length(); i++) {
if (i > 0 && i % 4 == 0) {
sb.append(' ');
}
sb.append(upper.charAt(i));
}
return sb.toString();
}
} }