---
title: "Two-step signatures - Rest PKI Core"
description: "Sign with Rest PKI Core on your own page with Web PKI, or with a key held by your server: PrepareSignatureAsync returns the hash, CompleteSignatureAsync ends it."
canonical: "https://docs.lacunasoftware.com/en-us/articles/rest-pki/core/integration/two-step-signatures"
locale: "en"
product: "Rest PKI Core"
last_updated: "2026-10-12"
---

# Two-step signatures - Rest PKI Core

With [signature sessions](https://docs.lacunasoftware.com/en-us/articles/rest-pki/core/integration/signature-sessions), the user signs on a page hosted by [Rest PKI Core](https://docs.lacunasoftware.com/en-us/articles/rest-pki/core). When the signature must happen
somewhere else, use the two-step signature API instead:

1. 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**.
1. The certificate's private key signs the hash: in the user's browser, with [Web PKI](https://docs.lacunasoftware.com/en-us/articles/web-pki), or on your server.
1. 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](https://docs.lacunasoftware.com/en-us/articles/web-pki/licensing).
* **Signing with a key held by your server**: your application signs with its own certificate, such as a company certificate, without a user.

> **Tip**
>
> From this point on we'll describe calls to Rest PKI Core. Make sure you've read the [Get Started](https://docs.lacunasoftware.com/en-us/articles/rest-pki/core/integration/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](https://docs.lacunasoftware.com/en-us/articles/rest-pki/core/integration/get-started#roles)).

## Preparing the signature

On .NET, call `IRestPkiService.PrepareSignatureAsync()` with the file to sign, the signer's certificate and the signature options:

```cs
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](https://docs.lacunasoftware.com/en-us/articles/rest-pki/core/integration/file-referencing)).
* 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 and `FromPem()` a PEM string.
* `PrepareSignatureOptions` sets the `SecurityContextId`, which decides which certificates are trusted (if omitted, the subscription's default security
  context or, without one, the instance's); the `SignatureType` (`Pdf`, `Cms` or `Xml`); and the options of each type, such as `PdfSignatureOptions`.

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()`:

```java
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:

```text
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:

1. On the page, list the user's certificates with Web PKI's `listCertificates()`. When the user chooses one, read its encoding with
   `readCertificate(thumbprint)` and send it, in Base64, to your server.
1. Your server [prepares the signature](https://docs.lacunasoftware.com/en-us/articles/rest-pki/core/integration/two-step-signatures#preparing-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.
1. 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](https://docs.lacunasoftware.com/en-us/articles/rest-pki/core/integration/two-step-signatures#completing-the-signature).

```javascript
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](https://docs.lacunasoftware.com/en-us/articles/web-pki/get-started).

### On your server

On .NET, sign the hash with the certificate's private key:

```cs
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:

```cs
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](https://docs.lacunasoftware.com/en-us/articles/rest-pki/core/integration/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()`:

```java
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:

```text
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](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/dotnet/mvc/PkiSuiteAspNetMvcSample/Controllers/PadesSignatureRestCoreController.cs)
* .NET, with a server key: [PadesServerKeyRestCoreController.cs](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/dotnet/mvc/PkiSuiteAspNetMvcSample/Controllers/PadesServerKeyRestCoreController.cs)
* Java, on your own page with Web PKI: [PadesSignatureCoreController.java](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/java/springmvc/src/main/java/com/lacunasoftware/pkisuite/controller/PadesSignatureCoreController.java)
* Node.js, on your own page with Web PKI: [pades-signature-core.js](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/nodejs/expressmvc/routes/pades-signature-core.js)

**Demos and samples**

- Live demo: [Sign a file with Rest PKI Core](https://try.lacunasoftware.com/en/pki-suite/sign-a-file-rest-pki-core)
- Sign a PDF with Rest PKI Core: [.NET](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/dotnet/mvc/PkiSuiteAspNetMvcSample/Controllers/PadesSignatureRestCoreController.cs) · [Java](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/java/springmvc/src/main/java/com/lacunasoftware/pkisuite/controller/PadesSignatureCoreController.java) · [Node.js](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/nodejs/expressmvc/routes/pades-signature-core.js) · [PHP](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/php/plain/public/pades-signature-rest-core/index.php) · [Python](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/python/flask/sample/views/pades_signature_rest_core.py) · [Ruby](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/ruby/rails/app/controllers/pades_signature_rest_core_controller.rb)
- Sign multiple PDFs with Rest PKI Core: [Node.js](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/nodejs/expressmvc/routes/batch-pades-signature-core.js)
- Sign a PDF with a server key using Rest PKI Core: [.NET](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/dotnet/mvc/PkiSuiteAspNetMvcSample/Controllers/PadesServerKeyRestCoreController.cs)
