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.
| Linguagem | Biblioteca de assinatura | Framework web |
|---|---|---|
| C# | PKI SDK | ASP.NET Core MVC |
| PHP | Rest PKI | PHP puro |
| Python | PKI Express | Flask |
| Node.js | Rest PKI | Express |
| Java | 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:
- Lê o certificado do signatário no CloudHub.
- 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).
- Envia o hash ao
sign-hashdo CloudHub. O provedor o assina com a chave do usuário. - 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#
- PHP
- Python
- Node.js
- Java
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
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
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.
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.
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.
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.SignatureSessione umlifetimeInSecondslongo 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. - 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.