Se hai mai implementato le firme digitali in Java usando direttamente la JCA (Java Cryptography Architecture), conosci la sensazione: quella che dovrebbe essere un'operazione semplice si trasforma in una cerimonia di factory method, gestione delle eccezioni e incantesimi arcani. Lascia che ti mostri cosa intendo.
Il modo JCA
Ecco un ciclo completo di firma e verifica usando solo la libreria standard:
import java.security.*;
public class JcaSignatureExample {
public static void main(String[] args) throws Exception {
// Genera una coppia di chiavi
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
KeyPair keyPair = keyGen.generateKeyPair();
byte[] message = "Trasferisci 500€ ad Alice".getBytes("UTF-8");
// Firma
Signature signer = Signature.getInstance("SHA256withRSA");
signer.initSign(keyPair.getPrivate());
signer.update(message);
byte[] signatureBytes = signer.sign();
// Verifica
Signature verifier = Signature.getInstance("SHA256withRSA");
verifier.initVerify(keyPair.getPublic());
verifier.update(message);
boolean isValid = verifier.verify(signatureBytes);
System.out.println("Valida: " + isValid);
}
}
Sono 16 righe di logica (senza contare import e boilerplate della classe)
solo per firmare un messaggio e verificare la firma. E questo è il
percorso felice — nessun caricamento di keystore, nessuna codifica,
nessuna gestione degli errori. Ogni chiamata può lanciare un'eccezione
checked. La stringa dell'algoritmo è duplicata. L'oggetto Signature è
mutabile e con stato: fai init, poi update, poi chiami sign o
verify. Salta un passaggio e ottieni una criptica SignatureException.
Ora immagina di caricare le chiavi da un keystore PKCS#12. Ecco come appare:
import java.io.*;
import java.security.*;
import java.security.cert.CertificateException;
import java.util.Base64;
public class JcaKeystoreSignature {
public static void main(String[] args)
throws KeyStoreException, IOException, CertificateException,
NoSuchAlgorithmException, UnrecoverableKeyException,
InvalidKeyException, SignatureException {
// Carica il keystore
KeyStore keystore = KeyStore.getInstance("PKCS12");
try (InputStream is = new FileInputStream("keystore.p12")) {
keystore.load(is, "changeit".toCharArray());
}
// Estrai le chiavi
PrivateKey privateKey = (PrivateKey)
keystore.getKey("alice", "changeit".toCharArray());
PublicKey publicKey =
keystore.getCertificate("alice").getPublicKey();
byte[] message = "Trasferisci 500€ ad Alice".getBytes("UTF-8");
// Firma
Signature signer = Signature.getInstance("SHA256withRSA");
signer.initSign(privateKey);
signer.update(message);
byte[] signatureBytes = signer.sign();
// Codifica per il trasporto
String encoded = Base64.getEncoder().encodeToString(signatureBytes);
// Decodifica e verifica
byte[] decoded = Base64.getDecoder().decode(encoded);
Signature verifier = Signature.getInstance("SHA256withRSA");
verifier.initVerify(publicKey);
verifier.update(message);
boolean isValid = verifier.verify(decoded);
System.out.println("Valida: " + isValid);
}
}
Trenta righe di logica. Sette eccezioni checked nella clausola throws.
Un cast a PrivateKey. Codifica Base64 manuale. Eppure tutto quello che
stiamo facendo è: caricare le chiavi, firmare un messaggio, verificare la
firma.
Il modo Bruce
Ecco la stessa operazione con Bruce:
import static com.mirkocaserta.bruce.Bruce.*;
import static com.mirkocaserta.bruce.Keystores.*;
var keys = keyPair("RSA", 2048);
var signer = signerBuilder()
.key(keys.getPrivate())
.algorithm("SHA256withRSA")
.build();
var verifier = verifierBuilder()
.key(keys.getPublic())
.algorithm("SHA256withRSA")
.build();
var message = Bytes.from("Trasferisci 500€ ad Alice");
var signature = signer.sign(message);
var isValid = verifier.verify(message, signature);
Tutto qui. Nessuna eccezione checked che inquina la tua API. Nessuno stato
mutabile da gestire. Nessuna danza manuale init/update/sign. Signer
e Verifier sono interfacce funzionali — immutabili, thread-safe e
componibili.
E con un keystore:
var keystore = keystore("classpath:keystore.p12", "changeit".toCharArray(), "PKCS12");
var privateKey = privateKey(keystore, "alice", "changeit".toCharArray());
var publicKey = publicKey(keystore, "alice");
var signer = signerBuilder()
.key(privateKey)
.algorithm("SHA256withRSA")
.build();
var verifier = verifierBuilder()
.key(publicKey)
.algorithm("SHA256withRSA")
.build();
var message = Bytes.from("Trasferisci 500€ ad Alice");
var signature = signer.sign(message);
var encoded = signature.encode(BASE64); // pronto per il trasporto
var isValid = verifier.verify(message, Bytes.from(encoded, BASE64));
Caricare le chiavi, firmare, codificare in Base64, decodificare e verificare — in codice leggibile e lineare.
Cosa fa Bruce sotto il cofano
Bruce non sostituisce la JCA. È un sottile wrapper in puro Java attorno ad essa. Nessuna dipendenza crittografica esterna, nessun codice nativo — solo il motore crittografico della JVM con un'API umana sopra. Ecco cosa ottieni:
SignereVerifiersono@FunctionalInterface. Fanno una sola cosa e sono immutabili. Puoi passarli, memorizzarli in campi, usarli nelle lambda.Bytesè il tipo universale di I/O di Bruce. Avvolgebyte[]con factory method convenienti (Bytes.from(String),Bytes.from(Path),Bytes.from(InputStream)) e supporto alla codifica (encode(BASE64),encode(HEX)). Niente più gestione manuale di charset o Base64.- I builder gestiscono la cerimonia
getInstance/inituna volta al momento della costruzione. Dopo, firmare e verificare sono chiamate di una riga. - Le eccezioni checked vengono catturate e wrappate in
BruceExceptionunchecked, così non inquinano le firme dei tuoi metodi.
Chiavi multiple? Nessun problema
Se gestisci firme per più soggetti, Bruce ha le varianti SignerByKey e
VerifierByKey:
var signerByKey = signerBuilder()
.keys(Map.of("alice", alicePrivateKey, "bob", bobPrivateKey))
.algorithm("SHA256withRSA")
.buildByKey();
var verifierByKey = verifierBuilder()
.keys(Map.of("alice", alicePublicKey, "bob", bobPublicKey))
.algorithm("SHA256withRSA")
.buildByKey();
var signature = signerByKey.sign("alice", message);
var isValid = verifierByKey.verify("alice", message, signature);
Prova a farlo in modo pulito con la JCA pura. Ti aspetto.
Per iniziare
Aggiungi Bruce al tuo progetto:
<dependency>
<groupId>com.mirkocaserta.bruce</groupId>
<artifactId>bruce</artifactId>
<version>2.0.0</version>
</dependency>
Oppure con Gradle:
implementation("com.mirkocaserta.bruce:bruce:2.0.0")
Bruce richiede Java 21+ e ha zero dipendenze esterne.
La vita è troppo breve per KeyPairGenerator.getInstance().