---
id: SVC-DIGITALSIGNATURE-PDF
tipo: servico
onda: 2
status: concluido
unidades: [SVC-DIGITALSIGNATURE]
fontes:
  - V2/Services/DigitalSignature/
depende_de: [CORE-BOOT]
consumidores: [MOD-INFRAESTRUTURA-CERTIFICADODIGITAL]
criticidade: alta
dono: SvcSignature
---

# DigitalSignature — pipeline de assinatura de PDF

> Documento de apoio ao `spec.md`. Cobre exclusivamente `PdfSigner`, `VisualStampBuilder` e os três temas: como o PDF de entrada é reconstruído, onde o selo é desenhado, quais metadados entram na assinatura e o que o resultado **não** garante.

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Converte um PDF arbitrário em um novo PDF (versão 1.7 do TCPDF, elevada à do original se maior) com todas as páginas re-renderizadas, um carimbo visual opcional desenhado em coordenadas mm e uma assinatura PKCS#7 detached (`/SubFilter /adbe.pkcs7.detached`) aplicada por TCPDF. |
| Entrada | `PdfSigner::sign($pdfContent, $certificatePem, $privateKeyPem, array $metadata, $addVisualStamp)`; `$metadata` aceita `name`, `location`, `reason`, `contact_info`, `pos_x`, `pos_y`, `pos_page`, `theme_type`, `logo_path` (este último não é produzido por `SignatureRequest`). |
| Saída | `string` binária do PDF assinado; `SignatureException` em qualquer falha; nenhum arquivo persistente (temporários são removidos no `finally`). |
| Efeitos colaterais | 2 arquivos temporários com prefixos `v2_sign_input_`/`v2_sign_output_`; `chmod 0600` no input; mutação do tema ativo em `PdfSigner::$stampBuilder`; sem cache, banco, log ou evento. |
| Particularidade central | O PDF assinado é uma **reconstrução** FPDI, não um incremental update: o conteúdo das páginas é copiado, mas anotações, formulários, bookmarks, anexos, metadados e assinaturas anteriores não são. |
| Como se integra | `PdfSigner` é type-hintado diretamente no construtor de `DigitalSignatureService`; não implementa `SignerInterface`; os temas são plugados por `StampThemeInterface` em `VisualStampBuilder`. |
| Se removido, quebra | `DigitalSignatureService::signPdf()` e todos os fluxos do módulo `CertificadoDigital` que devolvem PDF assinado; nenhuma outra rota/serviço gera PDF assinado. |
| Superfície pública | `PdfSigner::sign()`, `PdfSigner::getType()`, `PdfSigner::getStampBuilder()`; `VisualStampBuilder::getWidth/getHeight/getMarginLeft/calculateStampY/buildHtml/getTheme`; `StampThemeInterface`; `SignerInterface` (não implementada). |

## 1. Efeito

### 1.1 Transformação

Entrada concreta: bytes de um PDF (tipicamente A4 retrato, 1+ páginas) + certificado PEM + chave PEM + metadados. Saída concreta: um PDF **novo**, versão 1.7 (ou a do original, se maior), com o mesmo número de páginas e o mesmo tamanho de página, no qual:

1. cada página foi importada por FPDI e redesenhada por `useTemplate()` (`V2/Services/DigitalSignature/PdfSigner.php:133-149`);
2. o carimbo visual foi desenhado com `writeHTMLCell()` na página alvo, se `$addVisualStamp` (`:178-185`);
3. um widget de assinatura foi configurado (`setSignatureAppearance`) (`:188-194`);
4. o PKCS#7 foi gerado por `openssl_pkcs7_sign` sobre o ByteRange e embutido em `/Contents` (`vendor/tecnickcom/tcpdf/tcpdf.php:7700-7747`);
5. o resultado foi gravado em arquivo temporário e relido para a memória (`PdfSigner.php:219-231`).

O binário de saída **não preserva** o binário de entrada em nenhum nível: é um documento TCPDF distinto, com `/Producer` do TCPDF, `/CreationDate` e `/ModDate` do momento da assinatura (`vendor/tecnickcom/tcpdf/tcpdf.php:9601-9640`).

### 1.2 Efeitos sobre dados e sistema

| Efeito | Recurso | Ponto no código | Fonte |
|---|---|---|---|
| Cria arquivo `sys_get_temp_dir()/v2_sign_input_*.pdf` com o PDF original | disco local | `tempnam` + `file_put_contents` | `PdfSigner.php:86-103` |
| Cria arquivo `v2_sign_output_*.pdf` e o preenche via TCPDF | disco local | `Output($tempOutputPath, 'F')` | `PdfSigner.php:219` |
| Permissão `0600` no input | disco local | `chmod($tempInputPath, 0600)` | `PdfSigner.php:103` |
| Remove os dois temporários mesmo em exceção | disco local | `finally` + `safeUnlink` | `PdfSigner.php:246-253,304-311` |
| Troca o builder ativo conforme `theme_type` | memória (`PdfSigner`) | `resolveStampTheme` + `new VisualStampBuilder($theme)` | `PdfSigner.php:271-296` |
| Zera as variáveis locais `$signatureString` e `$privateKeyPem` | memória | `finally` | `PdfSigner.php:251-253` |
| Não grava log nem auditoria | — | responsabilidade de `DigitalSignatureService` | `DigitalSignatureService.php:166-201` |
| Não verifica o PDF resultante | — | ausência de `verify`/`pdfsig` | `[C]` `PdfSigner.php:1-330` |

### 1.3 Contrato observável

| Entrada | Resultado |
|---|---|
| PDF válido, página(s) importáveis | PDF assinado com versão ≥1.7 (a do original, se maior); `getType()` continua `'a1-file'`. |
| `$pdfContent` vazio | `SignatureException` `SIG_PDF_LOAD_FAILED`, `Conteúdo do PDF vazio.` (`PdfSigner.php:66-72`). |
| Certificado ou chave vazios | `SignatureException` `SIG_CERTIFICATE_MISMATCH`, `Certificado ou chave privada não fornecidos.` (`:74-80`). |
| PDF sem páginas | `SignatureException` `SIG_PDF_LOAD_FAILED`, `O PDF não contém páginas para assinar.` (`:118-123`). |
| Falha de escrita do temporário | `SignatureException` `SIG_PDF_LOAD_FAILED`, `Falha ao criar arquivo temporário para assinatura.` (`:94-100`). |
| Saída vazia | `SignatureException` `SIG_PDF_RENDER_FAILED`, `Falha ao gerar o PDF assinado.` (`:224-229`). |
| Exceção do TCPDF/FPDI | `SignatureException` `SIG_SIGNER_FAILED`, code 500, `Erro ao assinar PDF: <mensagem>`; mensagens com `FPDI`/`compression technique` são trocadas pelo texto que pede PDF 1.4 (`:235-245`). |
| `addVisualStamp=false` | Nenhum HTML de selo; a aparência default do TCPDF é página 1, retângulo `0 0 0 0` (assinatura invisível) (`vendor/tecnickcom/tcpdf/tcpdf.php:1293`). |
| Tema desconhecido | Usa `LotepGovernmentTheme` (`PdfSigner.php:289-292`). |

