Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add validation endpoint and support for formless signature level parameter #9

Merged
merged 2 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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