test(pkcs11): add softhsm2 integration test with assumptions and configure ci pipeline
This commit is contained in:
@@ -32,6 +32,13 @@ jobs:
|
||||
- name: Conceder permissão de execução ao Maven Wrapper
|
||||
run: chmod +x ./mvnw
|
||||
|
||||
- name: Instalar SoftHSM2 para testes de hardware PKCS#11
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y softhsm2
|
||||
sudo mkdir -p /var/lib/softhsm/tokens
|
||||
sudo chmod 777 /var/lib/softhsm/tokens
|
||||
|
||||
- name: Executar verificação Maven (Compilação, Testes, JaCoCo, SpotBugs, ArchUnit)
|
||||
run: ./mvnw -B clean verify
|
||||
|
||||
|
||||
+49
-1
@@ -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;
|
||||
|
||||
+155
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user