### 1.4 O que o pipeline deliberadamente NÃO faz

- **Não gera PAdES.** O dicionário usa `/Filter /Adobe.PPKLite` e `/SubFilter /adbe.pkcs7.detached`; não há `/ETSI.CAdES.detached`, `DSS`, `VRI` ou `Prop_Build` (`vendor/tecnickcom/tcpdf/tcpdf.php:13486-13492`). O comentário “Aplica a assinatura digital (PAdES)” (`PdfSigner.php:208`) e o README são imprecisos.
- **Não aplica TSA (RFC 3161).** `setTimeStamp()` nunca é chamado; o `applyTSA()` do TCPDF 6.11.3 é um `@TODO` que devolve a assinatura intacta (`vendor/tecnickcom/tcpdf/tcpdf.php:13738-13744`).
- **Não fornece LTV** (Long-Term Validation): sem timestamp, sem DSS, sem OCSP/CRL embutidos.
- **Não valida cadeia, revogação ou propósito do certificado.**
- **Não assina incrementalmente**: não preserva a assinatura anterior, anotações, formulários, bookmarks, links, camadas, anexos, JavaScript, fontes não usadas ou metadados XMP.
- **Não abre PDF criptografado** `[I]` (o FPDI não recebe senha em nenhum ponto) nem PDFs com Object Streams/Cross-Reference Streams no FPDI livre; a mensagem de erro orienta “PDF 1.4”.
- **Não normaliza para PDF/A** e não faz OCR.
- **Não garante determinismo**: `/M`, `/CreationDate`, `/ModDate` e o próprio CMS mudam a cada execução.
- **Não há validação do resultado** (tamanho, ByteRange, digest, `/Contents`, assinatura criptográfica).

## 2. Abordagem

### 2.1 Estratégia e bibliotecas

| Componente | Versão travada | Uso |
|---|---|---|
| `setasign/fpdi` | `v2.6.8` | Importação das páginas (`setSourceFile`, `importPage`, `useTemplate`) — edição gratuita, sem parser para object streams. |
| `setasign/fpdi-tcpdf` | `v2.3.0` | Integração `setasign\Fpdi\Tcpdf\Fpdi`. |
| `tecnickcom/tcpdf` | `6.11.3` | Renderização do HTML do selo, assinatura PKCS#7, escrita do PDF. |

Fontes: `composer.json:17-20`; `composer.lock:2257-2258,2343-2344,4226-4228`; import `use setasign\Fpdi\Tcpdf\Fpdi;` em `PdfSigner.php:5`.

A estratégia é “reconstruir e assinar”: em vez de manipular o PDF original, o código cria um `Fpdi` vazio, importa cada página como template, desenha o selo no novo documento e chama `setSignature()`. Isso simplifica o posicionamento e o desenho, ao custo de perder tudo o que não é conteúdo de página.

### 2.2 Mecânica passo a passo

```mermaid
flowchart TD
    A[pdfContent + cert PEM + key PEM + metadata] --> B{content/cert/key vazios?}
    B -- sim --> X[SignatureException]
    B -- não --> C[resolveStampTheme theme_type]
    C --> D[tempnam input/output .pdf]
    D --> E[grava input + chmod 0600]
    E --> F[new Fpdi; sem header/footer; margens 0; autopagebreak off]
    F --> G[setSourceFile -> pageCount]
    G --> H{pageCount == 0?}
    H -- sim --> X
    H -- não --> I[targetPage = pos_page válido ? pos_page : pageCount]
    I --> J[para cada página: importPage/getTemplateSize/AddPage/useTemplate]
    J --> K{addVisualStamp?}
    K -- sim --> L[calcula x/y; clamp superior; writeHTMLCell; setSignatureAppearance]
    K -- não --> M
    L --> M[signatureString = cert + newline + key]
    M --> N[setSignature cert_type=2 + info Name/Location/Reason/ContactInfo]
    N --> O[Output F]
    O --> P[file_get_contents + return]
    P --> Q[finally: unlink + limpa variáveis]
```

Passo a passo com nomes reais:

1. Validação de conteúdo e credenciais (`PdfSigner.php:66-80`).
2. `resolveStampTheme($metadata)` lê `theme_type`, compara com `getTheme()->getType()` e, se diferente, instancia `MinimalTheme`, `ProfessionalTheme($metadata['logo_path'])` ou `LotepGovernmentTheme` (`:271-296`).
3. Dois `tempnam` com sufixo `.pdf`; o arquivo sem extensão criado pelo PHP é removido com `@unlink(substr(..., 0, -4))` (`:86-91`).
4. `file_put_contents` do PDF original; `chmod 0600`; falha → `SIG_PDF_LOAD_FAILED` (`:94-103`).
5. `new Fpdi()`, `setPrintHeader(false)`, `setPrintFooter(false)`, `SetMargins(0,0,0)`, `SetAutoPageBreak(false, 0)` (`:106-113`).
6. `setSourceFile($tempInputPath)` → `$pageCount` (`:116`).
7. `targetPage`: `pos_page` numérico entre 1 e `pageCount`, senão `pageCount` (`:126-128`).
8. Para cada página: `importPage`, `getTemplateSize`, `AddPage($orientation, [$pageW, $pageH])`, `useTemplate($templateId, 0, 0, $pageW, $pageH, true)`; guarda as dimensões da página alvo (`:133-149`).
9. Se `addVisualStamp`: resolve `x`/`y` (defaults: `getMarginLeft()` e `calculateStampY($targetPageH)`), calcula `hash('sha256', $pdfContent)` (minúsculo), chama `buildHtml`, aplica clamp apenas superior (`maxStampX`, `maxStampY`), `setPage($targetPage)`, `writeHTMLCell(...)`, `setSignatureAppearance(...)` (`:155-194`).
10. `$signatureString = $certificatePem . "\n" . $privateKeyPem`; monta `$signatureInfo`; `setSignature($signatureString, '', '', '', 2, $signatureInfo)` (`:198-216`).
11. `Output($tempOutputPath, 'F')`; `file_get_contents`; retorna (`:219-231`).
12. `finally`: apaga os temporários e zera as variáveis sensíveis (`:246-253`).

