Autenticação com certificado em nuvem
Um certificado em nuvem permite que o usuário entre na sua aplicação do mesmo jeito que um smart card: o usuário prova que controla a chave do certificado assinando um desafio aleatório (um nonce), e a sua biblioteca de assinatura confere a assinatura e valida o certificado. Com o CloudHub, o usuário aprova essa assinatura no aplicativo do provedor, em vez de digitar um PIN em um token.
O fluxo é o fluxo de assinatura com um nonce no lugar do documento:
- Crie uma sessão
SingleSignaturee envie o usuário a um provedor. - Quando o usuário voltar, leia o certificado dele no CloudHub.
- Peça à sua biblioteca de assinatura que crie um nonce e peça ao CloudHub que assine o hash dele.
- Peça à sua biblioteca de assinatura que confira a assinatura e valide o certificado.
- Encontre o usuário pelo CPF do certificado e autentique-o.
Use SingleSignature para isso, não AuthenticationSession: a maioria dos provedores não oferece
sessões de autenticação, e uma sessão que não assina não prova nada sobre a chave.
O certificado sozinho não é prova. É o passo 4 que mostra que o usuário controla a chave neste momento e que o certificado é válido e confiável.
O exemplo
Cada exemplo é o handler da URI de redirecionamento; o handler que cria a sessão é o mesmo de Primeiros passos, sem identificador se você preferir que o usuário se identifique no provedor. Os exemplos seguem os do PKI Suite Samples:
| Linguagem | Biblioteca de assinatura |
|---|---|
| C# | PKI SDK (PKCertificateAuthentication) |
| PHP | PKI Express (Authentication) |
| Python | PKI Express (Authentication) |
| Node.js | Rest PKI (Authentication) |
| Java | PKI Express (Authentication) |
O PKI SDK e o Rest PKI fornecem o hash a assinar. O PKI Express fornece o próprio nonce, então esses exemplos calculam o hash dele com o algoritmo que o PKI Express indica.
- C#
- PHP
- Python
- Node.js
- Java
using Lacuna.Cloudhub.Api;
using Lacuna.Cloudhub.Client;
using Lacuna.Pki;
using Lacuna.Pki.Stores;
using Microsoft.AspNetCore.Mvc;
public class CloudLoginController : Controller {
private static readonly CloudhubClient cloudhub = new CloudhubClient(
"https://cloudhub.lacunasoftware.com/",
Environment.GetEnvironmentVariable("CLOUDHUB_API_KEY"));
// GET /CloudLogin/Complete?session=…: the redirect URI
public async Task<IActionResult> Complete(string session) {
if (string.IsNullOrEmpty(session)) {
return RedirectToAction("Index"); // not authorized
}
// 1. The user's certificate, from CloudHub
byte[] certificate = await cloudhub.GetCertificateAsync(session);
// 2. PKI SDK creates a nonce. Store nonces in your database in production.
var auth = new PKCertificateAuthentication(new FileSystemNonceStore(nonceFolder));
byte[] nonce = auth.Start();
// 3. CloudHub signs the nonce's hash with the user's key
var signature = await cloudhub.SignHashAsync(new SignHashRequest {
Session = session,
Hash = PKCertificateAuthentication.DigestAlgorithm.ComputeHash(nonce),
DigestAlgorithmOid = PKCertificateAuthentication.DigestAlgorithm.Oid,
});
// 4. PKI SDK checks the signature and validates the certificate
var validationResults = auth.Complete(nonce, certificate, signature,
TrustArbitrators.PkiBrazil, out PKCertificate userCert);
if (!validationResults.IsValid) {
return View("Failed", validationResults);
}
// 5. Find the user by userCert.PkiBrazil.CPF and sign them in
return RedirectToAction("Index", "Home");
}
}
<?php
require __DIR__ . '/../vendor/autoload.php';
use Lacuna\Cloudhub\CloudhubClient;
use Lacuna\Cloudhub\SignHashRequest;
use Lacuna\PkiExpress\Authentication;
$cloudhub = new CloudhubClient('https://cloudhub.lacunasoftware.com/', getenv('CLOUDHUB_API_KEY'));
$session = $_GET['session'] ?? null;
if (empty($session)) {
header('Location: /login.php'); // not authorized
exit;
}
// 1. The user's certificate, from CloudHub (base64)
$certificate = $cloudhub->getCertificateAsync($session);
// 2. PKI Express creates a nonce, base64-encoded
$auth = new Authentication();
$start = $auth->start();
// 3. CloudHub signs the nonce's hash with the user's key
$phpHashNames = [
'2.16.840.1.101.3.4.2.1' => 'sha256',
'2.16.840.1.101.3.4.2.2' => 'sha384',
'2.16.840.1.101.3.4.2.3' => 'sha512',
];
$hash = hash($phpHashNames[$start->digestAlgorithmOid], base64_decode($start->nonce), true);
$signature = $cloudhub->signHashAsync(new SignHashRequest(
$session,
base64_encode($hash),
null,
$start->digestAlgorithmOid
));
// 4. PKI Express checks the signature and validates the certificate
$auth->setNonce($start->nonce);
$auth->setCertificateBase64($certificate);
$auth->setSignature($signature);
$result = $auth->complete();
if (!$result->validationResults->isValid()) {
http_response_code(403);
echo nl2br(htmlspecialchars((string) $result->validationResults));
exit;
}
// 5. Find the user by $result->certificate->pkiBrazil->cpf and sign them in
import base64
import hashlib
from flask import Blueprint, abort, redirect, request, url_for
from pkiexpress import Authentication
from cloudhub import call_cloudhub # the helper from "Get started"
blueprint = Blueprint('cloud_login', __name__, url_prefix='/cloud-login')
HASH_FUNCTIONS = {
'2.16.840.1.101.3.4.2.1': hashlib.sha256,
'2.16.840.1.101.3.4.2.2': hashlib.sha384,
'2.16.840.1.101.3.4.2.3': hashlib.sha512,
}
@blueprint.route('/complete')
def complete():
cloudhub_session = request.args.get('session')
if not cloudhub_session:
return redirect(url_for('.index')) # not authorized
# 1. The user's certificate, from CloudHub (base64)
certificate = call_cloudhub('GET', '/api/sessions/certificate',
params={'session': cloudhub_session})
# 2. PKI Express creates a nonce, base64-encoded
auth = Authentication()
start = auth.start()
# 3. CloudHub signs the nonce's hash with the user's key
digest = HASH_FUNCTIONS[start.digest_algorithm_oid](base64.b64decode(start.nonce)).digest()
signature = call_cloudhub('POST', '/api/sessions/sign-hash', json={
'session': cloudhub_session,
'hash': base64.b64encode(digest).decode('ascii'),
'digestAlgorithmOid': start.digest_algorithm_oid,
})
# 4. PKI Express checks the signature and validates the certificate
auth.nonce_base64 = start.nonce
auth.certificate_base64 = certificate
auth.signature_base64 = signature
result = auth.complete()
if not result.validation_results.is_valid:
abort(403, str(result.validation_results))
# 5. Find the user by result.certificate.pki_brazil.cpf and sign them in
return redirect(url_for('home'))
const express = require('express');
const { CloudHubClient } = require('cloudhub-client');
const {
Authentication,
CompleteAuth,
RestPkiClient,
StandardSecurityContexts,
} = 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();
// GET /cloud-login/complete?session=…: the redirect URI
router.get('/complete', async (req, res, next) => {
try {
const { session } = req.query;
if (!session) {
return res.redirect('/cloud-login'); // not authorized
}
// 1. The user's certificate, from CloudHub (base64)
const certificate = await cloudhub.getCertificateAsync(session);
// 2. Rest PKI starts the authentication and returns the hash to sign
const auth = new Authentication(restPki);
const start = await auth.start(StandardSecurityContexts.PKI_BRAZIL);
// 3. CloudHub signs it with the user's key
const signature = await cloudhub.signHashAsync({
session,
hash: start.toSignHash,
digestAlgorithmOid: start.digestAlgorithmOid,
});
// 4. Rest PKI checks the signature and validates the certificate
const result = await auth.complete(start.token, new CompleteAuth({ signature, certificate }));
if (!result.validationResults.isValid()) {
return res.status(403).send(result.validationResults.toString());
}
// 5. Find the user by result.certificate.pkiBrazil.cpf and sign them in
res.redirect('/');
} catch (err) {
next(err);
}
});
module.exports = router;
import cloudhub.SessionsApi;
import cloudhub.client.CloudhubClient;
import cloudhub.client.model.SignHashRequest;
import com.lacunasoftware.pkiexpress.AuthCompleteResult;
import com.lacunasoftware.pkiexpress.AuthStartResult;
import com.lacunasoftware.pkiexpress.Authentication;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
@Controller
@RequestMapping("/cloud-login")
public class CloudLoginController {
private static final Map<String, String> HASH_NAMES = new HashMap<>();
static {
HASH_NAMES.put("2.16.840.1.101.3.4.2.1", "SHA-256");
HASH_NAMES.put("2.16.840.1.101.3.4.2.2", "SHA-384");
HASH_NAMES.put("2.16.840.1.101.3.4.2.3", "SHA-512");
}
private final SessionsApi cloudhub = new SessionsApi(
new CloudhubClient("https://cloudhub.lacunasoftware.com", System.getenv("CLOUDHUB_API_KEY")));
// GET /cloud-login/complete?session=…: the redirect URI
@GetMapping("/complete")
public String complete(@RequestParam(required = false) String session) throws Exception {
if (session == null || session.isEmpty()) {
return "redirect:/cloud-login"; // not authorized
}
// 1. The user's certificate, from CloudHub (base64)
String certificate = unquote(cloudhub.apiSessionsCertificateGet(session));
// 2. PKI Express creates a nonce, base64-encoded
Authentication auth = new Authentication();
AuthStartResult start = auth.start();
// 3. CloudHub signs the nonce's hash with the user's key
byte[] nonce = Base64.getDecoder().decode(start.getNonce());
byte[] hash = MessageDigest.getInstance(HASH_NAMES.get(start.getDigestAlgorithmOid())).digest(nonce);
SignHashRequest signRequest = new SignHashRequest();
signRequest.setSession(session);
signRequest.setHash(hash);
signRequest.setDigestAlgorithmOid(start.getDigestAlgorithmOid());
String signature = unquote(cloudhub.apiSessionsSignHashPost(signRequest));
// 4. PKI Express checks the signature and validates the certificate
auth.setNonce(start.getNonce());
auth.setCertificateBase64(certificate);
auth.setSignature(signature);
AuthCompleteResult result = auth.complete();
if (!result.getValidationResults().isValid()) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, result.getValidationResults().toString());
}
// 5. Find the user by result.getCertificate().getPkiBrazil().getCpf() and sign them in
return "redirect:/";
}
// 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("\"", "");
}
}
Associando o certificado a um usuário
O certificado validado identifica uma pessoa, não uma conta na sua aplicação. Faça a associação como o seu negócio exigir; com certificados ICP-Brasil, as chaves usuais são o CPF (e-CPF) ou o CNPJ e o CPF do responsável (e-CNPJ). Se você criou a sessão com um identificador, confira também se o certificado traz esse identificador: o usuário pode ter entrado no provedor com outra conta.