---
title: "Assinando um PDF"
description: "Uma assinatura PAdES completa com certificado em nuvem pelo CloudHub, com o PKI SDK, o Rest PKI ou o PKI Express montando o PDF assinado, em C#, PHP, Python, Node.js e Java."
canonical: "https://docs.lacunasoftware.com/articles/cloudhub/pades-signature"
locale: "pt-BR"
product: "CloudHub"
last_updated: "2026-10-12"
---

# Assinando um PDF

Esta página junta o fluxo inteiro: uma assinatura PAdES de um PDF com certificado em nuvem, em C#,
PHP, Python, Node.js e Java. O CloudHub fornece o certificado e a assinatura; uma biblioteca de
assinatura monta o PDF assinado. Cada exemplo segue o exemplo correspondente do
[PKI Suite Samples](https://github.com/LacunaSoftware/PkiSuiteSamples).

| Linguagem | Biblioteca de assinatura | Framework web |
|---|---|---|
| C# | [PKI SDK](https://docs.lacunasoftware.com/articles/pki-sdk) | ASP.NET Core MVC |
| PHP | [Rest PKI](https://docs.lacunasoftware.com/articles/rest-pki) | PHP puro |
| Python | [PKI Express](https://docs.lacunasoftware.com/articles/pki-express) | Flask |
| Node.js | [Rest PKI](https://docs.lacunasoftware.com/articles/rest-pki) | Express |
| Java | [Rest PKI](https://docs.lacunasoftware.com/articles/rest-pki) | Spring MVC |

Cada biblioteca de assinatura precisa da própria configuração antes: uma licença para o PKI SDK, um
access token para o Rest PKI, o PKI Express instalado no servidor. Os exemplos leem o token do Rest
PKI de `RESTPKI_ACCESS_TOKEN` e a chave do CloudHub de `CLOUDHUB_API_KEY`.

## Como as peças se encaixam

Todo exemplo tem dois handlers:

- **discover** recebe o CPF do signatário, cria uma sessão no CloudHub e lista os provedores, cada um
  com um link para a sua `authUrl`. Ele também guarda o CPF na sessão web do usuário, para conferir o
  certificado depois.
- **complete** é a URI de redirecionamento. Ele executa quatro passos:

1. Lê o certificado do signatário no CloudHub.
2. Entrega o certificado à biblioteca de assinatura, que **inicia** a assinatura e devolve o hash a
   assinar, o algoritmo de hash e o que ela precisa para finalizar (dados de transferência, um token
   ou um arquivo de transferência).
3. Envia o hash ao `sign-hash` do CloudHub. O provedor o assina com a chave do usuário.
4. Entrega a assinatura à biblioteca de assinatura, que **finaliza** o PDF assinado. Depois, confere
   se o CPF do signatário é o que foi guardado.

As bibliotecas de assinatura devolvem o hash em codificações diferentes. O CloudHub o recebe em
base64, e as bibliotecas cliente fazem a conversão a partir do tipo que aceitam:

| Exemplo | O hash vindo da biblioteca de assinatura | O que o cliente do CloudHub recebe |
|---|---|---|
| C#, PKI SDK | bytes, de `DigestAlgorithm.ComputeHash` | bytes |
| PHP, Rest PKI | bytes brutos em `$start->toSignHash` | base64: passe-o por `base64_encode` |
| Python, PKI Express | base64 em `start['toSignHash']` | base64 |
| Node.js, Rest PKI | base64 em `start.toSignHash` | base64 |
| Java, Rest PKI | bytes, de `getToSignHashRaw()` | bytes |

## O exemplo

**C#**

```csharp title="Controllers/CloudSignatureController.cs"
using System.Text.RegularExpressions;
using Lacuna.Cloudhub.Api;
using Lacuna.Cloudhub.Client;
using Lacuna.Pki;
using Lacuna.Pki.Pades;
using Microsoft.AspNetCore.Mvc;

public class CloudSignatureController : Controller {

    private static readonly CloudhubClient cloudhub = new CloudhubClient(
        "https://cloudhub.lacunasoftware.com/",
        Environment.GetEnvironmentVariable("CLOUDHUB_API_KEY"));

    // POST /CloudSignature/Discover: the form with the signer's CPF
    [HttpPost]
    public async Task<IActionResult> Discover(string fileId, string cpf) {
        cpf = DigitsOnly(cpf);
        // Remember whose signature this is, to check the certificate later
        TempData["cloudhubCpf"] = cpf;

        var session = await cloudhub.CreateSessionAsync(new SessionCreateRequest {
            Identifier = cpf,
            RedirectUri = Url.Action(nameof(Complete), "CloudSignature", new { fileId }, Request.Scheme),
            Type = TrustServiceSessionTypes.SingleSignature,
        });

        // The view links each entry of session.Services to its AuthUrl
        return View(session.Services);
    }

    // GET /CloudSignature/Complete?fileId=…&session=…: the redirect URI
    public async Task<IActionResult> Complete(string fileId, string session) {
        if (string.IsNullOrEmpty(session)) {
            return RedirectToAction("Index", new { fileId }); // not authorized
        }

        // 1. The signer's certificate, from CloudHub
        var certificate = PKCertificate.Decode(await cloudhub.GetCertificateAsync(session));
        if (DigitsOnly(certificate.PkiBrazil.CPF) != (string)TempData["cloudhubCpf"]) {
            return StatusCode(403); // someone else signed in at the provider
        }

        // 2. PKI SDK starts the signature. It throws a ValidationException
        //    when the certificate is not trusted.
        var signer = new PadesSigner();
        signer.SetPolicy(PadesPoliciesForGeneration.GetPadesBasic(TrustArbitrators.PkiBrazil));
        signer.SetPdfToSign(GetPdfPath(fileId));
        signer.SetSigningCertificate(certificate);
        var toSignBytes = signer.GetToSignBytes(out var signatureAlgorithm, out var transferData);

        // 3. CloudHub signs the hash with the user's key
        var signature = await cloudhub.SignHashAsync(new SignHashRequest {
            Session = session,
            Hash = signatureAlgorithm.DigestAlgorithm.ComputeHash(toSignBytes),
            DigestAlgorithmOid = signatureAlgorithm.DigestAlgorithm.Oid,
        });

        // 4. PKI SDK finishes the signed PDF
        signer.SetPreComputedSignature(signature, transferData);
        var signedPdf = signer.GetPadesSignature();

        return File(signedPdf, "application/pdf", "signed.pdf");
    }

    private static string DigitsOnly(string value) => Regex.Replace(value ?? "", @"\D", "");
}
```

Com o PKI SDK, o certificado é conhecido antes de qualquer assinatura, então a conferência do CPF vem
primeiro. `GetPdfPath` representa o seu próprio armazenamento.

**PHP**

```php title="discover.php"
<?php
require __DIR__ . '/../vendor/autoload.php';

use Lacuna\Cloudhub\CloudhubClient;
use Lacuna\Cloudhub\SessionCreateRequest;
use Lacuna\Cloudhub\TrustServiceSessionTypes;

session_start();

$cloudhub = new CloudhubClient('https://cloudhub.lacunasoftware.com/', getenv('CLOUDHUB_API_KEY'));

$fileId = $_GET['fileId'];
$cpf = preg_replace('/\D/', '', $_POST['cpf']);
// Remember whose signature this is, to check the certificate later
$_SESSION['cloudhubCpf'] = $cpf;

$session = $cloudhub->createSessionAsync(new SessionCreateRequest(
    $cpf,
    'https://app.example.com/cloud-signature/complete.php?fileId=' . urlencode($fileId),
    TrustServiceSessionTypes::SingleSignature
));
?>
<?php if (empty($session->services)) { ?>
    <p>No provider holds a certificate for this CPF.</p>
<?php } ?>
<?php foreach ($session->services as $service) { ?>
    <a href="<?= htmlspecialchars($service->authUrl) ?>">
        Sign with <?= htmlspecialchars($service->serviceInfo->provider) ?>
    </a>
<?php } ?>
```

```php title="complete.php"
<?php
require __DIR__ . '/../vendor/autoload.php';

use Lacuna\Cloudhub\CloudhubClient;
use Lacuna\Cloudhub\SignHashRequest;
use Lacuna\RestPki\PadesSignatureFinisher2;
use Lacuna\RestPki\PadesSignatureStarter;
use Lacuna\RestPki\RestPkiClient;
use Lacuna\RestPki\StandardSecurityContexts;
use Lacuna\RestPki\StandardSignaturePolicies;

session_start();

$cloudhub = new CloudhubClient('https://cloudhub.lacunasoftware.com/', getenv('CLOUDHUB_API_KEY'));
$restPki = new RestPkiClient('https://pki.rest/', getenv('RESTPKI_ACCESS_TOKEN'));

$fileId = $_GET['fileId'];
$session = $_GET['session'] ?? null;
if (empty($session)) {
    header('Location: /cloud-signature/index.php?fileId=' . urlencode($fileId)); // not authorized
    exit;
}

// 1. The signer's certificate, from CloudHub (base64)
$certificate = $cloudhub->getCertificateAsync($session);

// 2. Rest PKI starts the signature
$starter = new PadesSignatureStarter($restPki);
$starter->setSignerCertificateBase64($certificate);
$starter->setPdfToSignFromPath(getPdfPath($fileId));
$starter->signaturePolicy = StandardSignaturePolicies::PADES_BASIC;
$starter->securityContext = StandardSecurityContexts::PKI_BRAZIL;
$start = $starter->start();

// 3. CloudHub signs the hash with the user's key. Rest PKI's PHP client
//    returns the hash as raw bytes, and CloudHub takes it base64-encoded.
$signature = $cloudhub->signHashAsync(new SignHashRequest(
    $session,
    base64_encode($start->toSignHash),
    null,
    $start->digestAlgorithmOid
));

// 4. Rest PKI finishes the signed PDF
$finisher = new PadesSignatureFinisher2($restPki);
$finisher->token = $start->token;
$finisher->signatureBase64 = $signature;
$result = $finisher->finish();

if ($result->certificate->pkiBrazil->cpf !== $_SESSION['cloudhubCpf']) {
    http_response_code(403); // someone else signed in at the provider: discard the result
    exit;
}
$result->writeToFile(getSignedPdfPath($fileId));
```

`getPdfPath` e `getSignedPdfPath` representam o seu próprio armazenamento.

**Python**

```python title="cloud_signature.py"
import os

from flask import Blueprint, abort, redirect, render_template, request, url_for
from flask import session as web_session
from pkiexpress import PadesSignatureStarter, SignatureFinisher, standard_signature_policies

from cloudhub import call_cloudhub  # the helper from "Get started"

blueprint = Blueprint('cloud_signature', __name__, url_prefix='/cloud-signature')


@blueprint.route('/discover/<file_id>', methods=['POST'])
def discover(file_id):
    cpf = ''.join(c for c in request.form['cpf'] if c.isdigit())
    # Remember whose signature this is, to check the certificate later
    web_session['cloudhub_cpf'] = cpf

    session = call_cloudhub('POST', '/api/sessions', json={
        'identifier': cpf,
        'redirectUri': url_for('.complete', file_id=file_id, _external=True),
        'type': 'SingleSignature',
    })

    # The template links each entry of services to its authUrl
    return render_template('cloud_signature/discover.html', services=session['services'])


@blueprint.route('/complete/<file_id>')
def complete(file_id):
    cloudhub_session = request.args.get('session')
    if not cloudhub_session:
        return redirect(url_for('.index', file_id=file_id))  # not authorized

    # 1. The signer's certificate, from CloudHub (base64)
    certificate = call_cloudhub('GET', '/api/sessions/certificate',
                                params={'session': cloudhub_session})

    # 2. PKI Express starts the signature
    pdf_path = get_pdf_path(file_id)
    starter = PadesSignatureStarter()
    starter.signature_policy = standard_signature_policies.PADES_BASIC
    starter.set_pdf_to_sign_from_path(pdf_path)
    starter.set_certificate_from_base64(certificate)
    start = starter.start()

    # 3. CloudHub signs the hash with the user's key
    signature = call_cloudhub('POST', '/api/sessions/sign-hash', json={
        'session': cloudhub_session,
        'hash': start['toSignHash'],
        'digestAlgorithmOid': start['digestAlgorithmOid'],
    })

    # 4. PKI Express finishes the signed PDF
    finisher = SignatureFinisher()
    finisher.set_file_to_sign_from_path(pdf_path)
    finisher.set_transfer_file_from_path(start['transferFile'])
    finisher.signature = signature
    finisher.output_file = get_signed_pdf_path(file_id)
    signer_cert = finisher.complete(get_cert=True)

    if signer_cert.pki_brazil.cpf != web_session.get('cloudhub_cpf'):
        os.remove(finisher.output_file)  # someone else signed in at the provider
        abort(403)

    return render_template('cloud_signature/complete.html', signer_cert=signer_cert)
```

O valor do CloudHub se chama `cloudhub_session` para não conflitar com o `session` do Flask.
`get_pdf_path` e `get_signed_pdf_path` representam o seu próprio armazenamento.

**Node.js**

```js title="routes/cloud-signature.js"
const express = require('express');
const { CloudHubClient, TrustServiceSessionTypes } = require('cloudhub-client');
const {
  RestPkiClient,
  PadesSignatureStarter,
  PadesSignatureFinisher,
  StandardSecurityContexts,
  StandardSignaturePolicies,
} = require('restpki-client');

const cloudhub = new CloudHubClient('https://cloudhub.lacunasoftware.com/', process.env.CLOUDHUB_API_KEY);
const restPki = new RestPkiClient('https://pki.rest/', process.env.RESTPKI_ACCESS_TOKEN);
const router = express.Router();

// POST /cloud-signature/discover?fileId=…: the form with the signer's CPF
router.post('/discover', async (req, res, next) => {
  try {
    const { fileId } = req.query;
    const cpf = req.body.cpf.replace(/\D/g, '');
    // Remember whose signature this is, to check the certificate later (express-session)
    req.session.cloudhubCpf = cpf;

    const session = await cloudhub.createSessionAsync({
      identifier: cpf,
      redirectUri: `https://app.example.com/cloud-signature/complete?fileId=${encodeURIComponent(fileId)}`,
      type: TrustServiceSessionTypes.SingleSignature,
    });

    // The view links each entry of services to its authUrl
    res.render('cloud-signature/discover', { services: session.services || [] });
  } catch (err) {
    next(err);
  }
});