### 2.3 Trechos de código pivô

```php
// V2/Services/DigitalSignature/PdfSigner.php:116-131 — a leitura do original e a escolha da página alvo.
$pageCount = $pdf->setSourceFile($tempInputPath);

if ($pageCount === 0) {
    throw new SignatureException(
        'O PDF não contém páginas para assinar.',
        SignatureException::PDF_LOAD_FAILED
    );
}

$targetPage = isset($metadata['pos_page']) && is_numeric($metadata['pos_page']) && $metadata['pos_page'] >= 1 && $metadata['pos_page'] <= $pageCount
    ? (int) $metadata['pos_page']
    : $pageCount;
```

`pos_page` fora do intervalo (0, negativo, maior que o total ou não numérico) **não** é erro: cai silenciosamente na última página. O default do pipeline é, portanto, carimbar a última página.

```php
// V2/Services/DigitalSignature/PdfSigner.php:133-149 — o núcleo da reconstrução: todas as páginas
// são importadas e redesenhadas com as dimensões originais; nada além do conteúdo é copiado.
for ($pageNo = 1; $pageNo <= $pageCount; $pageNo++) {
    $templateId = $pdf->importPage($pageNo);
    $size = $pdf->getTemplateSize($templateId);

    $orientation = isset($size['orientation']) ? $size['orientation'] : 'P';
    $pageW = isset($size['width']) ? $size['width'] : 210;
    $pageH = isset($size['height']) ? $size['height'] : 297;

    if ($pageNo === $targetPage) {
        $targetPageW = $pageW;
        $targetPageH = $pageH;
    }

    $pdf->AddPage($orientation, array($pageW, $pageH));
    $pdf->useTemplate($templateId, 0, 0, $pageW, $pageH, true);
}
```

Consequência: anotações (inclusive a assinatura anterior), AcroForm, links, bookmarks e metadados não fazem parte do template e desaparecem. É a razão pela qual o serviço não suporta múltiplas assinaturas.

```php
// V2/Services/DigitalSignature/PdfSigner.php:155-172 — coordenadas, hash impresso e clamp.
if ($addVisualStamp) {
    $stampY = isset($metadata['pos_y']) && is_numeric($metadata['pos_y'])
        ? (float) $metadata['pos_y']
        : $this->stampBuilder->calculateStampY($targetPageH);

    $stampX = isset($metadata['pos_x']) && is_numeric($metadata['pos_x'])
        ? (float) $metadata['pos_x']
        : $this->stampBuilder->getMarginLeft();

    $documentHash = hash('sha256', $pdfContent);
    $stampHtml = $this->stampBuilder->buildHtml($metadata, $documentHash);

    $maxStampX = $targetPageW - $this->stampBuilder->getWidth();
    $maxStampY = $targetPageH - $this->stampBuilder->getHeight();

    if ($stampX > $maxStampX) $stampX = $maxStampX;
    if ($stampY > $maxStampY) $stampY = $maxStampY;
```

`pos_x`/`pos_y` são milímetros medidos do canto superior esquerdo, no sistema do TCPDF. O hash impresso é o do **PDF original** (minúsculo aqui; os temas convertem para maiúsculo). Não há clamp inferior: valores negativos desenham fora da página.

```php
// V2/Services/DigitalSignature/PdfSigner.php:178-194 — desenho do selo e configuração da área
// clicável da assinatura. Atenção à ordem dos argumentos de setSignatureAppearance.
$pdf->writeHTMLCell(
    $this->stampBuilder->getWidth(),
    $this->stampBuilder->getHeight(),
    $stampX,
    $stampY,
    $stampHtml,
    0, 1, false, true, 'L', true
);

$pdf->setSignatureAppearance(
    $targetPage,
    $stampX,
    $stampY,
    $this->stampBuilder->getWidth(),
    $this->stampBuilder->getHeight()
);
```

O `writeHTMLCell` desenha o selo corretamente em `(x, y)` mm. Já `setSignatureAppearance` no TCPDF 6.11.3 tem a assinatura `($x=0, $y=0, $w=0, $h=0, $page=-1, $name='')` (`vendor/tecnickcom/tcpdf/tcpdf.php:13644`), de modo que a chamada acima passa `$targetPage` como **x**, `$stampX` como **y**, `$stampY` como **w**, a largura como **h** e a altura como **page**. Ver §3.2 para o efeito exato.

```php
// V2/Services/DigitalSignature/PdfSigner.php:198-216 — a assinatura propriamente dita:
// cert + chave concatenados, sem senha, sem cadeia, cert_type=2 e metadados do dicionário /Sig.
$signatureString = $certificatePem . "\n" . $privateKeyPem;

$signatureInfo = array(
    'Name' => isset($metadata['name']) ? $metadata['name'] : '',
    'Location' => isset($metadata['location']) ? $metadata['location'] : '',
    'Reason' => isset($metadata['reason']) ? $metadata['reason'] : 'Assinatura Digital de Documento',
    'ContactInfo' => isset($metadata['contact_info']) ? $metadata['contact_info'] : '',
);

$pdf->setSignature(
    $signatureString,
    '',                // private_key (combinada no signatureString)
    '',                // private_key_password (já decriptada)
    '',                // extracerts
    2,                 // cert_type (approval/certification)
    $signatureInfo     // info (metadados da assinatura)
);
```

O TCPDF aceita a chave combinada no primeiro parâmetro (se `$private_key` for vazio, usa `$signing_cert`, `vendor/tecnickcom/tcpdf/tcpdf.php:13620-13624`) e chama `openssl_pkcs7_sign` com `PKCS7_BINARY | PKCS7_DETACHED` (`:7730-7733`). `extracerts=''` significa que a cadeia intermediária do PKCS#12 não é embutida.

```php
// V2/Services/DigitalSignature/Signers/Themes/LotepGovernmentTheme.php:66-75 — conteúdo do selo:
// nome, local · motivo, data/hora do servidor, hash em duas linhas e URL fixa do ITI.
$signerName = isset($metadata['name']) ? $metadata['name'] : 'Desconhecido';
$signerLocation = isset($metadata['location']) ? $metadata['location'] : '';
$signerReason = isset($metadata['reason']) ? $metadata['reason'] : 'Assinatura Digital de Documento';
$currentDateTime = date('d/m/Y H:i:s');

$fullCode = strtoupper($documentHash);
$hashLine1 = substr($fullCode, 0, 32);
$hashLine2 = substr($fullCode, 32, 32);
$verificationUrl = 'https://validar.iti.gov.br';

$meta = ($signerLocation ? htmlspecialchars($signerLocation, ENT_QUOTES, 'UTF-8') . ' · ' : '') . htmlspecialchars($signerReason, ENT_QUOTES, 'UTF-8');
```

