PKI Express quick reference
The essentials of PKI Express on one page, with links to the full pages.
What it is for
PKI Express creates CMS/CAdES, PDF/PAdES and XmlDSig/XAdES digital signatures from virtually any programming language, on your own Windows or Linux servers. You install it on the server and drive it through a client library for Java, Node.js, PHP, Python or Ruby; to sign with the user's certificate on a web page, it works together with Web PKI.
| Choose instead | When it fits better |
|---|---|
| PKI SDK | Your application is .NET: PKI SDK is a library for .NET applications, and the PKI guide recommends it for server signatures in .NET |
| Rest PKI Core | You would rather call a REST API than install software on your servers: use it as a service at core.pki.rest or as your own instance; it also hosts the page where the user signs |
| Web PKI | You need the browser side: listing the user's certificates and signing with them on a web page. It complements PKI Express rather than replacing it |
| Signer | You want to send documents to people and collect their signatures with a ready-made workflow, as SaaS or on-premises, instead of building the flow |
Install
Two parts: PKI Express itself, on each server that runs your application, and the client package for your language.
| Language | Package | Current major | Registry | Source code |
|---|---|---|---|---|
| PKI Express, Windows Server 2008 R2+ / Windows 7+ (needs .NET Framework 4.6.1) | pkie-1.38.0-x64.msi installer; x86 and zip packages on the setup page | 1 (1.38.0) | cdn.lacunasoftware.com | — |
| PKI Express, Linux (Ubuntu/Mint, RHEL, CentOS/Oracle Linux/Fedora) | pkie-1.38.0.tar.gz (setup) | 1 (1.38.0) | cdn.lacunasoftware.com | — |
| Java | com.lacunasoftware.pkiexpress:pki-express | 1 (1.23.1) | Maven Central | PkiExpressJava |
| Node.js | pki-express | 1 (1.16.2) | npm | PkiExpressNode |
| PHP 5.5+ | lacuna/pki-express | 1 (1.17.0) | Packagist | PkiExpressPhp |
| Python | pkiexpress | 1 (1.10.4) | PyPI | PkiExpressPython |
| Ruby | pki_express | 1 (1.3.2) | RubyGems | PkiExpressRuby |
The client libraries are open source, in the LacunaSoftware GitHub organization under the names above, with no hyphen. Each release
has a tag named after the package version, so a file of the version you use is at, for example,
https://raw.githubusercontent.com/LacunaSoftware/PkiExpressPhp/1.17.0/src/CommandException.php. The default branch is master for
Java and develop for the others.
On Linux, extract the package to /usr/share/pkie, link /usr/share/pkie/pkie into /usr/local/bin, set the log folder and run sudo pkie activate LacunaPkiLicense.config; the Ubuntu, RHEL and CentOS pages give the commands. To update, run the new installer (Windows) or replace the contents of /usr/share/pkie with the new package (Linux).
Configuration and authentication
License, no API key. PKI Express needs a license file, LacunaPkiLicense.config; ask Lacuna for a free trial license. Activate it once per machine: pkie activate on Linux (manual activation if the network activation fails), the PKI Express Configuration Manager (pkiemgr.exe, in the installation folder) on Windows. Your application does not read the license, and there is no service URL: the client library runs the PKI Express installed on the same machine.
Server settings are set with pkie config --set name=value (administrator privileges); pkie config --effective shows the values in use.
| Setting | Keys | Page |
|---|---|---|
| Log folder and minimum level | logDir, logLevel | Logging |
| Temporary folder (cache of CRLs and CA certificates) | tempDir | Temporary directory |
| Proxy | proxyHost, proxyPort, proxyUsername, proxyPassword | Proxy |
LAPP, to fetch CRLs and CA certificates through *.lacunasoftware.com (support must also authorize it) | lappEnabled | LAPP |
| Keys in Azure Key Vault | azureAppId, azureAppSecret, azureKeyVaultEndpoint | Azure |
| Cloud certificates (BirdID, NeoID, RemoteID, SafeID, VIDaaS, others) | trustServices:<service>:clientId, trustServices:<service>:clientSecret, trustServices:<service>:endpoint | Cloud certificates |
| Keys on PKCS#11 tokens and HSMs | pkcs11LibraryPath | Changelog 1.38.0 |
TODO(docs): pkcs11LibraryPath (PKI Express 1.38.0) has no configuration page; only the changelog names it, and neither the client libraries nor any published help text describe it.
In your code, every operation class inherits PkiExpressOperator, whose settings the samples apply in one helper, Util.setPkiDefaults(), called by every block below:
- what to trust: only ICP-Brasil certificates by default; add roots with
addTrustedRootSync(), or the Lacuna test root withtrustLacunaTestRoot(trusting custom certificates); timestampAuthority, aTimestampAuthorityfor policies that need a timestamp: the samples use REST PKI's,https://pki.rest/tsp/a402df41-8559-47b2-a05c-be555bf66310, with a REST PKI access token (setOAuthTokenAuthentication()); user and password, and mutual SSL, also work (timestamp authority);offline, to run without network access.
Each class also takes an optional PkiExpressConfig (PKI Express folder, temporary folder, transfer data folder): without it, the libraries look for PKI Express in the default Windows installation folders, and run pkie from the PATH on Linux. On Linux, only the Java library can run PKI Express 1.31.0 or later from a custom folder; see client library settings.
Core operations
The blocks are trimmed from the Node.js samples (nodejs/expressmvc/routes/ in PkiSuiteSamples), the language with the most PKI Express samples. Policy names come from StandardSignaturePolicies and differ from Rest PKI's: see the equivalence table.
Each operation has its own page: Sign a PDF (PAdES), with the server-key variant; Sign with CAdES; Sign an XML document; Validate signatures; Timestamp a PDF; Printer-friendly version; Certificate authentication; Generate a key and a PKCS #12 file.
Sign a PDF with the user's certificate
PAdES with the key on the user's computer, in two server steps: PadesSignatureStarter.start() returns the hash that Web PKI signs in the browser (signHash()), and SignatureFinisher.complete() writes the signed PDF. Step by step in Java: how it works; signature stamp: visual representation. Details: Sign a PDF (PAdES).
// POST /start: certContent is the user's certificate, read by Web PKI
const signatureStarter = new PadesSignatureStarter();
Util.setPkiDefaults(signatureStarter);
signatureStarter.signaturePolicy = StandardSignaturePolicies.PADES_BASIC_WITH_LTV;
signatureStarter.setPdfToSignFromPathSync(StorageMock.getDataPath(fileId));
signatureStarter.setCertificateFromBase64Sync(certContent);
signatureStarter.start().then((response) => { /* toSignHash, digestAlgorithm, transferFile */ });
// POST /complete: signature is the result of Web PKI's signHash()
const signatureFinisher = new SignatureFinisher();
Util.setPkiDefaults(signatureFinisher);
signatureFinisher.setFileToSignFromPathSync(StorageMock.getDataPath(fileId));
signatureFinisher.setTransferFileFromPathSync(transferFile);
signatureFinisher.signature = signature;
signatureFinisher.outputFile = path.join(APP_ROOT, 'app-data', outputFile);
signatureFinisher.complete(true).then((result) => { /* signed PDF at outputFile */ });
Sign a PDF with a server key
One step, no browser: PadesSigner.sign() signs with a PKCS #12 file on the server. Details: Sign with a server key.
const signer = new PadesSigner();
Util.setPkiDefaults(signer);
signer.signaturePolicy = StandardSignaturePolicies.PADES_BASIC_WITH_LTV;
await signer.setPdfToSignFromPath(StorageMock.getDataPath(fileId));
await signer.setPkcs12FromPath(StorageMock.getSamplePkcs12Path());
signer.certPassword = '1234';
signer.outputFile = path.join(APP_ROOT, 'app-data', outputFile);
let signerCert = await signer.sign(true);
Sign a file with CAdES
CadesSignatureStarter follows the same start/complete flow, finished by SignatureFinisher (output .p7s); if the file is already a CMS, PKI Express co-signs it. Details: Sign with CAdES.
const signatureStarter = new CadesSignatureStarter();
Util.setPkiDefaults(signatureStarter);
signatureStarter.signaturePolicy = StandardSignaturePolicies.PKI_BRAZIL_CADES_ADR_BASICA;
signatureStarter.setFileToSignFromPathSync(StorageMock.getDataPath(fileId));
signatureStarter.setCertificateFromBase64Sync(certContent);
signatureStarter.start().then((response) => { /* same as the PDF flow */ });
Sign an XML document
XmlSigner.sign() signs with a server key; with the user's certificate, use XmlSignatureStarter and SignatureFinisher as in the PDF flow. Details: Sign an XML document.
const signer = new XmlSigner();
Util.setPkiDefaults(signer);
signer.signaturePolicy = StandardSignaturePolicies.XADES_BES;
await signer.setXmlToSignFromPath(StorageMock.getSampleDocPath(SampleDocs.SAMPLE_XML));
await signer.setPkcs12FromPath(StorageMock.getSamplePkcs12Path());
signer.certPassword = '1234';
signer.outputFile = path.join(APP_ROOT, 'app-data', outputFile);
let signerCert = await signer.sign(true);
Validate a signed PDF
PadesSignatureExplorer.open() returns the file's signatures and, with validate = true, validates them; CadesSignatureExplorer does the same for CMS/CAdES. Details: Validate signatures.
const sigExplorer = new PadesSignatureExplorer();
Util.setPkiDefaults(sigExplorer);
sigExplorer.validate = true;
sigExplorer.setSignatureFileFromPathSync(filePath);
sigExplorer.signaturePolicy = StandardSignaturePolicies.PADES_BASIC;
sigExplorer.open().then((r) => res.render('open-pades-express', { signature: r }));
Timestamp a PDF
PadesTimestamper.stamp() adds a timestamp to a PDF, using the operator's timestamp authority. Details: Timestamp a PDF.
const stamper = new PadesTimestamper();
Util.setPkiDefaults(stamper);
stamper.setPdfFromPath(pdfPath).then(() => {
stamper.setOutputFilePath(outputFilePath);
stamper.stamp().then(() => { /* timestamped PDF at outputFilePath */ });
});
Make a printer-friendly version
After opening the signatures with PadesSignatureExplorer, PdfMarker.apply() stamps texts and images built with PdfHelper on the PDF, such as the signers' names and a verification code. Details: Printer-friendly version.
const pdfMarker = new PdfMarker();
Util.setPkiDefaults(pdfMarker);
pdfMarker.setFileFromPathSync(pdfPath);
const pdf = new PdfHelper();
pdfMarker.marks.push(pdf.mark().onAllPages()
.onContainer(pdf.container().width(1).anchorRight(1).height(1).anchorBottom(1))
.addElement(pdf.imageElement().withOpacity(75).withImageContent(Util.getIcpBrasilLogoContent(), 'image/png')));
pdfMarker.outputFile = path.join(APP_ROOT, 'app-data', outputFile);
pdfMarker.apply();
Generate a key and a PKCS #12 file
KeyGenerator.generate() makes an RSA key (1024, 2048 or 4096 bits; formats json, blob, xml) and, with genCsr, a PEM CSR for your CA;
Pkcs12Generator.generate() then combines the key with the issued certificate into a PKCS #12 file to sign with. Ruby has neither class.
Details, PHP code and each language's names: Generate a key and a PKCS #12 file.
const keyGenerator = new KeyGenerator({ keySize: 2048, genCsr: true });
const genKey = await keyGenerator.generate(); // genKey.key, genKey.csr
// ...the CA issues the certificate from genKey.csr...
const pkcs12Generator = new Pkcs12Generator({ key: genKey.key });
pkcs12Generator.setCertFileFromPathSync(certPath);
const result = await pkcs12Generator.generate(password); // result.pfx.contentRaw
Authenticate with a certificate
Authentication.start() returns a nonce and digest algorithm for Web PKI to sign; complete() checks the signature and validates the certificate. Details: Certificate authentication.
const auth = new Authentication();
Util.setPkiDefaults(auth);
auth.setNonce(nonce); // from auth.start(), signed in the browser by Web PKI
await auth.setCertificateFromBase64(certContent);
auth.setSignature(signature);
const result = await auth.complete();
if (result.validationResults.isValid()) { const userCert = result.certificate; }
Errors
Each PKI Express operation ends with a return code, 00 - Ok or an error from 01 to 43; signatures, authentications and inspections with validation also report validation errors such as CertificateChainRootNotTrusted. Full list: return codes.
In Node.js, a failed operation rejects its promise with a CommandError whose code is the return code (ValidationError for code 14, InstallationNotFoundError when PKI Express is not found); the samples pass it to next(err). PHP throws CommandException ($e->returnCode), Ruby PkiExpress::CommandError (e.code.exitstatus); Java and Python throw a plain exception with the output in the message: see return codes in the client libraries.
| Error | Meaning | What to do |
|---|---|---|
17 - NotActivated | PKI Express is not activated on this machine | Activate it with the license (setup) |
14 - ValidationFailed with CertificateChainRootNotTrusted | The certificate's root is not trusted; by default, only ICP-Brasil is | Trust the root: trustLacunaTestRoot or addTrustedRootSync() (trusting custom certificates) |
CertificateRevocationStatusUnknown | PKI Express could not access the certificate's CRLs | Configure the proxy or LAPP |
32 - TimestampOptionsRequired | No timestamp authority given, and the policy requires a timestamp | Set the operator's timestampAuthority (timestamp authority) |
Test certificates and credentials
- Test certificates: PKCS #12 files from the Lacuna Test PKI, password
1234; never trust this PKI in production. - PKI Express accepts them only after you trust the Lacuna test root:
trustLacunaTestRoot(Node.js, Java, PHP) ortrust_lacuna_test_root(Python, Ruby). - License: request a free trial license and activate it as in Configuration and authentication.
Try it
Run the demos, or start from a sample in your language: