Skip to content

Commit bc40acc

Browse files
committed
Use 'eu-trusted-lists-resources' to parse and load certificates
DEVSIX-9215
1 parent 9d8f35e commit bc40acc

15 files changed

+639
-430
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
This file is part of the iText (R) project.
3+
Copyright (c) 1998-2025 Apryse Group NV
4+
Authors: Apryse Software.
5+
6+
This program is offered under a commercial and under the AGPL license.
7+
For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below.
8+
9+
AGPL licensing:
10+
This program is free software: you can redistribute it and/or modify
11+
it under the terms of the GNU Affero General Public License as published by
12+
the Free Software Foundation, either version 3 of the License, or
13+
(at your option) any later version.
14+
15+
This program is distributed in the hope that it will be useful,
16+
but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
GNU Affero General Public License for more details.
19+
20+
You should have received a copy of the GNU Affero General Public License
21+
along with this program. If not, see <https://www.gnu.org/licenses/>.
22+
*/
23+
package com.itextpdf.commons.utils;
24+
25+
/**
26+
* Utility class for runtime-related operations.
27+
*/
28+
public final class RuntimeUtil {
29+
30+
private RuntimeUtil() {
31+
// Private constructor to prevent instantiation
32+
}
33+
34+
/**
35+
* Checks if a class is loaded in the current runtime environment.
36+
*
37+
* @param fullyQualifiedClassName the fully qualified name of the class to check
38+
* @return true if the class is loaded, false otherwise
39+
*/
40+
public static boolean isClassLoaded(String fullyQualifiedClassName) {
41+
try {
42+
Class.forName(fullyQualifiedClassName);
43+
return true;
44+
} catch (ClassNotFoundException ignored) {
45+
// ignore the exception, it means the resources are not available
46+
return false;
47+
}
48+
}
49+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
This file is part of the iText (R) project.
3+
Copyright (c) 1998-2025 Apryse Group NV
4+
Authors: Apryse Software.
5+
6+
This program is offered under a commercial and under the AGPL license.
7+
For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below.
8+
9+
AGPL licensing:
10+
This program is free software: you can redistribute it and/or modify
11+
it under the terms of the GNU Affero General Public License as published by
12+
the Free Software Foundation, either version 3 of the License, or
13+
(at your option) any later version.
14+
15+
This program is distributed in the hope that it will be useful,
16+
but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
GNU Affero General Public License for more details.
19+
20+
You should have received a copy of the GNU Affero General Public License
21+
along with this program. If not, see <https://www.gnu.org/licenses/>.
22+
*/
23+
package com.itextpdf.commons.utils;
24+
25+
import com.itextpdf.test.ExtendedITextTest;
26+
import org.junit.jupiter.api.Assertions;
27+
import org.junit.jupiter.api.Tag;
28+
import org.junit.jupiter.api.Test;
29+
30+
31+
@Tag("UnitTest")
32+
public class RuntimeUtilTest extends ExtendedITextTest {
33+
34+
@Test
35+
void isClassLoaded() {
36+
Assertions.assertTrue(
37+
RuntimeUtil.isClassLoaded("com.itextpdf.commons.utils.RuntimeUtil"),
38+
"The RuntimeUtil class should be loaded"
39+
);
40+
}
41+
42+
@Test
43+
void isClassNotLoaded() {
44+
Assertions.assertFalse(
45+
RuntimeUtil.isClassLoaded("com.itextpdf.commons.utils.NonExistentClass"),
46+
"The NonExistentClass should not be loaded"
47+
);
48+
}
49+
}

sharpenConfiguration.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@
5353
<file path="com/itextpdf/commons/datastructures/ConcurrentWeakMap.java" />
5454
<file path="com/itextpdf/commons/datastructures/ConcurrentWeakMapTest.java" />
5555
</fileset>
56+
<fileset reason="Checking class instance is different">
57+
<file path="com/itextpdf/commons/utils/RuntimeUtil.java"/>
58+
<file path="com/itextpdf/commons/utils/RuntimeUtilTest.java"/>
59+
</fileset>
5660
<fileset reason="SystemUtil is a manual class, and the methods of this class can have input and output other than java despite similar names.
5761
Therefore, the test for this class also cannot be ported automatically">
5862
<file path="com/itextpdf/commons/utils/SystemUtilTest.java"/>
@@ -470,6 +474,10 @@
470474
<file path="com/itextpdf/signatures/validation/lotl/CertificateSelector.java" />
471475
<file path="com/itextpdf/signatures/validation/lotl/xml/XmlSaxProcessor.java" />
472476
</fileset>
477+
<fileset reason="uri handeling and certificate handeling is different">
478+
<file path="com/itextpdf/signatures/validation/EuropeanTrustedCertificatesResourceLoader.java"/>
479+
<file path="com/itextpdf/signatures/validation/LoadFromModuleEuropeanTrustedListConfigurationFactory.java"/>
480+
</fileset>
473481
<file path="com/itextpdf/signatures/ProviderDigest.java"/>
474482
<fileset reason="ProviderDigest class exists only on Java.">
475483
<file path="com/itextpdf/signatures/ProviderDigestUnitTest.java"/>

sign/pom.xml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
34
<modelVersion>4.0.0</modelVersion>
45

56
<parent>
@@ -61,10 +62,14 @@
6162
<artifactId>xmlsec</artifactId>
6263
<version>${apache.santuario.version}</version>
6364
</dependency>
65+
<dependency>
66+
<groupId>com.itextpdf</groupId>
67+
<artifactId>eu-trusted-lists-resources</artifactId>
68+
<version>1.0.0</version>
69+
<optional>true</optional>
70+
</dependency>
6471
</dependencies>
65-
6672
<profiles>
67-
6873
<profile>
6974
<id>bouncy-castle-test</id>
7075
<activation>

sign/src/main/java/com/itextpdf/signatures/exceptions/SignExceptionMessageConstant.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ public final class SignExceptionMessageConstant {
107107
public static final String INVALID_ARGUMENTS = "Invalid parameters provided.";
108108
public static final String CMS_SIGNERINFO_READONLY =
109109
"Updating the signed attributes of this SignerInfo instance is" +
110-
" not possible because it has been serialized or been initiated from a serialized version.";
110+
" not possible because it has been serialized or been initiated from a serialized version.";
111111
public static final String CMS_SIGNERINFO_NOT_INITIALIZED = "Signer info is not yet initialized";
112112
public static final String CMS_INVALID_CONTAINER_STRUCTURE = "Provided data is not a CMS container";
113113
public static final String CMS_ONLY_ONE_SIGNER_ALLOWED = "Only one signer per CMS container is allowed";
@@ -116,6 +116,12 @@ public final class SignExceptionMessageConstant {
116116
"The certificate set must at least contains the signer certificate";
117117
public static final String FAILED_TO_RETRIEVE_CERTIFICATE = "Failed to retrieve certificates from binary data.";
118118
public static final String FAILED_TO_GET_EU_LOTL = "Failed to get European List of Trusted Lists (LOTL) from {0}.";
119+
public static final String CERTIFICATE_HASH_MISMATCH = "Certificate {0} hash mismatch.";
120+
public static final String CERTIFICATE_HASH_NULL = "Hash was null.";
121+
public static final String EU_RESOURCES_NOT_LOADED = "European Trusted List resources are not available. " +
122+
"Please ensure that the itextpdf-eutrustedlistsresources module is included in your project. " +
123+
"Alternatively, " +
124+
" you can use the EuropeanTrustedListConfigurationFactory to load the resources from a custom location.";
119125

120126
private SignExceptionMessageConstant() {
121127
// Private constructor will prevent the instantiation of this class directly
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
This file is part of the iText (R) project.
3+
Copyright (c) 1998-2025 Apryse Group NV
4+
Authors: Apryse Software.
5+
6+
This program is offered under a commercial and under the AGPL license.
7+
For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below.
8+
9+
AGPL licensing:
10+
This program is free software: you can redistribute it and/or modify
11+
it under the terms of the GNU Affero General Public License as published by
12+
the Free Software Foundation, either version 3 of the License, or
13+
(at your option) any later version.
14+
15+
This program is distributed in the hope that it will be useful,
16+
but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
GNU Affero General Public License for more details.
19+
20+
You should have received a copy of the GNU Affero General Public License
21+
along with this program. If not, see <https://www.gnu.org/licenses/>.
22+
*/
23+
package com.itextpdf.signatures.validation;
24+
25+
import com.itextpdf.commons.utils.MessageFormatUtil;
26+
import com.itextpdf.eutrustedlistsresources.EuropeanTrustedListConfiguration;
27+
import com.itextpdf.kernel.exceptions.PdfException;
28+
import com.itextpdf.signatures.CertificateUtil;
29+
import com.itextpdf.signatures.exceptions.SignExceptionMessageConstant;
30+
31+
import java.io.ByteArrayInputStream;
32+
import java.nio.charset.StandardCharsets;
33+
import java.security.MessageDigest;
34+
import java.security.NoSuchAlgorithmException;
35+
import java.security.cert.Certificate;
36+
import java.security.cert.CertificateException;
37+
import java.security.cert.X509Certificate;
38+
import java.util.ArrayList;
39+
import java.util.Base64;
40+
import java.util.List;
41+
42+
/**
43+
* Loads the certificates as published as part of the european journal publication "Information related to data on
44+
* Member States' trusted lists as notified under Commission Implementing Decision (EU) 2015/1505"
45+
*/
46+
class EuropeanTrustedCertificatesResourceLoader {
47+
48+
private final EuropeanTrustedListConfiguration configuration;
49+
50+
/**
51+
* Creates a new instance of {@link EuropeanTrustedCertificatesResourceLoader}
52+
*/
53+
EuropeanTrustedCertificatesResourceLoader(EuropeanTrustedListConfiguration config) {
54+
this.configuration = config;
55+
}
56+
57+
static void verifyCertificate(
58+
String hashB64Encoded, Certificate certificate)
59+
throws CertificateException, NoSuchAlgorithmException {
60+
if (hashB64Encoded == null) {
61+
throw new PdfException(SignExceptionMessageConstant.CERTIFICATE_HASH_NULL);
62+
}
63+
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
64+
final byte[] certHash = digest.digest(certificate.getEncoded());
65+
if (!MessageDigest.isEqual(certHash, Base64.getDecoder().decode(hashB64Encoded))) {
66+
if (certificate instanceof X509Certificate) {
67+
X509Certificate x509Certificate = (X509Certificate) certificate;
68+
throw new PdfException(MessageFormatUtil.format(SignExceptionMessageConstant.CERTIFICATE_HASH_MISMATCH,
69+
x509Certificate.getIssuerX500Principal().getName()));
70+
}
71+
throw new PdfException(MessageFormatUtil.format(
72+
SignExceptionMessageConstant.CERTIFICATE_HASH_MISMATCH, hashB64Encoded));
73+
}
74+
}
75+
76+
/**
77+
* Loads the certificates from the European Trusted List configuration.
78+
* And verifies if they match the expected SHA-256 hash.
79+
*
80+
* @return A list of X509Certificates.
81+
*/
82+
public List<Certificate> loadCertificates() {
83+
final ArrayList<Certificate> result = new ArrayList<>();
84+
for (EuropeanTrustedListConfiguration.PemCertificateWithHash pemContainer : configuration.getCertificates()) {
85+
Certificate certificate = CertificateUtil.readCertificatesFromPem(
86+
new ByteArrayInputStream(pemContainer.getPemCertificate().getBytes(
87+
StandardCharsets.UTF_8)))[0];
88+
result.add(certificate);
89+
try {
90+
verifyCertificate(pemContainer.getHash(), certificate);
91+
} catch (CertificateException | NoSuchAlgorithmException e) {
92+
throw new RuntimeException(e);
93+
94+
}
95+
}
96+
return result;
97+
}
98+
}
99+
100+
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
This file is part of the iText (R) project.
3+
Copyright (c) 1998-2025 Apryse Group NV
4+
Authors: Apryse Software.
5+
6+
This program is offered under a commercial and under the AGPL license.
7+
For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below.
8+
9+
AGPL licensing:
10+
This program is free software: you can redistribute it and/or modify
11+
it under the terms of the GNU Affero General Public License as published by
12+
the Free Software Foundation, either version 3 of the License, or
13+
(at your option) any later version.
14+
15+
This program is distributed in the hope that it will be useful,
16+
but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
GNU Affero General Public License for more details.
19+
20+
You should have received a copy of the GNU Affero General Public License
21+
along with this program. If not, see <https://www.gnu.org/licenses/>.
22+
*/
23+
package com.itextpdf.signatures.validation;
24+
25+
import com.itextpdf.kernel.exceptions.PdfException;
26+
import com.itextpdf.signatures.exceptions.SignExceptionMessageConstant;
27+
28+
import java.security.cert.Certificate;
29+
import java.util.List;
30+
import java.util.function.Supplier;
31+
32+
/**
33+
* Abstract factory class for configuring and retrieving European Trusted List configurations.
34+
* This class provides methods to get and set the factory implementation, as well as abstract
35+
* methods to retrieve trusted list-related information.
36+
*/
37+
public abstract class EuropeanTrustedListConfigurationFactory {
38+
39+
/**
40+
* Supplier for the current factory implementation.
41+
* By default, it uses {@link LoadFromModuleEuropeanTrustedListConfigurationFactory}.
42+
*/
43+
private static Supplier<EuropeanTrustedListConfigurationFactory> configuration =
44+
() -> {
45+
try {
46+
return new LoadFromModuleEuropeanTrustedListConfigurationFactory();
47+
} catch (Exception e) {
48+
throw new PdfException(SignExceptionMessageConstant.EU_RESOURCES_NOT_LOADED);
49+
}
50+
};
51+
52+
/**
53+
* Retrieves the current factory supplier.
54+
*
55+
* @return the current factory supplier
56+
*/
57+
public static Supplier<EuropeanTrustedListConfigurationFactory> getFactory() {
58+
return configuration;
59+
}
60+
61+
/**
62+
* Sets the factory supplier.
63+
*
64+
* @param factory the new factory supplier to set
65+
* @throws IllegalArgumentException if the provided factory is null
66+
*/
67+
public static void setFactory(Supplier<EuropeanTrustedListConfigurationFactory> factory) {
68+
if (factory == null) {
69+
throw new IllegalArgumentException("Factory cannot be null");
70+
}
71+
configuration = factory;
72+
}
73+
74+
/**
75+
* Retrieves the URI of the trusted list.
76+
*
77+
* @return the trusted list URI
78+
*/
79+
public abstract String getTrustedListUri();
80+
81+
/**
82+
* Retrieves the currently supported publication of the trusted list.
83+
*
84+
* @return the currently supported publication
85+
*/
86+
public abstract String getCurrentlySupportedPublication();
87+
88+
/**
89+
* Retrieves the list of certificates from the trusted list.
90+
*
91+
* @return a list of certificates
92+
*/
93+
public abstract List<Certificate> getCertificates();
94+
}

0 commit comments

Comments
 (0)