// GET /cloud-signature/complete?fileId=…&session=…: the redirect URI
router.get('/complete', async (req, res, next) => {
  try {
    const { fileId, session } = req.query;
    if (!session) {
      return res.redirect(`/cloud-signature?fileId=${encodeURIComponent(fileId)}`); // not authorized
    }

    // 1. The signer's certificate, from CloudHub (base64)
    const certificate = await cloudhub.getCertificateAsync(session);

    // 2. Rest PKI starts the signature. It base64-encodes the certificate
    //    itself, so give it the decoded bytes.
    const starter = new PadesSignatureStarter(restPki);
    starter.setPdfToSignFromPath(getPdfPath(fileId));
    starter.signerCertificate = Buffer.from(certificate, 'base64');
    starter.signaturePolicy = StandardSignaturePolicies.PADES_BASIC;
    starter.securityContext = StandardSecurityContexts.PKI_BRAZIL;
    const start = await starter.start();

    // 3. CloudHub signs the hash with the user's key
    const signature = await cloudhub.signHashAsync({
      session,
      hash: start.toSignHash,
      digestAlgorithmOid: start.digestAlgorithmOid,
    });

    // 4. Rest PKI finishes the signed PDF
    const finisher = new PadesSignatureFinisher(restPki);
    finisher.token = start.token;
    finisher.signature = signature;
    const result = await finisher.finish();

    if (result.certificate.pkiBrazil.cpf !== req.session.cloudhubCpf) {
      return res.sendStatus(403); // someone else signed in at the provider: discard the result
    }
    await result.writeToFile(getSignedPdfPath(fileId));
    res.render('cloud-signature/complete', { signerCert: result.certificate });
  } catch (err) {
    next(err);
  }
});

