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
|
- name: Conceder permissão de execução ao Maven Wrapper
|
||||||
run: chmod +x ./mvnw
|
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)
|
- name: Executar verificação Maven (Compilação, Testes, JaCoCo, SpotBugs, ArchUnit)
|
||||||
run: ./mvnw -B clean verify
|
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/aetpkss1.dll", // SafeSign AET
|
||||||
"C:/Windows/System32/acospkcs11.dll", // ACO / Giesecke & Devrient
|
"C:/Windows/System32/acospkcs11.dll", // ACO / Giesecke & Devrient
|
||||||
"C:/Windows/System32/opensc-pkcs11.dll",// OpenSC Windows
|
"C:/Windows/System32/opensc-pkcs11.dll",// OpenSC Windows
|
||||||
|
"C:/SoftHSM2/lib/softhsm2.dll", // SoftHSM2 Windows
|
||||||
// Linux
|
// Linux
|
||||||
"/usr/lib/x86_64-linux-gnu/opensc-pkcs11.so",
|
"/usr/lib/x86_64-linux-gnu/opensc-pkcs11.so",
|
||||||
"/usr/lib/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;
|
private Provider sunPkcs11Provider;
|
||||||
@@ -79,6 +83,50 @@ public class Pkcs11CertificateProvider implements CertificateProvider {
|
|||||||
this.keyStore = keyStore;
|
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
|
@Override
|
||||||
public String getProviderId() {
|
public String getProviderId() {
|
||||||
return PROVIDER_ID;
|
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