Authentication with a cloud certificate
A cloud certificate can sign users in to your application the same way a smart card does: the user proves that they control the certificate's key by signing a random challenge (a nonce), and your signature library checks the signature and validates the certificate. With CloudHub, the user approves that signature in their provider's app instead of typing a PIN into a token.
The flow is the signature flow with a nonce in place of a document:
- Create a session with
SingleSignatureand send the user to a provider. - When the user comes back, read their certificate from CloudHub.
- Have your signature library create a nonce, and have CloudHub sign the nonce's hash.
- Have your signature library check the signature and validate the certificate.
- Find the user by the certificate's CPF and sign them in.
Use SingleSignature for this, not AuthenticationSession: most providers don't support
authentication sessions, and a session that cannot sign cannot prove anything about the key.
The certificate alone is not proof. Step 4 is what shows that the user controls the key right now and that the certificate is valid and trusted.
The example
Each example is the redirect URI's handler; the handler that creates the session is the same as in Get started, without an identifier if you prefer to let the user identify themselves at the provider. The examples follow the samples in PKI Suite Samples:
| Language | Signature library |
|---|---|
| C# | PKI SDK (PKCertificateAuthentication) |
| PHP | PKI Express (Authentication) |
| Python | PKI Express (Authentication) |
| Node.js | Rest PKI (Authentication) |
| Java | PKI Express (Authentication) |
PKI SDK and Rest PKI give you the hash to sign. PKI Express gives you the nonce itself, so those examples hash it with the digest algorithm PKI Express names.
- 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("\"", "");
}
}
Matching the certificate to a user
The validated certificate identifies a person, not an account in your application. Map it the way your business requires; with ICP-Brasil certificates, the CPF (e-CPF) or the CNPJ and the responsible person's CPF (e-CNPJ) are the usual keys. If you created the session with an identifier, also check that the certificate carries that identifier: the user may have signed in at the provider with another account.