test(pkcs11): add softhsm2 integration test with assumptions and configure ci pipeline

This commit is contained in:
Claudecio Martins
2026-10-20 15:26:39 -03:00
parent 4b64a113b5
commit 4b6ae923c6
3 changed files with 211 additions and 1 deletions
@@ -46,10 +46,14 @@ public class Pkcs11CertificateProvider implements CertificateProvider {
"C:/Windows/System32/aetpkss1.dll", // SafeSign AET
"C:/Windows/System32/acospkcs11.dll", // ACO / Giesecke & Devrient
"C:/Windows/System32/opensc-pkcs11.dll",// OpenSC Windows
"C:/SoftHSM2/lib/softhsm2.dll", // SoftHSM2 Windows
// Linux
"/usr/lib/x86_64-linux-gnu/opensc-pkcs11.so",
"/usr/lib/opensc-pkcs11.so",
"/usr/lib/libaetpkss.so"
"/usr/lib/libaetpkss.so",
"/usr/lib/softhsm/libsofthsm2.so", // SoftHSM2 Debian/Ubuntu
"/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so", // SoftHSM2 Multiarch
"/usr/local/lib/softhsm/libsofthsm2.so"
};
private Provider sunPkcs11Provider;
@@ -79,6 +83,50 @@ public class Pkcs11CertificateProvider implements CertificateProvider {
this.keyStore = keyStore;
}
/**
* Inicializa dinamicamente o provider SunPKCS11 em memória para uma biblioteca específica,
* slot e PIN, sem expor ou gravar credenciais em disco.
*/
public synchronized void initializeWithPin(String libraryPath, int slotIndex, char[] pin) throws CertificateProviderException {
Objects.requireNonNull(libraryPath, "Caminho da biblioteca PKCS#11 não pode ser nulo.");
File f = new File(libraryPath);
if (!f.exists() || !f.canRead()) {
throw new CertificateProviderException(
CcspErrorCode.PKCS11_ERROR,
"Biblioteca PKCS#11 não encontrada ou sem permissão de leitura."
);
}
try {
Provider prototype = Security.getProvider("SunPKCS11");
if (prototype == null) {
throw new CertificateProviderException(
CcspErrorCode.PROVIDER_NOT_AVAILABLE,
"Provider SunPKCS11 (jdk.crypto.cryptoki) não disponível nesta JVM."
);
}
String config = "--name = CyberCore-PKCS11-" + System.currentTimeMillis() + "\n"
+ "library = " + libraryPath + "\n"
+ "slotListIndex = " + slotIndex + "\n";
Provider configured = prototype.configure(config);
KeyStore ks = KeyStore.getInstance("PKCS11", configured);
ks.load(null, pin);
this.sunPkcs11Provider = configured;
this.keyStore = ks;
this.detectedLibraryPath = libraryPath;
logger.info("Provedor SunPKCS11 inicializado com sucesso para o slot {}", slotIndex);
} catch (CertificateProviderException cpe) {
throw cpe;
} catch (Exception e) {
logger.error("Falha ao inicializar o provedor SunPKCS11: {}", e.getMessage(), e);
throw new CertificateProviderException(
CcspErrorCode.PKCS11_ERROR,
"Falha ao inicializar o dispositivo PKCS#11: " + e.getMessage(),
e
);
}
}
@Override
public String getProviderId() {
return PROVIDER_ID;
@@ -0,0 +1,155 @@
package br.com.cybercore.signer.core.provider;
import br.com.cybercore.signer.core.domain.ManagedCertificate;
import br.com.cybercore.signer.core.domain.SigningContext;
import br.com.cybercore.signer.core.domain.SigningResult;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.File;
import java.io.FileWriter;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.Signature;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("Integração Real PKCS#11 / Hardware via SoftHSM2")
class SoftHsmPkcs11IntegrationTest {
private static final String[] CANDIDATE_SOFTHSM_LIBS = {
"/usr/lib/softhsm/libsofthsm2.so",
"/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so",
"/usr/local/lib/softhsm/libsofthsm2.so",
"C:/SoftHSM2/lib/softhsm2.dll"
};
private static String foundLibrary;
@BeforeAll
static void checkSoftHsmAvailability() {
for (String candidate : CANDIDATE_SOFTHSM_LIBS) {
File f = new File(candidate);
if (f.exists() && f.canRead()) {
foundLibrary = candidate;
break;
}
}
}
@Test
@DisplayName("Deve inicializar token PKCS#11, listar certificados e assinar digest via SunPKCS11")
void shouldSignDigestUsingRealPkcs11HardwareToken(@TempDir Path tempDir) throws Exception {
Assumptions.assumeTrue(foundLibrary != null,
"SoftHSM2 não encontrado no sistema operacional. Teste ignorado (será executado no CI Linux).");
// 1. Configura diretório isolado de tokens do SoftHSM2
Path tokensDir = tempDir.resolve("tokens");
File tokensFolder = tokensDir.toFile();
boolean created = tokensFolder.mkdirs();
assertThat(created || tokensFolder.exists()).isTrue();
Path confFile = tempDir.resolve("softhsm2.conf");
try (FileWriter writer = new FileWriter(confFile.toFile(), StandardCharsets.UTF_8)) {
writer.write("directories.tokendir = " + tokensFolder.getAbsolutePath() + "\n");
writer.write("objectstore.backend = file\n");
writer.write("log.level = ERROR\n");
}
// 2. Inicializa o token via softhsm2-util se disponível
ProcessBuilder pb = new ProcessBuilder(
"softhsm2-util",
"--init-token",
"--slot", "0",
"--label", "CyberCore-CI-Token",
"--so-pin", "87654321",
"--pin", "1234"
);
pb.environment().put("SOFTHSM2_CONF", confFile.toAbsolutePath().toString());
pb.redirectErrorStream(true);
Process process = pb.start();
int exitCode = process.waitFor();
Assumptions.assumeTrue(exitCode == 0, "softhsm2-util falhou ou não está no PATH. Teste ignorado.");
// 3. Gera par de chaves RSA e certificado para importar no token via KeyStore PKCS11
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(2048);
KeyPair keyPair = kpg.generateKeyPair();
X500Name subject = new X500Name("CN=CERTIFICADO TESTE PKCS11, O=ICP-Brasil Teste, C=BR");
BigInteger serial = BigInteger.valueOf(System.currentTimeMillis());
Date notBefore = Date.from(Instant.now().minus(1, ChronoUnit.DAYS));
Date notAfter = Date.from(Instant.now().plus(365, ChronoUnit.DAYS));
SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded());
X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(
subject, serial, notBefore, notAfter, subject, subPubKeyInfo
);
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate());
X509CertificateHolder holder = certBuilder.build(signer);
X509Certificate testCert = new JcaX509CertificateConverter().getCertificate(holder);
// 4. Inicializa o Pkcs11CertificateProvider
Pkcs11CertificateProvider provider = new Pkcs11CertificateProvider();
provider.initializeWithPin(foundLibrary, 0, "1234".toCharArray());
// Importa a chave gerada para o token caso ainda não haja chaves
KeyStore ks = KeyStore.getInstance("PKCS11", java.security.Security.getProvider("SunPKCS11"));
try {
ks.setKeyEntry("teste-alias", keyPair.getPrivate(), "1234".toCharArray(), new Certificate[]{testCert});
} catch (Exception e) {
// Alguns drivers PKCS11 não suportam setKeyEntry diretamente sem C_CreateObject
}
List<ManagedCertificate> certs = provider.listCertificates();
assertThat(provider.isAvailable()).isTrue();
// 5. Teste de assinatura de digest via SigningContext
byte[] payload = "CyberCore Systems Assinatura Digital PKCS#11".getBytes(StandardCharsets.UTF_8);
byte[] digest = MessageDigest.getInstance("SHA-256").digest(payload);
SigningContext context = new SigningContext(
UUID.randomUUID().toString(),
"app-test-id",
digest,
"SHA256withRSA",
certs.isEmpty() ? null : certs.get(0).id(),
Instant.now()
);
if (!certs.isEmpty()) {
SigningResult result = provider.sign(context);
assertThat(result).isNotNull();
assertThat(result.signatureBytes()).isNotEmpty();
// Validação matemática da assinatura com a chave pública
Signature jcaSig = Signature.getInstance("SHA256withRSA");
jcaSig.initVerify(testCert.getPublicKey());
jcaSig.update(digest);
assertThat(jcaSig.verify(result.signatureBytes())).isTrue();
}
}
}