Skip to content

Examples of using it

Risto Seene edited this page Mar 8, 2024 · 55 revisions

How to use it

Here is a set of examples for creating and validating containers and signatures.

Maven

You can use the library as a Maven dependency from the Maven Central (http://mvnrepository.com/artifact/org.digidoc4j/digidoc4j)

<dependency>
	<groupId>org.digidoc4j</groupId>
	<artifactId>digidoc4j</artifactId>
	<version>5.x.x</version>
</dependency>

Library dependencies

You can download the library (digidoc4j.jar) and all its dependencies from the Releases page.

  • digidoc4j-library.zip contains all the library dependencies.
  • digidoc4j-util.zip contains the Command Line Utility Tool. It is a separate application for handling signatures from a command line. Do NOT add digidoc4j-util.jar to your application classpath.

Simple external signing example (e.g. signing in Web)

This is a typical example of signing in the Web where the user provides a certificate to be used for signing and then signs the container by entering a pin code. It is an example of performing two-step external signing process (since version 2.0.0):

  1. A signature dataset containing the digest of the file(s) to be signed is generated (DataToSign object) from the signature parameters and container content, and the dataset is then signed in an “external” service (e.g. using Mobile-ID or Smart-ID).
  2. The signature value returned from the external service is then used to create a fully valid signature (using dataToSign.finalize method).
//Create a container with a text file to be signed
Container container = ContainerBuilder
		.aContainer()
		.withDataFile("testFiles/legal_contract_1.txt", "text/plain")
		.build();

//Get the certificate (with a browser plugin, for example)
X509Certificate signingCert = getSignerCertSomewhere();

//Get the data to be signed by the user
DataToSign dataToSign = SignatureBuilder
		.aSignature(container)
		.withSigningCertificate(signingCert)
		.withSignatureDigestAlgorithm(DigestAlgorithm.SHA256)
		.buildDataToSign();

//Data to sign contains the signature dataset including the digest of the file(s) that should be signed
byte[] signableData = dataToSign.getDataToSign();

//Sign the signature dataset
byte[] signatureValue = signDataSomewhereRemotely(signableData, DigestAlgorithm.SHA256);

//Finalize the signature with OCSP response and timestamp
Signature signature = dataToSign.finalize(signatureValue);

//Add signature to the container
container.addSignature(signature);

//Save the container as a .asice file
container.saveAsFile("test-container.asice");

So what are getSignerCertSomewhere and signDataSomewhereRemotely methods? getSignerCertSomewhere must be implemented to fetch a user certificate used in the signing process and signDataSomewhereRemotely method must be implemented to create a signature over the digest of the signable data (via Web plugin, for example). Take a look at how to integrate DigiDoc4j with Web browsers.

Simple signing example with a signature token

This example uses a private key stored on a disk to sign two text files.

The private key is stored in the file called "signout.p12" which is protected with password "test".

//Create a container with two text files to be signed
Container container = ContainerBuilder
		.aContainer()
		.withDataFile("testFiles/legal_contract_1.txt", "text/plain")
		.withDataFile("testFiles/legal_contract_2.txt", "text/plain")
		.build();

//Using the private key stored in the "signout.p12" file with password "test"
String privateKeyPath = "testFiles/signout.p12";
char[] password = "test".toCharArray();
PKCS12SignatureToken signatureToken = new PKCS12SignatureToken(privateKeyPath, password);

//Create a signature
Signature signature = SignatureBuilder
		.aSignature(container)
		.withSignatureToken(signatureToken)
		.invokeSigning();

//Add the signature to the container
container.addSignature(signature);

//Save the container as a .asice file
container.saveAsFile("test-container.asice");

Validating a container

It is possible to validate a container to see if the signatures are valid and the container is intact. Full container validation starts validating signatures in multiple threads so it's much faster than validating signatures one after another.

// Open an existing container from the file "test-container.asice"
Container container = ContainerOpener
		.open("test-container.asice");

// Validate the container
ContainerValidationResult result = container.validate();

//Check if the container is valid
boolean isContainerValid = result.isValid();

//Get the validation errors and warnings
List<DigiDoc4JException> validationErrors = result.getErrors();
List<DigiDoc4JException> validationWarnings = result.getWarnings();
List<DigiDoc4JException> containerErrors = result.getContainerErrors();//Container format errors
List<DigiDoc4JException> containerWarnings = result.getContainerWarnings();

//See the validation report in XML (for debugging only - DO NOT BASE YOUR APPLICATION LOGIC ON IT)
String validationReport = result.getReport();

More detailed examples

Creating a new container

Let's create a new container with some data files. We use ContainerBuilder for creating new containers. We provide the container builder with all the necessary data and then invoke build() method on it that creates the container. By default, ASiC-E container is created.

NB! There is known issue on concurrent container handling with ContainerBuilder. For more details see here

// We can provide configuration. "Configuration.Mode.TEST" should be used for testing.
// Use only a single configuration object for all the containers so operation times would be faster.
Configuration configuration = Configuration.of(Configuration.Mode.TEST);

// Creating an ASiC-E container
Container container = ContainerBuilder
		.aContainer(Container.DocumentType.ASICE)  // Specifying container type. Default is ASICE.
		.withConfiguration(configuration)  // Using our configuration
		.withDataFile("testFiles/legal_contract_1.txt", "text/plain")  // Adding a document from a hard drive
		.withDataFile(inputStream, "legal_contract_2.txt", "text/plain")  // Adding a document from a stream
		.build();

Opening an existing container

Open a container located in testFiles/test-container.asice

Container container = ContainerOpener
		.open("testFiles/test-container.asice");

Opening a container with more parameters

Open a DDoc container located in testFiles/test-container.ddoc using our configuration

// Testing configuration
// Use only a single configuration object for all the containers so operation times would be faster.
Configuration configuration = Configuration.of(Configuration.Mode.TEST);

// Open container from a file
Container container = ContainerOpener
		.open("testFiles/test-container.ddoc", configuration);

Opening container from an input stream

// Reading a file to a stream
InputStream inputStream = FileUtils.openInputStream(new File("test-container.asice"));

// Open container from a stream
Container container = ContainerOpener
		.open(inputStream, true); // With big files support enabled

Getting data to sign

When we need to sign a container externally (in the Web for example) then we need to get the signature dataset of the container to be signed.

First we need to get the certificate that is used in signing the document. That certificate is used in calculating the signable data (containing the digest of the signable file(s)) of the container to be signed.

We also have to specify which digest algorithm is used (SHA-256, SHA-512 etc). Default is SHA-256.

//Select the certificate with a browser plugin, for example
X509Certificate signingCert = getSignerCertSomewhere();
DataToSign dataToSign = SignatureBuilder
		.aSignature(container)
		.withSigningCertificate(signingCert)
		.withSignatureDigestAlgorithm(DigestAlgorithm.SHA256)
		.buildDataToSign();

// Get the data to be signed
byte[] signableData = dataToSign.getDataToSign();
DigestAlgorithm digestAlgorithm = dataToSign.getDigestAlgorithm(); // Will return SHA256 in this example

Adding signature role

Here we are creating a signature that is signed in the city of San Pedro, in the state of Puerto Vallarta, with postal code 13456 and in the country of Val Verde. The signer has two roles: Manager and Suspicious Fisherman.

SignatureBuilder builder = SignatureBuilder
		.aSignature(container)
		.withCity("San Pedro")
		.withStateOrProvince("Puerto Vallarta")
		.withPostalCode("13456")
		.withCountry("Val Verde")
		.withRoles("Manager", "Suspicious Fisherman");

Adding technical parameters for a signature

Here we specify datafile digest algorithm to be SHA-256, signature digest algorithm to be SHA-512, signature profile to be LT (Time-stamp and OCSP), signature ID to be S0 and X509 certificate used in the signing process.

The possible signature profiles are:

  • LT - Time-stamp and OCSP confirmation
  • LTA - Archive timestamp, same as XAdES LTA (Long Term Archive time-stamp)
  • B_BES - no profile
// Signature certificate used in the signing process
X509Certificate signerCert = getSigningCert();

// Create data to sign
DataToSign dataToSign = SignatureBuilder
		.aSignature(container)
		.withDataFileDigestAlgorithm(DigestAlgorithm.SHA256)
		.withSignatureDigestAlgorithm(DigestAlgorithm.SHA512)
		.withSignatureProfile(SignatureProfile.LT)
		.withSignatureId("S0") // We do not recommend setting signature Id by yourself (it should be unique as generated by default).
		.withSigningCertificate(signerCert)
		.buildDataToSign();

//Create the signature
Signature signature = signDataSomewhereRemotely(dataToSign);

//Add the signature to the container
container.addSignature(signature);

Invoking signing with signature token

//Using the private key stored in the "signout.p12" file with password "test"
String privateKeyPath = "testFiles/signout.p12";
char[] password = "test".toCharArray();
PKCS12SignatureToken signatureToken = new PKCS12SignatureToken(privateKeyPath, password);

//Create a signature
Signature signature = SignatureBuilder
		.aSignature(container)
		.withDataFileDigestAlgorithm(DigestAlgorithm.SHA512) // Datafile digest algorithm is SHA-512
		.withSignatureDigestAlgorithm(DigestAlgorithm.SHA256) // Signature digest algorithm is SHA-256
		.withSignatureProfile(SignatureProfile.LT) // Signature profile is TS and OCSP based
		.withSignatureToken(signatureToken) // Use signature token to sign with private key
		.invokeSigning(); // Creates a signature with the private key

//Add the signature to the container
container.addSignature(signature);

Signing with a smart card or other hardware module (using PKCS#11)

It is possible to sign directly with a smart card, USB token, HSM or other hardware module using PKCS#11 interface.

// Using PKCS#11 module from /usr/local/lib/opensc-pkcs11.so (depends on your installed smart card or hardware token library)
// Using 22975 as pin/password
// Using slot index 1 (depends on the hardware token).
// When the client computer has only one smartcard reader then for Estonian ID-cards
// there are usually two slots available. However, this depends on reader/driver:
// slot 0 - for authentication (PIN1);
// slot 1 - for signing (PIN2)
PKCS11SignatureToken signatureToken = new PKCS11SignatureToken("/usr/local/lib/opensc-pkcs11.so", "22975".toCharArray(), 1);

// Create a signature
Signature signature = SignatureBuilder
		.aSignature(container)
		.withSignatureToken(signatureToken)
		.invokeSigning();

Validation details of a single signature

It is possible to see validation details of a single signature in a container. It is best to do a full container validation before accessing signature validation details for better performance by invoking container.validate(). Full container validation starts validating signatures in multiple threads so it's much faster than validating signatures one after another.

// Get a signature from a container
Signature signature = container.getSignatures().get(0);

// Get the signature validation result. If the container has already been validated, then an existing validation result is returned, otherwise a full validation is done on the signature.
SignatureValidationResult result = (SignatureValidationResult) signature.validateSignature();

// Check if the signature is valid
boolean isSignatureValid = result.isValid();

// See the signature validation errors and warnings
List<DigiDoc4JException> validationErrors = result.getErrors();
List<DigiDoc4JException> validationWarnings = result.getWarnings();

Signature details

It is possible to see signature details and information about the signer.

// Signature creation time confirmed by OCSP or TimeStamp authority.
Date trustedSigningTime = signature.getTrustedSigningTime();

// Signature creation time in the signer's computer (unofficial signing time)
Date claimedSigningTime = signature.getClaimedSigningTime();

// Signer info: city, state, postal code, country and signer roles
String city = signature.getCity();
String stateOrProvince = signature.getStateOrProvince();
String postalCode = signature.getPostalCode();
String country = signature.getCountryName();
List<String> signerRoles = signature.getSignerRoles();

// Signature profile: LT, LTA etc.
SignatureProfile signatureProfile = signature.getProfile();

// The full (XAdES) signature in bytes containing OCSP, TimeStamp etc
byte[] adESSignature = signature.getAdESSignature();

// Signer's certificate information: ID Code, first name, last name, country code etc.
X509Cert certificate = signature.getSigningCertificate();
String signerIdCode = certificate.getSubjectName(SERIALNUMBER);
String signerFirstName = certificate.getSubjectName(GIVENNAME);
String signerLastName = certificate.getSubjectName(SURNAME);
String signerCountryCode = certificate.getSubjectName(C);

Using configuration

It is possible to configure DigiDoc4j via the Configuration object. A Configuration object can be altered via the API or by using a custom configuration file.

It is a good idea to use only a single configuration object for all the containers, so the operation times would be faster. For example, TSL is cached with configuration and TSL loading is a time-consuming operation.

// Getting the singleton configuration object
// This is the default configuration object used in all containers
Configuration configuration = Configuration.getInstance();

In addition to the default (production mode) configuration, it is also possible to use a default testing configuration. This will instantiate the configuration with default test values (using test OCSP and Timestamp server URLs, test TSL URL, etc.) and will make it possible to sign and validate containers with test certificates. Default TSL, OCSP and Timestamp server URLs in specific versions of DigiDoc4j and for both production and testing configurations can be seen here, in respective nested classes called Production and Test.

One way to use the default testing configuration, is by setting the system environment variable digidoc4j.mode to TEST (digidoc4j.mode=TEST).

// Set the environment to the test mode
System.setProperty("digidoc4j.mode", "TEST");
// The default configuration is instantiated in the test mode
Configuration configuration = Configuration.getInstance();

It is also possible to create the test (or production) configuration directly without setting the digidoc4j.mode environment variable. Make sure to use only one configuration object for all the containers for better performance.

// Testing configuration
Configuration configuration = Configuration.of(Configuration.Mode.TEST);
// Production configuration
Configuration configuration = Configuration.of(Configuration.Mode.PROD);

If you prefer to create a configuration object yourself, then make sure to pass it on to the ContainerBuilder with the withConfiguration method when creating and opening containers.

// Test configuration. Use only a single configuration object for all the containers so operation times would be faster.
Configuration configuration = Configuration.of(Configuration.Mode.TEST);

// Creating an ASiC-E container with the test configuration
Container container = ContainerBuilder
		.aContainer()
		.withConfiguration(configuration)  // Using our configuration
		.build();

Pre-loading TSL

TSL takes a long time to load (5-15 seconds, depending on the weather). EU TSL is the EU Trusted Lists of Certificates. It is possible to load TSL separately (e.g. in application startup) by calling

configuration.getTSL().refresh();

This triggers TSL download and later operations (validations, signature creations) would not need to download TSL.

TSL is loaded lazily by default - only when necessary. Creating new containers or opening containers without signatures does not trigger TSL download. TSL is downloaded once a day by default. Make sure to use only one instance of the Configuration object. TSL is stored within the Configuration object memory.

Filtering trusted countries

It is possible accept signatures (and certificates) only from particular countries by filtering trusted territories. Only the TSL (and certificates) from those countries are then downloaded and others are skipped.

For example, it is possible to trust signatures only from these three countries: Estonia, Latvia and France, and skip all other countries. The filtering can be done in Java code or in YAML configuration.

When using TEST mode, Digidoc4j uses TEST LOTL which uses country code EE_T for Estonia. If you have set specific countries to be allowed then signature finalization will fail in TEST mode.

// Filtering trusted territories in Java
configuration.setTrustedTerritories("EE", "LV", "FR");

or

# Filtering trusted territories in YAML configuration file
TRUSTED_TERRITORIES: EE, LV, FR

Configuring custom OCSP responders (for other than Estonian certificates issued by SK ID Solutions AS)

If you would like to create signatures using certificates which are not issued by any issuer recognized by the SK ID Solutions AS OCSP responder (http://ocsp.sk.ee/), which is used by default (when AIA OCSP usage is disabled or an OCSP URL is not present in the signing certificate), then you have to configure DigiDoc4j to use appropriate OCSP responders for such certificates.

The preferred method is to enable AIA OCSP usage (enabled by default since DigiDoc4j 5.3.0), which by default fetches OCSP URL-s from certificates. For cases when OCSP URL is not present in a certificate, a list of default AIA OCSP-s can be configured by mapping each specific issuer CN to a specific URL. For more information, see the link above.

For fallback purposes or in case the AIA OCSP usage cannot be enabled, the default OCSP source must be configured. This can be done via digidoc4j.yaml:

OCSP_SOURCE: http://custom.ocsp/

Or programmatically through Configuration object:

configuration.setOcspSource("http://custom.ocsp/");

In case support for multiple different OCSP responders is needed, you can create multiple configuration objects: one configuration object instance for each OCSP responder. In order to decide which configuration object instance to use for signing, you can, for example, determine the country of the person who is signing by looking at the signer certificate:

//Get the country code of the signer
X509Certificate signerCert;
X509Cert cert = new X509Cert(signerCert);
String countryCode = cert.getSubjectName(X509Cert.SubjectName.C);

Saving Container and DataToSign objects during signature creation

In some applications it may be necessary to save the state of the objects (Container and DataToSign objects) when creating a signature in multiple steps. If you need such functionality, then it is possible to save the objects on a disk or in a serialized form during the signature creation process before finalizing the signature. This functionality is optional and should be used only when necessary.

// Let's say you have a container with a legal_contract_1.txt data file you would like to sign
Container container = ContainerBuilder
		.aContainer()
		.withDataFile("testFiles/legal_contract_1.txt", "text/plain")
		.build();

// You get a signer's certificate you'd like to use for signing from somewhere
X509Certificate signingCert = getSignerCertSomewhere();

// You build data to be signed
DataToSign dataToSign = SignatureBuilder
		.aSignature(container)
		.withSigningCertificate(signingCert)
		.buildDataToSign();

// You get the signable data (containing the digest(s) of the signable file(s)), over whose digest the signature will be calculated
byte[] signableData = dataToSign.getDataToSign();

// In this point you would like to save all the data
// and finish the signature creation process later
// when the user (signer) has finished signing the digest

// You can save the container on disk for later usage. The container doesn't contain any signatures yet.
// Or you could just serialize the container object
container.saveAsFile("test-container.asice");

// You can save the DataToSign object on disk for later usage. Or you could just serialize it.
// You can use Apache Commons SerializationUtils for serialization or use the built-in Helper class.
Helper.serialize(dataToSign, "data-to-sign.bin");

// Here finally the user (signer) has signed the signature dataset and has provided the signature value
byte[] signatureValue = signDataSomewhereRemotely(signableData, DigestAlgorithm.SHA256);

// In this point you would like to continue the signature creation process to finalize the signature

// You can open the DataToSign object from the disk again
dataToSign = Helper.deserializer("data-to-sign.bin");

// You can open the container from the disk
container = ContainerBuilder
		.aContainer()
		.fromExistingFile("test-container.asice")
		.build();

// You can finish the signature creation process
Signature signature = dataToSign.finalize(signatureValue);
container.addSignature(signature);

// And save the container with the signature on the disk
container.saveAsFile("test-container.asice");

The example above demonstrates how to save Container and DataToSign objects on a disk before signature is finalized and how to restore the objects later for finalizing the signature.

In the example above

  1. A Container and DataToSign objects are created.
  2. A signature dataset (containing the digest of file(s) to be signed) is calculated.
  3. The container and dataToSign objects are saved on disk (or just serialized).
  4. The signature dataset is signed by the user.
  5. The container and dataToSign objects state is restored from the disk (or deserialized).
  6. The signature creation is finalized.

Since version 3.2.0 it is no longer necessary to store/serialize entire DataToSign object and signature finalization can be executed through SignatureFinalizer. SignatureFinalizer can be built through SignatureFinalizerBuilder (from Container and SignatureParameters). Even though DataToSign serialization was also optimized in 3.2.0, now all you need to store/serialize is SignatureParameters (addition to the Container itself).

// Let's say you have a container with a legal_contract_1.txt data file you would like to sign
Container container = ContainerBuilder
		.aContainer()
		.withDataFile("testFiles/legal_contract_1.txt", "text/plain")
		.build();

// You get a signer's certificate you'd like to use for signing from somewhere
X509Certificate signingCert = getSignerCertSomewhere();

// You build data to be signed
DataToSign dataToSign = SignatureBuilder
		.aSignature(container)
		.withSigningCertificate(signingCert)
		.buildDataToSign();

DigestAlgorithm digestAlgorithm = dataToSign.getDigestAlgorithm();
byte[] signableData = dataToSign.getDataToSign();
// Pass DataToSign object or just signableData and digestAlgorithm
// to user (signer) for external signing

// At this point you would like to save all the data and finish the signature creation
// process later when the user (signer) has finished signing the digest.

// Saving container to the disk.
// You can also serialize the container object, but that is less effective.
container.saveAsFile("test-container.asice");

// Serialize signature parameters. Here Apache Commons SerializationUtils class is used.
byte[] signatureParametersSerialized = SerializationUtils.serialize(dataToSign.getSignatureParameters());

// Here finally the user (signer) has signed the signature dataset and has provided the signature value
byte[] signatureValue = signDataSomewhereRemotely(signableData, digestAlgorithm);

// At this point you would like to continue the signature creation process by finalizing the signature

// You can open the container from the disk
container = ContainerBuilder
		.aContainer()
		.fromExistingFile("test-container.asice")
		.build();

// Deserialize signature parameters
SignatureParameters signatureParameters = SerializationUtils.deserialize(signatureParametersSerialized);

// Build signature finalizer from loaded container and deserialized signature parameters.
SignatureFinalizer signatureFinalizer = SignatureFinalizerBuilder.aFinalizer(container, signatureParameters);
Signature signature = signatureFinalizer.finalizeSignature(signatureValue);

// Add signature to the container
container.addSignature(signature);

// And save the container with the signature on the disk
container.saveAsFile("test-container.asice");

Quirks of deserialization

NB: On deserialization of any objects created by a security provider (e.g. an implementation of X509Certificate) may result in a different post-deserialization implementation of that object than it was before. On deserialization of such objects, Java uses the first applicable security provider from its list of registered providers.

For example, Digidoc4J creates X509Certificate objects implemented by BouncyCastle, but after deserialization these objects may end up, for example, as Sun's implementations, if BouncyCastle is not registered as the first security provider. This may cause unexpected behaviour in some edge cases when serialization and deserialization of certificates is involved (e.g. certificates loaded into TSL, contained in a Configuration object used by DataToSign).

In order to use, for example, BouncyCastle as the primary security provider, it must be registered as the first provider. This only works if BouncyCastle security provider has not been registered yet or immediately after it has been unregistered.

Using custom container implementation

It is possible to add new container implementations in an easy way. You might want to add support for signing PDF containers or extend the existing BDOC implementation.

Let's say we have our own container implementation in TestContainer.class for container types TEST-FORMAT

public class TestContainer implements Container {

  // Required constructors
  public TestContainer() { ... } // Creating an empty container
  public TestContainer(Configuration configuration) { ... } // Creating an empty container with configuration
  public TestContainer(String filePath) { ... } // Opening existing container from file
  public TestContainer(String filePath, Configuration configuration) { ... } // Opening existing container from file and with configuration
  public TestContainer(InputStream openFromStream) { ... } // Opening existing container from stream
  public TestContainer(InputStream openFromStream, Configuration configuration) { ... } // Opening existing container from stream and with configuration

  // Get type must return the correct type
  public String getType() {
    return "TEST-FORMAT";
  }

  // Other container methods implemented below
  ...
}

Then we have to register the new container type

// Register TestContainer.class to be opened with TEST-FORMAT container types
ContainerBuilder.setContainerImplementation("TEST-FORMAT", TestContainer.class);

We have to create a signature builder class for creating signatures for that type of containers.

public class TestSignatureBuilder extends SignatureBuilder {

  // Calculate the digest to be signed of a container
  public DataToSign buildDataToSign() throws SignerCertificateRequiredException, ContainerWithoutFilesException { ... }

  // This method is called when invokeSigning() method is called for signing with a signature token.
  // It should create a new signature using a SignatureToken object that is provided.
  protected Signature invokeSigningProcess() { ... }
}

Then we have to register the new signature builder

SignatureBuilder.setSignatureBuilderForContainerType("TEST-FORMAT", TestSignatureBuilder.class);

Here is an example of using the custom container implementation.

// Set TestContainer class to be used for TEST-FORMAT container types
ContainerBuilder.setContainerImplementation("TEST-FORMAT", TestContainer.class);
Container container = ContainerBuilder
		.aContainer("TEST-FORMAT")
		.withDataFile("testFiles/legal_contract_1.txt", "text/plain")
		.build();

//Get the certificate (with a browser plugin, for example)
X509Certificate signingCert = getSignerCertSomewhere();

// Set TestSignatureBuilder class to be used for TEST-FORMAT container types
SignatureBuilder.setSignatureBuilderForContainerType("TEST-FORMAT", TestSignatureBuilder.class);
//Get the data to be signed by the user
DataToSign dataToSign = SignatureBuilder
		.aSignature(container)
		.withSigningCertificate(signingCert)
		.withSignatureDigestAlgorithm(DigestAlgorithm.SHA256)
		.buildDataToSign();

//Data to sign contains the signature dataset (including digest of file(s) that should be signed)
byte[] signableData = dataToSign.getDataToSign();
byte[] signatureValue = signDataSomewhereRemotely(signableData, DigestAlgorithm.SHA256);

//Finalize the signature
Signature signature = dataToSign.finalize(signatureValue);

//Add signature to the container
container.addSignature(signature);

Detached XAdES (containerless) signature handling.

This is a possibility to create and validate signatures (not containers!) without providing the data file itself (due to confidentiality, etc). You need to specify the data file name, it's contents' digest, digest algorithm and mime type. If created signature is later added to container (which already have signatures) then given DigestDataFiles' mime types must match with the mime types used in existing signatures.

In DigiDoc4J 3.2.0 and earlier versions, the mime type can be specified in the following way:

DigestDataFile digestDataFile = new DigestDataFile("test.txt", DigestAlgorithm.SHA256, digest);
digestDataFile.setMediaType("text/plain");

Starting from DigiDoc4J version 3.3.0, the preferred way is to specify the mime type via the constructor. Starting from DigiDoc4J version 5.0.0, an instance of DigestDataFile cannot be created without specifying the mime type via its constructor:

DigestDataFile digestDataFile = new DigestDataFile("test.txt", DigestAlgorithm.SHA256, digest, "text/plain");

Here is an example of creating detached XAdES signature with signature token from scratch:

byte[] digest = MessageDigest.getInstance("SHA-256").digest("test".getBytes());
DigestDataFile digestDataFile = new DigestDataFile("test.txt", DigestAlgorithm.SHA256, digest, "text/plain");

Configuration configuration = Configuration.of(Configuration.Mode.TEST);

Signature signature = DetachedXadesSignatureBuilder
    .withConfiguration(configuration)
    .withDataFile(digestDataFile)
    .withSignatureProfile(SignatureProfile.LT)
    .withSignatureToken(pkcs12EccSignatureToken)
    .invokeSigning();

And signing externally:

byte[] digest = MessageDigest.getInstance("SHA-256").digest("test".getBytes());
DigestDataFile digestDataFile = new DigestDataFile("test.txt", DigestAlgorithm.SHA256, digest, "text/plain");

Configuration configuration = Configuration.of(Configuration.Mode.TEST);
X509Certificate signingCert = getSignerCertSomewhere();

DataToSign dataToSign = DetachedXadesSignatureBuilder.withConfiguration(configuration)
    .withDataFile(digestDataFile)
    .withSigningCertificate(signingCert)
    .buildDataToSign();

// sign the data
byte[] signatureValue = signDigestSomewhereRemotely(dataToSign.getDataToSign(), dataToSign.getDigestAlgorithm());

// Finalize the signature with OCSP response and timestamp
Signature signature = dataToSign.finalize(signatureValue);

You can get the xml bytes of the signature like this:

byte[] signatureXmlBytes = signature.getAdESSignature();

You can also read a existing signature into object from xml:

// Get the existing detached XAdES signature
byte[] xadesSignature = getSignatureXmlFromSomeWhere();

// Construct the digest-based data file object from original file that was signed with the above-mentioned signature
DigestDataFile digestDataFile = new DigestDataFile(getFileName(), DigestAlgorithm.SHA256, getFileDigest(), "text/plain");

Configuration configuration = Configuration.of(Configuration.Mode.TEST);

Signature signature = DetachedXadesSignatureBuilder
    .withConfiguration(configuration)
    .withDataFile(digestDataFile)
    .openAdESSignature(xadesSignature);

Once you've constructed the signature object, you can validate the signature like this:

boolean valid = signature.validateSignature().isValid();