Two-step signatures - Rest PKI Core
With signature sessions, the user signs on a page hosted by Rest PKI Core. When the signature must happen somewhere else, use the two-step signature API instead:
- Your server asks Rest PKI Core to prepare the signature of a file with a certificate. Rest PKI Core validates the certificate and returns the hash to be signed (the to-sign hash) and the signature state.
- The certificate's private key signs the hash: in the user's browser, with Web PKI, or on your server.
- Your server sends the signature and the state to Rest PKI Core, which completes the signature and returns the signed document.
This covers two cases:
- Signing on your own page: the user signs with a certificate installed on their computer, on a page of your application, through Web PKI. Outside of localhost, Web PKI needs a license.
- Signing with a key held by your server: your application signs with its own certificate, such as a company certificate, without a user.
From this point on we'll describe calls to Rest PKI Core. Make sure you've read the Get Started article to be able to keep up. The application's API key needs a role that allows signatures, such as Worker (see Choosing roles).
Preparing the signature
On .NET, call IRestPkiService.PrepareSignatureAsync() with the file to sign, the signer's certificate and the signature options:
var fileReference = FileReference.FromFile(pdfPath, "document.pdf");
var certificateReference = CertificateReference.FromBytes(certificateContent);
var prepareSignatureResult = await restPkiService.PrepareSignatureAsync(fileReference, certificateReference, new PrepareSignatureOptions() {
SecurityContextId = securityContextId,
});
- The file is a
FileReference, built from a path, bytes, a stream or a URL (see Referencing files). - The certificate is a
CertificateReference:FromBytes()takes the certificate's encoding, such as the one Web PKI reads in the browser (decoded from Base64);FromX509Certificate2()takes a .NET certificate andFromPem()a PEM string. PrepareSignatureOptionssets theSecurityContextId, which decides which certificates are trusted (if omitted, the subscription's default security context or, without one, the instance's); theSignatureType(Pdf,CmsorXml); and the options of each type, such asPdfSignatureOptions.
The result has the hash to sign in ToSignHash (Value, with its digest algorithm in Algorithm) and the signature state in State, an opaque string that
Rest PKI Core needs to complete the signature. When Rest PKI Core refuses the file, for instance a corrupted PDF, the .NET client throws a
PrepareSignatureException.
On Java, call RestPkiService.startSignature():
PrepareSignatureRequest request = new PrepareSignatureRequest();
request.setSecurityContextId(securityContextId);
FileReferenceModel file = new FileReferenceModel();
file.setContent(Files.readAllBytes(pdfPath));
file.setName("document.pdf");
request.setFile(file);
CertificateReferenceModel certificate = new CertificateReferenceModel();
certificate.setContent(Base64.getDecoder().decode(certContent));
request.setCertificate(certificate);
request.setSignatureType(SignatureTypes.PDF);
PrepareSignatureResponse result = restPkiService.startSignature(request);
if (!Boolean.TRUE.equals(result.isSuccess())) {
// result.getFailure() says why, and result.getValidationResults() why the certificate was rejected
throw new Exception("The signature could not be prepared: " + result.getFailure());
}
The hash to sign is result.getToSign().getHash(), its digest algorithm result.getToSign().getSignatureAlgorithm().getDigestAlgorithmOid(), and the
state result.getState().
If calling the API directly, do:
POST {endpoint}/api/signature
{
"file": {
"content": "Base64EncodedBytes==",
"name": "document.pdf"
},
"certificate": {
"content": "Base64EncodedCertificate=="
},
"securityContextId": "...",
"signatureType": "Pdf"
}
When success is true, the response has the hash to sign in toSign.hash (Base64), its digest algorithm in toSign.signatureAlgorithm.digestAlgorithmOid,
the state in state and the signer's certificate in certificate. Otherwise, failure says why: PdfAlreadySigned, CorruptedPdf,
CertificateValidationFailed (with the reasons in validationResults), PasswordProtectedPdf, PdfAConversionFailed or PdfANonCompliant. The response
also repeats the hash in toSignHash, for older clients.
Signing the hash
On your own page, with Web PKI
The user's certificate never leaves their computer, so the signature takes two round trips between the page and your server:
- On the page, list the user's certificates with Web PKI's
listCertificates(). When the user chooses one, read its encoding withreadCertificate(thumbprint)and send it, in Base64, to your server. - Your server prepares the signature with that certificate (
CertificateReference.FromBytes()on .NET) and sends back to the page the hash to sign, its digest algorithm and the state. - The page signs the hash with Web PKI's
signHash()and sends the signature, in Base64, and the state to your server, which completes the signature.
pki.signHash({
thumbprint: thumbprint, // the certificate chosen by the user
hash: toSignHash, // the hash to sign, in Base64
digestAlgorithm: digestAlgorithm // the digest algorithm OID, such as 2.16.840.1.101.3.4.2.1 for SHA-256
}).success(function (signature) {
// Send the signature (in Base64) and the state to your server
});
The state is protected by Rest PKI Core, so you can keep it in a hidden field of the page between the two steps. For listCertificates(), readCertificate()
and signHash(), see Web PKI's Get started.
On your server
On .NET, sign the hash with the certificate's private key:
var certificate = new X509Certificate2(pfxPath, pfxPassword);
var certificateReference = CertificateReference.FromX509Certificate2(certificate);
var prepareSignatureResult = await restPkiService.PrepareSignatureAsync(fileReference, certificateReference, signatureOptions);
var signature = certificate.GetRSAPrivateKey().SignHash(
prepareSignatureResult.ToSignHash.Value,
prepareSignatureResult.ToSignHash.Algorithm.HashAlgorithmName,
RSASignaturePadding.Pkcs1
);
Completing the signature
On .NET, call IRestPkiService.CompleteSignatureAsync() with the state and the signature:
var signedDocument = await restPkiService.CompleteSignatureAsync(prepareSignatureResult.State, signature);
using (var stream = await signedDocument.SignedFile.OpenReadAsync()) {
// Store the signed file
}
The result is the signed Document, the same type the signature sessions return: SignedFile.OpenReadAsync() reads the signed
file, and SignedFile.Url is a temporary link to download it. If the signature or the certificate fails validation, for instance a revoked certificate, the
.NET client throws a ValidationException, whose ValidationResults say why.
On Java, call RestPkiService.completeSignature():
CompleteSignatureRequestV2 request = new CompleteSignatureRequestV2();
request.setState(state);
request.setSignature(Base64.getDecoder().decode(signature));
DocumentModel signedDocument = restPkiService.completeSignature(request);
try (InputStream signedContent = restPkiService.openRead(signedDocument.getSignedFile().getUrl())) {
// Store the signed file
}
If calling the API directly, do:
POST {endpoint}/api/signature/completion
{
"state": "...",
"signature": "Base64EncodedSignature=="
}
The response is the signed document's DocumentModel, the same model that GET api/documents/{id} returns.
Samples
- .NET, on your own page with Web PKI: PadesSignatureRestCoreController.cs
- .NET, with a server key: PadesServerKeyRestCoreController.cs
- Java, on your own page with Web PKI: PadesSignatureCoreController.java
- Node.js, on your own page with Web PKI: pades-signature-core.js