module.exports = router;
```

`getPdfPath` e `getSignedPdfPath` representam o seu próprio armazenamento.

**Java**

```java title="CloudSignatureController.java"
import cloudhub.SessionsApi;
import cloudhub.client.CloudhubClient;
import cloudhub.client.model.SessionCreateRequest;
import cloudhub.client.model.SessionModel;
import cloudhub.client.model.SignHashRequest;
import cloudhub.client.model.TrustServiceSessionTypes;
import com.lacunasoftware.restpki.PKCertificate;
import com.lacunasoftware.restpki.PadesSignatureFinisher2;
import com.lacunasoftware.restpki.PadesSignatureStarter2;
import com.lacunasoftware.restpki.RestPkiClient;
import com.lacunasoftware.restpki.SecurityContext;
import com.lacunasoftware.restpki.SignaturePolicy;
import com.lacunasoftware.restpki.SignatureResult;
import com.lacunasoftware.restpki.SignatureStartResult;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

import javax.servlet.http.HttpSession; // jakarta.servlet on Spring Boot 3
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.Base64;
import java.util.Objects;

@Controller
@RequestMapping("/cloud-signature")
public class CloudSignatureController {

    private final SessionsApi cloudhub = new SessionsApi(
        new CloudhubClient("https://cloudhub.lacunasoftware.com", System.getenv("CLOUDHUB_API_KEY")));