A data/hora é a do **render**, não um timestamp criptográfico; a URL é fixa e não aponta para o documento. O “código de verificação” é apenas o SHA-256 do original, sem registro pesquisável.

```php
// V2/Services/DigitalSignature/Signers/VisualStampBuilder.php:84-90 — posição vertical default.
public function calculateStampY($pageHeight)
{
    $stampY = $pageHeight - $this->theme->getHeight() - 15;
    if ($stampY < 50) {
        $stampY = 50;
    }
    return $stampY;
}
```

O default reserva 15 mm de margem inferior e nunca sobe acima de 50 mm; em páginas muito baixas, o piso de 50 mm pode empurrar o selo para fora.

### 2.4 Algoritmos e custos

- **Importação**: `setSourceFile` parseia o PDF inteiro; `importPage` + `useTemplate` por página copiam o conteúdo para o novo documento. Custo O(páginas + bytes).
- **Selo**: `writeHTMLCell` parseia uma tabela HTML pequena (uma célula, `<div>`s) com a fonte core `helvetica` e `courier`; custo desprezível frente às páginas.
- **Assinatura**: `openssl_pkcs7_sign` percorre o buffer inteiro e calcula o digest; `signature_max_length = 11742` bytes são reservados e preenchidos com zeros (`vendor/tecnickcom/tcpdf/tcpdf.php:1286,7705-7747`).
- **Memória**: o PDF de entrada, o buffer do TCPDF e o PDF de saída coexistem; não há streaming nem limite no serviço.
- **Disco**: dois arquivos temporários; sem timeout interno.
- **Concorrência**: cada chamada cria seu próprio `Fpdi` e seus próprios temporários; o único estado compartilhado é o builder do `PdfSigner` reutilizado (tema mutável).

## 3. Particularidades

### 3.1 O que só existe aqui

- É o único ponto do V2 que combina FPDI + TCPDF + PKCS#7 + HTML de selo.
- É o único lugar onde a **posição do selo** é expressa em mm e passada diretamente ao TCPDF, sem normalização pela rotação da página.
- É o único conjunto de classes que implementa `StampThemeInterface`; a interface é o ponto de extensão real do serviço.
- É a única assinatura do sistema que usa `cert_type=2`, gerando `/Reference` com `DocMDP` e `/P 2` (`vendor/tecnickcom/tcpdf/tcpdf.php:13496-13512`).

### 3.2 A armadilha de `setSignatureAppearance`

O TCPDF 6.11.3 define `setSignatureAppearance($x=0, $y=0, $w=0, $h=0, $page=-1, $name='')` (`vendor/tecnickcom/tcpdf/tcpdf.php:13644`). A chamada do serviço passa cinco argumentos na ordem `(page, x, y, width, height)` (`PdfSigner.php:188-194`). O mapeamento efetivo é:

| Parâmetro TCPDF | Valor recebido |
|---|---|
| `$x` | `$targetPage` (1..N) |
| `$y` | `$stampX` (mm) |
| `$w` | `$stampY` (mm) |
| `$h` | largura do tema (105/115/95 mm) |
| `$page` | altura do tema (35/33/26 mm) |

`getSignatureAppearanceArray()` converte mm em pontos (`* $this->k`) e usa `$page` somente se estiver entre 1 e `numpages`; caso contrário, usa a página corrente (`vendor/tecnickcom/tcpdf/tcpdf.php:13678-13702`). Efeitos:

- Para um documento com `numpages < altura do tema` (tipicamente menos de 35 páginas no tema Lotep, 33 no professional e 26 no minimal), `page` cai no `else` e o widget fica na **última página importada**, não necessariamente na `targetPage`.
- Para um documento com **35+ páginas** e tema Lotep, `page = 35`, então o widget fica na página 35, mesmo que o selo tenha sido desenhado na página 1.
- O retângulo clicável fica com largura ≈ `stampY` mm (por exemplo, 237 mm) e altura ≈ largura do tema (105 mm), começando em `x ≈ targetPage × 2,835` pt e `y ≈ pageH - (stampX + width)` mm.
- O **desenho** do selo (`writeHTMLCell`) está correto; o que está errado é a área de widget/aparência da assinatura. A assinatura criptográfica em si não é afetada.

Uma reimplementação fiel ao comportamento atual deve reproduzir esse erro; uma correção deve passar `($stampX, $stampY, $width, $height, $targetPage)` e ser validada em leitor.

### 3.3 Temas

| `theme_type` | Classe | Dimensões (código) | Margem esq. | Conteúdo exibido | Observações |
|---|---|---|---|---|---|
| `lotep-government` (default) | `LotepGovernmentTheme` | 105 × 35 mm | 12 mm | nome; `local · motivo`; `d/m/Y H:i:s`; hash em 2×32; `https://validar.iti.gov.br` | Tabela com borda esquerda azul, fundo `#f8fafc`; fonte 5,5 pt. |
| `professional` | `ProfessionalTheme` | 115 × 33 mm | 10 mm | coluna esquerda: logo (18 mm) ou badge `Maxima`/`Gov. Paraíba`; direita: nome, local, data/hora, hash | `reason` é recebido por `buildDataCell()` mas **não é impresso** (`ProfessionalTheme.php:175-201`); sem URL do ITI. |
| `minimal` | `MinimalTheme` | 95 × 26 mm | 10 mm | nome; `local · motivo` (ou `—`); data/hora; hash em 2×32 | Sem borda/logo/URL; linhas separadas. |

Fontes: `LotepGovernmentTheme.php:24-42,64-104`; `ProfessionalTheme.php:39-57,83-101,143-162,175-201`; `MinimalTheme.php:21-39,60-96`.

- O tema é escolhido **por chamada**, em `resolveStampTheme`, e substitui o builder inteiro; um `PdfSigner` reutilizado mantém o último tema até a próxima troca (`PdfSigner.php:276-295`). Se o `theme_type` for o mesmo do builder atual, `resolveStampTheme` retorna sem reconstruir: um novo `logo_path` para o mesmo `professional` é ignorado (`PdfSigner.php:276-280`; `:284-287`).
- `logo_path` só é lido no tema `professional`, do array `$metadata`; `SignatureRequest` não expõe essa chave, então o fluxo canônico sempre usa o badge textual. O `file_exists` + extensão `png/jpg/jpeg/gif` é a única validação (`ProfessionalTheme.php:145-150`).
- Todos os campos textuais efetivamente impressos são escapados com `htmlspecialchars(..., ENT_QUOTES, 'UTF-8')`; o hash e a data são gerados internamente. Não há CPF/CNPJ, serial, emissor ou algoritmo no selo.
- O CSS é o subconjunto aceito pelo TCPDF: tabelas, `div`, fontes core, cores inline. Propriedades como `word-break`, `line-height` e `margin-top` podem ser ignoradas ou aproximadas pelo parser (`[I]`, o código as usa e o TCPDF não implementa CSS completo).

