Un’infrastruttura a chiave pubblica (PKI o Public Key Infrastructure) è un insieme di ruoli, politiche e procedure necessari per creare, gestire, distribuire, utilizzare, archiviare e revocare i certificati digitali e gestire la crittografia a chiave pubblica. All’interno di tale infrastruttura assume un ruolo fondamentale l’algoritmo utilizzato per la validazione di una catena di certificati (certificate path). Una catena di certificati è costituita da un certificato foglia, da N certificati intermedi e da un certificato root, quest’ultimo generalmente emesso da una Certification Authority (CA) affidabile.
Per i certificati in formato X.509 il processo di validazione è standardizzato dalla RFC 5280 e prevede una serie di step di verifica che si applicano a tutti i certificati a partire da quello root (detto trust anchor). Al primo fallimento l’intero processo fallisce. Alcuni esempi delle verifiche eseguito sono:
- l’algoritmo utilizzato per la chiave pubblica;
- la validità del certificato rispetto alla data corrente;
- lo stato di revoca del certificato;
- il nome dell’issuer che deve coincidere con il subject name del certificato che lo precede nella catena;
- la presenza degli OID ammessi e richiesti;
- la lunghezza della catena;
- etc.
L’architettura JCA di java fornisce le API necessarie all’esecuzione dell’operazione di validazione della catena di certificati in modo sufficientemente semplice.
Caso d’Uso
Mostriamo un caso pratico di validazione di una catena di certificati utilizzando i dati di test forniti da Apple per la realizzazione dei servizi di crittografia coinvolti nel protocollo Apple Pay.
Si tratta di una catena composta da soli tre certificati: un certificato root (trust anchor), uno sub (certificato intermedio) ed uno leaf (foglia). Le revocation list utilizzate possono invece essere scaricate alla pagina Apple PKI nella sezione apposita.
Tutti i certificati sono forniti come file separati e possono essere recuperati dalla cartella src/main/resources
del progetto di esempio.
Implementazione
Per la gestione dei certificati l’architettura JCA fornisce la classe CertificateFactory
che utilizziamo per convertire i file di input. In particolare il metodo generateCertificates(
) converte un array di byte in una Collection di certificati. Nel progetto è inserita una classe di utilità FileUtils
che recupera da filesystem i certificati intermedi, i certificati root (nel nostro caso uno solo) e le revocation list, e li restituisce in forma di un unico array di byte.
Per la generazione della catena di certificati la classe CertificateFactory
espone il metodo generateCertPath()
che restituisce un oggetto di tipo CertPath
che poi andremo a validare. Il codice seguente mostra quindi come ricostruire la sequenza di certificati:
1 2 3 4 5 6 7 |
CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); // Generate Certification Path byte[] certsByte = FileUtils.loadAllCertificates(); InputStream certsStream = new ByteArrayInputStream( certsByte ); Collection<? extends Certificate> certs = certFactory.generateCertificates(certsStream); CertPath certPath = certFactory.generateCertPath( (List<Certificate>) certs ); |
In modo analogo recuperiamo da file system la lista dei certificati root e convertiamoli in oggetti TrustAnchor
:
1 2 3 4 5 6 7 8 9 10 |
// Load trust Anchors byte[] rootsByte = FileUtils.loadRootCertificate(); InputStream rootsStream = new ByteArrayInputStream( rootsByte ); Collection<? extends Certificate> roots = certFactory.generateCertificates(rootsStream); // Converto to TrustAnchor Set<TrustAnchor> anchors = new HashSet<TrustAnchor>(); for ( Certificate root : roots ) { anchors.add( new TrustAnchor( (X509Certificate) root, null ) ); } |
La validazione della catena di certificati avviene utilizzando un oggetto di tipo CertPathValidator
il quale espone il metodo validate()
. Tale metodo, oltre alla catena di certificati (CertPath
), richiede in input un oggetto di tipo PKIXParameters
, utilizzato per parametrizzare il comportamento del processo ed in particolare per passare al metodo di validazione la Collection di trusted anchor:
1 2 3 4 5 6 |
// Config Parameter PKIXParameters params = new PKIXParameters( anchors ); // validate (generate Exception if not valid) CertPathValidator validator = CertPathValidator.getInstance( "PKIX" ); validator.validate( certPath, params ); |
Il metodo validate()
solleva una eccezione di tipo CertPathValidatorException
nel caso in cui non sia possibile validare la catena di certificati. Tale eccezione fornisce informazioni sul motivo della mancata validazione restituendo un indice, che identifica quale certificato nella catena ha sollevato l’eccezione, ed una motivazione (Reason
). Il codice potrebbe quindi essere modificato nel seguente modo:
1 2 3 4 5 6 7 8 9 10 11 |
// Config Parameter PKIXParameters params = new PKIXParameters( anchors ); CertPathValidator validator = CertPathValidator.getInstance( "PKIX" ); try { validator.validate( certPath, params ); System.out.println( "Valid certificate path!!!" ); } catch (CertPathValidatorException e) { e.printStackTrace(); System.err.println( e.getIndex() + " " + e.getReason() ); } |
L’esecuzione della validazione con i certificati di test produrrà l’eccezione “Could not determine revocation status” dovuto al fatto che la l’url della CRL specificato nei certificate è fake e quindi non raggiungibile. Disabilitiamo il check della revocation list configurando l’opzione relativa nella classe PKIXParameters
, il codice diviene quindi:
1 2 3 4 |
// Config Parameter PKIXParameters params = new PKIXParameters( anchors ); params.setRevocationEnabled( false ); ... |
Eseguiamo ancora la validazione ma questa volta otteniamo una eccezione di tipo CertificateExpiredException dovuta al fatto che il certificato foglia non è valido dopo il 20 maggio 2017. Possiamo ancora ovviare al problema impostando la data sull’oggetto PKIXParameters
:
1 2 3 4 |
// Config Parameter PKIXParameters params = new PKIXParameters( anchors ); params.setRevocationEnabled( false ); params.setDate( Date.from(LocalDate.of(2017, 1, 1).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()) ); |
Verifica della CRL
Nel caso in cui non sia possibile accedere alla revocation list, ad esempio perché l’applicazione non ha i diritti per raggiungere l’URL della CRL, è possibile eseguire la verifica utilizzando una o più file di CRL forniti, ad esempio, su file system.
In questo caso utilizziamo ancora la classe CertificateFactory
per convertire uno stream di byte in una Collection di CRL
, ed il metodo isRevoked()
per determinare se il certificato in input è contenuto nella lista dei certificati revocati.
1 2 3 4 5 6 7 8 9 10 11 |
// Verify CRLs byte[] crlsByte = FileUtils.loadAllCrl(); InputStream crlsStream = new ByteArrayInputStream( crlsByte ); Collection<? extends CRL> crls = certFactory.generateCRLs(crlsStream); for (CRL crl : crls) { for (Certificate cert : certs) { if ( crl.isRevoked( cert ) ) { throw new RuntimeException("A certificate is revoked: "); } } } |
Validazione delle Estensioni
In un certificato X.509 le estensioni sono un meccanismo che consente di arricchire il certificato con informazioni aggiuntive. Ogni estensione è caratterizzata da un OID (Object Identifier) ed un valore ad esso associato. Le estensioni possono essere definite come CRITICAL
o NON CRITICAL
. Durante il processo di validazione se si incontra una estensione CRITICAL
non riconosciuta la validazione fallisce.
Naturalmente esistono una serie di OID standard e riconosciute dal processo implementato in JCA. Vediamo invece come poter riconoscere una estensione critica non standard ed utilizzata nei certificati della nostra applicazione.
Per i nostri scopi ci viene in aiuto la classe astratta PKIXCertPathChecker
che, opportunamente estesa, può definire nuovi criteri di verifica nel processo. la classe definisce quattro metodi: check
, getSupportedExtensions
, init
e isForwardCheckingSupported
. Il metodo check()
è invocato su ogni certificato della catena, mentre l’ordine di invocazione (dal certificato root al foglia o viceversa) è dichiarato dal processo attraverso il metodo init()
che comunque verifica prima se l’implementazione supporta il formward checking eseguendo il test sul metodo isForwardCheckingSupported
.
Implementiamo quindi una estensione della classe PKIXCertPathChecker
per validare un OID custom e dichiarato critico.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public class CustomOIDCertPathChecker extends PKIXCertPathChecker { private String oid; public CustomOIDCertPathChecker( String oid ) { this.oid = oid; } @Override public void init(boolean forward) throws CertPathValidatorException {} @Override public boolean isForwardCheckingSupported() { return true; } @Override public Set<String> getSupportedExtensions() { return new HashSet<String>( Arrays.asList( oid ) ); } @Override public void check(Certificate cert, Collection<String> unresolvedCritExts) throws CertPathValidatorException { unresolvedCritExts.remove( oid ); } } |
Il trucco è semplicemente quello di rimuovere l’OID di interesse dalla lista delle estensioni critiche non riconosciute e fornita in input la metodo check()
attraverso il parametro unresolvedCritExts
.
Codice Sorgente
Il codice sorgente completo di tutti gli esempi presentati è scaricabile qui certpath.