    private final RestPkiClient restPki = new RestPkiClient(
        "https://pki.rest/", System.getenv("RESTPKI_ACCESS_TOKEN"));

    // POST /cloud-signature/discover: the form with the signer's CPF
    @PostMapping("/discover")
    public String discover(@RequestParam String fileId, @RequestParam String cpf,
                           HttpSession httpSession, Model model) throws Exception {
        String plainCpf = cpf.replaceAll("\\D", "");
        // Remember whose signature this is, to check the certificate later
        httpSession.setAttribute("cloudhubCpf", plainCpf);

        SessionCreateRequest request = new SessionCreateRequest();
        request.setIdentifier(plainCpf);
        request.setRedirectUri("https://app.example.com/cloud-signature/complete?fileId="
            + URLEncoder.encode(fileId, "UTF-8"));
        request.setType(TrustServiceSessionTypes.SINGLE_SIGNATURE);
        SessionModel session = cloudhub.apiSessionsPost(request);

        // The template links each entry of services to its authUrl
        model.addAttribute("services", session.getServices());
        return "cloud-signature/discover";
    }

    // GET /cloud-signature/complete?fileId=…&session=…: the redirect URI
    @GetMapping("/complete")
    public String complete(@RequestParam String fileId,
                           @RequestParam(required = false) String session,
                           HttpSession httpSession, Model model) throws Exception {
        if (session == null || session.isEmpty()) {
            return "redirect:/cloud-signature?fileId=" + fileId; // not authorized
        }

        // 1. The signer's certificate, from CloudHub (base64)
        String certificate = unquote(cloudhub.apiSessionsCertificateGet(session));

        // 2. Rest PKI starts the signature
        PadesSignatureStarter2 starter = new PadesSignatureStarter2(restPki);
        starter.setPdfToSign(getPdfPath(fileId));
        starter.setSignerCertificateBase64(certificate);
        starter.setSignaturePolicy(SignaturePolicy.PadesBasic);
        starter.setSecurityContext(SecurityContext.pkiBrazil);
        SignatureStartResult start = starter.start();

        // 3. CloudHub signs the hash with the user's key
        SignHashRequest signRequest = new SignHashRequest();
        signRequest.setSession(session);
        signRequest.setHash(start.getToSignHashRaw());
        signRequest.setDigestAlgorithmOid(start.getDigestAlgorithmOid());
        byte[] signature = Base64.getDecoder().decode(unquote(cloudhub.apiSessionsSignHashPost(signRequest)));

        // 4. Rest PKI finishes the signed PDF
        PadesSignatureFinisher2 finisher = new PadesSignatureFinisher2(restPki);
        finisher.setToken(start.getToken());
        finisher.setSignature(signature);
        SignatureResult result = finisher.finish();

        PKCertificate signerCert = result.getCertificate();
        if (!Objects.equals(signerCert.getPkiBrazil().getCpf(), httpSession.getAttribute("cloudhubCpf"))) {
            // Someone else signed in at the provider: discard the result
            throw new ResponseStatusException(HttpStatus.FORBIDDEN);
        }
        try (InputStream signedPdf = result.openRead()) {
            Files.copy(signedPdf, getSignedPdfPath(fileId), StandardCopyOption.REPLACE_EXISTING);
        }

        model.addAttribute("signerCert", signerCert);
        return "cloud-signature/complete";
    }