### 3.4 Metadados e dicionário de assinatura

| Campo | Origem | Efeito |
|---|---|---|
| `/Name` | `metadata['name']` (CN do certificado, via serviço) | Nome exibido no painel de assinaturas do leitor. |
| `/Location` | `metadata['location']` | Só é escrito se não vazio (`TCPDF_STATIC::empty_string`). |
| `/Reason` | `metadata['reason']`, default `Assinatura Digital de Documento` | Escrito se não vazio. |
| `/ContactInfo` | `metadata['contact_info']` | Escrito se não vazio. |
| `/M` | `doc_modification_timestamp` do TCPDF | Data/hora local do servidor com offset (`date('YmdHisO')`, `vendor/tecnickcom/tcpdf/include/tcpdf_static.php:390-392`). |
| `/Filter` | fixo | `Adobe.PPKLite`. |
| `/SubFilter` | fixo | `adbe.pkcs7.detached`. |
| `/ByteRange` | calculado pelo TCPDF | Cobre o arquivo exceto o `/Contents`. |
| `/Contents` | PKCS#7 | Assinatura detached em hexadecimal, preenchida até 11.742 bytes. |
| `/Reference` | `cert_type=2` | `DocMDP` com `/P 2` e `/V /1.2`; permite preenchimento de formulários e novas assinaturas segundo o dicionário. |
| `/Producer` | TCPDF | `TCPDF 6.11.3` (aproximado); o PDF original não contribui. |
| `/CreationDate`, `/ModDate` | TCPDF | Momento da geração; não vêm do original. |

O `/Sig` não contém `/Prop_Build` com informações do produto, nem `/TSA`, nem `/DSS`. O `reason` do selo e o `Reason` do dicionário podem divergir se o chamador montar `metadata` manualmente.

### 3.5 Validação do resultado

Não há validação interna. Para verificar externamente um PDF produzido:

1. **Estrutura**: confirmar o header `%PDF-1.7` (ou superior, herdado do original), `/Type /Sig`, `/SubFilter /adbe.pkcs7.detached` e que `/ByteRange` cobre todo o arquivo exceto `/Contents`.
2. **Criptografia**: `pdfsig arquivo.pdf` (Poppler) ou Adobe Reader; conferir se o certificado é o esperado e se o digest/assinatura batem.
3. **Cadeia**: como `extracerts` não é embutido, o verificador precisa da cadeia ICP-Brasil no repositório local; sem isso pode reportar “cadeia não confiável”.
4. **Tempo**: o `/M` é o relógio do servidor; não há prova criptográfica de tempo. A validade deve ser interpretada na data da assinatura, não na data da verificação; sem TSA/LTV isso é uma afirmação do sistema, não do PDF.
5. **Hash de auditoria**: comparar `document_hash` do log com `sha256sum` do arquivo entregue; é o hash do PDF **assinado**, não do original.
6. **Selo**: o código impresso é o SHA-256 do PDF original; sem o original, não é recalculável. A URL do ITI é apenas informativa.

### 3.6 Limites e o que não é garantido

| Limite | Detalhe | Fonte |
|---|---|---|
| PAdES | Não é PAdES; é PKCS#7 detached (`adbe.pkcs7.detached`). | `vendor/tecnickcom/tcpdf/tcpdf.php:13486-13492` |
| TSA/RFC 3161 | Nunca chamado; `applyTSA()` é no-op. | `vendor/tecnickcom/tcpdf/tcpdf.php:13738-13744` |
| LTV | Sem DSS/VRI/OCSP/CRL embutidos. | `[C]` ausência no `PdfSigner` e no dicionário |
| Cadeia | `extracerts=''`; intermediárias do PFX descartadas. | `PdfSigner.php:213`; `CertificateManager.php:30-39` |
| Revogação | Nenhuma consulta CRL/OCSP. | `[C]` |
| Múltiplas assinaturas | Re-render apaga a assinatura anterior e a área de assinatura. | `PdfSigner.php:133-149` |
| Anotações/formulários/bookmarks | Não importados. | `PdfSigner.php:133-149` |
| PDF moderno | Object Streams/XRef Streams não suportados pelo FPDI livre. | `PdfSigner.php:235-244` |
| PDF criptografado | Falha no `setSourceFile`. | `[I]` a partir do uso de FPDI |
| PDF/A | Sem normalização; fontes/propriedades não são convertidas. | `[C]` |
| Determinismo | Datas e CMS variam; o hash muda a cada execução. | `PdfSigner.php:219`; TCPDF `doc_creation_timestamp` |
| Tamanho do PKCS#7 | 11.742 bytes fixos; cadeia/propriedades maiores estouram o slot e desalinham o `/ByteRange`. | `vendor/tecnickcom/tcpdf/tcpdf.php:1286,7747` |
| Aparência | Ordem de `setSignatureAppearance` errada (§3.2). | `PdfSigner.php:188-194` |
| Validade do selo | Data/hora do render, não do ato criptográfico; sem fuso no HTML. | `LotepGovernmentTheme.php:68` |
| Assinatura de certificado | Não há checagem de propósito (`digitalSignature`/`nonRepudiation`), issuer ou raiz. | `[C]` |

## 4. Integrabilidade

### 4.1 API pública

| Símbolo | Assinatura | Retorno | Exceções | Consumidores |
|---|---|---|---|---|
| `PdfSigner::__construct` | `(VisualStampBuilder $stampBuilder = null)` | — | — | container/serviço |
| `PdfSigner::sign` | `($pdfContent, $certificatePem, $privateKeyPem, array $metadata = array(), $addVisualStamp = true)` | `string` binário | `SignatureException` | `DigitalSignatureService` |
| `PdfSigner::getType` | `()` | `'a1-file'` | — | nenhum |
| `PdfSigner::getStampBuilder` | `()` | `VisualStampBuilder` | — | nenhum |
| `VisualStampBuilder::__construct` | `(StampThemeInterface $theme = null)` | — | — | `PdfSigner` |
| `VisualStampBuilder::getWidth/getHeight/getMarginLeft` | `()` | `float` mm | — | `PdfSigner` |
| `VisualStampBuilder::calculateStampY` | `($pageHeight)` | `float` mm | — | `PdfSigner` |
| `VisualStampBuilder::buildHtml` | `(array $metadata, $documentHash)` | `string` HTML | — | `PdfSigner` |
| `VisualStampBuilder::getTheme` | `()` | `StampThemeInterface` | — | `PdfSigner::resolveStampTheme` |
| `StampThemeInterface::buildHtml` | `(array $metadata, $documentHash)` | `string` HTML | — | temas |
| `SignerInterface::sign` | `($pdfContent, $certificatePem, $privateKeyPem, array $metadata = [], $addVisualStamp = true)` | `string` | `SignatureException` | nenhuma implementação |

