1O essencial em dois minutos
Não há serviço de assinatura na VM. O seu sistema guarda o pedido e o TCloud Assinador instalado no computador do usuário faz todo o trabalho: baixa os documentos, assina o arquivo inteiro ali mesmo e devolve os assinados ao seu sistema.
- 1NavegadorUsuário clica em Assinar
- 2VMO PHP cria o pedido e o ticket
TCloudAssinadorLocal - 3Computador do usuárioO link
tcloudsign://localabre o TCloud Assinador, que baixa os documentos - 4Computador do usuárioEscolhe o certificado, digita o PIN e assina o arquivo inteiro
- 5VMRecebe os assinados e grava no sistema
- 6NavegadorMostra o resultado
O que o seu sistema faz:
- autenticar o usuário e decidir se ele pode assinar aquele documento (o assinador não faz isso);
- criar o pedido com os documentos (
iniciar) e devolver o link ao navegador; - atender o app da estação pelas ações
ticket_*(a classe PHP faz tudo; você só encaminha); - acompanhar o estado e, ao concluir, gravar os arquivos assinados.
O que o assinador faz, no computador do usuário: mostra a janela de confirmação com os documentos e os certificados, pede o PIN do token, assina (PDF com chancela e bloqueio contra alterações, XML ou JSON), aplica o carimbo de tempo se a estação tiver ACT, e devolve os arquivos.
Quem assina é a versão instalada na estação. Atualizar o assinador é atualizar as estações; na VM não há nada do assinador instalado. Ele existe para Windows, Mac e Linux, com a mesma interface e o mesmo protocolo: o seu sistema não precisa saber em qual deles o usuário está.
Peças do lado do sistema (pasta atlas\ do pacote):
| Arquivo | Papel |
|---|---|
TCloudAssinadorLocal.php |
A classe: cria pedidos e tickets, guarda os documentos, atende a estação, entrega os assinados. |
TCloudAssinador.php |
Traz a TCloudAssinadorException, usada pela classe acima. |
assinador-endpoint.php |
Endpoint de exemplo, pronto para adaptar (três AJUSTES). |
tcloud-assinar.js |
A tela: abre o link, acompanha o pedido e mostra as mensagens. |
Requisitos: PHP 5.6 ou mais novo, sem extensões especiais; a estação precisa alcançar o endereço do endpoint (é o mesmo que o navegador já usa).
2Integração mínima, de ponta a ponta
2.1Endpoint do sistema
O assinador-endpoint.php do pacote já é a integração completa; basta fazer os três AJUSTES. A estrutura dele:
<?php
require_once __DIR__ . '/TCloudAssinador.php';
require_once __DIR__ . '/TCloudAssinadorLocal.php';
$CONFIG_LOCAL = [
'sistema' => 'Atlas',
// 'url_endpoint' => 'https://atlas.cartorio/signum/assinador-endpoint.php', // se houver proxy
// 'exigir_mesmo_ip' => false, // idem
];
$acao = isset($_REQUEST['acao']) ? $_REQUEST['acao'] : '';
// 1) O app da estação fala pelo ticket, SEM sessão: atende ANTES do login.
if (strpos($acao, 'ticket_') === 0) {
(new TCloudAssinadorLocal($CONFIG_LOCAL))->atenderEstacao($acao);
exit;
}
// 2) Daqui para baixo, só o navegador do usuário logado.
session_start();
header('Content-Type: application/json; charset=utf-8');
// AJUSTE 1 — a sua autenticação.
$usuario = isset($_SESSION['usuario_login']) ? $_SESSION['usuario_login'] : null;
$nome = isset($_SESSION['usuario_nome']) ? $_SESSION['usuario_nome'] : $usuario;
if (!$usuario) responder(['ok' => false, 'erro' => 'Sessão expirada.'], 401);
// AJUSTE 2 — onde está o documento de cada id (e se o usuário pode assiná-lo).
function caminho_do_documento($id) { /* ... */ }
// AJUSTE 3 — o que fazer com cada documento assinado (PDF, XML ou JSON).
function gravar_assinado($id, $caminhoOriginal, $conteudo, $nomeAssinado = null) { /* ... */ }
$assinador = new TCloudAssinadorLocal($CONFIG_LOCAL);
switch ($acao) {
case 'iniciar': // POST documentos[]=id&titulo=...
$ids = isset($_POST['documentos']) ? (array)$_POST['documentos'] : [];
$docs = []; $mapa = [];
foreach ($ids as $id) {
$caminho = caminho_do_documento($id);
if (!$caminho) responder(['ok' => false, 'erro' => "Documento $id não encontrado."], 404);
$docs[] = ['nome' => basename($caminho), 'arquivo' => $caminho];
$mapa[] = ['id' => $id, 'caminho' => $caminho];
}
$r = $assinador->iniciar($usuario, $docs, [
'nome' => $nome,
'titulo' => isset($_POST['titulo']) ? $_POST['titulo'] : null,
'motivo' => 'Assinatura digital',
'aparencia' => ['visivel' => true, 'pagina' => -1, 'x' => 36, 'y' => 36,
'largura' => 220, 'altura' => 80, 'ancorar_direita' => true],
]);
$_SESSION['tcloud_pedidos'][$r['id']] = $mapa; // o pedido pertence a esta sessão
responder($r); // {ok, id, estado, modo, link}
case 'status': // GET id=...
$id = isset($_GET['id']) ? $_GET['id'] : '';
if (!isset($_SESSION['tcloud_pedidos'][$id])) responder(['ok' => false, 'erro' => 'Pedido desconhecido.'], 404);
$s = $assinador->status($id);
if ($s['estado'] === 'concluido') {
// Concluiu: grava aqui mesmo. O JavaScript só acompanha o estado.
$res = $assinador->resultado($id);
foreach ($res['documentos'] as $i => $d) {
if (!empty($d['pdf'])) { // 'pdf' traz os bytes (de qualquer formato)
$m = $_SESSION['tcloud_pedidos'][$id][$i];
gravar_assinado($m['id'], $m['caminho'], $d['pdf'], $d['nome_assinado']);
}
}
unset($_SESSION['tcloud_pedidos'][$id]);
}
responder($s);
case 'cancelar': // POST id=...
responder($assinador->cancelar(isset($_POST['id']) ? $_POST['id'] : ''));
}
Pontos que o exemplo real trata e que valem para qualquer adaptação:
- As ações
ticket_*vêm antes da sessão. O app da estação não tem o cookie do navegador; a credencial dele é o ticket. - O pedido fica preso à sessão (
$_SESSION['tcloud_pedidos']): um usuário não consulta nem cancela o pedido de outro. - O navegador manda formulário, não JSON:
documentos[]etitulopor POST eminiciar,idpor GET emstatuse por POST emcancelar. É o formato que otcloud-assinar.jsusa. - Grave na ação
status, quando o estado viraconcluido. O exemplo real também guarda o resultado numa segunda chave de sessão, para que uma consulta repetida não grave duas vezes.
2.2Tela
<script src="tcloud-assinar.js"></script>
<script>
const assinador = new TCloudAssinar({
endpoint: 'assinador-endpoint.php',
cabecalhos: { 'X-CSRF-Token': tokenCsrf }, // opcional
urlInstalacao: 'https://tcloudsoft.app/download' // opcional: "Não abriu? Instale…"
});
document.getElementById('btn-assinar').onclick = async () => {
try {
const s = await assinador.assinar([12, 13], { titulo: 'Protocolo 5521' });
console.log(s.mensagem); // resolve com o status do pedido concluído
location.reload();
} catch (e) {
if (e.codigo === 'cancelado') return; // o usuário fechou a janela
alert(e.message + ' (' + (e.codigo || 'erro') + ')'); // recusado, expirado, erro…
}
};
</script>
O tcloud-assinar.js chama iniciar, abre o link tcloudsign://local, mostra o botão Abrir o TCloud Assinador se o navegador não abrir sozinho, consulta o estado e mostra as mensagens. Nenhuma dependência. O JavaScript nunca vê o conteúdo dos documentos.
| Opção | Padrão | Para quê |
|---|---|---|
endpoint |
'assinador-endpoint.php' |
Endereço do seu endpoint. |
intervalo |
1500 |
Intervalo do polling, em milissegundos. |
cabecalhos |
{} |
Cabeçalhos extras (CSRF, por exemplo). |
urlInstalacao |
— | Link de instalação mostrado quando o assinador não abre. |
assinar(documentos, extra) devolve uma Promise: resolve com o status final e rejeita com um Error que traz codigo (cancelado, recusado, expirado, erro…).
2.3A classe TCloudAssinadorLocal
$assinador = new TCloudAssinadorLocal([
'sistema' => 'Atlas', // nome na janela ("Atlas pede a sua assinatura")
'url_endpoint' => null, // endereço deste endpoint visto pela estação (padrão: o da requisição)
'pasta' => null, // onde guardar os pedidos (padrão: pasta temporária do sistema)
'minutos_abrir' => 3, // prazo para a estação abrir o link
'minutos_concluir' => 15, // prazo para concluir depois de aberto
'exigir_mesmo_ip' => true, // o link só abre no IP de quem clicou
]);
| Método | Devolve | Para quê |
|---|---|---|
iniciar($usuario, $documentos, $opcoes) |
['ok', 'id', 'estado', 'modo' => 'local', 'link'] |
Cria o pedido. Cada documento leva arquivo (caminho) ou pdf (bytes — o nome do campo é pdf para qualquer formato), e nome opcional. |
status($id) |
['ok', 'id', 'estado', 'finalizado', 'mensagem', 'estacao', 'documentos'] |
Estado do pedido; documentos traz nome, assinado e, se houver, erro. |
resultado($id) |
['ok', 'id', 'documentos'] |
Só com o pedido concluido (senão, exceção com status 409). Cada documento traz pdf em bytes, nome_assinado, assinado, erro e avisos. |
cancelar($id) |
['ok'] |
Encerra o pedido. |
atenderEstacao($acao) |
— (imprime o JSON) | Atende as ações ticket_* do app da estação. |
acaoEstacao($acao, $corpo) |
array |
O mesmo, sem HTTP (útil em testes e frameworks). |
Erros viram TCloudAssinadorException, com ->codigo e ->status (HTTP): sem_documentos, arquivo, documento, pasta, pedido (404), ticket (403/404), outro_computador (403), acao (404), ou o nome do estado quando o pedido já foi encerrado.
Os pedidos ficam em arquivos na pasta temporária do servidor (sem banco de dados) e são apagados 30 minutos depois de encerrados, ou 1 hora depois de criados se ninguém os concluir.
3O que acontece em cada etapa
3.1Criar o pedido (iniciar)
O PHP guarda os documentos, gera um ticket de 256 bits e devolve:
{ "ok": true, "id": "LDTJw8VJ_gx3h", "estado": "aguardando_estacao", "modo": "local",
"link": "tcloudsign://local?u=https%3A%2F%2Fatlas.cartorio%2Fsignum%2Fassinador-endpoint.php&t=Qm9…" }
O link leva o endereço do seu endpoint (u) e o ticket (t). Ele precisa ser aberto no navegador do computador de quem clicou — o tcloud-assinar.js faz isso por um <iframe> escondido e oferece também um botão, porque um clique de verdade é o que os navegadores mais aceitam para abrir outro programa.
3.2A estação abre o link
O sistema operacional entrega o link ao TCloud Assinador (o instalador registrou o tcloudsign:// no Windows, no Mac e no Linux). Na primeira vez que um sistema aparece, o assinador pergunta "Confiar neste servidor?" e lembra a resposta. Então ele chama as ações ticket_* do seu endpoint (seção 4): abre o pedido, baixa os documentos, mostra a janela de confirmação, assina e devolve cada arquivo.
3.3Acompanhar e gravar
O tcloud-assinar.js consulta status a cada 1,5 s. Quando o estado vira concluido, o seu endpoint chama resultado() e grava os arquivos. Um pedido pode concluir parcialmente: percorra documentos olhando assinado e erro de cada um.
4Protocolo entre a estação e o seu endpoint (ticket_*)
Só interessa a quem não usa a classe TCloudAssinadorLocal (outra linguagem, outro framework). Todas as ações são POST <seu endpoint>?acao=<ação>, com corpo JSON contendo o ticket, e respondem JSON.
| Ação | Corpo além do ticket |
Resposta |
|---|---|---|
ticket_abrir |
— | {ok, pedido: {id, sistema, titulo, usuario, nome, documentos: [nomes], opcoes: {…}, minutos}} |
ticket_documento |
indice |
{ok, nome, conteudo_base64} |
ticket_entregar |
indice, nome_assinado, conteudo_base64, avisos — ou indice, erro |
{ok} |
ticket_concluir |
— | {ok, mensagem} |
ticket_recusar |
motivo |
{ok} |
Formato do link: tcloudsign://local?u=<URL do endpoint, codificada>&t=<ticket>. O u precisa ser http ou https; o ticket, base64url com pelo menos 20 caracteres.
Regras que a implementação precisa garantir (a classe PHP já faz):
- ticket aleatório de pelo menos 128 bits (a classe usa 256), guardado só como hash;
ticket_abrirsó aceita o IP de quem criou o pedido (configurável), grava esse IP, e as demais ações só aceitam esse IP;- depois de encerrado, o pedido não reabre: o ticket é de uso único na prática;
- prazos: 3 minutos para abrir, 15 para concluir; depois,
expirado; - erros em JSON
{ok: false, erro, codigo}com status HTTP 4xx — o app mostra oerroao usuário.
opcoes leva as opções do pedido da seção 5, que a estação aplica na assinatura.
5Opções do pedido
As opções vão no terceiro argumento de iniciar() e chegam à estação em opcoes.
5.1Gerais
| Opção | Padrão | Descrição |
|---|---|---|
nome |
o usuario |
Nome de quem pediu, mostrado na janela ("Solicitado por …"). |
titulo |
— | Texto de contexto na janela (ex.: protocolo). Não vai para o documento. |
sistema |
o da configuração | Nome do sistema na janela. |
motivo |
o da estação | Motivo gravado na assinatura e mostrado na chancela. |
local |
o da estação | Local gravado na assinatura. |
nivel |
o da estação | basico (sem carimbo), carimbo ou ltv. A ACT é sempre a da estação, configurada no app: sem ACT, o documento sai sem carimbo e com um aviso. No Windows, ltv acrescenta a validação de longo prazo; no Mac e no Linux, vale como carimbo. |
bloqueio |
assinaturas |
Bloqueio contra alterações aplicado pela primeira assinatura de um PDF: assinaturas (bloqueia edição e permite novas assinaturas), total ou nenhum. |
5.2XML e JSON
O formato é detectado pelo conteúdo de cada documento (a extensão é o desempate); um mesmo pedido pode misturar PDF, XML e JSON.
| Formato | Padrão | Detalhes |
|---|---|---|
| PAdES | Chancela, bloqueio contra alterações e carimbo. | |
| XML | XMLDSig envelopada + XAdES básico, RSA-SHA256, canonicalização exclusiva | A assinatura entra como último filho da raiz; o resto do arquivo não muda. Com carimbo, vira XAdES-T. |
| JSON | JWS (RFC 7515) achatado, com x5c, x5t#S256 e sigT (JAdES) |
Embutida (padrão) ou destacada. Com carimbo, o token da ACT vai em header.etsiU[].sigTst. |
| Opção | Padrão | Descrição |
|---|---|---|
xml_multiplas |
false |
Assinar XML que já tem assinatura. Numa envelopada sobre o documento inteiro, a nova invalida a anterior (é do formato — por isso a NF-e admite só uma). |
json_destacado |
false |
Assinatura em arquivo .jws ao lado, com o JSON original intacto. |
Nomes dos arquivos devolvidos (nome_assinado): nome-assinado.pdf, nome-assinado.xml, nome-assinado.json e, na forma destacada, nome.jws.
5.3Chancela (aparencia, só PDF)
Sem aparencia, a assinatura é invisível (vale juridicamente, mas não marca a página).
| Opção | Padrão | Descrição |
|---|---|---|
visivel |
true |
Desenha a chancela. |
pagina |
1 |
1 = primeira, -1 = última, N = página N. |
x / y |
36 |
Distância em pontos até a borda de referência. |
largura / altura |
200 / 70 |
Tamanho em pontos. |
ancorar_direita / ancorar_topo |
false |
Mede x pela direita e y pelo topo. |
titulo |
— | Linha em negrito no topo (aceita \n). |
texto_adicional |
— | Linhas extras depois dos dados. |
texto |
— | Substitui todas as linhas automáticas. |
logo_base64 |
— | Logomarca ou brasão (PNG recomendado; JPG e outros são convertidos na estação). |
logo_posicao |
esquerda |
esquerda, acima ou fundo (marca d'água). |
borda |
true |
Borda em volta da chancela. |
Unidades: pontos. 1 cm ≈ 28,35 pt; A4 = 595 × 842 pt. A posição é medida a partir do canto escolhido, então a chancela fica no mesmo lugar em páginas de tamanhos diferentes.
6Estados e tempos
| Estado | Significa | finalizado |
|---|---|---|
aguardando_estacao |
Criado; esperando o TCloud Assinador abrir o link. | não |
na_estacao |
A janela de confirmação está aberta no computador do usuário. | não |
assinando |
A estação já baixou os documentos e está assinando. | não |
concluido |
Pelo menos um documento foi assinado. Chame resultado() e grave. |
sim |
recusado |
O usuário recusou, fechou a janela ou cancelou o PIN. | sim |
expirado |
A estação não abriu o link a tempo (o assinador está instalado?) ou não concluiu no prazo. | sim |
erro |
Nenhum documento pôde ser assinado. Veja mensagem. |
sim |
cancelado |
O sistema cancelou. | sim |
Tempos padrão: 3 minutos para a estação abrir o link (minutos_abrir), 15 para concluir depois de aberto (minutos_concluir). O pedido é apagado 30 minutos depois de encerrado; daí em diante, consultar dá erro pedido (404).
Repetição. Criar pedido não é idempotente: cada chamada cria um pedido e um link novos. Consultar o estado e ler o resultado podem ser repetidos à vontade enquanto o pedido existir. Um pedido por clique; ao trocar de tela ou fechar o modal, chame cancelar.
7Boas práticas
- Permissão é sua. O assinador assina o que o seu sistema mandou. Confira no
caminho_do_documentose aquele usuário pode assinar aquele documento. - Prenda o pedido à sessão, como o exemplo faz, para ninguém consultar ou cancelar o pedido de outro.
- Atrás de proxy ou balanceador, informe
url_endpoint(o endereço que a estação deve usar) e, se o IP real de quem clicou não chegar ao PHP,'exigir_mesmo_ip' => false. - Documentos grandes: o assinado volta num POST com o arquivo em base64, cerca de um terço maior que o original. Ajuste o
post_max_sizedo PHP de acordo. - Mostre a
mensagemdo estado ao usuário: ela já vem escrita para ele. - Trate
avisos: um documento sem carimbo porque a estação não tinha ACT continua válido; decida se isso basta para o seu fluxo. - Nunca registre em log o ticket nem o conteúdo dos documentos.
8Segurança
| Camada | O que protege |
|---|---|
| Sessão do seu sistema | Só o usuário logado cria, consulta e cancela os próprios pedidos. |
| Ticket | 256 bits aleatórios, guardado só como hash, de uso único, com prazo curto. |
| IP de quem clicou | Por padrão, o link só abre no computador de quem clicou, e depois de aberto fica preso a ele. |
| "Confiar neste servidor?" | Um link tcloudsign:// pode ser aberto por qualquer site; o assinador confirma o sistema na primeira vez e lembra a resposta. |
| Chave privada | Nunca sai do token (A3) nem do computador do usuário (A1). |
| Documentos no servidor | Na pasta temporária, fora da pasta pública do site, apagados depois do pedido. |
| Bloqueio do PDF | A primeira assinatura certifica o documento; qualquer edição posterior aparece como violação na validação. |
No endpoint, aplique o de sempre às ações do navegador: sessão autenticada, CSRF e limitação de taxa. As ações ticket_* não usam sessão, e por isso a proteção delas é o ticket e o IP.
9Homologação
Dá para testar tudo num computador só: rode o Atlas/BookC nele (XAMPP, por exemplo), instale o TCloud Assinador e assine pelo sistema em http://localhost/....
10Erros comuns
| Sintoma | Causa | O que fazer |
|---|---|---|
| Fica em "Abrindo o TCloud Assinador…" e expira | O assinador não está instalado nesse computador, ou o navegador bloqueou o link. | Instale-o; ofereça o botão "Abrir o TCloud Assinador" e permita abrir o programa no navegador. |
| "Este link foi gerado para outro computador" | O IP que chega ao PHP não é o do computador (proxy no caminho). | Informe url_endpoint e use 'exigir_mesmo_ip' => false. |
| O assinador não consegue falar com o sistema | A estação não alcança o url_endpoint (proxy, DNS, firewall). |
Abra o endereço do endpoint no navegador da estação; ajuste url_endpoint. |
| "Este link de assinatura não vale mais" | Pedido encerrado, expirado ou já apagado. | Clicar em "Assinar" de novo cria um pedido novo. |
| O assinador mostra "Sessão expirada" (ou a mensagem de login do seu sistema) ao abrir o pedido | As ações ticket_* estão passando pela verificação de login. |
Encaminhe-as à classe antes do session_start e da autenticação. |
| O documento grande não volta | O POST com o assinado passou do post_max_size. |
Aumente o post_max_size (e o upload_max_filesize, se o servidor exigir). |
| Documento sem carimbo, com aviso | A estação não tem ACT configurada. | Configure a ACT nas Configurações do app, na estação. |
pasta ao criar a classe |
O PHP não consegue escrever na pasta temporária. | Informe pasta com uma pasta gravável, fora da pasta pública do site. |
11Compatibilidade e histórico
O contrato só cresce: campos novos entram sem quebrar quem já integrou. Faça o seu cliente ignorar campos desconhecidos.
| Versão | Mudança |
|---|---|
| 2.6.0 | Versão para Linux, com a mesma interface e o mesmo protocolo. Nada muda no seu sistema. |
| 2.5.0 | O instalador passou a instalar só o app da estação: não instala mais o servidor de assinatura da VM (e remove o das versões anteriores). |
| 2.4.0 | Modo local (tcloudsign://local): classe TCloudAssinadorLocal, ações ticket_* no endpoint, assinatura inteira na estação. |
| 2.2.0 | Pedidos aceitam XML e JSON além de PDF, detectados pelo conteúdo; resultado com nome_assinado. |
| 2.0.0 | Opção bloqueio; a primeira assinatura passou a certificar o PDF contra alterações. |
Integrações antigas, com o serviço da VM (classe TCloudAssinador, porta 9480, X-TCloud-Token, rotas /sistema/*): continuam funcionando onde o serviço ainda estiver instalado, mas ele não é mais distribuído. Para migrar, troque new TCloudAssinador(...) por new TCloudAssinadorLocal(...) — os métodos iniciar, status, resultado e cancelar têm os mesmos nomes e o mesmo formato — e encaminhe as ações ticket_* antes do login, como na seção 2.1. O tcloud-assinar.js não muda. O código do serviço continua no projeto (src\TCloud.PdfSigner.Agent) e o manual geral descreve esse modo na seção 2.7.