Examples of using it

naare edited this page Aug 10, 2018 · 32 revisions

How to use it

Here is a simple example of creating and signing a container with a private key and a password.

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>1.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 doing two step external signing process (since version 2.0.0):

  1. A signature dataset with digest of file 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 via DigiDocService).
  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 with digest of file that should be signed
byte[] signatureToSign = dataToSign.getDataToSign();

//Sign the signature dataset with digest of file
byte[] signatureValue = signDigestSomewhereRemotely(signatureToSign, DigestAlgorithm.SHA256);

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

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

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

So what are getSignerCertSomewhere and signDigestSomewhereRemotely methods? getSignerCertSomewhere must be implemented to fetch a user certificate used in the signing process and signDigestSomewhereRemotely method must be implemented to get a signature of the dataset with digest of file (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 .bdoc file
container.saveAsFile("test-container.bdoc");

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 = ContainerBuilder.
  aContainer().
  fromExistingFile("test-container.asice").
  build();

// Validate the container
ValidationResult 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

//See the validation report in XML (for debugging only - DO NOT USE 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 (and opening existing ones). We provide the container builder with all the necessary data and then invoke build() method on it that creates the container.

// 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 = new Configuration(Configuration.Mode.TEST);

// Creating a BDOC container
Container container = ContainerBuilder.
    aContainer("BDOC").  // Specifying container type: "BDOC" or "DDOC". Default is BDOC.
    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.bdoc

Container container = ContainerBuilder.
    aContainer().  //Default is BDOC
    fromExistingFile("testFiles/test-container.bdoc").
    build();

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 = new Configuration(Configuration.Mode.TEST);

// Open container from a file
Container container = ContainerBuilder.
    aContainer("DDOC").  // Container type is DDoc
    withConfiguration(configuration).  // Using our configuration
    fromExistingFile("testFiles/test-container.ddoc").
    build();

Opening container from an input stream

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

// Open container from a stream
Container container = ContainerBuilder.
    aContainer("BDOC").  // Container type is BDoc
    fromStream(inputStream).
    build();

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 data (with digest of files) of the container to be signed.

We also have to specify which digest algorithm is used (SHA-1, SHA-256 etc). Default is SHA-256 for BDoc containers and DDoc containers allow only SHA-1.

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

// Use that data for providing the signature
byte[] signatureToSign = dataToSign.getDataToSign();
DigestAlgorithm digestAlgorithm = dataToSign.getDigestAlgorithm(); // Will return SHA256 in this example

Adding signature role

Here we are creating a signature that is signed on 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 the digest algorithm to be SHA-256, signature profile to be LT_TM (Time-Mark), signature ID to be S0 and X509 certificate used in the signing process.

The possible signature profiles are

  • LT - Time-stamp and OCSP confirmation
  • LT_TM - Time-mark, similar to LT
  • 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();

SignatureBuilder builder = SignatureBuilder.
    aSignature(container).
    withSignatureDigestAlgorithm(DigestAlgorithm.SHA256).
    withSignatureProfile(SignatureProfile.LT_TM).
    withSignatureId("S0").
    withSigningCertificate(signerCert);

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

Determining signature algorithm

The default signature digest algorithm is SHA-256 which is a pretty good and secure option.

Older Estonian ID cards support only SHA-224 and would fail with SHA-256. If you are using an Estonian ID card, then it is helpful to determine digest algorithm supported by the card.

A handy method to use for determining the digest algorithm supported by the ID card is TokenAlgorithmSupport.determineSignatureDigestAlgorithm. This method looks for the certificate and returns SHA-224 if it belongs to an older Estonian ID card, otherwise it returns SHA-256.

X509Certificate certificate = testSignatureToken.getCertificate();
DigestAlgorithm digestAlgorithm = TokenAlgorithmSupport.determineSignatureDigestAlgorithm(certificate);

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 testSignatureToken = new PKCS12SignatureToken(privateKeyPath, password);

//Create a signature
Signature signature = SignatureBuilder.
  aSignature(container).
  withSignatureDigestAlgorithm(DigestAlgorithm.SHA256). // Digest algorithm is SHA-256
  withSignatureProfile(SignatureProfile.LT_TM). // Signature profile is Time-Mark
  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: 
// 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 = 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 (ASIC-E), LT_TM (BDOC) 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 specify configuration parameters using a Configuration object.

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 singelton configuration object
// This is the default configuration object used in all containers
Configuration configuration = Configuration.getInstance();

It is possible to use a testing configuration by setting the system environment variable digidoc4j.mode with the value TEST (digidoc4j.mode=TEST). This will instantiate the default configuration with the 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.

// 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 = new Configuration(Configuration.Mode.TEST);
// Production configuration
Configuration configuration = new Configuration(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 = new Configuration(Configuration.Mode.TEST);

// Creating a BDOC container with the test configuration
Container container = ContainerBuilder.
    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. Looking at container and signature details 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.

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

or

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

Signing with Finnish, Latvian or Lithuanian cards

If you would like to create signatures with Finnish, Latvian or Lithuanian tokens, then you have to change the OCSP responder URL. Certification Centre (Sertifitseerimiskeskus) provides a proxy OCSP responder service which proxies Finnish, Latvian and Lithuanian OCSP responders and allows creation of signatures with tokens from those countries. The proxy OCSP responder URL is http://ocsp.sk.ee/_proxy.

Estonian OCSP responder is used by default.

// Use the proxy OCSP responder
configuration.setOcspSource("http://ocsp.sk.ee/_proxy");

You can create multiple configuration objects for different OCSP responders: one configuration object instance for Estonian OCSP responder (default) and one configuration instance for proxy OCSP responder.

You can 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 a signature dataset with digest of file to be signed
byte[] signatureToSign = 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.bdoc");

// 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 digest and has provided the signature value
byte[] signatureValue = signDigestSomewhereRemotely(signatureToSign, 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.bdoc").
    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.bdoc");

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 with digest of file 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.

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 with digest of file that should be signed
byte[] signatureToSign = dataToSign.getDataToSign();
byte[] signatureValue = signDigestSomewhereRemotely(signatureToSign, DigestAlgorithm.SHA256);

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

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

Detached XadES (containerless) signature handling.

NB! this feature is currently available in develop branch only!

This is a possibility to create and validate signatures (not containers!) without providing the data file itself (due confidentiality, etc). You need to specify only the name, contents' digest and digest algorithm of the file and you're good to go.

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);

Configuration configuration = new Configuration(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);

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

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

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

// Finalize the signature with OCSP response and timestamp (or timemark)
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());

Configuration configuration = new Configuration(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();
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.