`PdfSigner` **não** implementa `SignerInterface`; a interface é aspiracional e não é usada pelo serviço (`V2/Services/DigitalSignature/Signers/SignerInterface.php:15-36`; `PdfSigner.php:32`).

### 4.2 Componentes Blade e contrato de UI

Não se aplica: nenhum Blade/JS/CSS pertence a este serviço. O consumidor tem a UI de assinatura (`Views/cms/sign*`) e envia `theme_type`, `pos_x`, `pos_y`, `pos_page`; o pipeline só consome esses valores. A UI pré-preenche `pos_x=20` e `pos_y=237` (portanto o default 12/247 do serviço não é usado no fluxo CMS) e cria `pos_page` com valor `1` quando o campo não existe, atualizando-o para a página exibida no preview (`V2/Modules/Infraestrutura/CertificadoDigital/Views/cms/sign/partials/form-fields.blade.php:152-153`; `V2/Modules/Infraestrutura/CertificadoDigital/Views/cms/sign/partials/scripts.blade.php:40-57,80`). A UI normaliza a posição em uma wireframe **A4 retrato 210×297 mm** (`V2/Modules/Infraestrutura/CertificadoDigital/Views/cms/sign/partials/scripts.blade.php:280-284`), enquanto o pipeline usa as dimensões reais de cada página; em páginas paisagem ou de outro tamanho, a mesma coordenada cai em outro ponto relativo.

### 4.3 JS e ciclo de vida

Não se aplica ao serviço. O único contrato indireto é o significado de `pos_x`/`pos_y` (mm a partir do topo-esquerdo) e `pos_page` (1-based), descrito em §4.4.

### 4.4 Ordem de inicialização e registro

1. Dependências travadas (`setasign/fpdi v2.6.8`, `setasign/fpdi-tcpdf v2.3.0`, `tecnickcom/tcpdf 6.11.3`) e extensão `openssl`.
2. `sys_get_temp_dir()` gravável; sem isso, `file_put_contents` falha e `sign()` lança `SIG_PDF_LOAD_FAILED`.
3. `PdfSigner` é construído com `VisualStampBuilder` default (tema Lotep) se nenhum for injetado (`PdfSigner.php:44-47`).
4. `DigitalSignatureService` recebe `PdfSigner` pronto; não há provider/binding.
5. O tema é resolvido **antes** de desenhar qualquer coisa, no início de `sign()` (`:83`). Se o mesmo objeto `PdfSigner` for reutilizado, o builder é trocado a cada `theme_type` diferente e permanece no último tema após a chamada.
6. A assinatura é aplicada após o desenho do selo; qualquer desenho posterior ao `setSignature` fica fora do ByteRange e invalida a assinatura.

### 4.5 Receitas de uso

**Assinar com o serviço (forma canônica).**

```php
$result = app(\V2\Services\DigitalSignature\DigitalSignatureService::class)->signPdf(
    new \V2\Services\DigitalSignature\DTOs\SignatureRequest(
        $pdfBytes,
        'oficio.pdf',
        $certificateId,
        $user->id,
        $user->email,
        [
            'location' => 'João Pessoa, Paraíba',
            'reason' => 'Assinatura de Diário Oficial',
            'add_visual_stamp' => true,
            'theme_type' => 'lotep-government',
            'pos_page' => 1,
            'pos_x' => 12,
            'pos_y' => 247,
        ]
    )
);

file_put_contents('oficio-assinado.pdf', $result->signedPdfContent);
```

**Assinar sem selo (assinatura invisível).**

```php
$request = new SignatureRequest($pdfBytes, 'documento.pdf', $certId, $userId, $email, [
    'add_visual_stamp' => false,
]);

$result = $service->signPdf($request);
```

Resultado: o PKCS#7 é aplicado, mas nenhum HTML é desenhado e a aparência default do TCPDF é `page=1, rect=0 0 0 0` — o leitor mostra “assinado” sem área clicável visível.

**Trocar o tema em um `PdfSigner` isolado.**

```php
$signer = new \V2\Services\DigitalSignature\PdfSigner(
    new \V2\Services\DigitalSignature\Signers\VisualStampBuilder(
        new \V2\Services\DigitalSignature\Signers\Themes\MinimalTheme()
    )
);

$pdf = $signer->sign($pdfBytes, $certPem, $keyPem, [
    'name' => 'Fulano',
    'theme_type' => 'minimal',
    'pos_x' => 10,
    'pos_y' => 260,
], true);
```

**Assinar usando a API de baixo nível sem o serviço** (só para manutenção/teste): exige entregar certificado e chave PEM em claro; não há cofre nesse caminho e o chamador assume a responsabilidade pela chave.

### 4.6 Anti-uso

**1. Chamar `PdfSigner::sign()` com chave/PDF vindos diretamente do request.** O signer não valida dono, permissão, tamanho, magic bytes nem tipo de PDF; ele apenas tenta o FPDI e lança `SIG_SIGNER_FAILED`. A fronteira de autorização e de validação de upload é o controller + `DigitalSignatureService`.

**2. Passar `pos_x`/`pos_y` em pixels ou em coordenadas de tela.** O pipeline espera milímetros no sistema do TCPDF; a UI converte porcentagens da wireframe A4 para mm. Enviar pixels desloca o selo e, se o valor for maior que `targetPageW - width`, ele é colado na borda direita (`PdfSigner.php:168-172`).

**3. Esperar que o PDF assinado preserve o original.** Comparar bytes, anotações, bookmarks ou assinatura anterior falha por desenho. Para múltiplas assinaturas, é necessário outro mecanismo (incremental update), que este código não implementa.

**4. Usar `theme_type` como dado do usuário sem validar.** O `default` cai em Lotep, mas `professional` com `logo_path` controlado permite leitura de arquivo local pelo TCPDF. O consumidor valida `in:lotep-government,professional,minimal`; chamadas diretas não.

