-
Notifications
You must be signed in to change notification settings - Fork 39
Examples of using it
Here is a set of examples for creating and validating containers and signatures.
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>6.x.x</version>
</dependency>
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 adddigidoc4j-util.jar
to your application classpath.
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):
- 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). - 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.
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");
It is possible to add timestamp tokens (time assertion files) to ASiC-S containers. An ASiC-S container can contain multiple time assertions, each covering the data file of the container and previous time assertions.
// Create or load an ASiC-S container
Container container = ...;
// Create a timestamp token
Timestamp timestamp = TimestampBuilder
.aTimestamp(container)
.invokeTimestamping();
// Add the timestamp token to the container
container.addTimestamp(timestamp);
// Save the container as a .asics file
container.saveAsFile("test-container.asics");
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; do not affect the result of isValid()
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();
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();
Open a container located in testFiles/test-container.asice
Container container = ContainerOpener
.open("testFiles/test-container.asice");
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);
// 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
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
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");
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)
- T - Time-stamp only
- B_BES - Basic 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);
//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);
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();
The most common reason to extend a signature is to maintain its long term availability and integrity.
This can be done by extending an existing LT
signature to LTA
profile, or by adding an additional LTA
archive
timestamp to an existing LTA
signature.
Extending all signatures in an ASiC container to LTA
profile can be done as follows:
container.extendSignatureProfile(SignatureProfile.LTA);
When extending the whole container to LTA
profile, the following must be taken into account:
- All signatures are extended, including the ones that already have
LTA
profile (additional archive timestamp is taken). - Separate archive timestamp is taken for each individual signature.
- Extension of any signature could fail for various reasons, disrupting the whole extension process.
- NB: It is advisable to validate signatures before trying to extend them, and only extend valid signatures!
For extending specific signatures in a container with multiple signatures, Container
method
extendSignatureProfile(SignatureProfile, List<Signature>)
can be used.
New LTA archive timestamps use the digest algorithm configured in the Configuration
of the Container
(either via
setArchiveTimestampDigestAlgorithm
API method or via the ARCHIVE_TIMESTAMP_DIGEST_ALGORITHM
YAML configuration file
parameter).
If not configured in the Configuration
, SHA-512 is used by default.
New LTA archive timestamps use the timestamp service URL configured in the Configuration
of the container (either via
setTspSourceForArchiveTimestamps
API method or via the TSP_SOURCE_FOR_ARCHIVE_TIMESTAMPS
YAML configuration file
parameter).
If not configured in the Configuration
, defaults to the same URL that is used for signature timestamps (for more
information, see here).
As extending the signatures involves making (possibly paid) requests to external services, it is useful to validate whether the signatures can be extended at all before actually trying to extend them.
This can be done for ASiC containers by using one of these two methods in AsicContainer
class:
public Map<String, DigiDoc4JException> getExtensionValidationErrors(SignatureProfile targetProfile)
public Map<String, DigiDoc4JException> getExtensionValidationErrors(SignatureProfile targetProfile, List<Signature> signaturesToExtend)
The first one checks all the signatures in the container and the second one can be used for checking only some specific signatures in the container.
If the validation succeeds, the returned Map is empty. Otherwise, there is an entry for each signature that failed
the validation. Map's key is the unique ID of the signature (returned by Signature.getUniqueId()
method) and
value contains the thrown exception. The exceptions that originate from DSS are wrapped inside DigiDoc4JException
,
while the exceptions thrown by DigiDoc4j itself are already of type DigiDoc4JException
or any of its subtypes.
Checking the extendability of all the signatures in the container and extending only the valid ones:
List<Signature> signatures = container.getSignatures();
List<Signature> extendableSignatures = new ArrayList<>();
// Validate and save exceptions to map
Map<String, DigiDoc4JException> validationErrors = ((AsicContainer) container)
.getExtensionValidationErrors(SignatureProfile.LTA, signatures);
// Find the signatures that can be extended and save them into a separate List
for (Signature signature : signatures) {
if (validationErrors.containsKey(signature.getUniqueId())) {
log.warn("Signature {} cannot be extended: {}",
signature.getUniqueId(),
validationErrors.get(signature.getUniqueId()).getMessage()
);
} else {
extendableSignatures.add(signature);
}
}
// Extend the signatures that passed the validation
if (extendableSignatures.isEmpty()) {
throw new Exception("Error: None of the signatures can be extended");
} else {
container.extendSignatureProfile(SignatureProfile.LTA, extendableSignatures);
}
Checking the extendability of a specific signature in the container, known by its unique ID:
// Find the specific signature from the container
Signature signature = container
.getSignatures()
.stream()
.filter(signature -> "SPECIFIC-UNIQUE-ID".equals(signature.getUniqueId()))
.findFirst()
.get();
// Validate and save exceptions to map
Map<String, DigiDoc4JException> validationErrors = ((AsicContainer) container)
.getExtensionValidationErrors(SignatureProfile.LTA, Collections.singletonList(signature);
// Check the validation result and extend the signature, if possible
if (!validationErrors.isEmpty()) {
throw new Exception("Error: the following signatures cannot be extended:" + String.join(",", validationErrors.keySet()));
} else {
container.extendSignatureProfile(SignatureProfile.LTA);
}
// Create a timestamp token
Timestamp timestamp = TimestampBuilder
.aTimestamp(container)
.withReferenceDigestAlgorithm(DigestAlgorithm.SHA384) // Reference digest algorithm is SHA-384
.withTimestampDigestAlgorithm(DigestAlgorithm.SHA256) // Timestamp digest algorithm is SHA-256
.withTspSource("http://timestamp.service/ts") // Use the specified timestamp service URL
.invokeTimestamping(); // Creates a new timestamp token for the specified container
// Add the timestamp token to the container
container.addTimestamp(timestamp);
Starting from the second timestamp token, each time assertion file in a container is accompanied by an ASiCArchiveManifest file, which contains digests of previous time assertion files and their manifests, as well as the digest of the data file of the container. These digests are calculated using the reference digest algorithm.
If not specified via the builder, reference digest algorithm defaults to the value configured in the Configuration
of the container (either via setArchiveTimestampReferenceDigestAlgorithm
API method or via the
ARCHIVE_TIMESTAMP_REFERENCE_DIGEST_ALGORITHM
YAML configuration file parameter).
If not configured in the Configuration
, the value of timestamp digest algorithm is used.
If not specified via the builder, timestamp digest algorithm defaults to the value configured in the Configuration
of the container (either via setArchiveTimestampDigestAlgorithm
API method or via the
ARCHIVE_TIMESTAMP_DIGEST_ALGORITHM
YAML configuration file parameter).
If not configured in the Configuration
, SHA-512 is used by default.
If not specified via the builder, TSP source defaults to the value configured in the Configuration
of the
container (either via setTspSourceForArchiveTimestamps
API method or via the TSP_SOURCE_FOR_ARCHIVE_TIMESTAMPS
YAML
configuration file parameter).
If not configured in the Configuration
, defaults to the same URL that is used for signature timestamps (for more
information, see here).
In case it is not possible or feasible to extend signatures of an existing container for the purpose of maintaining their long term availability and integrity, the whole container can instead be wrapped into an ASiC-S container and timestamped.
CompositeContainerBuilder
allows to conveniently wrap and timestamp existing container objects:
CompositeContainer nestingContainer = CompositeContainerBuilder
.fromContainer(containerToBeNested, "container-name.asice")
.buildTimestamped(timestampBuilder -> {});
... existing container files:
CompositeContainer nestingContainer = CompositeContainerBuilder
.fromContainerFile("testFiles/container-name.asice")
.withConfiguration(configuration)
.buildTimestamped(timestampBuilder -> {});
... or otherwise serialized containers:
CompositeContainer nestingContainer = CompositeContainerBuilder
.fromContainerStream(containerInputStream, "container-name.asice")
.withConfiguration(configuration)
.buildTimestamped(timestampBuilder -> {});
NB: When wrapping previously existing containers (containers which have already been serialized) without the need
to modify them before wrapping, it is recommended to use either fromContainerFile
or fromContainerStream
method if
byte-wise equivalence to the original serialized container needs to be preserved.
The method fromContainer
depends on the serialization mechanism of specific container implementations for producing
the data file of the wrapping container - when parsing a serialized container into a Container
object and then
re-serializing it, the result might not be byte-wise identical to the original serialized form of the container.
If needed, additional timestamping parameters can be specified as described here:
.buildTimestamped(timestampBuilder -> timestampBuilder
.withReferenceDigestAlgorithm(DigestAlgorithm.SHA384)
.withTimestampDigestAlgorithm(DigestAlgorithm.SHA256)
.withTspSource("http://timestamp.service/ts")
);
CompositeContainerBuilder
returns CompositeContainer
which is an extension of regular Container
, providing
additional API methods for accessing the contents of its nested container.
NB: After a container has been nested into another container, its contents cannot be changed anymore!
A timestamped composite container is an ASiC-S container that contains another container as its data file. DigiDoc4j recognizes an ASiC-S container as a composite container if all the following conditions are met:
- The container does not contain any signatures.
- The container contains at least one timestamp token (time assertion file).
- The container contains exactly one data file.
- The data file of the container is an ASiC, BDOC or DDOC container.
When opening an ASiC-S container in DigiDoc4j, it will be automatically parsed as a composite container if it meets all the above conditions. When parsing a composite container, only one level of nesting is recognized.
Implementations of composite containers implement
CompositeContainer
interface.
// Open an existing container
Container container = ContainerOpener.open(...);
// In case of a composite container, cast it to access additional API methods
if (container instanceof CompositeContainer) {
CompositeContainer compositeContainer = (CompositeContainer) container;
// Query the type of the nested container
String type = compositeContainer.getNestedContainerType();
// Query the data files of the nested container
List<DataFile> dataFiles = compositeContainer.getNestedContainerDataFiles();
// Query the signatures of the nested container
List<Signature> signatures = compositeContainer.getNestedContainerSignatures();
// Query the timestamp tokens of the nested container
List<Timestamp> timestamps = compositeContainer.getNestedContainerTimestamps();
}
Validating a composite container, triggers validation of the nesting container as well as the nested container. The contents of the nested container will be validated against the creation time of the earliest valid timestamp token that covers the nested container.
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();
Starting from DigiDoc4j version 6.0.0-RC.1, the validation results of individual signatures can also be accessed from the validation result of the whole container:
// Validate the container and obtain its validation result
ContainerValidationResult containerValidationResult = container.validate();
// Acquire the unique ID of the signature of your choice
String signatureUniqueId = container.getSignatures().get(0).getUniqueId();
// Using the signature's unique ID, request the validation result of that specific signature
ValidationResult signatureValidationResult = containerValidationResult.getValidationResult(signatureUniqueId);
// Check if the signature is valid
boolean isSignatureValid = signatureValidationResult.isValid();
// See the signature validation errors and warnings
List<DigiDoc4JException> validationErrors = signatureValidationResult.getErrors();
List<DigiDoc4JException> validationWarnings = signatureValidationResult.getWarnings();
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);
It is possible to see details for individual timestamp tokens.
// Obtain the timestamp token of your choice from a container
Timestamp timestamp = container.getTimestamps().get(0);
// Timestamp creation time
Date creationTime = timestamp.getCreationTime();
// Timestamp digest algorithm
DigestAlgorithm digestAlgorithm = timestamp.getDigestAlgorithm();
// The signing certificate of the timestamp, if available
X509Cert certificate = timestamp.getCertificate();
// TimeStampToken object encapsulating the CMS signed data of the timestamp token
TimeStampToken timeStampToken = timestamp.getTimeStampToken();
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();
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.
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);
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
- A Container and DataToSign objects are created.
- A signature dataset (containing the digest of file(s) to be signed) is calculated.
- The container and dataToSign objects are saved on disk (or just serialized).
- The signature dataset is signed by the user.
- The container and dataToSign objects state is restored from the disk (or deserialized).
- 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");
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.
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);
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 DigestDataFile
s' 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();
Official builds are provided through releases. If you want support, you need to be using official builds. For assistance, contact us by email [email protected]. Additional information can be found in wiki Q&A and on ID.ee portal.
For staying up to date with news impacting services and applications that use the DigiDoc4j library, join DigiDoc4j library newsletter.
Source code is provided on "as is" terms with no warranty (see license for more information). Do not file GitHub issues with generic support requests.