Primeiros passos
Esta página vai da chave de API até um hash assinado: instale um cliente, crie uma sessão, envie o usuário a um provedor e use a sessão que volta. Cada passo tem código em C#, PHP, Python, Node.js e Java. Como funciona explica o fluxo por trás dos passos.
O código assina um hash calculado pela sua biblioteca de assinatura. Assinando um PDF e Autenticação mostram handlers completos, que calculam esse hash e finalizam a assinatura com PKI SDK, Rest PKI ou PKI Express.
1. Obter uma chave de API
Peça ao suporte da Lacuna Software (suporte@lacunasoftware.com) uma chave de API do CloudHub e
informe as URIs de redirecionamento que a sua aplicação vai usar, em produção e em desenvolvimento.
Se você for acrescentar os seus próprios parâmetros à URI de redirecionamento, como os exemplos
abaixo fazem com fileId, peça uma entrada de host, como https://app.example.com, em vez de uma
URI exata. Veja URIs de redirecionamento.
Os exemplos leem a chave da variável de ambiente CLOUDHUB_API_KEY. Mantenha-a no servidor.
2. Instalar um cliente
- C#
- PHP
- Python
- Node.js
- Java
dotnet add package Lacuna.Cloudhub.Client
Para a injeção de dependência do ASP.NET Core, adicione também o Lacuna.Cloudhub.ClientCore.
composer require lacuna/cloudhub-client
O cliente precisa do PHP 7.4 ou posterior e usa o Guzzle.
pip install requests
Os exemplos em Python chamam a API REST diretamente; são só oito operações.
npm install cloudhub-client
dependencies {
implementation 'com.lacunasoftware.cloudhub:cloudhub-client:1.0.5'
}
Com Maven:
<dependency>
<groupId>com.lacunasoftware.cloudhub</groupId>
<artifactId>cloudhub-client</artifactId>
<version>1.0.5</version>
</dependency>
3. Criar um cliente
- C#
- PHP
- Python
- Node.js
- Java
using Lacuna.Cloudhub.Api;
using Lacuna.Cloudhub.Client;
var cloudhub = new CloudhubClient(
"https://cloudhub.lacunasoftware.com/",
Environment.GetEnvironmentVariable("CLOUDHUB_API_KEY"));
Com o Lacuna.Cloudhub.ClientCore, registre o cliente uma vez e injete ICloudhubService, que tem
os mesmos métodos:
using Lacuna.Cloudhub.ClientCore;
builder.Services.AddCloudhub().Configure(builder.Configuration.GetSection("Cloudhub"));
{
"Cloudhub": {
"Endpoint": "https://cloudhub.lacunasoftware.com/",
"ApiKey": "set it in user secrets or an environment variable"
}
}
<?php
require __DIR__ . '/vendor/autoload.php';
use Lacuna\Cloudhub\CloudhubClient;
// The client appends "api/..." to the address, so keep the trailing slash
$cloudhub = new CloudhubClient('https://cloudhub.lacunasoftware.com/', getenv('CLOUDHUB_API_KEY'));
Os métodos do cliente PHP terminam em Async, mas são síncronos: cada um devolve o resultado.
import os
import requests
CLOUDHUB_URL = 'https://cloudhub.lacunasoftware.com'
_http = requests.Session()
_http.headers['X-Api-Key'] = os.environ['CLOUDHUB_API_KEY']
class CloudHubError(Exception):
"""An error CloudHub reported, such as SessionExpired."""
def __init__(self, code, message):
super().__init__(f'{code}: {message}')
self.code = code
def call_cloudhub(method, path, **kwargs):
# A signature can wait on the provider, so allow a generous timeout
response = _http.request(method, CLOUDHUB_URL + path, timeout=120, **kwargs)
if response.status_code == 422:
error = response.json()
raise CloudHubError(error.get('code'), error.get('message'))
response.raise_for_status()
return response.json()
const { CloudHubClient, TrustServiceSessionTypes } = require('cloudhub-client');
const cloudhub = new CloudHubClient('https://cloudhub.lacunasoftware.com/', process.env.CLOUDHUB_API_KEY);
import cloudhub.SessionsApi;
import cloudhub.client.CloudhubClient;
// No trailing slash: the client appends "/api/..." to the address
SessionsApi cloudhub = new SessionsApi(
new CloudhubClient("https://cloudhub.lacunasoftware.com", System.getenv("CLOUDHUB_API_KEY")));
Um CloudhubClient guarda estado de cada requisição, então não compartilhe uma instância entre
threads que a usem ao mesmo tempo.
4. Criar uma sessão
Envie o CPF do signatário, só com os dígitos, e a URI para onde o CloudHub deve devolver o usuário. Os exemplos levam o ID do arquivo a assinar na URI de redirecionamento, para que a requisição que volta saiba de qual arquivo se trata.
- C#
- PHP
- Python
- Node.js
- Java
var session = await cloudhub.CreateSessionAsync(new SessionCreateRequest {
Identifier = "12345678909",
RedirectUri = "https://app.example.com/sign/complete?fileId=42",
Type = TrustServiceSessionTypes.SingleSignature,
LifetimeInSeconds = 300,
});
foreach (var service in session.Services) {
Console.WriteLine($"{service.ServiceInfo.Provider}: {service.AuthUrl}");
}
use Lacuna\Cloudhub\SessionCreateRequest;
use Lacuna\Cloudhub\TrustServiceSessionTypes;
$session = $cloudhub->createSessionAsync(new SessionCreateRequest(
'12345678909', // the signer's CPF
'https://app.example.com/sign/complete?fileId=42', // redirect URI
TrustServiceSessionTypes::SingleSignature,
300 // lifetime in seconds; 300 if omitted
));
foreach ($session->services as $service) {
echo $service->serviceInfo->provider . ': ' . $service->authUrl . PHP_EOL;
}
from cloudhub import call_cloudhub
session = call_cloudhub('POST', '/api/sessions', json={
'identifier': '12345678909',
'redirectUri': 'https://app.example.com/sign/complete?fileId=42',
'type': 'SingleSignature',
'lifetimeInSeconds': 300,
})
for service in session['services']:
print(service['serviceInfo']['provider'], service['authUrl'])
const session = await cloudhub.createSessionAsync({
identifier: '12345678909',
redirectUri: 'https://app.example.com/sign/complete?fileId=42',
type: TrustServiceSessionTypes.SingleSignature,
lifetimeInSeconds: 300,
});
for (const service of session.services || []) {
console.log(`${service.serviceInfo.provider}: ${service.authUrl}`);
}
import cloudhub.client.model.SessionCreateRequest;
import cloudhub.client.model.SessionModel;
import cloudhub.client.model.TrustServiceAuthParametersModel;
import cloudhub.client.model.TrustServiceSessionTypes;
SessionCreateRequest request = new SessionCreateRequest();
request.setIdentifier("12345678909");
request.setRedirectUri("https://app.example.com/sign/complete?fileId=42");
request.setType(TrustServiceSessionTypes.SINGLE_SIGNATURE);
request.setLifetimeInSeconds(300);
SessionModel session = cloudhub.apiSessionsPost(request);
for (TrustServiceAuthParametersModel service : session.getServices()) {
System.out.println(service.getServiceInfo().getProvider() + ": " + service.getAuthUrl());
}
Uma lista vazia significa que nenhum provedor tem um certificado para esse CPF. Informe o usuário e ofereça a opção de tentar outro CPF.
5. Enviar o usuário a um provedor
Mostre uma opção para cada entrada e leve o navegador para a authUrl que o usuário escolher.
badgeUrl é o logotipo do provedor e pode ser nula.
<!-- One link per entry of "services" -->
<a href="https://api.birdid.com.br/oauth/authorize?client_id=…">
<img src="https://cdn.lacunasoftware.com/img/psc/birdid.png" alt="BirdID" height="48">
Sign with SOLUTI
</a>
O usuário entra no provedor e aprova. O provedor devolve o navegador ao CloudHub, e o CloudHub o envia à sua URI de redirecionamento.
6. Ler o certificado quando o usuário voltar
A sua URI de redirecionamento recebe o parâmetro session ao lado dos seus. Quando session não
vem, o usuário não autorizou: leve-o de volta para escolher de novo.
- C#
- PHP
- Python
- Node.js
- Java
// ASP.NET Core: GET /sign/complete?fileId=42&session=…
public async Task<IActionResult> Complete(string fileId, string session) {
if (string.IsNullOrEmpty(session)) {
return RedirectToAction("Index", new { fileId });
}
byte[] certificate = await cloudhub.GetCertificateAsync(session);
// ...
}
// complete.php?fileId=42&session=…
$fileId = $_GET['fileId'];
$session = $_GET['session'] ?? null;
if (empty($session)) {
header('Location: /sign.php?fileId=' . urlencode($fileId));
exit;
}
$certificate = $cloudhub->getCertificateAsync($session); // base64
from flask import redirect, request, url_for
@app.route('/sign/complete')
def complete():
file_id = request.args['fileId']
session = request.args.get('session')
if not session:
return redirect(url_for('sign', fileId=file_id))
certificate = call_cloudhub('GET', '/api/sessions/certificate',
params={'session': session}) # base64
# ...
router.get('/sign/complete', async (req, res, next) => {
try {
const { fileId, session } = req.query;
if (!session) {
return res.redirect(`/sign?fileId=${encodeURIComponent(fileId)}`);
}
const certificate = await cloudhub.getCertificateAsync(session); // base64
// ...
} catch (err) {
next(err);
}
});
import cloudhub.client.ApiException;
import java.nio.charset.StandardCharsets;
@GetMapping("/sign/complete")
public String complete(@RequestParam String fileId,
@RequestParam(required = false) String session) throws ApiException {
if (session == null || session.isEmpty()) {
return "redirect:/sign?fileId=" + fileId;
}
String certificate = unquote(cloudhub.apiSessionsCertificateGet(session)); // base64
// ...
}
// 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("\"", "");
}
Antes de assinar, confira se o certificado pertence à pessoa esperada: compare o CPF dele com o que você enviou. Os exemplos completos fazem isso com a biblioteca de assinatura.
7. Assinar um hash
A sua biblioteca de assinatura fornece o hash a assinar e o algoritmo de hash dele. Envie os dois com
a sessão; o resultado é o valor da assinatura, que a biblioteca insere no documento. Identifique o
algoritmo pelo OID (digestAlgorithmOid), como as bibliotecas de assinatura o informam, ou pelo nome
(digestAlgorithm, como SHA-256).
- C#
- PHP
- Python
- Node.js
- Java
byte[] signature = await cloudhub.SignHashAsync(new SignHashRequest {
Session = session,
Hash = toSignHash, // the hash, as bytes
DigestAlgorithmOid = digestAlgorithmOid,
});
use Lacuna\Cloudhub\SignHashRequest;
$signature = $cloudhub->signHashAsync(new SignHashRequest(
$session,
$toSignHashBase64, // the hash, base64-encoded
null, // the digest algorithm name...
$digestAlgorithmOid // ...or its OID
)); // base64
signature = call_cloudhub('POST', '/api/sessions/sign-hash', json={
'session': session,
'hash': to_sign_hash_base64,
'digestAlgorithmOid': digest_algorithm_oid,
}) # base64
const signature = await cloudhub.signHashAsync({
session,
hash: toSignHashBase64,
digestAlgorithmOid,
}); // base64
import cloudhub.client.model.SignHashRequest;
import java.util.Base64;
SignHashRequest signRequest = new SignHashRequest();
signRequest.setSession(session);
signRequest.setHash(toSignHash); // the hash, as bytes
signRequest.setDigestAlgorithmOid(digestAlgorithmOid);
byte[] signature = Base64.getDecoder().decode(unquote(cloudhub.apiSessionsSignHashPost(signRequest)));
8. Tratar erros
O CloudHub informa um problema na sua requisição com HTTP 422 e um corpo JSON com code, message
e, às vezes, details. O código mais comum é SessionExpired: a sessão expirou antes de ser usada,
então envie o usuário ao provedor de novo. A página API REST lista todos os
códigos.
- C#
- PHP
- Python
- Node.js
- Java
try {
var signature = await cloudhub.SignHashAsync(request);
} catch (CloudhubApiException ex) when (ex.CodeStr == "SessionExpired") {
// Create a new session and send the user to the provider again
}
Compare CodeStr. A propriedade Code, do tipo enum, não é preenchida a partir da resposta.
use GuzzleHttp\Exception\ClientException;
try {
$signature = $cloudhub->signHashAsync($request);
} catch (ClientException $e) {
$error = json_decode((string) $e->getResponse()->getBody(), true);
if (($error['code'] ?? null) === 'SessionExpired') {
// Create a new session and send the user to the provider again
}
}
from cloudhub import CloudHubError
try:
signature = call_cloudhub('POST', '/api/sessions/sign-hash', json=sign_request)
except CloudHubError as e:
if e.code == 'SessionExpired':
pass # Create a new session and send the user to the provider again
try {
const signature = await cloudhub.signHashAsync(request);
} catch (err) {
// cloudhub-client 1.x throws a string: CloudHub's JSON error followed by the HTTP message
if (String(err).includes('"SessionExpired"')) {
// Create a new session and send the user to the provider again
}
}
try {
byte[] signature = cloudhub.apiSessionsSignHashPost(request);
} catch (ApiException e) {
if (e.getCode() == 422 && e.getResponseBody().contains("\"SessionExpired\"")) {
// Create a new session and send the user to the provider again
}
}
Uma requisição sem uma chave de API válida recebe HTTP 401. Uma falha inesperada, como um provedor fora do ar, recebe HTTP 500 com um código curto no corpo; informe-o ao falar com o suporte.
Próximos passos
- Assinando um PDF: o fluxo PAdES completo, com a biblioteca de assinatura calculando o hash e finalizando o PDF.
- Autenticação: permita que os usuários entrem com o certificado em nuvem.
- Como funciona:
SignatureSessionpara assinar vários documentos com uma única autorização.