**5. Tratar o código impresso no selo como prova de verificação.** Ele não é registrado em nenhum serviço de validação e não corresponde ao `document_hash` auditado; a URL do ITI não recebe esse código.

### 4.7 Custos e limites

- Sem limite de páginas/tamanho no signer; o consumidor limita o upload a 20 MB.
- Memória ~3× o PDF + overhead do TCPDF/FPDI; sem streaming.
- Dois arquivos temporários por chamada; `chmod 0600` apenas no input.
- Tempo dominado por `setSourceFile`/`importPage`/`useTemplate` (O(páginas)) e `openssl_pkcs7_sign` (O(bytes)).
- `/Contents` fixo em 11.742 bytes; sem tratamento de overflow.
- Sem timeout, retry ou idempotência: repetir gera outro PDF e outro hash.

## 5. Rotas e endpoints

Não se aplica: `PdfSigner` e os temas não têm rotas, controllers, middlewares ou requests. O acionamento HTTP é do módulo `MOD-INFRAESTRUTURA-CERTIFICADODIGITAL` (`POST /cms/certificado-digital/sign`), que valida `mimes:pdf`, `max:20480`, `theme_type` e coordenadas antes de chamar o serviço.

## 6. Dados e migrations

Não se aplica ao pipeline: ele não escreve no banco. O único dado derivado que chega à persistência é `document_hash` (SHA-256 do PDF assinado, 64 hex maiúsculos), gravado por `SignatureLogRepository::log()` em `v2_signature_logs.document_hash` (`char(64)`, `NOT NULL`) (`V2/Services/DigitalSignature/Migrations/2026_07_01_000002_create_v2_signature_logs_table.php:36`; `Repositories/SignatureLogRepository.php:18-37`). O tamanho gravado é o do PDF de entrada, não o do assinado.

## 7. Configuração e permissões

- Não há chave de config própria do pipeline. As dependências de ambiente são `APP_KEY` (indireta, via decifragem da chave antes de chamar `sign`), `sys_get_temp_dir()` e `app.timezone` (usado por `date()` nos temas e pelo TCPDF nas datas).
- As dimensões e margens são constantes de classe nos temas (`105×35`, `95×26`, `115×33`; margens `12/10/10`), não configuráveis por env.
- Não há permissão checada no pipeline; `theme_type` é validado apenas no controller consumidor (`in:lotep-government,professional,minimal`), não no `PdfSigner` nem no `SignatureRequest`.
- Não há feature flag própria.

## 8. Observabilidade e erros

- O pipeline não loga nada. Erros viram `SignatureException` com `getCode()` 400 (validação/PDF) ou 500 (falha genérica do signer) e `previous` com a exceção original.
- Mensagens exatas: §1.3. A tradução de PDF moderno é o texto mais longo do serviço e é contrato observável (`PdfSigner.php:237-239`).
- O sucesso é observável apenas pelo retorno binário e, no serviço, pela linha `success` em `v2_signature_logs` com `document_hash`.
- Nenhum dado sensível é logado; as variáveis `$signatureString`/`$privateKeyPem` são zeradas no `finally`, embora cópias temporárias do PHP/OpenSSL possam permanecer até o GC (`[I]`).
- Não há verificação pós-escrita; um PDF inválido pode ser retornado como sucesso se o TCPDF não lançar exceção.

## 9. Consumidores (mapa de integração)

| Consumidor | O que usa | Como usa | Risco de quebra |
|---|---|---|---|
| `DigitalSignatureService::signPdf` | `PdfSigner::sign` | Passa conteúdo, PEM, chave decifrada, metadados e `addVisualStamp` (`DigitalSignatureService.php:156-162`) | Mudar a assinatura do método ou a semântica de `metadata` quebra o único chamador. |
| `DigitalSignatureService` | `PdfSigner` no construtor | Type-hint concreto (`:26-31`) | Trocar por `SignerInterface` exige mudar o construtor e o binding. |
| `CertificadoDigitalCmsController@signDocument` | Resultado do serviço | Download com `Content-Type: application/pdf` e headers `X-Document-Hash`, `X-Signer-Name`, `X-Signed-At` | Se `signPdf` passar a devolver algo diferente de `SignatureResult`, o download quebra. |
| Leitores de PDF (Adobe/Poppler) | PDF produzido | Exibem a assinatura e o selo | A área clicável errada (§3.2) e a ausência de cadeia afetam a experiência de validação. |

## 10. Lacunas

| ID | Lacuna | Impacto | Onde procurar |
|---|---|---|---|
| LG-SVC-DIGITALSIGNATURE-07 | O digest efetivo do PKCS#7 não é verificado em um PDF real; o código não parametriza o algoritmo. | Pode não ser SHA-256, apesar do log afirmar isso. | `vendor/tecnickcom/tcpdf/tcpdf.php:7730-7733`; validar com `pdfsig`/`openssl pkcs7 -print`. |
| LG-SVC-DIGITALSIGNATURE-08 | Não há confirmação visual do comportamento de `setSignatureAppearance` em Adobe/Poppler; a análise é estática. | O widget pode ficar em página/retângulo diferentes do previsto. | `PdfSigner.php:188-194`; `vendor/tecnickcom/tcpdf/tcpdf.php:13644,13678-13702`. |
| LG-SVC-DIGITALSIGNATURE-09 | O suporte do TCPDF 6.11.3 ao CSS usado nos temas (`word-break`, `line-height`, `margin-top`) não foi medido. | O selo pode renderizar diferente entre versões/leitores. | `LotepGovernmentTheme.php:79-104`; `ProfessionalTheme.php:103-201`; `MinimalTheme.php:70-95`. |
| LG-SVC-DIGITALSIGNATURE-10 | Não há teste de PDF com 35+ páginas, paisagem, página pequena ou `pos_x/pos_y` negativos. | Casos-limite de posição não são conhecidos empiricamente. | `PdfSigner.php:126-172`; `VisualStampBuilder.php:84-90`. |
| LG-SVC-DIGITALSIGNATURE-11 | Não foi verificado se `openssl_pkcs7_sign` embute o certificado do signatário quando `extracerts=''`; a cadeia intermediária com certeza não é embutida. | Verificação pode exigir o certificado do signatário no repositório do leitor. | `vendor/tecnickcom/tcpdf/tcpdf.php:7730-7733`; `PdfSigner.php:213`. |
| LG-SVC-DIGITALSIGNATURE-12 | Não há medição de memória/tempo com PDFs grandes; o consumidor limita 20 MB, mas o serviço não. | Risco de exaustão de memória/timeout em PDFs grandes. | `PdfSigner.php:61-231`; `CertificadoDigitalCmsController.php:218-227`. |

## 11. Checklist de paridade