    // The certificate and sign-hash operations answer with a JSON string ("MIIG..."),
    // and the client hands back its raw bytes, quotes included
    private static String unquote(byte[] body) {
        return new String(body, StandardCharsets.UTF_8).replace("\"", "");
    }
}
```

`getPdfPath` e `getSignedPdfPath` representam o seu próprio armazenamento e devolvem um
`java.nio.file.Path`.

Os exemplos só confiam em certificados ICP-Brasil (`TrustArbitrators.PkiBrazil`, `PKI_BRAZIL`,
`SecurityContext.pkiBrazil`). Para testar com os certificados de teste de um provedor, confie na raiz
deles só em desenvolvimento; os exemplos do PKI Suite Samples mostram como fazer isso em cada
biblioteca.

## Variações

- **Um carimbo visível da assinatura.** Adicione uma representação visual pela sua biblioteca de
  assinatura, exatamente como com qualquer outro certificado. Todos os exemplos citados acima
  adicionam uma.
- **Vários documentos com uma única autorização.** Crie a sessão com
  `TrustServiceSessionTypes.SignatureSession` e um `lifetimeInSeconds` longo o bastante para o lote,
  e repita os passos 2 a 4 para cada documento com a mesma sessão. Veja
  [Tipos de sessão](https://docs.lacunasoftware.com/articles/cloudhub/how-it-works#tipos-de-sessão).
- **CAdES e XAdES.** O fluxo é o mesmo com o *starter* de CAdES ou de XML da sua biblioteca de
  assinatura. Os exemplos em PHP incluem
  [uma assinatura XML com o CloudHub](https://github.com/LacunaSoftware/PkiSuiteSamples/tree/master/php/plain/public/xml-signature-cloudhub-rest).

**Demonstrações e exemplos**

- Demonstração: [Sign a PDF with your cloud certificate](https://try.lacunasoftware.com/pt/pki-suite/sign-with-cloud-certificate)
- Sign a PDF with a cloud certificate using CloudHub: [.NET](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/dotnet/mvc/PkiSuiteAspNetMvcSample/Controllers/PadesCloudHubSdkController.cs) · [Java](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/java/springmvc/src/main/java/com/lacunasoftware/pkisuite/controller/PadesSignatureCloudhubRestController.java) · [Node.js](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/nodejs/expressmvc/routes/pades-signature-cloudhub-restpki.js) · [PHP](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/php/plain/public/pades-signature-cloudhub-rest/index.php)
- Sign an XML document with a cloud certificate using CloudHub: [PHP](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/php/plain/public/xml-signature-cloudhub-rest/index.php)
