Part II - Proprietary APIs and Custom Providers
In contexts with higher security requirements the private keys usually are not available in local key stores but instead in external devices which are hardened against attempts to copy the private keys (e.g. smart cards or HSMs) or even in remote servers. To sign data with such private keys you have to use an API provided by the manufacturer of the device or the operator of the service respectively.
Such APIs may follow some standard. E.g.
PKCS#11 drivers - the PKCS#11 standard defines a platform-independent API to cryptographic tokens.
Java security providers / MS Windows CryptoAPI or CNG key storage providers - the Java and MS Windows platforms offer their specific security modules which are easy to access from Java and .NET respectively. These modules allow plugging in external devices and remote services via a provider architecture.
But these APIs may also be completely proprietary.
The manufacturer/operator actually might offer access via multiple APIs, e.g., they might offer both a platform independent PKCS#11 driver and platform specific providers. Be aware though, that this does not mean that you can freely switch back and forth between them. In particular individual keys or information associated with them might only be accessible via the API which was used to create or add them in the first place. So if you plan to implement an application on multiple platforms in parallel, you should use the same platform independent API in all implementations, not the (probably easier-to-use) respective platform specific ones.
In this part we take a look at the platform specific providers and proprietary APIs. The next part will focus on PKCS#11 drivers.
Signing With External Services (proprietary APIs)
In particular in the context of external signing services you often find proprietary web APIs. Some of them want the original data to sign as input, some want a hash of that data as-is, some want that hash packaged in a specific data structure. Some of them return naked signature bytes, some a full signature container. Some merely require a normal username-password authorization, some require a two-factor authorization. Etc.
Thus, one can hardly provide any general rules here. Merely that based on the signature structure returned by the API in question you need to implement either IExternalSignature
or IExternalSignatureContainer
. And that you should study the documentation of the API to know exactly what to provide and what to expect in return.
As an example you might want to take a look at the technical note “Using iText and AWS KMS to digitally sign a PDF document” which explains how to use the Key Management Service (KMS) of the Amazon Web Services (AWS) to sign PDFs.
Another example is the usage of Singapore's National Digital identity (NDI) project to create PDF signatures with iText. You can find example code and documentation here. (Beware, that example code is based on an earlier version of NDI; thus, it most likely won’t work out of the box with the current NDI.)
Signing With Custom JCA/JCE Security Providers (Java)
If a security token manufacturer or a signing service operator provides a Java security provider for signing, you’re essentially in the situation of section “Signing with Java JCA/JCE keys” of Part I except that you may have to do some configuration beforehand (please consult the documentation of the token or service).
Utimaco, for example, provides a Java security provider to access their HSMs. Using this provider one can sign PDFs as follows:
String config = "Device = 3001@192.168.178.49\n" +
"DefaultUser = JCE\nKeyGroup = JCE";
char[] pin = "5678".toCharArray();
CryptoServerProvider provider = new CryptoServerProvider(new ByteArrayInputStream(config.getBytes()));
Security.removeProvider(provider.getName());
Security.addProvider(provider);
KeyStore ks = KeyStore.getInstance("CryptoServer", provider);
ks.load(null, pin);
Enumeration<String> aliases = ks.aliases();
String alias = aliases.nextElement();
PrivateKey pk = (PrivateKey) ks.getKey(alias, pin);
Certificate[] chain = ks.getCertificateChain(alias);
IExternalSignature signature = new PrivateKeySignature(pk, "SHA256", provider.getName());
try ( PdfReader pdfReader = new PdfReader(SOURCE);
OutputStream result = new FileOutputStream(RESULT)) {
PdfSigner pdfSigner = new PdfSigner(pdfReader, result, new StampingProperties().useAppendMode());
IExternalDigest externalDigest = new BouncyCastleDigest();
pdfSigner.signDetached(externalDigest, signature, chain, null, null, null, 0, CryptoStandard.CMS);
}
(Full code in test method testSignSimpleGeneric
in ...jce.utimaco.TestSignSimple on GitHub)
Unfortunately the situation sometimes is more difficult because a custom Java Security Provider may require unusual parameters differing from those the PrivateKeySignature
class uses. In such a situation work-arounds are needed.
For example, the Utimaco JCE CryptoServerProvider
(at least in the version I have used) for RSASSA-PSS signatures expects the signature algorithm be given without any indication that the PSS scheme is used, e.g. as "SHA256withRSA" instead of "SHA256withRSASSA-PSS" or "SHA256withRSAandMGF1". It then uses the PSS scheme if one sets a PSSParameterSpec
as AlgorithmParameterSpec
parameter. And in that PSSParameterSpec
it expects the hash algorithm to be given with a dash, e.g. "SHA-256" instead of "SHA256". The iText PrivateKeySignature
, though, uses "RSASSA-PSS" in the signature algorithm and "SHA256" in the PSS parameters. This way the CMS signature container parameters all are correct, merely the signing fails. As a work-around, therefore, we can use a PrivateKeySignature
and override its sign method like this:
String digestName = "SHA-256";
IExternalSignature signature = new PrivateKeySignature(pk, digestName, "RSASSA-PSS", provider.getName(), RSASSAPSSMechanismParams.createForDigestAlgorithm(digestName)) {
@Override
public byte[] sign(byte[] message) throws GeneralSecurityException {
Signature sig = Signature.getInstance("SHA256withRSA", provider);
MGF1ParameterSpec mgf1Spec = new MGF1ParameterSpec(digestName);
PSSParameterSpec spec = new PSSParameterSpec(digestName, "MGF1", mgf1Spec, 32, RSASSAPSSMechanismParams.DEFAULT_TRAILER_FIELD);
sig.setParameter(spec);
sig.initSign(pk);
sig.update(message);
return sig.sign();
}
};
(Full code in method testSignSimpleGenericPss
in ...jce.utimaco.TestSignSimple on GitHub)
In iText version 7 RSASSA-PSS was not supported using IExternalSignature
implementations, so an IExternalSignatureContainer
implementation was needed, for example using BouncyCastle CMS builder classes directly. Even here, though, the quirk of the Utimaco JCE crypto provider requires a work around to be used because the default BouncyCastle classes for this job use the signature algorithm name "SHA256WITHRSAANDMGF1" instead of the "SHA256WITHRSA" required by the Utimaco provider.
Thus, we create a custom IExternalSignatureContainer
implementation which replaces the BouncyCastle class doing that to and fro transformation with its own one that doesn’t. The BouncyCastle class in question is a ContentSigner generated by JcaContentSignerBuilder. So we build our own ContentSigner:
public UtimacoJceSignatureContainer with(String algorithm, AlgorithmParameterSpec paramSpec) {
if (paramSpec instanceof PSSParameterSpec) {
PSSParameterSpec pssSpec = (PSSParameterSpec)paramSpec;
algorithmIdentifier = new AlgorithmIdentifier(PKCSObjectIdentifiers.id_RSASSA_PSS, createPSSParams(pssSpec));
} else {
algorithmIdentifier = new DefaultSignatureAlgorithmIdentifierFinder().find(algorithm);
}
contentSigner = new ContentSigner() {
private ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
@Override
public byte[] getSignature() {
try {
Signature sig = Signature.getInstance(algorithm, provider);
sig.initSign(pk);
if (paramSpec != null)
sig.setParameter(paramSpec);
sig.update(outputStream.toByteArray());
return sig.sign();
} catch (Exception e) {
if (e instanceof RuntimeException) throw (RuntimeException)e;
throw new RuntimeException(e);
} finally {
outputStream.reset();
}
}
@Override
public OutputStream getOutputStream() {
return outputStream;
}
@Override
public AlgorithmIdentifier getAlgorithmIdentifier() {
return algorithmIdentifier;
}
};
return this;
}
(From UtimacoJceSignatureContainer.java
; full code here on GitHub)
If you set the algorithm in this way, you can sign with RSASSA-PSS like this:
String config = "Device = 3001@192.168.178.49\n"
+ "DefaultUser = JCE\n"
+ "KeyGroup = JCE";
CryptoServerProvider provider = new CryptoServerProvider(new ByteArrayInputStream(config.getBytes()));
Security.removeProvider(provider.getName());
Security.addProvider(provider);
UtimacoJceSignatureContainer signature =
new UtimacoJceSignatureContainer(provider, PdfName.Adbe_pkcs7_detached)
.select(null, "5678".toCharArray())
.with("SHA256withRSA", new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1));
try (PdfReader pdfReader = new PdfReader(SOURCE);
OutputStream result = new FileOutputStream(RESULT))
{
PdfSigner pdfSigner = new PdfSigner(pdfReader, result, new StampingProperties().useAppendMode());
pdfSigner.signExternalContainer(signature, 8192);
}
(Full code in test method testSignSimpleUtimacoJceSignatureContainerRsaSsaPss
in ...jce.utimaco.TestSignSimple on github)
Signing With Custom CNG Integrations (.NET)
If a security token manufacturer or a signing service operator provides a .NET CryptoAPI or CNG key storage provider for signing, you’re essentially in the situation of the “Signing with .NET CryptoAPI/CNG Keys” section of Part I except that you may have to do some configuration beforehand (please consult the documentation of the token or service).
Utimaco, for example, provides a CNG key storage provider to access their HSMs. Let’s assume you have associated a certificate in your personal certificate store with a Utimaco CNG key using "Utimaco CNG Signing Test
" as the subject common name.
With the X509Certificate2Signature
class from Part I one can then sign PDFs like this:
X509Certificate2 certificate;
using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser, OpenFlags.ReadOnly))
{
X509Certificate2Collection certificates = store.Certificates;
X509Certificate2Collection signingcertificates = certificates.Find(X509FindType.FindBySubjectName,
"Utimaco CNG Signing Test", false);
certificate = signingcertificates[0];
}
Org.BouncyCastle.X509.X509Certificate bcCertificate =
new Org.BouncyCastle.X509.X509Certificate(X509CertificateStructure.GetInstance(certificate.RawData));
Org.BouncyCastle.X509.X509Certificate[] chain = { bcCertificate };
using (PdfReader pdfReader = new PdfReader(SOURCE))
using (FileStream result = File.Create(RESULT))
{
PdfSigner pdfSigner = new PdfSigner(pdfReader, result);
X509Certificate2Signature signature = new X509Certificate2Signature(certificate, "SHA512");
pdfSigner.SignDetached(signature, chain, null, null, null, 0, CryptoStandard.CMS);
}
(Full test code in method TestCngSignRsaSimpleGeneric
in CngUtimaco/TestSignSimple.cs on GitHub)
Using the Utimaco provider with the X509Certificate2SignatureContainer
class from Part I you can sign PDFs like this:
X509Certificate2 certificate;
using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser, OpenFlags.ReadOnly))
{
X509Certificate2Collection certificates = store.Certificates;
X509Certificate2Collection signingcertificates =
certificates.Find(X509FindType.FindBySubjectName, "Utimaco CNG Signing Test", false);
certificate = signingcertificates[0];
}
X509Certificate2SignatureContainer signature =
new X509Certificate2SignatureContainer(certificate, signer => {
signer.DigestAlgorithm = Oid.FromFriendlyName("SHA512", OidGroup.HashAlgorithm);
signer.SignedAttributes.Add(new Pkcs9SigningTime());
});
using (PdfReader pdfReader = new PdfReader(SOURCE))
using (FileStream result = File.Create(RESULT))
{
PdfSigner pdfSigner = new PdfSigner(pdfReader, result, new StampingProperties().UseAppendMode());
pdfSigner.SignExternalContainer(signature, 8192);
}
(Full test code in method TestCngSignEcdsaSimpleGenericContainer
in CngUtimaco/TestSignSimple.cs on GitHub)