- [ ] `PC-SVC-DIGITALSIGNATURE-16` — O PDF de saída deve começar com `%PDF-1.7` (ou a versão do original, se maior) e conter `/Filter /Adobe.PPKLite` + `/SubFilter /adbe.pkcs7.detached`. Fonte: `vendor/tecnickcom/tcpdf/tcpdf.php:560,13486-13492`; `vendor/setasign/fpdi/src/FpdiTrait.php:113-119`.
- [ ] `PC-SVC-DIGITALSIGNATURE-17` — Com `pos_page` fora do intervalo, o selo deve ser desenhado na última página (não deve lançar erro). Fonte: `PdfSigner.php:126-128`.
- [ ] `PC-SVC-DIGITALSIGNATURE-18` — Sem `pos_x`/`pos_y`, o selo deve usar `getMarginLeft()` e `calculateStampY()` (A4: y=247 mm no tema Lotep); com valores acima do limite, deve ser colado em `maxStampX`/`maxStampY`. Fonte: `PdfSigner.php:156-172`; `VisualStampBuilder.php:84-90`.
- [ ] `PC-SVC-DIGITALSIGNATURE-19` — O HTML do tema Lotep deve conter nome/local/motivo escapados, data/hora `d/m/Y H:i:s`, duas linhas de 32 caracteres do hash e `https://validar.iti.gov.br`. Fonte: `LotepGovernmentTheme.php:64-104`.
- [ ] `PC-SVC-DIGITALSIGNATURE-20` — O tema `professional` sem `logo_path` deve exibir o badge `Maxima`/`Gov. Paraíba` e **não** deve exibir o motivo. Fonte: `ProfessionalTheme.php:143-201`.
- [ ] `PC-SVC-DIGITALSIGNATURE-21` — `theme_type` desconhecido deve resultar em `LotepGovernmentTheme`. Fonte: `PdfSigner.php:280-295`.
- [ ] `PC-SVC-DIGITALSIGNATURE-22` — Um PDF com 1 página e `add_visual_stamp=false` deve sair sem o HTML do selo; o dicionário de assinatura deve continuar presente. Fonte: `PdfSigner.php:155,198-216`; `vendor/tecnickcom/tcpdf/tcpdf.php:1293`.
- [ ] `PC-SVC-DIGITALSIGNATURE-23` — O hash impresso no selo deve ser o SHA-256 do PDF **de entrada**, e o `document_hash` do log o SHA-256 do **de saída**; os dois devem divergir. Fonte: `PdfSigner.php:164`; `DigitalSignatureService.php:164`.
- [ ] `PC-SVC-DIGITALSIGNATURE-24` — Um PDF de entrada com AcroForm, bookmark ou assinatura anterior não deve conter esses elementos no output (apenas conteúdo de página). Fonte: `PdfSigner.php:133-149`.
- [ ] `PC-SVC-DIGITALSIGNATURE-25` — `PdfSigner` não deve implementar `SignerInterface` nem ser registrado como singleton; cada resolução deve criar um novo builder default. Fonte: `PdfSigner.php:32-47`.
- [ ] `PC-SVC-DIGITALSIGNATURE-26` — PDF com Object Streams deve produzir `SIG_SIGNER_FAILED` com o texto que menciona `Object Streams / Cross-Reference Streams` e PDF 1.4. Fonte: `PdfSigner.php:235-245`.

### Checklist de extração (A–M)

| Grupo | Itens cobertos | Onde |
|---|---|---|
| A. Efeito | A1–A7 | §0, §1.1–§1.4 |
| B. Abordagem | B1–B7 | §2.1–§2.4 (diagrama, passos, 7 trechos, custos, `finally`) |
| C. Particularidades | C1–C7 | §3.1–§3.6 (exclusividade, temas, dicionário, validação, limites) |
| D. Integrabilidade | D1–D9 | §4.1–§4.7 (API, ordem, receitas, anti-uso, custos, consumidores) |
| E. Rotas/HTTP | E1–E7 | §5: não se aplica; o endpoint é do módulo consumidor. |
| F. Dados | F1–F6 | §6: não se aplica ao pipeline; só `document_hash` é derivado. |
| G. UI CMS | G1–G6 | Não se aplica: o pipeline não tem Blade; a UI de posicionamento é do módulo. |
| H. UI Portal | H1–H6 | Não se aplica: nenhum PDF é gerado no portal. |
| I. Contrato de UI/JS | I1–I8 | Não se aplica ao pipeline; o contrato de `theme_type/pos_*` está em §4.2–§4.4. |
| J. Config/permissões | J1–J5 | §7: sem config própria; validação de tema é do controller. |
| K. Observabilidade | K1–K5 | §8 |
| L. Seeds | L1–L3 | Não se aplica: pipeline sem persistência. |
| M. Paridade/lacunas | M1–M4 | §3.6, §10, §11, §12. |

## 12. Evidências consultadas

- `V2/Services/DigitalSignature/PdfSigner.php:1-330`
- `V2/Services/DigitalSignature/Signers/VisualStampBuilder.php:1-104`
- `V2/Services/DigitalSignature/Signers/StampThemeInterface.php:1-58`
- `V2/Services/DigitalSignature/Signers/SignerInterface.php:1-36`
- `V2/Services/DigitalSignature/Signers/Themes/LotepGovernmentTheme.php:1-108`
- `V2/Services/DigitalSignature/Signers/Themes/MinimalTheme.php:1-97`
- `V2/Services/DigitalSignature/Signers/Themes/ProfessionalTheme.php:1-202`
- `V2/Services/DigitalSignature/DigitalSignatureService.php:119-205`
- `V2/Services/DigitalSignature/DTOs/SignatureRequest.php:123-178`
- `V2/Services/DigitalSignature/DTOs/SignatureResult.php:1-83`
- `V2/Services/DigitalSignature/Exceptions/SignatureException.php:1-43`
- `V2/Modules/Infraestrutura/CertificadoDigital/Controllers/CertificadoDigitalCmsController.php:210-317`
- `V2/Modules/Infraestrutura/CertificadoDigital/Views/cms/sign/partials/scripts.blade.php:270-315`
- `V2/Modules/Infraestrutura/CertificadoDigital/Views/cms/sign/partials/form-fields.blade.php:96-153`
- `composer.json:17-20`
- `composer.lock:2257-2258,2343-2344,4226-4228`
- `vendor/tecnickcom/tcpdf/tcpdf.php:560,1286,7700-7747,9601-9640,13486-13512,13607-13645,13678-13703,13738-13744`
- `vendor/tecnickcom/tcpdf/include/tcpdf_static.php:390-392`
