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 |
|---|---|---|---|
| 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 |
| Node.js | pki-express | 1 (1.16.2) | npm |
| PHP 5.5+ | lacuna/pki-express | 1 (1.17.0) | Packagist |
| Python | pkiexpress | 1 (1.10.4) | PyPI |
| Ruby | pki_express | 1 (1.3.2) | RubyGems |
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.
TODO(docs): apart from the Java PAdES walkthrough (how it works) and the PHP project map (PHP samples), no page documents these operations; the code below comes from the samples.
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.
// 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.
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.
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.
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.
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.
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.
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();
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.
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: