Skip to content

Commit

Permalink
add validation endpoint and support for formless signature level para…
Browse files Browse the repository at this point in the history
…meter (#9)

* add validation and support for formless siugnature level parameter

* do not use code+description for qualification result
  • Loading branch information
celuchmarek authored Aug 5, 2024
1 parent a0ee0d7 commit 53e47e9
Show file tree
Hide file tree
Showing 12 changed files with 671 additions and 48 deletions.
4 changes: 2 additions & 2 deletions src/main/java/digital/slovensko/avm/core/AVM.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ public boolean checkPDFACompliance(SigningJob job) {
return result.isCompliant();
}

public void initializeSignatureValidator(ScheduledExecutorService scheduledExecutorService, ExecutorService cachedExecutorService, List<String> tlCountries) {
SignatureValidator.getInstance().initialize(cachedExecutorService, tlCountries);
public void initializeSignatureValidator(ScheduledExecutorService scheduledExecutorService, ExecutorService cachedExecutorService) {
SignatureValidator.getInstance().initialize(cachedExecutorService);

scheduledExecutorService.scheduleAtFixedRate(() -> SignatureValidator.getInstance().refresh(),
480, 480, java.util.concurrent.TimeUnit.MINUTES);
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/digital/slovensko/avm/core/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ public static void printHelp() {
public static void run(int port, TSPSource tspSource) throws Exception {
var avm = new AVM(tspSource, false);

// new Thread(() -> {
// avm.initializeSignatureValidator(scheduledExecutorService, Executors.newFixedThreadPool(8));
// }).start();
new Thread(() -> {
avm.initializeSignatureValidator(scheduledExecutorService, Executors.newFixedThreadPool(8));
}).start();

var server = new Server(avm, "0.0.0.0", port, cachedExecutorService);
server.start();
Expand Down
45 changes: 30 additions & 15 deletions src/main/java/digital/slovensko/avm/core/SignatureValidator.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package digital.slovensko.avm.core;

import static digital.slovensko.avm.util.DSSUtils.*;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
Expand All @@ -9,22 +11,14 @@
import java.util.List;
import java.util.concurrent.ExecutorService;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import digital.slovensko.avm.util.DSSUtils;
import digital.slovensko.avm.core.dto.ReportsAndValidator;
import digital.slovensko.avm.util.XMLUtils;
import eu.europa.esig.dss.enumerations.SignatureLevel;
import eu.europa.esig.dss.simplereport.SimpleReport;
import eu.europa.esig.dss.tsl.function.TLPredicateFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import eu.europa.esig.dss.enumerations.SignatureLevel;
import eu.europa.esig.dss.model.DSSDocument;
import eu.europa.esig.dss.model.DSSException;
import eu.europa.esig.dss.service.crl.OnlineCRLSource;
Expand All @@ -42,8 +36,14 @@
import eu.europa.esig.dss.validation.CommonCertificateVerifier;
import eu.europa.esig.dss.validation.SignedDocumentValidator;
import eu.europa.esig.dss.validation.reports.Reports;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import static digital.slovensko.avm.util.DSSUtils.createDocumentValidator;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

public class SignatureValidator {
private static final String LOTL_URL = "https://ec.europa.eu/tools/lotl/eu-lotl.xml";
Expand All @@ -65,18 +65,34 @@ public synchronized static SignatureValidator getInstance() {
return instance;
}

public synchronized Reports validate(SignedDocumentValidator docValidator) {
private synchronized Reports validate(SignedDocumentValidator docValidator) {
docValidator.setCertificateVerifier(verifier);

// TODO: do not print stack trace inside DSS
return docValidator.validateDocument();
}

public synchronized CertificateVerifier getVerifier() {
return verifier;
}

public synchronized ReportsAndValidator validate(DSSDocument document) {
var documentValidator = createDocumentValidator(document);
if (documentValidator == null)
return null;

try {
return new ReportsAndValidator(validate(documentValidator), documentValidator);
} catch (NullPointerException e) {
return null;
}
}

public synchronized void refresh() {
validationJob.offlineRefresh();
}

public synchronized void initialize(ExecutorService executorService, List<String> tlCountries) {
public synchronized void initialize(ExecutorService executorService) {
SimpleDateFormat formatter = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
logger.debug("Initializing signature validator at {}", formatter.format(new Date()));

Expand All @@ -87,10 +103,9 @@ public synchronized void initialize(ExecutorService executorService, List<String
lotlSource.setSigningCertificatesAnnouncementPredicate(new OfficialJournalSchemeInformationURI(OJ_URL));
lotlSource.setUrl(LOTL_URL);
lotlSource.setPivotSupport(true);
lotlSource.setTlPredicate(TLPredicateFactory.createEUTLCountryCodePredicate(tlCountries.toArray(new String[0])));

var offlineFileLoader = new FileCacheDataLoader();
offlineFileLoader.setCacheExpirationTime(21600000);
offlineFileLoader.setCacheExpirationTime(21600000); // 6 hours
offlineFileLoader.setDataLoader(new CommonsDataLoader());
validationJob.setOfflineDataLoader(offlineFileLoader);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package digital.slovensko.avm.core.dto;

import eu.europa.esig.dss.validation.DocumentValidator;
import eu.europa.esig.dss.validation.reports.Reports;

public record ReportsAndValidator(Reports reports, DocumentValidator validator) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package digital.slovensko.avm.core.errors;

public class DocumentNotSignedYetException extends AutogramException {
public DocumentNotSignedYetException() {
super("Document not signed", "Document is not signed yet", "The provided document is not eligible for signature validation because the document is not signed yet.");
}
}
4 changes: 4 additions & 0 deletions src/main/java/digital/slovensko/avm/server/Server.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ public void start() {
server.createContext("/sign", new SignEndpoint(avm)).getFilters()
.add(new AutogramCorsFilter("POST"));

// POST Validation
server.createContext("/validate", new ValidationEndpoint()).getFilters()
.add(new AutogramCorsFilter("POST"));

// POST DataToSign
server.createContext("/datatosign", new DataToSignEndpoint(avm)).getFilters()
.add(new AutogramCorsFilter("POST"));
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/digital/slovensko/avm/server/dto/Document.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package digital.slovensko.avm.server.dto;

import eu.europa.esig.dss.model.InMemoryDocument;

import java.util.Base64;

public class Document {
private String filename;
private String content;
Expand All @@ -20,4 +24,8 @@ public String getFilename() {
public String getContent() {
return content;
}

public InMemoryDocument getDecodedContent() {
return new InMemoryDocument(Base64.getDecoder().decode(content));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ private ErrorResponse(int statusCode, String code, AutogramException e) {
this(statusCode, new ErrorResponseBody(code, e.getSubheading(), e.getDescription()));
}

private ErrorResponse(int statusCode, String code, String message, String details) {
public ErrorResponse(int statusCode, String code, String message, String details) {
this(statusCode, new ErrorResponseBody(code, message, details));
}

Expand Down Expand Up @@ -44,6 +44,7 @@ public static ErrorResponse buildFromException(Exception e) {
case "AutogramException" -> new ErrorResponse(502, "SIGNING_FAILED", (AutogramException) e);
case "EmptyBodyException" -> new ErrorResponse(400, "EMPTY_BODY", (AutogramException) e);
case "DataToSignMismatchException" -> new ErrorResponse(400, "DATATOSIGN_MISMATCH", (AutogramException) e);
case "DocumentNotSignedYetException" -> new ErrorResponse(422, "DOCUMENT_NOT_SIGNED", (AutogramException) e);
default -> new ErrorResponse(500, "INTERNAL_ERROR", "Unexpected exception signing document", e.getMessage());
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;

import javax.xml.crypto.dsig.CanonicalizationMethod;

Expand All @@ -13,18 +14,13 @@
import digital.slovensko.avm.core.errors.MalformedBodyException;
import digital.slovensko.avm.core.errors.RequestValidationException;
import digital.slovensko.avm.core.errors.UnsupportedSignatureLevelException;
import eu.europa.esig.dss.enumerations.ASiCContainerType;
import eu.europa.esig.dss.enumerations.DigestAlgorithm;
import eu.europa.esig.dss.enumerations.MimeType;
import eu.europa.esig.dss.enumerations.MimeTypeEnum;
import eu.europa.esig.dss.enumerations.SignatureForm;
import eu.europa.esig.dss.enumerations.SignatureLevel;
import eu.europa.esig.dss.enumerations.SignaturePackaging;
import eu.europa.esig.dss.enumerations.*;
import eu.europa.esig.dss.model.DSSDocument;
import eu.europa.esig.dss.model.InMemoryDocument;
import eu.europa.esig.dss.spi.x509.tsp.TSPSource;

import static digital.slovensko.avm.core.AutogramMimeType.*;
import static eu.europa.esig.dss.enumerations.SignatureForm.*;

public class ServerSigningParameters {
public enum LocalCanonicalizationMethod {
Expand All @@ -42,8 +38,19 @@ public enum TransformationOutputMimeType {
XHTML
}

public enum LocalSignatureLevel {
XAdES_BASELINE_T, XAdES_BASELINE_B, CAdES_BASELINE_T, CAdES_BASELINE_B, PAdES_BASELINE_B, PAdES_BASELINE_T, B, T;

public LocalSignatureLevel getTimestampingLevel() {
if (name().lastIndexOf('_') == -1)
return this;

return valueOf(name().substring(name().lastIndexOf('_')));
}
}

private ASiCContainerType container;
private SignatureLevel level;
private LocalSignatureLevel level;
private final String containerXmlns;
private final String schema;
private final String transformation;
Expand All @@ -66,7 +73,7 @@ public enum TransformationOutputMimeType {
private final String fsFormId;


public ServerSigningParameters(SignatureLevel level, ASiCContainerType container,
public ServerSigningParameters(LocalSignatureLevel level, ASiCContainerType container,
String containerFilename, String containerXmlns, SignaturePackaging packaging,
DigestAlgorithm digestAlgorithm,
Boolean en319132, LocalCanonicalizationMethod infoCanonicalization,
Expand Down Expand Up @@ -220,15 +227,15 @@ private static String getCanonicalizationMethodString(LocalCanonicalizationMetho
}

private SignatureLevel getSignatureLevel() {
return level;
return SignatureLevel.valueByName(level.name());
}

private ASiCContainerType getContainer() {
return container;
}

public void resolveSigningLevel(InMemoryDocument document) throws RequestValidationException {
if (level != null)
if (level != null && level.name().length() > 4)
return;

var report = SignatureValidator.getSignedDocumentSimpleReport(document);
Expand All @@ -237,17 +244,23 @@ public void resolveSigningLevel(InMemoryDocument document) throws RequestValidat
throw new RequestValidationException("Parameters.Level can't be empty if document is not signed yet", "");

container = report.getContainerType();
level = switch (signedLevel.getSignatureForm()) {
case PAdES -> SignatureLevel.PAdES_BASELINE_B;
case XAdES -> SignatureLevel.XAdES_BASELINE_B;
case CAdES -> SignatureLevel.CAdES_BASELINE_B;
default -> null;
};
level = getMergedLevel(signedLevel, level);
if (!List.of(PAdES, XAdES, CAdES).contains(SignatureLevel.valueOf(level.name()).getSignatureForm()))
level = null;

if (level == null)
throw new RequestValidationException("Signed document has unsupported SignatureLevel", "");
}

private static LocalSignatureLevel getMergedLevel(SignatureLevel signedLevel, LocalSignatureLevel level) {
var timestampingLevel = "B";

if (level != null)
timestampingLevel = level.getTimestampingLevel().name();

return LocalSignatureLevel.valueOf(signedLevel.getSignatureForm().name() + "_BASELINE_" + timestampingLevel);
}

private String getFsFormId() {
if (fsFormId == null || fsFormId.isEmpty())
return null;
Expand All @@ -260,17 +273,19 @@ public void validate(MimeType mimeType) throws RequestValidationException {
throw new RequestValidationException("Parameters.Level is required", "");

var supportedLevels = Arrays.asList(
SignatureLevel.XAdES_BASELINE_B,
SignatureLevel.PAdES_BASELINE_B,
SignatureLevel.CAdES_BASELINE_B,
SignatureLevel.XAdES_BASELINE_T,
SignatureLevel.CAdES_BASELINE_T,
SignatureLevel.PAdES_BASELINE_T);
LocalSignatureLevel.XAdES_BASELINE_B,
LocalSignatureLevel.PAdES_BASELINE_B,
LocalSignatureLevel.CAdES_BASELINE_B,
LocalSignatureLevel.XAdES_BASELINE_T,
LocalSignatureLevel.CAdES_BASELINE_T,
LocalSignatureLevel.PAdES_BASELINE_T,
LocalSignatureLevel.B,
LocalSignatureLevel.T);

if (!supportedLevels.contains(level))
throw new UnsupportedSignatureLevelException(level.name());

if (level.getSignatureForm() == SignatureForm.PAdES) {
if (getSignatureLevel().getSignatureForm() == PAdES) {
if (!isPDF(mimeType))
throw new RequestValidationException("PayloadMimeType and Parameters.Level mismatch",
"Parameters.Level: PAdES is not supported for this payload: " + mimeType.getMimeTypeString());
Expand All @@ -280,7 +295,7 @@ public void validate(MimeType mimeType) throws RequestValidationException {
"PAdES signature cannot be in a container");
}

if (level.getSignatureForm() == SignatureForm.XAdES) {
if (getSignatureLevel().getSignatureForm() == XAdES) {
if (!isXML(mimeType) && !isXDC(mimeType) && !isAsice(mimeType) && container == null)
if (!(packaging != null && packaging == SignaturePackaging.ENVELOPING))
throw new RequestValidationException(
Expand Down
Loading

0 comments on commit 53e47e9

Please sign in to comment.