diff --git a/src/main/java/org/olat/core/util/crypto/CryptoUtil.java b/src/main/java/org/olat/core/util/crypto/CryptoUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..4306185c402a5ab599a29d886b55d16efcacd6f8
--- /dev/null
+++ b/src/main/java/org/olat/core/util/crypto/CryptoUtil.java
@@ -0,0 +1,69 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.core.util.crypto;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.security.Key;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.Enumeration;
+
+/**
+ * 
+ * Initial date: 16 févr. 2017<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class CryptoUtil {
+
+	/**
+	 * 
+	 * @param certificate
+	 * @return
+	 * @throws Exception
+	 */
+	public static X509CertificatePrivateKeyPair getX509CertificatePrivateKeyPairPfx(File certificate, String password) throws Exception {
+
+		KeyStore keyStore = KeyStore.getInstance("JKS");
+		keyStore.load(new FileInputStream(certificate), password.toCharArray());
+
+		PrivateKey privateKey = null;
+        X509Certificate x509Cert = null;
+		
+		Enumeration<String> aliases = keyStore.aliases();
+		while (aliases.hasMoreElements()) {
+			String alias = aliases.nextElement();
+			Certificate cert = keyStore.getCertificate(alias);
+			if (cert instanceof X509Certificate) {
+				x509Cert = (X509Certificate)cert;
+				Key key = keyStore.getKey(alias, null);
+				if(key instanceof PrivateKey) {
+					privateKey = (PrivateKey)key;
+				}
+				break;
+			}
+		}
+
+		return new X509CertificatePrivateKeyPair(x509Cert, privateKey);
+	}
+}
diff --git a/src/main/java/org/olat/core/util/crypto/X509CertificatePrivateKeyPair.java b/src/main/java/org/olat/core/util/crypto/X509CertificatePrivateKeyPair.java
new file mode 100644
index 0000000000000000000000000000000000000000..54865a56d99c9741d8cb5055a648ce7171fdbedf
--- /dev/null
+++ b/src/main/java/org/olat/core/util/crypto/X509CertificatePrivateKeyPair.java
@@ -0,0 +1,49 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.core.util.crypto;
+
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+
+/**
+ * 
+ * Initial date: 16 févr. 2017<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class X509CertificatePrivateKeyPair {
+	
+	private final PrivateKey privateKey;
+    private final X509Certificate x509Cert;
+    
+    public X509CertificatePrivateKeyPair(X509Certificate x509Cert, PrivateKey privateKey) {
+    	this.x509Cert = x509Cert;
+    	this.privateKey = privateKey;
+    }
+
+	public PrivateKey getPrivateKey() {
+		return privateKey;
+	}
+
+	public X509Certificate getX509Cert() {
+		return x509Cert;
+	}
+
+}
diff --git a/src/main/java/org/olat/core/util/mail/MailBundle.java b/src/main/java/org/olat/core/util/mail/MailBundle.java
index 8e6203e084c79bb6cf7903fc0483cbd7f2ce2101..1ec7904d54572c7a97e9d79ec6c7e66bda264a87 100644
--- a/src/main/java/org/olat/core/util/mail/MailBundle.java
+++ b/src/main/java/org/olat/core/util/mail/MailBundle.java
@@ -25,6 +25,7 @@ import java.util.Collections;
 import java.util.List;
 
 import org.olat.core.id.Identity;
+import org.olat.core.util.mail.model.SimpleMailContent;
 
 /**
  * 
@@ -129,34 +130,6 @@ public class MailBundle {
 				}
 			}
 		}
-		content = new SimpleContent(subject, body, attachmentList);
-	}
-
-	private static class SimpleContent implements MailContent {
-		
-		private String subject;
-		private String body;
-		private List<File> attachments;
-		
-		public SimpleContent(String subject, String body, List<File> attachments) {
-			this.subject = subject;
-			this.body = body;
-			this.attachments = attachments;
-		}
-
-		@Override
-		public String getSubject() {
-			return subject;
-		}
-
-		@Override
-		public String getBody() {
-			return body;
-		}
-
-		@Override
-		public List<File> getAttachments() {
-			return attachments;
-		}
+		content = new SimpleMailContent(subject, body, attachmentList);
 	}
 }
diff --git a/src/main/java/org/olat/core/util/mail/MailContent.java b/src/main/java/org/olat/core/util/mail/MailContent.java
index 3d26217964e1c27bbfcf51d75a3e6fe9cf1f6156..808386e679cda04c671dbc9a5ac6f13612036aa7 100644
--- a/src/main/java/org/olat/core/util/mail/MailContent.java
+++ b/src/main/java/org/olat/core/util/mail/MailContent.java
@@ -32,8 +32,14 @@ public interface MailContent {
 	
 	public String getSubject();
 	
+	public void setSubject(String subject);
+	
 	public String getBody();
 	
+	public void setBody(String body);
+	
 	public List<File> getAttachments();
+	
+	public void setAttachments(List<File> attachments);
 
 }
diff --git a/src/main/java/org/olat/core/util/mail/manager/MailManagerImpl.java b/src/main/java/org/olat/core/util/mail/manager/MailManagerImpl.java
index 70de7ce09bcb7e8dcc316f03011602d38cf53277..de50b5560a43609c494e956ae5763d7ee5c873bb 100644
--- a/src/main/java/org/olat/core/util/mail/manager/MailManagerImpl.java
+++ b/src/main/java/org/olat/core/util/mail/manager/MailManagerImpl.java
@@ -104,6 +104,7 @@ import org.olat.core.util.mail.model.DBMailImpl;
 import org.olat.core.util.mail.model.DBMailLight;
 import org.olat.core.util.mail.model.DBMailLightImpl;
 import org.olat.core.util.mail.model.DBMailRecipient;
+import org.olat.core.util.mail.model.SimpleMailContent;
 import org.olat.core.util.vfs.FileStorage;
 import org.olat.core.util.vfs.VFSContainer;
 import org.olat.core.util.vfs.VFSItem;
@@ -717,7 +718,7 @@ public class MailManagerImpl implements MailManager, InitializingBean  {
 		} else {
 			decoratedBody = content.getBody();
 		}
-		return new MessageContent(content.getSubject(), decoratedBody, content.getAttachments());
+		return new SimpleMailContent(content.getSubject(), decoratedBody, content.getAttachments());
 	}
 	
 	protected MailContent createWithContext(Identity recipient, MailTemplate template, MailerResult result) {
@@ -744,7 +745,7 @@ public class MailManagerImpl implements MailManager, InitializingBean  {
 		String body = bodyWriter.toString();
 		List<File> checkedFiles = MailHelper.checkAttachments(template.getAttachments(), result);
 		File[] attachments = checkedFiles.toArray(new File[checkedFiles.size()]);
-		return new MessageContent(subject, body, attachments);
+		return new SimpleMailContent(subject, body, attachments);
 	}
 	
 	/**
@@ -778,51 +779,6 @@ public class MailManagerImpl implements MailManager, InitializingBean  {
 			mailerResult.setReturnCode(MailerResult.TEMPLATE_GENERAL_ERROR);
 		}
 	}
-	
-	private static class MessageContent implements MailContent {
-		private final String subject;
-		private final String body;
-		private final List<File> attachments;
-		
-		public MessageContent(String subject, String body, File[] attachmentArr) {
-			this.subject = subject;
-			this.body = body;
-			
-			attachments = new ArrayList<File>();
-			if(attachmentArr != null && attachmentArr.length > 0) {
-				for(File attachment:attachmentArr) {
-					if(attachment != null && attachment.exists()) {
-						attachments.add(attachment);
-					}
-				}
-			}
-		}
-		
-		public MessageContent(String subject, String body, List<File> attachmentList) {
-			this.subject = subject;
-			this.body = body;
-			if(attachmentList == null) {
-				this.attachments = new ArrayList<File>(1);
-			} else {
-				this.attachments = new ArrayList<File>(attachmentList);
-			}
-		}
-
-		@Override
-		public String getSubject() {
-			return subject;
-		}
-
-		@Override
-		public String getBody() {
-			return body;
-		}
-
-		@Override
-		public List<File> getAttachments() {
-			return attachments;
-		}
-	}
 
 	@Override
 	public MailerResult forwardToRealInbox(Identity identity, DBMail mail, MailerResult result) {
diff --git a/src/main/java/org/olat/core/util/mail/model/SimpleMailContent.java b/src/main/java/org/olat/core/util/mail/model/SimpleMailContent.java
new file mode 100644
index 0000000000000000000000000000000000000000..d413f9ae73f9bf4fac8088b6fe4b5c14a5214f02
--- /dev/null
+++ b/src/main/java/org/olat/core/util/mail/model/SimpleMailContent.java
@@ -0,0 +1,93 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.core.util.mail.model;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.olat.core.util.mail.MailContent;
+
+/**
+ * 
+ * Initial date: 17 févr. 2017<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class SimpleMailContent implements MailContent {
+	
+	private String subject;
+	private String body;
+	private List<File> attachments;
+	
+	public SimpleMailContent(String subject, String body, File[] attachmentArr) {
+		this.subject = subject;
+		this.body = body;
+		
+		attachments = new ArrayList<File>();
+		if(attachmentArr != null && attachmentArr.length > 0) {
+			for(File attachment:attachmentArr) {
+				if(attachment != null && attachment.exists()) {
+					attachments.add(attachment);
+				}
+			}
+		}
+	}
+	
+	public SimpleMailContent(String subject, String body, List<File> attachmentList) {
+		this.subject = subject;
+		this.body = body;
+		if(attachmentList == null) {
+			this.attachments = new ArrayList<File>(1);
+		} else {
+			this.attachments = new ArrayList<File>(attachmentList);
+		}
+	}
+
+	@Override
+	public String getSubject() {
+		return subject;
+	}
+	
+	@Override
+	public void setSubject(String subject) {
+		this.subject = subject;
+	}
+
+	@Override
+	public String getBody() {
+		return body;
+	}
+	
+	@Override
+	public void setBody(String body) {
+		this.body = body;
+	}
+
+	@Override
+	public List<File> getAttachments() {
+		return attachments;
+	}
+
+	@Override
+	public void setAttachments(List<File> attachments) {
+		this.attachments = attachments;
+	}
+}
diff --git a/src/main/java/org/olat/core/util/xml/XMLDigitalSignatureUtil.java b/src/main/java/org/olat/core/util/xml/XMLDigitalSignatureUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..c65203f84dfe6ee81c40704e004acdc81840fd69
--- /dev/null
+++ b/src/main/java/org/olat/core/util/xml/XMLDigitalSignatureUtil.java
@@ -0,0 +1,390 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.core.util.xml;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Writer;
+import java.nio.file.Files;
+import java.security.GeneralSecurityException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.xml.crypto.Data;
+import javax.xml.crypto.MarshalException;
+import javax.xml.crypto.OctetStreamData;
+import javax.xml.crypto.URIDereferencer;
+import javax.xml.crypto.URIReference;
+import javax.xml.crypto.URIReferenceException;
+import javax.xml.crypto.XMLCryptoContext;
+import javax.xml.crypto.dsig.CanonicalizationMethod;
+import javax.xml.crypto.dsig.DigestMethod;
+import javax.xml.crypto.dsig.Reference;
+import javax.xml.crypto.dsig.SignatureMethod;
+import javax.xml.crypto.dsig.SignedInfo;
+import javax.xml.crypto.dsig.Transform;
+import javax.xml.crypto.dsig.XMLSignature;
+import javax.xml.crypto.dsig.XMLSignatureException;
+import javax.xml.crypto.dsig.XMLSignatureFactory;
+import javax.xml.crypto.dsig.dom.DOMSignContext;
+import javax.xml.crypto.dsig.dom.DOMValidateContext;
+import javax.xml.crypto.dsig.keyinfo.KeyInfo;
+import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory;
+import javax.xml.crypto.dsig.keyinfo.X509Data;
+import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec;
+import javax.xml.crypto.dsig.spec.TransformParameterSpec;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.TransformerFactoryConfigurationError;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+import org.olat.core.logging.OLog;
+import org.olat.core.logging.Tracing;
+import org.olat.core.util.StringHelper;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.w3c.dom.Text;
+import org.xml.sax.SAXException;
+
+/**
+ * 
+ * Initial date: 16 févr. 2017<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class XMLDigitalSignatureUtil {
+	
+	private static final OLog log = Tracing.createLoggerFor(XMLDigitalSignatureUtil.class);
+	
+	/**
+	 * Validate a XML file with a XML Digital Signature saved in an extenral file.
+	 * 
+	 * 
+	 * @param xmlFile
+	 * @param xmlSignatureFile
+	 * @param publicKey
+	 * @return
+	 * @throws ParserConfigurationException
+	 * @throws SAXException
+	 * @throws IOException
+	 * @throws MarshalException
+	 * @throws XMLSignatureException
+	 */
+	public static boolean validate(String uri, File xmlFile, File xmlSignatureFile, PublicKey publicKey)
+	throws ParserConfigurationException, SAXException, IOException, MarshalException, XMLSignatureException {  
+
+		Document doc = getDocument(xmlSignatureFile);
+        NodeList nl = doc.getElementsByTagName("Signature");
+        if (nl.getLength() == 0) {
+            return false;
+        }
+        
+		DOMValidateContext validContext = new DOMValidateContext(publicKey, nl.item(0));
+		validContext.setBaseURI(uri);
+		validContext.setURIDereferencer(new FileURIDereferencer(uri, xmlFile));
+
+		XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
+		XMLSignature signature = fac.unmarshalXMLSignature(validContext);
+		boolean validFlag = signature.validate(validContext);
+		if(!validFlag) {
+            // log and throw if not valid
+            boolean sv = signature.getSignatureValue().validate(validContext);
+            String msg = "signature validation status: " + sv;
+            
+            int numOfReferences = signature.getSignedInfo().getReferences().size();
+            for (int j=0; j<numOfReferences; j++) {
+            	Reference ref = (Reference)signature.getSignedInfo().getReferences().get(j);
+                boolean refValid = ref.validate(validContext);
+                msg += " ref["+j+"] validity status: " + refValid;
+            }
+            log.warn(msg);
+		}
+		return validFlag;
+	}
+	
+	public static boolean validate(File signedXmlFile, PublicKey publicKey)
+	throws ParserConfigurationException, SAXException, IOException, MarshalException, XMLSignatureException {  
+
+		Document doc = getDocument(signedXmlFile);
+		
+        NodeList nl = doc.getElementsByTagName("Signature");
+        if (nl.getLength() == 0) {
+            return false;
+        }
+        
+		DOMValidateContext validContext = new DOMValidateContext(publicKey, nl.item(0));
+
+		XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
+		XMLSignature signature = fac.unmarshalXMLSignature(validContext);
+		boolean validFlag = signature.validate(validContext);
+		if(!validFlag) {
+            // log and throw if not valid
+            boolean sv = signature.getSignatureValue().validate(validContext);
+            String msg = "signature validation status: " + sv;
+            
+            int numOfReferences = signature.getSignedInfo().getReferences().size();
+            for (int j=0; j<numOfReferences; j++) {
+            	Reference ref = (Reference)signature.getSignedInfo().getReferences().get(j);
+                boolean refValid = ref.validate(validContext);
+                msg += " ref["+j+"] validity status: " + refValid;
+            }
+            log.warn(msg);
+		}
+		return validFlag;
+	}
+	
+	/**
+	 * Produce a signed a XML file. The signature is added in the XML file.
+	 * 
+	 * @param xmlFile The original XML file
+	 * @param xmlSignedFile The signed XML file
+	 * @param x509Cert
+	 * @param privateKey
+	 * @throws IOException
+	 * @throws SAXException
+	 * @throws ParserConfigurationException
+	 * @throws NoSuchAlgorithmException
+	 * @throws GeneralSecurityException
+	 * @throws MarshalException
+	 * @throws XMLSignatureException
+	 * @throws TransformerException
+	 */
+	public static void signEmbedded(File xmlFile, File xmlSignedFile, X509Certificate x509Cert, PrivateKey privateKey)
+	throws IOException, SAXException, ParserConfigurationException, NoSuchAlgorithmException, GeneralSecurityException, MarshalException, XMLSignatureException, TransformerException {
+
+		Document doc = getDocument(xmlFile);
+
+        // Create the signature factory for creating the signature.
+        XMLSignatureFactory sigFactory = XMLSignatureFactory.getInstance("DOM");
+
+        List<Transform> transforms = new ArrayList<Transform>();
+        
+        Transform envelopped = sigFactory.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null);
+        transforms.add(envelopped);
+
+        // Create the canonicalization transform to be applied after the XSLT.
+        CanonicalizationMethod c14n = sigFactory.newCanonicalizationMethod(
+                CanonicalizationMethod.INCLUSIVE, (C14NMethodParameterSpec) null);
+        transforms.add(c14n);
+
+        // Create the Reference to the XML to be signed specifying the hash algorithm to be used
+        // and the list of transforms to apply. Also specify the XML to be signed as the current
+        // document (specified by the first parameter being an empty string).
+        Reference reference = sigFactory.newReference(
+        		"",
+                sigFactory.newDigestMethod(DigestMethod.SHA256, null),
+                transforms,
+                null,
+                null);
+
+        // Create the Signed Info node of the signature by specifying the canonicalization method
+        // to use (INCLUSIVE), the signing method (RSA_SHA1), and the Reference node to be signed.
+        SignedInfo si = sigFactory.newSignedInfo(c14n,
+                        sigFactory.newSignatureMethod(SignatureMethod.RSA_SHA1, null),
+                        Collections.singletonList(reference));
+
+        // Create the KeyInfo node containing the public key information to include in the signature.
+        KeyInfoFactory kif = sigFactory.getKeyInfoFactory();
+        X509Data xd = kif.newX509Data(Collections.singletonList(x509Cert));
+        KeyInfo ki = kif.newKeyInfo(Collections.singletonList(xd));
+
+        // Get the node to attach the signature.
+        Node signatureInfoNode = doc.getDocumentElement();
+     
+        // Create a signing context using the private key.
+        DOMSignContext dsc = new DOMSignContext(privateKey, signatureInfoNode);
+
+        // Create the signature from the signing context and key info
+        XMLSignature signature = sigFactory.newXMLSignature(si, ki);
+        signature.sign(dsc);
+
+        write(doc, xmlSignedFile);
+	}
+
+	/**
+	 * Create a separate XML file with the XML Digital Signature.
+	 * 
+	 * of the specified XML file.
+	 * @param xmlFile The XML File to sign
+	 * @param outputSignatureFile Where the Digital Signature is saved
+	 * @param signatureDoc A DOM which hold the signature (optional but if you give one, the root element must exists)
+	 * @throws ParserConfigurationException 
+	 * @throws GeneralSecurityException 
+	 * @throws NoSuchAlgorithmException 
+	 * @throws XMLSignatureException 
+	 * @throws MarshalException 
+	 * @throws TransformerException 
+	 */
+	public static void signDetached(String uri, File xmlFile, File outputSignatureFile, Document signatureDoc,
+			String keyName, X509Certificate x509Cert, PrivateKey privateKey)
+	throws IOException, SAXException, ParserConfigurationException, NoSuchAlgorithmException, GeneralSecurityException, MarshalException, XMLSignatureException, TransformerException {
+
+		Document doc = getDocument(xmlFile);
+
+        // Create the signature factory for creating the signature.
+        XMLSignatureFactory sigFactory = XMLSignatureFactory.getInstance("DOM");
+
+        List<Transform> transforms = new ArrayList<Transform>();
+        
+        //Transform envelopped = sigFactory.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null);
+        //transforms.add(envelopped);
+
+        // Create the canonicalization transform to be applied after the XSLT.
+        CanonicalizationMethod c14n = sigFactory.newCanonicalizationMethod(
+                CanonicalizationMethod.EXCLUSIVE, (C14NMethodParameterSpec) null);
+        transforms.add(c14n);
+
+        // Create the Reference to the XML to be signed specifying the hash algorithm to be used
+        // and the list of transforms to apply. Also specify the XML to be signed as the current
+        // document (specified by the first parameter being an empty string).
+        Reference reference = sigFactory.newReference(
+        		uri,
+                sigFactory.newDigestMethod(DigestMethod.SHA256, null),
+                transforms,
+                null,
+                null);
+
+        // Create the Signed Info node of the signature by specifying the canonicalization method
+        // to use (INCLUSIVE), the signing method (RSA_SHA1), and the Reference node to be signed.
+        SignedInfo si = sigFactory.newSignedInfo(c14n,
+                        sigFactory.newSignatureMethod(SignatureMethod.RSA_SHA1, null),
+                        Collections.singletonList(reference));
+
+        // Create the KeyInfo node containing the public key information to include in the signature.
+        KeyInfoFactory kif = sigFactory.getKeyInfoFactory();
+        X509Data xd = kif.newX509Data(Collections.singletonList(x509Cert));
+        
+        List<Object> keyInfoList = new ArrayList<>();
+        if(StringHelper.containsNonWhitespace(keyName)) {
+            keyInfoList.add(kif.newKeyName(keyName));
+        }
+        keyInfoList.add(xd);
+        KeyInfo ki = kif.newKeyInfo(keyInfoList);
+
+        // Get the node to attach the signature.
+        Node signatureInfoNode = doc.getDocumentElement();
+     
+        // Create a signing context using the private key.
+        DOMSignContext dsc = new DOMSignContext(privateKey, signatureInfoNode);
+        dsc.setBaseURI(uri);
+		dsc.setURIDereferencer(new FileURIDereferencer(uri, xmlFile));
+
+        // Create the signature from the signing context and key info
+        XMLSignature signature = sigFactory.newXMLSignature(si, ki);
+        signature.sign(dsc);
+
+        NodeList nl = doc.getElementsByTagName("Signature");
+        if (nl.getLength() == 1) {
+        	if(signatureDoc != null && signatureDoc.getDocumentElement() != null) {
+        		Element rootEl = signatureDoc.getDocumentElement();
+        		rootEl.appendChild(signatureDoc.importNode(nl.item(0), true));
+        		write(rootEl, outputSignatureFile);
+        	} else {
+        		write(nl.item(0), outputSignatureFile);
+        	}
+        }
+	}
+	
+	private static void write(Node node, File outputFile)
+	throws IOException, TransformerException, TransformerFactoryConfigurationError, IllegalArgumentException {
+        try (Writer ssw = new FileWriter(outputFile)) {
+			TransformerFactory tf = TransformerFactory.newInstance();
+			Transformer trans = tf.newTransformer();
+			trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
+			trans.transform(new DOMSource(node), new StreamResult(ssw));
+		} catch (TransformerException | TransformerFactoryConfigurationError | IllegalArgumentException e) {
+			throw e;
+		} catch (IOException e) {
+			throw e;
+		}
+	}
+	
+	public static Document getDocument(File xmlFile)
+	throws ParserConfigurationException, SAXException, IOException {
+		DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
+		dbFactory.setNamespaceAware(true);
+		return dbFactory.newDocumentBuilder().parse(xmlFile);
+	}
+	
+	public static Document createDocument()
+	throws ParserConfigurationException, SAXException, IOException {
+		DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
+		dbFactory.setNamespaceAware(true);
+		return dbFactory.newDocumentBuilder().newDocument();
+	}
+	
+	public  static String getElementText(Document doc, String elementName) {
+		StringBuilder sb = new StringBuilder();
+		
+		if(doc != null) {
+			NodeList nl = doc.getElementsByTagName(elementName);
+			if(nl.getLength() == 1) {
+				Node element = nl.item(0);
+				for(Node child=element.getFirstChild(); child != null; child = child.getNextSibling()) {
+					if(child instanceof Text) {
+						Text text = (Text)child;
+						sb.append(text.getTextContent());	
+					}
+				}
+			}
+		}
+		
+		return sb.toString();
+	}
+	
+	private static class FileURIDereferencer implements URIDereferencer {
+		
+		private final String uri;
+		private final File xmlFile;
+		
+		public FileURIDereferencer(String uri, File xmlFile) {
+			this.uri = uri;
+			this.xmlFile = xmlFile;
+		}
+		
+		@Override
+		public Data dereference(URIReference uriReference, XMLCryptoContext context) throws URIReferenceException {
+			try {
+				if(uri.equals(uriReference.getURI())) {
+					byte[] bytes = Files.readAllBytes(xmlFile.toPath());
+					ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
+					return new OctetStreamData(inputStream);
+				}
+				return null;
+			} catch (Exception e) {
+				throw new URIReferenceException(e);
+			}
+		}
+	}
+}
diff --git a/src/main/java/org/olat/course/nodes/iq/IQEditController.java b/src/main/java/org/olat/course/nodes/iq/IQEditController.java
index 51696d617bb6f3a0148be0e27e442795f4ed0953..e9ad569c57a9f70d344c4287c60b74e1757d28f9 100644
--- a/src/main/java/org/olat/course/nodes/iq/IQEditController.java
+++ b/src/main/java/org/olat/course/nodes/iq/IQEditController.java
@@ -124,6 +124,10 @@ public class IQEditController extends ActivateableTabbableDefaultController impl
 	public final static String CONFIG_CORRECTION_MODE = "correctionMode";
 	/** Test in full window mode*/
 	public final static String CONFIG_ALLOW_ANONYM = "allowAnonym";
+	/** Digitally signed the assessment results */
+	public final static String CONFIG_DIGITAL_SIGNATURE = "digitalSignature";
+	/** Send the signature per mail */
+	public final static String CONFIG_DIGITAL_SIGNATURE_SEND_MAIL = "digitalSignatureMail";
 	
 	public final static String CORRECTION_AUTO = "auto";
 	public final static String CORRECTION_MANUAL = "manual";
diff --git a/src/main/java/org/olat/course/nodes/iq/QTI21AssessmentRunController.java b/src/main/java/org/olat/course/nodes/iq/QTI21AssessmentRunController.java
index 0898ab93d6a8b2ecaef7b9cd2a6ea0b7e319e75c..88cc14afedb32598a5c095a983a7598aeb9773bb 100644
--- a/src/main/java/org/olat/course/nodes/iq/QTI21AssessmentRunController.java
+++ b/src/main/java/org/olat/course/nodes/iq/QTI21AssessmentRunController.java
@@ -23,8 +23,10 @@ import java.io.File;
 import java.net.URI;
 import java.util.Date;
 import java.util.List;
+import java.util.Locale;
 import java.util.concurrent.atomic.AtomicBoolean;
 
+import org.olat.core.CoreSpringFactory;
 import org.olat.core.commons.fullWebApp.LayoutMain3ColsController;
 import org.olat.core.gui.UserRequest;
 import org.olat.core.gui.components.Component;
@@ -37,6 +39,10 @@ import org.olat.core.gui.control.Event;
 import org.olat.core.gui.control.WindowControl;
 import org.olat.core.gui.control.controller.BasicController;
 import org.olat.core.gui.control.generic.iframe.IFrameDisplayController;
+import org.olat.core.gui.media.FileMediaResource;
+import org.olat.core.gui.media.MediaResource;
+import org.olat.core.gui.media.NotFoundMediaResource;
+import org.olat.core.gui.translator.Translator;
 import org.olat.core.id.Identity;
 import org.olat.core.id.OLATResourceable;
 import org.olat.core.logging.activity.ThreadLocalUserActivityLogger;
@@ -46,6 +52,7 @@ import org.olat.core.util.UserSession;
 import org.olat.core.util.Util;
 import org.olat.core.util.event.EventBus;
 import org.olat.core.util.event.GenericEventListener;
+import org.olat.core.util.mail.MailBundle;
 import org.olat.core.util.resource.OresHelper;
 import org.olat.core.util.vfs.VFSContainer;
 import org.olat.course.DisposedCourseRestartController;
@@ -57,6 +64,7 @@ import org.olat.course.nodes.IQSELFCourseNode;
 import org.olat.course.nodes.IQTESTCourseNode;
 import org.olat.course.nodes.QTICourseNode;
 import org.olat.course.nodes.SelfAssessableCourseNode;
+import org.olat.course.run.environment.CourseEnvironment;
 import org.olat.course.run.scoring.ScoreEvaluation;
 import org.olat.course.run.userview.UserCourseEnvironment;
 import org.olat.fileresource.FileResourceManager;
@@ -67,6 +75,7 @@ import org.olat.ims.qti21.QTI21DeliveryOptions;
 import org.olat.ims.qti21.QTI21DeliveryOptions.ShowResultsOnFinish;
 import org.olat.ims.qti21.QTI21LoggingAction;
 import org.olat.ims.qti21.QTI21Service;
+import org.olat.ims.qti21.model.DigitalSignatureOptions;
 import org.olat.ims.qti21.ui.AssessmentResultController;
 import org.olat.ims.qti21.ui.AssessmentTestDisplayController;
 import org.olat.ims.qti21.ui.QTI21Event;
@@ -76,6 +85,7 @@ import org.olat.modules.ModuleConfiguration;
 import org.olat.modules.assessment.AssessmentEntry;
 import org.olat.modules.assessment.model.AssessmentEntryStatus;
 import org.olat.repository.RepositoryEntry;
+import org.olat.user.UserManager;
 import org.olat.util.logging.activity.LoggingResourceable;
 import org.springframework.beans.factory.annotation.Autowired;
 
@@ -90,7 +100,7 @@ public class QTI21AssessmentRunController extends BasicController implements Gen
 	private static final OLATResourceable assessmentEventOres = OresHelper.createOLATResourceableType(AssessmentEvent.class);
 	private static final OLATResourceable assessmentInstanceOres = OresHelper.createOLATResourceableType(AssessmentInstance.class);
 	
-	private Link startButton, showResultsButton, hideResultsButton;
+	private Link startButton, showResultsButton, hideResultsButton, signatureDownloadLink;
 	private final VelocityContainer mainVC;
 	
 	private boolean assessmentStopped = true;
@@ -236,6 +246,23 @@ public class QTI21AssessmentRunController extends BasicController implements Gen
 					UserNodeAuditManager am = userCourseEnv.getCourseEnvironment().getAuditManager();
 					mainVC.contextPut("log", am.getUserNodeLog(courseNode, identity));
 				}
+				
+				if(deliveryOptions.isDigitalSignature()) {
+					AssessmentTestSession session = qtiService.getAssessmentTestSession(assessmentEntry.getAssessmentId());
+					if(session != null) {
+						File signature = qtiService.getAssessmentResultSignature(session);
+						if(signature != null && signature.exists()) {
+							signatureDownloadLink = LinkFactory.createLink("digital.signature.download.link", mainVC, this);
+							signatureDownloadLink.setIconLeftCSS("o_icon o_icon-fw o_icon_download");
+							signatureDownloadLink.setTarget("_blank");
+							
+							Date issueDate = qtiService.getAssessmentResultSignatureIssueDate(session);
+							if(issueDate != null) {
+								mainVC.contextPut("signatureIssueDate", Formatter.getInstance(getLocale()).formatDateAndTime(issueDate));
+							}
+						}
+					}
+				}
 			}
 		}
 		
@@ -353,6 +380,8 @@ public class QTI21AssessmentRunController extends BasicController implements Gen
 			doShowResults(ureq);
 		} else if (source == hideResultsButton) {
 			doHideResults();
+		} else if (source == signatureDownloadLink) {
+			doDownloadSignature(ureq);
 		}
 	}
 	
@@ -422,6 +451,23 @@ public class QTI21AssessmentRunController extends BasicController implements Gen
 	private void doHideResults() {
 		mainVC.contextPut("showResults", Boolean.FALSE);
 	}
+	
+	private void doDownloadSignature(UserRequest ureq) {
+		MediaResource resource = null;
+		if(courseNode instanceof IQTESTCourseNode) {
+			IQTESTCourseNode testCourseNode = (IQTESTCourseNode)courseNode;
+			AssessmentEntry assessmentEntry = testCourseNode.getUserAssessmentEntry(userCourseEnv);
+			AssessmentTestSession session = qtiService.getAssessmentTestSession(assessmentEntry.getAssessmentId());
+			File signature = qtiService.getAssessmentResultSignature(session);
+			if(signature.exists()) {
+				resource = new FileMediaResource(signature);
+			}
+		}
+		if(resource == null) {
+			resource = new NotFoundMediaResource("");
+		}
+		ureq.getDispatchResult().setResultingMediaResource(resource);
+	}
 
 	private void doStart(UserRequest ureq) {
 		removeAsListenerAndDispose(displayCtrl);
@@ -474,6 +520,8 @@ public class QTI21AssessmentRunController extends BasicController implements Gen
 		finalOptions.setShowResultsOnFinish(ShowResultsOnFinish.fromIQEquivalent(config.getStringValue(IQEditController.CONFIG_KEY_SUMMARY), ShowResultsOnFinish.compact));
 		finalOptions.setShowMenu(config.getBooleanSafe(IQEditController.CONFIG_KEY_ENABLEMENU, testOptions.isShowMenu()));
 		finalOptions.setAllowAnonym(config.getBooleanSafe(IQEditController.CONFIG_ALLOW_ANONYM, testOptions.isAllowAnonym()));
+		finalOptions.setDigitalSignature(config.getBooleanSafe(IQEditController.CONFIG_DIGITAL_SIGNATURE, testOptions.isDigitalSignature()));
+		finalOptions.setDigitalSignatureMail(config.getBooleanSafe(IQEditController.CONFIG_DIGITAL_SIGNATURE_SEND_MAIL, testOptions.isDigitalSignatureMail()));
 		return finalOptions;
 	}
 
@@ -506,6 +554,33 @@ public class QTI21AssessmentRunController extends BasicController implements Gen
 		
 		fireEvent(ureq, event);
 	}
+	
+	@Override
+	public void decorateConfirmation(AssessmentTestSession candidateSession, DigitalSignatureOptions options, Locale locale) {
+		decorateCourseConfirmation(candidateSession, options, userCourseEnv.getCourseEnvironment(), courseNode, testEntry, locale);
+	}
+	
+	public static void decorateCourseConfirmation(AssessmentTestSession candidateSession, DigitalSignatureOptions options,
+			CourseEnvironment courseEnv, CourseNode courseNode, RepositoryEntry testEntry, Locale locale)  {
+		MailBundle bundle = new MailBundle();
+		bundle.setToId(candidateSession.getIdentity());
+		String fullname = CoreSpringFactory.getImpl(UserManager.class).getUserDisplayName(candidateSession.getIdentity());
+
+		String[] args = new String[] {
+				courseEnv.getCourseTitle(),	// {0}
+				courseNode.getShortTitle(),								// {1}
+				testEntry.getDisplayname(),								// {2}
+				fullname,			// {3}
+				Formatter.getInstance(locale).formatDateAndTime(candidateSession.getFinishTime())
+		};
+
+		Translator translator = Util.createPackageTranslator(QTI21AssessmentRunController.class, locale);
+		String subject = translator.translate("digital.signature.mail.subject", args);
+		String body = translator.translate("digital.signature.mail.body", args);
+		bundle.setContent(subject, body);
+		options.setMailBundle(bundle);
+		options.setSubIdentName(courseNode.getShortTitle());
+	}
 
 	@Override
 	public void updateOutcomes(Float score, Boolean pass) {
diff --git a/src/main/java/org/olat/course/nodes/iq/QTI21EditForm.java b/src/main/java/org/olat/course/nodes/iq/QTI21EditForm.java
index dec418afdad74f158c617caec4aaa4f6bfd37b8d..0bcf6981431cd2f45fb45cb0eee852eeea305f1b 100644
--- a/src/main/java/org/olat/course/nodes/iq/QTI21EditForm.java
+++ b/src/main/java/org/olat/course/nodes/iq/QTI21EditForm.java
@@ -38,7 +38,9 @@ import org.olat.core.util.StringHelper;
 import org.olat.ims.qti.process.AssessmentInstance;
 import org.olat.ims.qti21.QTI21DeliveryOptions;
 import org.olat.ims.qti21.QTI21DeliveryOptions.ShowResultsOnFinish;
+import org.olat.ims.qti21.QTI21Module;
 import org.olat.modules.ModuleConfiguration;
+import org.springframework.beans.factory.annotation.Autowired;
 
 /**
  * 
@@ -62,6 +64,7 @@ public class QTI21EditForm extends FormBasicController {
 	private MultipleSelectionElement displayQuestionProgressEl, displayScoreProgressEl;
 	private MultipleSelectionElement showResultsOnFinishEl;
 	private MultipleSelectionElement allowAnonymEl;
+	private MultipleSelectionElement digitalSignatureEl, digitalSignatureMailEl;
 	private SingleSelection typeShowResultsOnFinishEl;
 	private DateChooser startDateElement, endDateElement;
 	
@@ -73,6 +76,9 @@ public class QTI21EditForm extends FormBasicController {
 	
 	private static final String[] correctionModeKeys = new String[]{ "auto", "manual" };
 	
+	@Autowired
+	private QTI21Module qtiModule;
+	
 	public QTI21EditForm(UserRequest ureq, WindowControl wControl, ModuleConfiguration modConfig,
 			QTI21DeliveryOptions deliveryOptions, boolean needManulCorrection) {
 		super(ureq, wControl);
@@ -134,7 +140,24 @@ public class QTI21EditForm extends FormBasicController {
 		
 		boolean fullWindow = modConfig.getBooleanSafe(IQEditController.CONFIG_FULLWINDOW);
 		fullWindowEl = uifactory.addCheckboxesHorizontal("fullwindow", "qti.form.fullwindow", formLayout, new String[]{"x"}, new String[]{""});
-		fullWindowEl.select("x", fullWindow);
+		if(fullWindow) {
+			fullWindowEl.select("x", fullWindow);
+		}
+		
+		boolean digitalSignature = modConfig.getBooleanSafe(IQEditController.CONFIG_DIGITAL_SIGNATURE, deliveryOptions.isDigitalSignature());
+		digitalSignatureEl = uifactory.addCheckboxesHorizontal("digital.signature", "digital.signature", formLayout, new String[]{"x"}, new String[]{""});
+		if(digitalSignature) {
+			digitalSignatureEl.select("x", digitalSignature);
+		}
+		digitalSignatureEl.setVisible(qtiModule.isDigitalSignatureEnabled());
+		digitalSignatureEl.addActionListener(FormEvent.ONCHANGE);
+		
+		boolean digitalSignatureSendMail = modConfig.getBooleanSafe(IQEditController.CONFIG_DIGITAL_SIGNATURE_SEND_MAIL, deliveryOptions.isDigitalSignatureMail());
+		digitalSignatureMailEl = uifactory.addCheckboxesHorizontal("digital.signature.mail", "digital.signature.mail", formLayout, new String[]{"x"}, new String[]{""});
+		if(digitalSignatureSendMail) {
+			digitalSignatureMailEl.select("x", digitalSignatureSendMail);
+		}
+		digitalSignatureMailEl.setVisible(qtiModule.isDigitalSignatureEnabled() && digitalSignatureEl.isAtLeastSelected(1));
 
 		boolean showTitles = modConfig.getBooleanSafe(IQEditController.CONFIG_KEY_QUESTIONTITLE, deliveryOptions.isShowTitles());
 		showTitlesEl = uifactory.addCheckboxesHorizontal("showTitles", "qti.form.questiontitle", formLayout, onKeys, onValues);
@@ -285,6 +308,8 @@ public class QTI21EditForm extends FormBasicController {
 			update();
 		} else if(showResultsDateDependentButton == source || showResultsOnHomePage == source) {
 			update();
+		} else if(digitalSignatureEl == source) {
+			digitalSignatureMailEl.setVisible(digitalSignatureEl.isAtLeastSelected(1));
 		}
 		super.formInnerEvent(ureq, source, event);
 	}
@@ -330,6 +355,14 @@ public class QTI21EditForm extends FormBasicController {
 		modConfig.setBooleanEntry(IQEditController.CONFIG_KEY_SCOREPROGRESS, displayScoreProgressEl.isSelected(0));
 		modConfig.setBooleanEntry(IQEditController.CONFIG_ALLOW_ANONYM, allowAnonymEl.isSelected(0));
 		
+		if(qtiModule.isDigitalSignatureEnabled() && digitalSignatureEl.isSelected(0)) {
+			modConfig.setBooleanEntry(IQEditController.CONFIG_DIGITAL_SIGNATURE, true);
+			modConfig.setBooleanEntry(IQEditController.CONFIG_DIGITAL_SIGNATURE_SEND_MAIL, digitalSignatureMailEl.isSelected(0));
+		} else {
+			modConfig.setBooleanEntry(IQEditController.CONFIG_DIGITAL_SIGNATURE, false);
+			modConfig.setBooleanEntry(IQEditController.CONFIG_DIGITAL_SIGNATURE_SEND_MAIL, false);
+		}
+
 		modConfig.setBooleanEntry(IQEditController.CONFIG_KEY_ENABLESCOREINFO, scoreInfo.isSelected(0));
 		modConfig.setBooleanEntry(IQEditController.CONFIG_KEY_DATE_DEPENDENT_RESULTS, showResultsDateDependentButton.isSelected(0));
 		
diff --git a/src/main/java/org/olat/course/nodes/iq/_content/assessment_run.html b/src/main/java/org/olat/course/nodes/iq/_content/assessment_run.html
index 1b0dbd4dde8d0a1cad489af8df9ac998f9792419..4ba585dd101549a552048fef032d7c42aa91c91c 100644
--- a/src/main/java/org/olat/course/nodes/iq/_content/assessment_run.html
+++ b/src/main/java/org/olat/course/nodes/iq/_content/assessment_run.html
@@ -38,6 +38,13 @@
 					#end
 					</td>
 				</tr>
+				#if($r.available("digital.signature.download.link"))
+				<tr>
+					<th>$r.translate("digital.signature.download")</th>
+					<td>$r.render("digital.signature.download.link")
+					#if($r.isNotNull($signatureIssueDate)) $r.translate("digital.signature.download.date", $signatureIssueDate)#end</td>
+				</tr>
+				#end
 			</tbody>
 			</table>
 		</div>
diff --git a/src/main/java/org/olat/course/nodes/iq/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/course/nodes/iq/_i18n/LocalStrings_de.properties
index 1096ed9c31c02041d5885e4ea81c9e4039116b60..4b5abd0328c730e97fec624db558902030a98fdc 100644
--- a/src/main/java/org/olat/course/nodes/iq/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/course/nodes/iq/_i18n/LocalStrings_de.properties
@@ -27,6 +27,13 @@ correction.manual=Manuell
 correction.mode=Korrektur
 correcttest=Test korrigieren
 coursefolder=Ablageordner Kurs "{0}"
+digital.signature=Digital Unterschrift
+digital.signature.mail=Unterschrift per Mail schicken
+digital.signature.mail.subject=Best\u00E4tigung Test {2} im Kurs {0} und baustein {1}
+digital.signature.mail.body=Best\u00E4tigung Test {0} im Kurs {0} und baustein {1}. sie finde unten als Anhang die Quittung Ihres Test.
+digital.signature.download=Digital Unterschrift
+digital.signature.download.link=Herunterladen
+digital.signature.download.date=( Erstellt am {0} )
 disclaimer=Information
 disclaimer.file.invalid=Gewisse Informationen k\u00F6nnen nicht angezeigt werden weil die referenzierte Datei {0} nicht mehr vorhanden ist. Bitte benachrichtigen Sie die Kursleitung.
 error.assessment.pulled=Das Test wurde von Ihrem Tutor eingezogen.
diff --git a/src/main/java/org/olat/course/nodes/iq/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/course/nodes/iq/_i18n/LocalStrings_en.properties
index ecaebf0b26704ab9eebe964262b84a0571d08f67..8a690c8bd55ff8e1f0d02e2d874c63bb4678dbf8 100644
--- a/src/main/java/org/olat/course/nodes/iq/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/course/nodes/iq/_i18n/LocalStrings_en.properties
@@ -27,6 +27,13 @@ correction.manual=Manual
 correction.mode=Correction
 correcttest=Correct test
 coursefolder=Storage folder of course "{0}"
+digital.signature=Digital signature
+digital.signature.mail=Send signature per mail
+digital.signature.mail.subject=Confirmation test {2} in course {0} in element {1}
+digital.signature.mail.body=Confirmation test {2} in course {0} in element {1}. The receipt of your test is below as attachment.
+digital.signature.download=Digital Signature
+digital.signature.download.link=Download
+digital.signature.download.date=( Issued the {0} )
 disclaimer=Information
 disclaimer.file.invalid=Some information cannot be displayed because the referenced file {0} is not available anymore. Please contact your course administrator.
 error.assessment.pulled=The assessment is finished.
diff --git a/src/main/java/org/olat/ims/qti21/OutcomesListener.java b/src/main/java/org/olat/ims/qti21/OutcomesListener.java
index 844b7e347d89e4cd7e34c5bbf5c58e97c049379a..a19af703d3474898c7f95b347d34d7a80a0bb733 100644
--- a/src/main/java/org/olat/ims/qti21/OutcomesListener.java
+++ b/src/main/java/org/olat/ims/qti21/OutcomesListener.java
@@ -19,6 +19,10 @@
  */
 package org.olat.ims.qti21;
 
+import java.util.Locale;
+
+import org.olat.ims.qti21.model.DigitalSignatureOptions;
+
 /**
  * 
  * Initial date: 20.05.2015<br>
@@ -27,6 +31,15 @@ package org.olat.ims.qti21;
  */
 public interface OutcomesListener {
 	
+	/**
+	 * Add more useful informations to the signature as a mail bundle to send the signature per email.
+	 * 
+	 * @param candidateSession
+	 * @param options
+	 * @param locale
+	 */
+	public void decorateConfirmation(AssessmentTestSession candidateSession, DigitalSignatureOptions options, Locale locale);
+	
 	/**
 	 * Update the outcomes.
 	 * 
diff --git a/src/main/java/org/olat/ims/qti21/QTI21DeliveryOptions.java b/src/main/java/org/olat/ims/qti21/QTI21DeliveryOptions.java
index 8f9d9720718cdcad8163ab08a04b2424d22beb94..1228f8f6f05f65f8e2a3fdbd07285f87487a94b0 100644
--- a/src/main/java/org/olat/ims/qti21/QTI21DeliveryOptions.java
+++ b/src/main/java/org/olat/ims/qti21/QTI21DeliveryOptions.java
@@ -46,6 +46,9 @@ public class QTI21DeliveryOptions {
 	
 	private boolean allowAnonym;
 	
+	private boolean digitalSignature;
+	private boolean digitalSignatureMail;
+	
 	private Integer templateProcessingLimit;
 	
 	private ShowResultsOnFinish showResultsOnFinish;
@@ -130,6 +133,22 @@ public class QTI21DeliveryOptions {
 		this.maxAttempts = maxAttempts;
 	}
 
+	public boolean isDigitalSignature() {
+		return digitalSignature;
+	}
+
+	public void setDigitalSignature(boolean digitalSignature) {
+		this.digitalSignature = digitalSignature;
+	}
+
+	public boolean isDigitalSignatureMail() {
+		return digitalSignatureMail;
+	}
+
+	public void setDigitalSignatureMail(boolean digitalSignatureMail) {
+		this.digitalSignatureMail = digitalSignatureMail;
+	}
+
 	public ShowResultsOnFinish getShowResultsOnFinish() {
 		return showResultsOnFinish;
 	}
@@ -158,6 +177,8 @@ public class QTI21DeliveryOptions {
 		defaultSettings.allowAnonym = false;
 		defaultSettings.blockAfterSuccess = false;
 		defaultSettings.maxAttempts = 0;
+		defaultSettings.digitalSignature = false;
+		defaultSettings.digitalSignatureMail = false;
 		defaultSettings.showResultsOnFinish = ShowResultsOnFinish.none;
 		return defaultSettings;
 	}
@@ -175,6 +196,8 @@ public class QTI21DeliveryOptions {
 		clone.allowAnonym = allowAnonym;
 		clone.blockAfterSuccess = blockAfterSuccess;
 		clone.maxAttempts = maxAttempts;
+		clone.digitalSignature = digitalSignature;
+		clone.digitalSignatureMail = digitalSignatureMail;
 		clone.showResultsOnFinish = showResultsOnFinish;
 		return clone;
 	}
diff --git a/src/main/java/org/olat/ims/qti21/QTI21Module.java b/src/main/java/org/olat/ims/qti21/QTI21Module.java
index 672fb4e4f5939b668683905d2b17c49af0f4e7e8..32222007dc67f4556746dd812fc03e93e1a66279 100644
--- a/src/main/java/org/olat/ims/qti21/QTI21Module.java
+++ b/src/main/java/org/olat/ims/qti21/QTI21Module.java
@@ -19,8 +19,18 @@
  */
 package org.olat.ims.qti21;
 
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+
 import org.olat.core.configuration.AbstractSpringModule;
+import org.olat.core.logging.OLog;
+import org.olat.core.logging.Tracing;
+import org.olat.core.util.FileUtils;
 import org.olat.core.util.StringHelper;
+import org.olat.core.util.WebappHelper;
 import org.olat.core.util.coordinate.CoordinatorManager;
 import org.olat.ims.qti21.repository.handlers.QTI21AssessmentTestHandler;
 import org.olat.repository.handlers.RepositoryHandlerFactory;
@@ -37,11 +47,19 @@ import org.springframework.stereotype.Service;
 @Service
 public class QTI21Module extends AbstractSpringModule {
 	
+	private static final OLog log = Tracing.createLoggerFor(QTI21Module.class);
+	
 	@Autowired
 	private QTI21AssessmentTestHandler assessmentHandler;
 	
 	@Value("${qti21.math.assessment.extension.enabled:false}")
 	private boolean mathAssessExtensionEnabled;
+	@Value("${qti21.digital.signature.enabled:false}")
+	private boolean digitalSignatureEnabled;
+	@Value("${qti21.digital.signature.certificate:}")
+	private String digitalSignatureCertificate;
+	@Value("${qti21.digital.signature.certificate.password:}")
+	private String digitalSignatureCertificatePassword;
 	
 	@Autowired
 	public QTI21Module(CoordinatorManager coordinatorManager) {
@@ -62,6 +80,21 @@ public class QTI21Module extends AbstractSpringModule {
 		if(StringHelper.containsNonWhitespace(mathExtensionObj)) {
 			mathAssessExtensionEnabled = "enabled".equals(mathExtensionObj);
 		}
+		
+		String digitalSignatureObj = getStringPropertyValue("digital.signature", true);
+		if(StringHelper.containsNonWhitespace(digitalSignatureObj)) {
+			digitalSignatureEnabled = "enabled".equals(digitalSignatureObj);
+		}
+		
+		String digitalSignatureCertificateObj = getStringPropertyValue("qti21.digital.signature.certificate", true);
+		if(StringHelper.containsNonWhitespace(digitalSignatureCertificateObj)) {
+			digitalSignatureCertificate = digitalSignatureCertificateObj;
+		}
+		
+		String digitalSignatureCertificatePasswordObj = getStringPropertyValue("qti21.digital.signature.certificate.password", true);
+		if(StringHelper.containsNonWhitespace(digitalSignatureObj)) {
+			digitalSignatureCertificatePassword = digitalSignatureCertificatePasswordObj;
+		}
 	}
 
 	public boolean isMathAssessExtensionEnabled() {
@@ -72,4 +105,49 @@ public class QTI21Module extends AbstractSpringModule {
 		mathAssessExtensionEnabled = enabled;
 		setStringProperty("math.extension", enabled ? "enabled" : "disabled", true);
 	}
+
+	public boolean isDigitalSignatureEnabled() {
+		return digitalSignatureEnabled;
+	}
+
+	public void setDigitalSignatureEnabled(boolean enabled) {
+		this.digitalSignatureEnabled = enabled;
+		setStringProperty("digital.signature", enabled ? "enabled" : "disabled", true);
+	}
+
+	public String getDigitalSignatureCertificate() {
+		return digitalSignatureCertificate;
+	}
+	
+	public File getDigitalSignatureCertificateFile() {
+		File file = new File(digitalSignatureCertificate);
+		if(!file.isAbsolute()) {
+			String userDataDirectory = WebappHelper.getUserDataRoot();
+			file = Paths.get(userDataDirectory, "system", "configuration", digitalSignatureCertificate).toFile();
+		}
+		return file;
+	}
+	
+	public void setDigitalSignatureCertificateFile(File file, String filename) {
+		try {
+			String userDataDirectory = WebappHelper.getUserDataRoot();
+			File newFile = Paths.get(userDataDirectory, "system", "configuration", filename).toFile();
+			String newName = FileUtils.rename(newFile);
+			File uniqueFile = Paths.get(userDataDirectory, "system", "configuration", newName).toFile();
+			Files.copy(file.toPath(), uniqueFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
+			digitalSignatureCertificate = uniqueFile.getName();
+			setStringProperty("qti21.digital.signature.certificate", digitalSignatureCertificate, true);
+		} catch (IOException e) {
+			log.error("", e);
+		}
+	}
+
+	public String getDigitalSignatureCertificatePassword() {
+		return digitalSignatureCertificatePassword;
+	}
+
+	public void setDigitalSignatureCertificatePassword(String digitalSignatureCertificatePassword) {
+		this.digitalSignatureCertificatePassword = digitalSignatureCertificatePassword;
+		setStringProperty("qti21.digital.signature.certificate.password", digitalSignatureCertificatePassword, true);
+	}
 }
diff --git a/src/main/java/org/olat/ims/qti21/QTI21Service.java b/src/main/java/org/olat/ims/qti21/QTI21Service.java
index 51d8bb33276c5012176b15a1cfc82c3f404bbe33..5eeec942c05fd7d432aa49e8e2fb3e7b8ac66aed 100644
--- a/src/main/java/org/olat/ims/qti21/QTI21Service.java
+++ b/src/main/java/org/olat/ims/qti21/QTI21Service.java
@@ -29,6 +29,7 @@ import java.util.Map;
 import org.olat.basesecurity.IdentityRef;
 import org.olat.core.gui.components.form.flexible.impl.MultipartFileInfos;
 import org.olat.core.id.Identity;
+import org.olat.ims.qti21.model.DigitalSignatureOptions;
 import org.olat.ims.qti21.model.ParentPartItemRefs;
 import org.olat.ims.qti21.model.ResponseLegality;
 import org.olat.ims.qti21.model.audit.CandidateEvent;
@@ -275,10 +276,32 @@ public interface QTI21Service {
 	public AssessmentTestSession recordTestAssessmentResult(AssessmentTestSession candidateSession, TestSessionState testSessionState, AssessmentResult assessmentResult,
 			AssessmentSessionAuditLogger auditLogger);
 	
-	public AssessmentTestSession finishTestSession(AssessmentTestSession candidateSession, TestSessionState testSessionState, AssessmentResult assessmentResul, Date timestamp);
+	/**
+	 * Finish the test session. The assessment result is for the last time and would not updated anymore.
+	 * 
+	 * @param candidateSession
+	 * @param testSessionState
+	 * @param assessmentResul
+	 * @param timestamp
+	 * @param digitalSignature
+	 * @param bundle
+	 * @return
+	 */
+	public AssessmentTestSession finishTestSession(AssessmentTestSession candidateSession, TestSessionState testSessionState, AssessmentResult assessmentResul,
+			Date timestamp, DigitalSignatureOptions signatureOptions, Identity assessedIdentity);
 	
 	public void cancelTestSession(AssessmentTestSession candidateSession, TestSessionState testSessionState);
 	
+	/**
+	 * Sign the assessment result. Be careful, the file must not be changed
+	 * after that!
+	 * 
+	 * @param candidateSession
+	 * @param sendMail
+	 * @param mail
+	 */
+	public void signAssessmentResult(AssessmentTestSession candidateSession, DigitalSignatureOptions signatureOptions, Identity assessedIdentity);
+	
 	public CandidateEvent recordCandidateTestEvent(AssessmentTestSession candidateSession, RepositoryEntryRef testEntry, RepositoryEntryRef entry,
 			CandidateTestEventType textEventType, TestSessionState testSessionState, NotificationRecorder notificationRecorder);
 
@@ -286,9 +309,30 @@ public interface QTI21Service {
 			CandidateTestEventType textEventType, CandidateItemEventType itemEventType,
 			TestPlanNodeKey itemKey, TestSessionState testSessionState, NotificationRecorder notificationRecorder);
 	
-	
+	/**
+	 * Return the assessment result for the specified test session.
+	 * 
+	 * @param candidateSession
+	 * @return The assessment result
+	 */
 	public AssessmentResult getAssessmentResult(AssessmentTestSession candidateSession);
 	
+	/**
+	 * Return the file where the XML Digital Signature of the assessment result
+	 * is saved or null if it not exists.
+	 * 
+	 * @return The file
+	 */
+	public File getAssessmentResultSignature(AssessmentTestSession candidateSession);
+	
+	/**
+	 * Return the issue date saved in the XML Digital Signature
+	 * 
+	 * @param candidateSession
+	 * @return
+	 */
+	public Date getAssessmentResultSignatureIssueDate(AssessmentTestSession candidateSession);
+	
 
 	public AssessmentTestSession finishItemSession(AssessmentTestSession candidateSession, AssessmentResult assessmentResul, Date timestamp);
 	
diff --git a/src/main/java/org/olat/ims/qti21/manager/AssessmentTestSessionDAO.java b/src/main/java/org/olat/ims/qti21/manager/AssessmentTestSessionDAO.java
index 6df900c283d6f24fd0f5fcc85cb8d85944e34d51..30b01e5a87d888ce9b5a17ee4d98944902d9cf1c 100644
--- a/src/main/java/org/olat/ims/qti21/manager/AssessmentTestSessionDAO.java
+++ b/src/main/java/org/olat/ims/qti21/manager/AssessmentTestSessionDAO.java
@@ -243,6 +243,12 @@ public class AssessmentTestSessionDAO {
 		return sessions == null || sessions.isEmpty() ? null : sessions.get(0);
 	}
 	
+	/**
+	 * Load the assessment test session and only fetch the user.
+	 * 
+	 * @param testSessionKey
+	 * @return
+	 */
 	public AssessmentTestSession loadFullByKey(Long testSessionKey) {
 		StringBuilder sb = new StringBuilder();
 		sb.append("select session from qtiassessmenttestsession session")
diff --git a/src/main/java/org/olat/ims/qti21/manager/QTI21ServiceImpl.java b/src/main/java/org/olat/ims/qti21/manager/QTI21ServiceImpl.java
index 3b2fa1acda3327f0733ac30bccf0a70ed03054ce..850bfd12325a7c036b93775801b5120b43d125e6 100644
--- a/src/main/java/org/olat/ims/qti21/manager/QTI21ServiceImpl.java
+++ b/src/main/java/org/olat/ims/qti21/manager/QTI21ServiceImpl.java
@@ -49,15 +49,25 @@ import javax.xml.transform.stream.StreamResult;
 import org.apache.commons.io.IOUtils;
 import org.olat.basesecurity.IdentityRef;
 import org.olat.core.gui.components.form.flexible.impl.MultipartFileInfos;
+import org.olat.core.helpers.Settings;
 import org.olat.core.id.Identity;
 import org.olat.core.id.Persistable;
+import org.olat.core.id.User;
 import org.olat.core.logging.OLATRuntimeException;
 import org.olat.core.logging.OLog;
 import org.olat.core.logging.Tracing;
 import org.olat.core.util.FileUtils;
+import org.olat.core.util.Formatter;
 import org.olat.core.util.StringHelper;
 import org.olat.core.util.cache.CacheWrapper;
+import org.olat.core.util.coordinate.Cacher;
 import org.olat.core.util.coordinate.CoordinatorManager;
+import org.olat.core.util.crypto.CryptoUtil;
+import org.olat.core.util.crypto.X509CertificatePrivateKeyPair;
+import org.olat.core.util.mail.MailBundle;
+import org.olat.core.util.mail.MailManager;
+import org.olat.core.util.mail.MailerResult;
+import org.olat.core.util.xml.XMLDigitalSignatureUtil;
 import org.olat.core.util.xml.XStreamHelper;
 import org.olat.fileresource.FileResourceManager;
 import org.olat.fileresource.types.ImsQTI21Resource;
@@ -75,6 +85,7 @@ import org.olat.ims.qti21.QTI21Module;
 import org.olat.ims.qti21.QTI21Service;
 import org.olat.ims.qti21.manager.audit.AssessmentSessionAuditFileLog;
 import org.olat.ims.qti21.manager.audit.AssessmentSessionAuditOLog;
+import org.olat.ims.qti21.model.DigitalSignatureOptions;
 import org.olat.ims.qti21.model.InMemoryAssessmentTestMarks;
 import org.olat.ims.qti21.model.InMemoryAssessmentTestSession;
 import org.olat.ims.qti21.model.ParentPartItemRefs;
@@ -92,6 +103,7 @@ import org.springframework.beans.factory.InitializingBean;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.w3c.dom.Document;
+import org.w3c.dom.Node;
 
 import com.thoughtworks.xstream.XStream;
 
@@ -168,6 +180,8 @@ public class QTI21ServiceImpl implements QTI21Service, UserDataDeletable, Initia
 	private QTI21Module qtiModule;
 	@Autowired
 	private CoordinatorManager coordinatorManager;
+	@Autowired
+	private MailManager mailManager;
 	
 
 	private JqtiExtensionManager jqtiExtensionManager;
@@ -197,8 +211,9 @@ public class QTI21ServiceImpl implements QTI21Service, UserDataDeletable, Initia
         
         jqtiExtensionManager.init();
 
-        assessmentTestsCache = coordinatorManager.getInstance().getCoordinator().getCacher().getCache("QTIWorks", "assessmentTests");
-        assessmentItemsCache = coordinatorManager.getInstance().getCoordinator().getCacher().getCache("QTIWorks", "assessmentItems");
+        Cacher cacher = coordinatorManager.getInstance().getCoordinator().getCacher();
+        assessmentTestsCache = cacher.getCache("QTIWorks", "assessmentTests");
+        assessmentItemsCache = cacher.getCache("QTIWorks", "assessmentItems");
 	}
 
     @Override
@@ -595,7 +610,9 @@ public class QTI21ServiceImpl implements QTI21Service, UserDataDeletable, Initia
 	public AssessmentTestSession recordTestAssessmentResult(AssessmentTestSession candidateSession, TestSessionState testSessionState,
 			AssessmentResult assessmentResult, AssessmentSessionAuditLogger auditLogger) {
 		// First record full result XML to filesystem
-        storeAssessmentResultFile(candidateSession, assessmentResult);
+		if(candidateSession.getFinishTime() == null) {
+			storeAssessmentResultFile(candidateSession, assessmentResult);
+		}
         // Then record test outcome variables to DB
         recordOutcomeVariables(candidateSession, assessmentResult.getTestResult(), auditLogger);
         // Set duration
@@ -606,9 +623,115 @@ public class QTI21ServiceImpl implements QTI21Service, UserDataDeletable, Initia
 		}
 		return candidateSession;
 	}
+	
+	@Override
+	public void signAssessmentResult(AssessmentTestSession candidateSession, DigitalSignatureOptions signatureOptions, Identity assessedIdentity) {
+		if(!qtiModule.isDigitalSignatureEnabled() || !signatureOptions.isDigitalSignature()) return;//nothing to do
+		
+		try {
+			File resultFile = getAssessmentResultFile(candidateSession);
+			File signatureFile = new File(resultFile.getParentFile(), "assessmentResultSignature.xml");
+			File certificateFile = qtiModule.getDigitalSignatureCertificateFile();
+			X509CertificatePrivateKeyPair kp =CryptoUtil.getX509CertificatePrivateKeyPairPfx(
+					certificateFile, qtiModule.getDigitalSignatureCertificatePassword());
+			
+			StringBuilder uri = new StringBuilder();
+			uri.append(Settings.getServerContextPathURI()).append("/")
+			   .append("RepositoryEntry/").append(candidateSession.getRepositoryEntry().getKey());
+			if(StringHelper.containsNonWhitespace(candidateSession.getSubIdent())) {
+				uri.append("/CourseNode/").append(candidateSession.getSubIdent());
+			}
+			uri.append("/TestSession/").append(candidateSession.getKey())
+			   .append("/assessmentResult.xml");
+			Document signatureDoc = createSignatureDocumentWrapper(uri.toString(), assessedIdentity, signatureOptions);
+			
+			XMLDigitalSignatureUtil.signDetached(uri.toString(), resultFile, signatureFile, signatureDoc,
+					certificateFile.getName(), kp.getX509Cert(), kp.getPrivateKey());
+			
+			if(signatureOptions.isDigitalSignature() && signatureOptions.getMailBundle() != null) {
+				MailBundle mail = signatureOptions.getMailBundle();
+				List<File> attachments = new ArrayList<>(2);
+				attachments.add(signatureFile);
+				mail.getContent().setAttachments(attachments);
+				MailerResult result = mailManager.sendMessage(mail);
+				if(result.getReturnCode() != MailerResult.OK) {
+					log.error("Confirmation mail cannot be send");
+				}
+			}
+			
+		} catch (Exception e) {
+			log.error("", e);
+		}
+	}
+	
+	private Document createSignatureDocumentWrapper(String url, Identity assessedIdentity, DigitalSignatureOptions signatureOptions) {
+		try {
+			Document signatureDocument = XMLDigitalSignatureUtil.createDocument();
+			Node rootNode = signatureDocument.appendChild(signatureDocument.createElement("assessmentTestSignature"));
+			Node urlNode = rootNode.appendChild(signatureDocument.createElement("url"));
+			urlNode.appendChild(signatureDocument.createTextNode(url));
+			Node dateNode = rootNode.appendChild(signatureDocument.createElement("date"));
+			dateNode.appendChild(signatureDocument.createTextNode(Formatter.formatDatetime(new Date())));
+
+			if(signatureOptions.getEntry() != null) {
+				Node courseNode = rootNode.appendChild(signatureDocument.createElement("course"));
+				courseNode.appendChild(signatureDocument.createTextNode(signatureOptions.getEntry().getDisplayname()));
+			}
+			if(signatureOptions.getSubIdentName() != null) {
+				Node courseNodeNode = rootNode.appendChild(signatureDocument.createElement("courseNode"));
+				courseNodeNode.appendChild(signatureDocument.createTextNode(signatureOptions.getSubIdentName()));
+			}
+			if(signatureOptions.getTestEntry() != null) {
+				Node testNode = rootNode.appendChild(signatureDocument.createElement("test"));
+				testNode.appendChild(signatureDocument.createTextNode(signatureOptions.getTestEntry().getDisplayname()));
+			}
+			
+			if(assessedIdentity != null && assessedIdentity.getUser() != null) {
+				User user = assessedIdentity.getUser();
+				Node firstNameNode = rootNode.appendChild(signatureDocument.createElement("firstName"));
+				firstNameNode.appendChild(signatureDocument.createTextNode(user.getFirstName()));
+				Node lastNameNode = rootNode.appendChild(signatureDocument.createElement("lastName"));
+				lastNameNode.appendChild(signatureDocument.createTextNode(user.getLastName()));
+			}
+
+			return signatureDocument;
+		} catch ( Exception e) {
+			log.error("", e);
+			return null;
+		}
+	}
+
+	@Override
+	public File getAssessmentResultSignature(AssessmentTestSession candidateSession) {
+		File resultFile = getAssessmentResultFile(candidateSession);
+		File signatureFile = new File(resultFile.getParentFile(), "assessmentResultSignature.xml");
+		return signatureFile.exists() ? signatureFile : null;
+	}
 
 	@Override
-	public AssessmentTestSession finishTestSession(AssessmentTestSession candidateSession, TestSessionState testSessionState, AssessmentResult assessmentResul, Date timestamp) {
+	public Date getAssessmentResultSignatureIssueDate(AssessmentTestSession candidateSession) {
+		Date issueDate = null;
+		File signatureFile = null;
+		try {
+			signatureFile = getAssessmentResultSignature(candidateSession);
+			if(signatureFile != null) {
+				Document doc = XMLDigitalSignatureUtil.getDocument(signatureFile);
+				if(doc != null) {
+					String date = XMLDigitalSignatureUtil.getElementText(doc, "date");
+					if(StringHelper.containsNonWhitespace(date)) {
+						issueDate = Formatter.parseDatetime(date);
+					}
+				}
+			}
+		} catch (Exception e) {
+			log.error("Cannot read the issue date of the signature: " + signatureFile, e);
+		}
+		return issueDate;
+	}
+
+	@Override
+	public AssessmentTestSession finishTestSession(AssessmentTestSession candidateSession, TestSessionState testSessionState, AssessmentResult assessmentResult,
+			Date timestamp, DigitalSignatureOptions digitalSignature, Identity assessedIdentity) {
 		/* Mark session as finished */
         candidateSession.setFinishTime(timestamp);
         // Set duration
@@ -620,6 +743,11 @@ public class QTI21ServiceImpl implements QTI21Service, UserDataDeletable, Initia
 		if(candidateSession instanceof Persistable) {
 			candidateSession = testSessionDao.update(candidateSession);
 		}
+		
+		storeAssessmentResultFile(candidateSession, assessmentResult);
+		if(qtiModule.isDigitalSignatureEnabled() && digitalSignature.isDigitalSignature()) {
+    		signAssessmentResult(candidateSession, digitalSignature, assessedIdentity);
+    	}
 
         /* Finally schedule LTI result return (if appropriate and sane) */
         //maybeScheduleLtiOutcomes(candidateSession, assessmentResult);
diff --git a/src/main/java/org/olat/ims/qti21/model/DigitalSignatureOptions.java b/src/main/java/org/olat/ims/qti21/model/DigitalSignatureOptions.java
new file mode 100644
index 0000000000000000000000000000000000000000..920986c46ad3a5ae5981447becc4db00fd62cf3f
--- /dev/null
+++ b/src/main/java/org/olat/ims/qti21/model/DigitalSignatureOptions.java
@@ -0,0 +1,79 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.ims.qti21.model;
+
+import org.olat.core.util.mail.MailBundle;
+import org.olat.repository.RepositoryEntry;
+
+/**
+ * 
+ * Initial date: 17 févr. 2017<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class DigitalSignatureOptions {
+	
+	private final boolean digitalSignature;
+	private final boolean sendMail;
+	private MailBundle mailBundle;
+	
+	private String subIdentName;
+	private final RepositoryEntry entry;
+	private final RepositoryEntry testEntry;
+	
+	public DigitalSignatureOptions(boolean digitalSignature, boolean sendMail, RepositoryEntry entry, RepositoryEntry testEntry) {
+		this.digitalSignature = digitalSignature;
+		this.sendMail = sendMail;
+		this.entry = entry;
+		this.testEntry = testEntry;
+	}
+
+	public boolean isDigitalSignature() {
+		return digitalSignature;
+	}
+
+	public boolean isSendMail() {
+		return sendMail;
+	}
+
+	public String getSubIdentName() {
+		return subIdentName;
+	}
+
+	public RepositoryEntry getEntry() {
+		return entry;
+	}
+
+	public RepositoryEntry getTestEntry() {
+		return testEntry;
+	}
+
+	public void setSubIdentName(String subIdentName) {
+		this.subIdentName = subIdentName;
+	}
+
+	public MailBundle getMailBundle() {
+		return mailBundle;
+	}
+	
+	public void setMailBundle(MailBundle mailBundle) {
+		this.mailBundle = mailBundle;
+	}
+}
diff --git a/src/main/java/org/olat/ims/qti21/model/InMemoryOutcomeListener.java b/src/main/java/org/olat/ims/qti21/model/InMemoryOutcomeListener.java
index a5b7cab32cb12397b1c65cf3e596c7d60b5f4236..7e0733a34de7ed0e145220ff1adb7477a64cf6df 100644
--- a/src/main/java/org/olat/ims/qti21/model/InMemoryOutcomeListener.java
+++ b/src/main/java/org/olat/ims/qti21/model/InMemoryOutcomeListener.java
@@ -19,6 +19,9 @@
  */
 package org.olat.ims.qti21.model;
 
+import java.util.Locale;
+
+import org.olat.ims.qti21.AssessmentTestSession;
 import org.olat.ims.qti21.OutcomesListener;
 
 /**
@@ -29,6 +32,11 @@ import org.olat.ims.qti21.OutcomesListener;
  */
 public class InMemoryOutcomeListener implements OutcomesListener {
 
+	@Override
+	public void decorateConfirmation(AssessmentTestSession candidateSession, DigitalSignatureOptions options, Locale locale) {
+		//do nothing
+	}
+
 	@Override
 	public void updateOutcomes(Float score, Boolean pass) {
 		//
diff --git a/src/main/java/org/olat/ims/qti21/ui/AssessmentEntryOutcomesListener.java b/src/main/java/org/olat/ims/qti21/ui/AssessmentEntryOutcomesListener.java
index 9a5d76044d15f2e843be71b21a4c713569073100..b19a9fd6c3e5b5a31e4c5431a7a4d5770077e582 100644
--- a/src/main/java/org/olat/ims/qti21/ui/AssessmentEntryOutcomesListener.java
+++ b/src/main/java/org/olat/ims/qti21/ui/AssessmentEntryOutcomesListener.java
@@ -20,14 +20,24 @@
 package org.olat.ims.qti21.ui;
 
 import java.math.BigDecimal;
+import java.util.Locale;
 import java.util.concurrent.atomic.AtomicBoolean;
 
+import org.olat.core.CoreSpringFactory;
+import org.olat.core.gui.translator.Translator;
 import org.olat.core.logging.activity.ThreadLocalUserActivityLogger;
+import org.olat.core.util.Formatter;
+import org.olat.core.util.Util;
+import org.olat.core.util.mail.MailBundle;
+import org.olat.ims.qti21.AssessmentTestSession;
 import org.olat.ims.qti21.OutcomesListener;
 import org.olat.ims.qti21.QTI21LoggingAction;
+import org.olat.ims.qti21.model.DigitalSignatureOptions;
 import org.olat.modules.assessment.AssessmentEntry;
 import org.olat.modules.assessment.AssessmentService;
 import org.olat.modules.assessment.model.AssessmentEntryStatus;
+import org.olat.repository.RepositoryEntry;
+import org.olat.user.UserManager;
 
 /**
  * 
@@ -53,7 +63,34 @@ public class AssessmentEntryOutcomesListener implements OutcomesListener {
 		this.authorMode = authorMode;
 		this.needManualCorrection = needManualCorrection;
 	}
+
+	@Override
+	public void decorateConfirmation(AssessmentTestSession candidateSession, DigitalSignatureOptions options, Locale locale) {
+		decorateResourceConfirmation(candidateSession, options, locale);
+	}
 	
+	public static void decorateResourceConfirmation(AssessmentTestSession candidateSession, DigitalSignatureOptions options, Locale locale) {
+		MailBundle bundle = new MailBundle();
+		bundle.setToId(candidateSession.getIdentity());
+		String fullname = CoreSpringFactory.getImpl(UserManager.class).getUserDisplayName(candidateSession.getIdentity());
+		
+		Translator translator = Util.createPackageTranslator(QTI21RuntimeController.class, locale);
+		RepositoryEntry entry = candidateSession.getRepositoryEntry();
+		RepositoryEntry testEntry = candidateSession.getTestEntry();
+		String[] args = new String[] {
+				entry.getDisplayname(),			// {0}
+				"",								// {1}
+				testEntry.getDisplayname(),		// {2}
+				fullname,						// {3}
+				Formatter.getInstance(locale).formatDateAndTime(candidateSession.getFinishTime())
+		};
+
+		String subject = translator.translate("digital.signature.mail.subject", args);
+		String body = translator.translate("digital.signature.mail.body", args);
+		bundle.setContent(subject, body);
+		options.setMailBundle(bundle);
+	}
+
 	@Override
 	public void updateOutcomes(Float updatedScore, Boolean updatedPassed) {
 		AssessmentEntryStatus assessmentStatus = AssessmentEntryStatus.inProgress;
diff --git a/src/main/java/org/olat/ims/qti21/ui/AssessmentResultController.java b/src/main/java/org/olat/ims/qti21/ui/AssessmentResultController.java
index 3c3e82d93fba305242c08b9770a4400dd981abf2..de5b90c97f0e3f1d311b4e7246e4ffdc10d3ffe7 100644
--- a/src/main/java/org/olat/ims/qti21/ui/AssessmentResultController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/AssessmentResultController.java
@@ -27,7 +27,10 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import javax.servlet.http.HttpServletRequest;
+
 import org.olat.admin.user.UserShortDescription;
+import org.olat.core.dispatcher.mapper.Mapper;
 import org.olat.core.gui.UserRequest;
 import org.olat.core.gui.components.Component;
 import org.olat.core.gui.components.form.flexible.FormItemContainer;
@@ -37,7 +40,11 @@ import org.olat.core.gui.control.Controller;
 import org.olat.core.gui.control.Event;
 import org.olat.core.gui.control.WindowControl;
 import org.olat.core.gui.control.creator.ControllerCreator;
+import org.olat.core.gui.media.FileMediaResource;
+import org.olat.core.gui.media.MediaResource;
+import org.olat.core.gui.media.NotFoundMediaResource;
 import org.olat.core.id.Identity;
+import org.olat.core.util.CodeHelper;
 import org.olat.core.util.StringHelper;
 import org.olat.course.assessment.AssessmentHelper;
 import org.olat.fileresource.types.ImsQTI21Resource;
@@ -88,8 +95,9 @@ import uk.ac.ed.ph.jqtiplus.xmlutils.locators.ResourceLocator;
  *
  */
 public class AssessmentResultController extends FormBasicController {
-
+	
 	private final String mapperUri;
+	private String signatureMapperUri;
 	private final ShowResultsOnFinish resultsOnfinish;
 	
 	private final boolean anonym;
@@ -138,6 +146,12 @@ public class AssessmentResultController extends FormBasicController {
 		
 		resolvedAssessmentTest = qtiService.loadAndResolveAssessmentTest(fUnzippedDirRoot, false, false);
 		
+		File signature = qtiService.getAssessmentResultSignature(candidateSession);
+		if(signature != null) {
+			signatureMapperUri = registerCacheableMapper(null, "QTI21Signature::" + CodeHelper.getForeverUniqueID(),
+					new SignatureMapper(signature));
+		}
+		
 		testSessionState = qtiService.loadTestSessionState(candidateSession);
 		assessmentResult = qtiService.getAssessmentResult(candidateSession);
 		candidateSessionContext = new TerminatedStaticCandidateSessionContext(candidateSession);
@@ -172,6 +186,11 @@ public class AssessmentResultController extends FormBasicController {
 			if(testResult != null) {
 				extractOutcomeVariable(testResult.getItemVariables(), results);
 			}
+			
+			if(signatureMapperUri != null) {
+				String signatureUrl = signatureMapperUri + "/assessmentResultSignature.xml";
+				layoutCont.contextPut("signatureUrl", signatureUrl);
+			}
 
 			if(resultsOnfinish == ShowResultsOnFinish.sections || resultsOnfinish == ShowResultsOnFinish.details) {
 				initFormSections(layoutCont);
@@ -486,4 +505,25 @@ public class AssessmentResultController extends FormBasicController {
 			return interactionResults;
 		}
 	}
+	
+	public class SignatureMapper implements Mapper {
+		
+		private final File signature;
+		
+		public SignatureMapper(File signature) {
+			this.signature = signature;
+		}
+
+		@Override
+		public MediaResource handle(String relPath, HttpServletRequest request) {
+
+			MediaResource resource;
+			if(signature.exists()) {
+				resource = new FileMediaResource(signature);
+			} else {
+				resource = new NotFoundMediaResource(relPath);
+			}
+			return resource;
+		}
+	}
 }
\ No newline at end of file
diff --git a/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java b/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java
index ecfbb805bb054038cb82b9111a256aa03df4cb18..54bb968ceed65709175492fafd78f380a0fe15c7 100644
--- a/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java
@@ -74,8 +74,10 @@ import org.olat.ims.qti21.OutcomesListener;
 import org.olat.ims.qti21.QTI21Constants;
 import org.olat.ims.qti21.QTI21DeliveryOptions;
 import org.olat.ims.qti21.QTI21DeliveryOptions.ShowResultsOnFinish;
+import org.olat.ims.qti21.QTI21Module;
 import org.olat.ims.qti21.QTI21Service;
 import org.olat.ims.qti21.manager.ResponseFormater;
+import org.olat.ims.qti21.model.DigitalSignatureOptions;
 import org.olat.ims.qti21.model.InMemoryAssessmentTestMarks;
 import org.olat.ims.qti21.model.ParentPartItemRefs;
 import org.olat.ims.qti21.model.ResponseLegality;
@@ -98,11 +100,11 @@ import org.springframework.beans.factory.annotation.Autowired;
 import uk.ac.ed.ph.jqtiplus.JqtiPlus;
 import uk.ac.ed.ph.jqtiplus.exception.QtiCandidateStateException;
 import uk.ac.ed.ph.jqtiplus.node.item.interaction.Interaction;
-import uk.ac.ed.ph.jqtiplus.node.result.AbstractResult;
 import uk.ac.ed.ph.jqtiplus.node.result.AssessmentResult;
 import uk.ac.ed.ph.jqtiplus.node.result.ItemResult;
 import uk.ac.ed.ph.jqtiplus.node.result.ItemVariable;
 import uk.ac.ed.ph.jqtiplus.node.result.OutcomeVariable;
+import uk.ac.ed.ph.jqtiplus.node.result.TestResult;
 import uk.ac.ed.ph.jqtiplus.node.test.AssessmentTest;
 import uk.ac.ed.ph.jqtiplus.node.test.NavigationMode;
 import uk.ac.ed.ph.jqtiplus.node.test.SubmissionMode;
@@ -179,6 +181,8 @@ public class AssessmentTestDisplayController extends BasicController implements
 	private OutcomesListener outcomesListener;
 	private AssessmentSessionAuditLogger candidateAuditLogger;
 
+	@Autowired
+	private QTI21Module qtiModule;
 	@Autowired
 	private QTI21Service qtiService;
 	@Autowired
@@ -243,7 +247,8 @@ public class AssessmentTestDisplayController extends BasicController implements
 		/* Handle immediate end of test session */
         if (testSessionController.getTestSessionState() != null && testSessionController.getTestSessionState().isEnded()) {
         	AssessmentResult assessmentResult = null;
-            qtiService.finishTestSession(candidateSession, testSessionController.getTestSessionState(), assessmentResult, currentRequestTimestamp);
+            qtiService.finishTestSession(candidateSession, testSessionController.getTestSessionState(), assessmentResult,
+            		currentRequestTimestamp, getDigitalSignatureOptions(), getIdentity());
         	mainVC = createVelocityContainer("end");
         } else {
         	mainVC = createVelocityContainer("run");
@@ -680,7 +685,8 @@ public class AssessmentTestDisplayController extends BasicController implements
 
 	    /* If we ended the testPart and there are now no more available testParts, then finish the session now */
 	    if (nextItemNode==null && testSessionController.findNextEnterableTestPart()==null) {
-	    	candidateSession = qtiService.finishTestSession(candidateSession, testSessionState, assessmentResult, requestTimestamp);
+	    	candidateSession = qtiService.finishTestSession(candidateSession, testSessionState, assessmentResult,
+	    			requestTimestamp, getDigitalSignatureOptions(), getIdentity());
 	    }
 
 	    // Record and log event 
@@ -892,7 +898,8 @@ public class AssessmentTestDisplayController extends BasicController implements
         // Record current result state
 	    final AssessmentResult assessmentResult = computeAndRecordTestAssessmentResult(ureq, testSessionState, nextTestPart == null);
         if(nextTestPart == null) {
-        	candidateSession = qtiService.finishTestSession(candidateSession, testSessionState, assessmentResult, requestTimestamp);
+        	candidateSession = qtiService.finishTestSession(candidateSession, testSessionState, assessmentResult,
+        			requestTimestamp, getDigitalSignatureOptions(), getIdentity());
         }
 	}
 	
@@ -956,8 +963,7 @@ public class AssessmentTestDisplayController extends BasicController implements
                 testSessionController.exitTest(currentTimestamp);
                 candidateSession.setTerminationTime(currentTimestamp);
                 candidateSession = qtiService.updateAssessmentTestSession(candidateSession);
-            }
-            else {
+            } else {
                 eventType = CandidateTestEventType.ADVANCE_TEST_PART;
             }
         }
@@ -1083,7 +1089,8 @@ public class AssessmentTestDisplayController extends BasicController implements
 
         /* Handle immediate end of test session */
         if (ended) {
-            qtiService.finishTestSession(candidateSession, testSessionState, assessmentResult, timestamp);
+            qtiService.finishTestSession(candidateSession, testSessionState, assessmentResult,
+            		timestamp, getDigitalSignatureOptions(), getIdentity());
         } else {
         	TestPart currentTestPart = testSessionController.getCurrentTestPart();
         	if(currentTestPart != null && currentTestPart.getNavigationMode() == NavigationMode.NONLINEAR) {
@@ -1097,6 +1104,16 @@ public class AssessmentTestDisplayController extends BasicController implements
         return testSessionController;
 	}
 	
+	private DigitalSignatureOptions getDigitalSignatureOptions() {
+		boolean sendMail = deliveryOptions.isDigitalSignatureMail();
+		boolean digitalSignature = deliveryOptions.isDigitalSignature() && qtiModule.isDigitalSignatureEnabled();
+		DigitalSignatureOptions options = new DigitalSignatureOptions(digitalSignature, sendMail, entry, testEntry);
+		if(digitalSignature) {
+			outcomesListener.decorateConfirmation(candidateSession, options, getLocale());
+		}
+		return options;
+	}
+	
 	private TestSessionController createNewTestSessionStateAndController(NotificationRecorder notificationRecorder) {
 		TestProcessingMap testProcessingMap = getTestProcessingMap();
 		/* Generate a test plan for this session */
@@ -1175,7 +1192,7 @@ public class AssessmentTestDisplayController extends BasicController implements
 		return assessmentResult;
 	}
 	
-	private void processOutcomeVariables(AbstractResult resultNode, boolean submit) {
+	private void processOutcomeVariables(TestResult resultNode, boolean submit) {
 		Float score = null;
 		Boolean pass = null;
 		
@@ -1496,14 +1513,14 @@ public class AssessmentTestDisplayController extends BasicController implements
 			fireEvent(ureq, new Event("suspend"));
 		}
 		
-		private void doSaveMenuWidth(UserRequest ureq, String menuWidth) {
-			this.menuWidth = menuWidth;
-			if(StringHelper.containsNonWhitespace(menuWidth)) {
-				flc.contextPut("menuWidth", menuWidth);
+		private void doSaveMenuWidth(UserRequest ureq, String newMenuWidth) {
+			this.menuWidth = newMenuWidth;
+			if(StringHelper.containsNonWhitespace(newMenuWidth)) {
+				flc.contextPut("menuWidth", newMenuWidth);
 				if(testEntry != null) {
 					UserSession usess = ureq.getUserSession();
 					if (usess.isAuthenticated() && !usess.getRoles().isGuestOnly()) {
-						usess.getGuiPreferences().putAndSave(this.getClass(), getMenuPrefsKey(), menuWidth);
+						usess.getGuiPreferences().putAndSave(this.getClass(), getMenuPrefsKey(), newMenuWidth);
 					}
 				}
 			}
diff --git a/src/main/java/org/olat/ims/qti21/ui/QTI21AdminController.java b/src/main/java/org/olat/ims/qti21/ui/QTI21AdminController.java
index 0f3c58a3850ee812070cdceaf37da6e9d1a5c05e..d48800df2d51ab3c913676eba47b7bf72813edef 100644
--- a/src/main/java/org/olat/ims/qti21/ui/QTI21AdminController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/QTI21AdminController.java
@@ -19,14 +19,22 @@
  */
 package org.olat.ims.qti21.ui;
 
+import java.io.File;
+
 import org.olat.core.gui.UserRequest;
 import org.olat.core.gui.components.form.flexible.FormItem;
 import org.olat.core.gui.components.form.flexible.FormItemContainer;
+import org.olat.core.gui.components.form.flexible.elements.FileElement;
 import org.olat.core.gui.components.form.flexible.elements.MultipleSelectionElement;
+import org.olat.core.gui.components.form.flexible.elements.TextElement;
 import org.olat.core.gui.components.form.flexible.impl.FormBasicController;
 import org.olat.core.gui.components.form.flexible.impl.FormEvent;
+import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer;
 import org.olat.core.gui.control.Controller;
 import org.olat.core.gui.control.WindowControl;
+import org.olat.core.util.StringHelper;
+import org.olat.core.util.crypto.CryptoUtil;
+import org.olat.core.util.crypto.X509CertificatePrivateKeyPair;
 import org.olat.ims.qti21.QTI21Module;
 import org.springframework.beans.factory.annotation.Autowired;
 
@@ -38,17 +46,21 @@ import org.springframework.beans.factory.annotation.Autowired;
  *
  */
 public class QTI21AdminController extends FormBasicController {
+	
+	private static final String PASSWORD_PLACEHOLDER = "xOOx32x00x";
 
-	private static final String[] mathExtensionKeys = new String[]{ "on" };
+	private static final String[] onKeys = new String[]{ "on" };
+	private static final String[] onValues = new String[]{ "" };
 	
-	private MultipleSelectionElement mathExtensionEl;
+	private MultipleSelectionElement mathExtensionEl, digitalSignatureEl;
+	private FileElement certificateEl;
+	private TextElement certificatePasswordEl;
 	
 	@Autowired
 	private QTI21Module qtiModule;
 	
 	public QTI21AdminController(UserRequest ureq, WindowControl wControl) {
 		super(ureq, wControl);
-		
 		initForm(ureq);
 	}
 
@@ -56,36 +68,116 @@ public class QTI21AdminController extends FormBasicController {
 	protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
 		setFormTitle("admin.title");
 		
-		String[] mathExtensionValues = new String[]{ "" };
+		digitalSignatureEl = uifactory.addCheckboxesHorizontal("digital.signature", "digital.signature", formLayout,
+				onKeys, onValues);
+		if(qtiModule.isDigitalSignatureEnabled()) {
+			digitalSignatureEl.select(onKeys[0], true);
+		}
+		digitalSignatureEl.setExampleKey("digital.signature.text", null);
+		digitalSignatureEl.addActionListener(FormEvent.ONCHANGE);
+		
+		certificateEl = uifactory.addFileElement(getWindowControl(), "digital.signature.certificate", "digital.signature.certificate", formLayout);
+		certificateEl.setExampleKey("digital.signature.certificate.example", null);
+		certificateEl.setHelpText(translate("digital.signature.certificate.hint"));
+		if(StringHelper.containsNonWhitespace(qtiModule.getDigitalSignatureCertificate())) {
+			File certificate = qtiModule.getDigitalSignatureCertificateFile();
+			certificateEl.setInitialFile(certificate);
+		}
+		
+		String certificatePassword = qtiModule.getDigitalSignatureCertificatePassword();
+		String password = StringHelper.containsNonWhitespace(certificatePassword) ? PASSWORD_PLACEHOLDER : "";
+		certificatePasswordEl = uifactory.addPasswordElement("digital.signature.certificate.password", "digital.signature.certificate.password",
+				256, password, formLayout);
+
 		mathExtensionEl = uifactory.addCheckboxesHorizontal("math.extension", "math.extension", formLayout,
-				mathExtensionKeys, mathExtensionValues);
+				onKeys, onValues);
 		if(qtiModule.isMathAssessExtensionEnabled()) {
-			mathExtensionEl.select(mathExtensionKeys[0], true);
+			mathExtensionEl.select(onKeys[0], true);
 		}
 		mathExtensionEl.setExampleKey("math.extension.text", null);
 		mathExtensionEl.addActionListener(FormEvent.ONCHANGE);
+		
+		FormLayoutContainer buttonsCont = FormLayoutContainer.createButtonLayout("buttons", getTranslator());
+		formLayout.add(buttonsCont);
+		uifactory.addFormSubmitButton("save", buttonsCont);
+	}
+	
+	private void updateUI() {
+		certificateEl.setVisible(digitalSignatureEl.isSelected(0));
+		certificatePasswordEl.setVisible(digitalSignatureEl.isSelected(0));
 	}
 	
 	@Override
 	protected void doDispose() {
 		//
 	}
+
+	@Override
+	protected boolean validateFormLogic(UserRequest ureq) {
+		boolean allOk = true;
+		
+		if(certificateEl.getUploadFile() != null) {
+			File uploadedCertificate = certificateEl.getUploadFile();
+			if(uploadedCertificate != null && uploadedCertificate.exists()) {
+				validateCertificatePassword(uploadedCertificate);
+			}
+		} else {
+			String password = certificatePasswordEl.getValue();
+			if(!PASSWORD_PLACEHOLDER.equals(password) && certificateEl.getInitialFile() != null) {
+				validateCertificatePassword(certificateEl.getInitialFile());
+			}
+		}
+		return allOk & super.validateFormLogic(ureq);
+	}
 	
+	private boolean validateCertificatePassword(File file) {
+		boolean allOk = true;
+		
+		try {
+			String password = certificatePasswordEl.getValue();
+			X509CertificatePrivateKeyPair kp = CryptoUtil.getX509CertificatePrivateKeyPairPfx(file, password);
+			if(kp.getX509Cert() == null) {
+				certificateEl.setErrorKey("error.digital.certificate.noX509", null);
+				allOk &= false;
+			} else if(kp.getPrivateKey() == null) {
+				certificateEl.setErrorKey("error.digital.certificate.noPrivateKey", null);
+				allOk &= false;
+			}
+		} catch (Exception e) {
+			logError("", e);
+			String message = e.getMessage() == null ? "" : e.getMessage();
+			String [] errorArgs = new String[]{ message };
+			certificateEl.setErrorKey("error.digital.certificate.cannotread", errorArgs);
+			allOk &= false;
+		}
+		
+		return allOk;
+	}
+
 	@Override
 	protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) {
-		if(mathExtensionEl == source) {
-			qtiModule.setMathAssessExtensionEnabled(mathExtensionEl.isSelected(0));
+		if(digitalSignatureEl == source) {
+			updateUI();
 		}
 		super.formInnerEvent(ureq, source, event);
 	}
 
 	@Override
 	protected void formOK(UserRequest ureq) {
-		//
+		qtiModule.setMathAssessExtensionEnabled(mathExtensionEl.isSelected(0));
+		qtiModule.setDigitalSignatureEnabled(digitalSignatureEl.isSelected(0));
+		if(digitalSignatureEl.isSelected(0)) {
+			File uploadedCertificate = certificateEl.getUploadFile();
+			if(uploadedCertificate != null && uploadedCertificate.exists()) {
+				qtiModule.setDigitalSignatureCertificateFile(uploadedCertificate, certificateEl.getUploadFileName());
+				File newFile = qtiModule.getDigitalSignatureCertificateFile();
+				certificateEl.reset();// make sure the same certificate is not load twice
+				certificateEl.setInitialFile(newFile);
+			}
+			String password = certificatePasswordEl.getValue();
+			if(!PASSWORD_PLACEHOLDER.equals(password)) {
+				qtiModule.setDigitalSignatureCertificatePassword(password);
+			}
+		}
 	}
-
-
-	
-	
-
-}
+}
\ No newline at end of file
diff --git a/src/main/java/org/olat/ims/qti21/ui/QTI21AssessmentDetailsController.java b/src/main/java/org/olat/ims/qti21/ui/QTI21AssessmentDetailsController.java
index 97798f72f83996ea275cd321ba6bbda54bea8f1c..be55e7b4a37242f76db0f32e488fded090a064a1 100644
--- a/src/main/java/org/olat/ims/qti21/ui/QTI21AssessmentDetailsController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/QTI21AssessmentDetailsController.java
@@ -53,17 +53,25 @@ import org.olat.core.id.Identity;
 import org.olat.core.id.OLATResourceable;
 import org.olat.core.util.coordinate.CoordinatorManager;
 import org.olat.core.util.resource.OresHelper;
+import org.olat.course.CourseFactory;
 import org.olat.course.nodes.IQTESTCourseNode;
+import org.olat.course.nodes.iq.IQEditController;
+import org.olat.course.nodes.iq.QTI21AssessmentRunController;
+import org.olat.course.run.environment.CourseEnvironment;
 import org.olat.course.run.scoring.ScoreEvaluation;
 import org.olat.course.run.userview.UserCourseEnvironment;
 import org.olat.fileresource.FileResourceManager;
 import org.olat.ims.qti21.AssessmentSessionAuditLogger;
 import org.olat.ims.qti21.AssessmentTestSession;
+import org.olat.ims.qti21.QTI21DeliveryOptions;
 import org.olat.ims.qti21.QTI21DeliveryOptions.ShowResultsOnFinish;
+import org.olat.ims.qti21.QTI21Module;
 import org.olat.ims.qti21.QTI21Service;
+import org.olat.ims.qti21.model.DigitalSignatureOptions;
 import org.olat.ims.qti21.ui.QTI21TestSessionTableModel.TSCols;
 import org.olat.ims.qti21.ui.assessment.IdentityAssessmentTestCorrectionController;
 import org.olat.ims.qti21.ui.event.RetrieveAssessmentTestSessionEvent;
+import org.olat.modules.ModuleConfiguration;
 import org.olat.modules.assessment.AssessmentEntry;
 import org.olat.modules.assessment.AssessmentService;
 import org.olat.modules.assessment.AssessmentToolOptions;
@@ -90,6 +98,7 @@ public class QTI21AssessmentDetailsController extends FormBasicController {
 	private QTI21TestSessionTableModel tableModel;
 	
 	private RepositoryEntry entry;
+	private RepositoryEntry testEntry;
 	private final String subIdent;
 	private final boolean manualCorrections;
 	private final Identity assessedIdentity;
@@ -110,6 +119,8 @@ public class QTI21AssessmentDetailsController extends FormBasicController {
 	@Autowired
 	private UserManager userManager;
 	@Autowired
+	private QTI21Module qtiModule;
+	@Autowired
 	protected QTI21Service qtiService;
 	@Autowired
 	private RepositoryManager repositoryManager;
@@ -135,7 +146,7 @@ public class QTI21AssessmentDetailsController extends FormBasicController {
 		subIdent = courseNode.getIdent();
 		readOnly = coachCourseEnv.isCourseReadOnly();
 		this.assessedUserCourseEnv = assessedUserCourseEnv;
-		RepositoryEntry testEntry = courseNode.getReferencedRepositoryEntry();
+		testEntry = courseNode.getReferencedRepositoryEntry();
 		assessedIdentity = assessedUserCourseEnv.getIdentityEnvironment().getIdentity();
 		manualCorrections = qtiService.needManualCorrection(testEntry);
 		
@@ -158,6 +169,7 @@ public class QTI21AssessmentDetailsController extends FormBasicController {
 			RepositoryEntry assessableEntry, Identity assessedIdentity) {
 		super(ureq, wControl, "assessment_details");
 		entry = assessableEntry;
+		testEntry = assessableEntry;
 		subIdent = null;
 		readOnly = false;
 		courseNode = null;
@@ -193,8 +205,7 @@ public class QTI21AssessmentDetailsController extends FormBasicController {
 		tableModel = new QTI21TestSessionTableModel(columnsModel, getTranslator());
 		tableEl = uifactory.addTableElement(getWindowControl(), "sessions", tableModel, 20, false, getTranslator(), formLayout);
 		tableEl.setEmtpyTableMessageKey("results.empty");
-		
-		
+
 		if(reSecurity.isEntryAdmin() && !readOnly) {
 			AssessmentToolOptions asOptions = new AssessmentToolOptions();
 			asOptions.setAdmin(reSecurity.isEntryAdmin());
@@ -342,7 +353,12 @@ public class QTI21AssessmentDetailsController extends FormBasicController {
 	}
 	
 	private void doPullSession(AssessmentTestSession session) {
+		session = qtiService.getAssessmentTestSession(session.getKey());
+		
 		if(session.getFinishTime() == null) {
+			if(qtiModule.isDigitalSignatureEnabled()) {
+				qtiService.signAssessmentResult(session, getSignatureOptions(session), session.getIdentity());
+			}
 			session.setFinishTime(new Date());
 		}
 		session.setTerminationTime(new Date());
@@ -356,6 +372,32 @@ public class QTI21AssessmentDetailsController extends FormBasicController {
 		CoordinatorManager.getInstance().getCoordinator().getEventBus()
 			.fireEventToListenersOf(new RetrieveAssessmentTestSessionEvent(session.getKey()), sessionOres);
 	}
+	
+	private DigitalSignatureOptions getSignatureOptions(AssessmentTestSession session) {
+		RepositoryEntry sessionTestEntry = session.getTestEntry();
+		QTI21DeliveryOptions deliveryOptions = qtiService.getDeliveryOptions(sessionTestEntry);
+		
+		boolean digitalSignature = deliveryOptions.isDigitalSignature();
+		boolean sendMail = deliveryOptions.isDigitalSignatureMail();
+		if(courseNode != null) {
+			ModuleConfiguration config = courseNode.getModuleConfiguration();
+			digitalSignature = config.getBooleanSafe(IQEditController.CONFIG_DIGITAL_SIGNATURE,
+					deliveryOptions.isDigitalSignature());
+			sendMail = config.getBooleanSafe(IQEditController.CONFIG_DIGITAL_SIGNATURE_SEND_MAIL,
+					deliveryOptions.isDigitalSignatureMail());
+		}
+		
+		DigitalSignatureOptions options = new DigitalSignatureOptions(digitalSignature, sendMail, entry, testEntry);
+		if(digitalSignature) {
+			if(courseNode == null) {
+				 AssessmentEntryOutcomesListener.decorateResourceConfirmation(session, options, getLocale());
+			} else {
+				CourseEnvironment courseEnv = CourseFactory.loadCourse(entry).getCourseEnvironment();
+				QTI21AssessmentRunController.decorateCourseConfirmation(session, options, courseEnv, courseNode, sessionTestEntry, getLocale());
+			}
+		}
+		return options;
+	}
 
 	private void doOpenResult(UserRequest ureq, AssessmentTestSession session) {
 		if(resultCtrl != null) return;
diff --git a/src/main/java/org/olat/ims/qti21/ui/QTI21DeliveryOptionsController.java b/src/main/java/org/olat/ims/qti21/ui/QTI21DeliveryOptionsController.java
index c1bfa6fd34dd3d62b4600861c063a41350b31b2d..1f4451027e2e3bffca2ffe1bea5a8099c458f04a 100644
--- a/src/main/java/org/olat/ims/qti21/ui/QTI21DeliveryOptionsController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/QTI21DeliveryOptionsController.java
@@ -39,6 +39,7 @@ import org.olat.core.id.context.StateEntry;
 import org.olat.core.util.StringHelper;
 import org.olat.ims.qti21.QTI21DeliveryOptions;
 import org.olat.ims.qti21.QTI21DeliveryOptions.ShowResultsOnFinish;
+import org.olat.ims.qti21.QTI21Module;
 import org.olat.ims.qti21.QTI21Service;
 import org.olat.repository.RepositoryEntry;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -61,6 +62,7 @@ public class QTI21DeliveryOptionsController extends FormBasicController implemen
 	private MultipleSelectionElement displayQuestionProgressEl, displayScoreProgressEl;
 	private MultipleSelectionElement showResultsOnFinishEl;
 	private MultipleSelectionElement allowAnonymEl;
+	private MultipleSelectionElement digitalSignatureEl, digitalSignatureMailEl;
 	private SingleSelection typeShowResultsOnFinishEl;
 	private TextElement maxAttemptsEl;
 	
@@ -68,6 +70,8 @@ public class QTI21DeliveryOptionsController extends FormBasicController implemen
 	private final RepositoryEntry testEntry;
 	private final QTI21DeliveryOptions deliveryOptions;
 	
+	@Autowired
+	private QTI21Module qtiModule;
 	@Autowired
 	private QTI21Service qtiService;
 	
@@ -150,6 +154,22 @@ public class QTI21DeliveryOptionsController extends FormBasicController implemen
 			enableCancelEl.select(onKeys[0], true);
 		}
 		
+		boolean digitalSignature = deliveryOptions.isDigitalSignature();
+		digitalSignatureEl = uifactory.addCheckboxesHorizontal("digital.signature", "digital.signature.test.option", formLayout, new String[]{"x"}, new String[]{""});
+		if(digitalSignature) {
+			digitalSignatureEl.select("x", digitalSignature);
+		}
+		digitalSignatureEl.setVisible(qtiModule.isDigitalSignatureEnabled());
+		digitalSignatureEl.addActionListener(FormEvent.ONCHANGE);
+		
+		boolean digitalSignatureSendMail = deliveryOptions.isDigitalSignatureMail();
+		digitalSignatureMailEl = uifactory.addCheckboxesHorizontal("digital.signature.mail", "digital.signature.mail.test.option", formLayout, new String[]{"x"}, new String[]{""});
+		if(digitalSignatureSendMail) {
+			digitalSignatureMailEl.select("x", digitalSignatureSendMail);
+		}
+		digitalSignatureMailEl.setVisible(qtiModule.isDigitalSignatureEnabled() && digitalSignatureEl.isAtLeastSelected(1));
+
+		
 		showResultsOnFinishEl = uifactory.addCheckboxesHorizontal("resultOnFiniish", "qti.form.results.onfinish", formLayout, onKeys, onValues);
 		showResultsOnFinishEl.addActionListener(FormEvent.ONCHANGE);
 		showResultsOnFinishEl.setElementCssClass("o_sel_qti_show_results");
@@ -218,9 +238,10 @@ public class QTI21DeliveryOptionsController extends FormBasicController implemen
 	protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) {
 		if(limitAttemptsEl == source) {
 			maxAttemptsEl.setVisible(limitAttemptsEl.isAtLeastSelected(1));
-		}
-		if(showResultsOnFinishEl == source) {
+		} else if(showResultsOnFinishEl == source) {
 			typeShowResultsOnFinishEl.setVisible(showResultsOnFinishEl.isAtLeastSelected(1));
+		} else if(digitalSignatureEl == source) {
+			digitalSignatureMailEl.setVisible(digitalSignatureEl.isAtLeastSelected(1));
 		}
 		super.formInnerEvent(ureq, source, event);
 	}
@@ -251,6 +272,14 @@ public class QTI21DeliveryOptionsController extends FormBasicController implemen
 		} else {
 			deliveryOptions.setShowResultsOnFinish(ShowResultsOnFinish.none);
 		}
+		if(qtiModule.isDigitalSignatureEnabled() && digitalSignatureEl.isAtLeastSelected(1)) {
+			deliveryOptions.setDigitalSignature(true);
+			deliveryOptions.setDigitalSignatureMail(digitalSignatureMailEl.isAtLeastSelected(1));
+		} else {
+			deliveryOptions.setDigitalSignature(false);
+			deliveryOptions.setDigitalSignatureMail(false);
+		}
+		
 		qtiService.setDeliveryOptions(testEntry, deliveryOptions);
 		changes = true;
 		fireEvent(ureq, Event.DONE_EVENT);
diff --git a/src/main/java/org/olat/ims/qti21/ui/QTI21TestSessionTableModel.java b/src/main/java/org/olat/ims/qti21/ui/QTI21TestSessionTableModel.java
index a629e1759b64bab5637e2ac905b6efcdf3d0cb61..402c0f4c8149d2fca9a16a9f85c36df161bdce34 100644
--- a/src/main/java/org/olat/ims/qti21/ui/QTI21TestSessionTableModel.java
+++ b/src/main/java/org/olat/ims/qti21/ui/QTI21TestSessionTableModel.java
@@ -66,6 +66,7 @@ public class QTI21TestSessionTableModel extends DefaultFlexiTableDataModel<Asses
 				}
 				return "<span class='o_ochre'>" + translator.translate("assessment.test.open") + "</span>";
 			}
+			case test: return session.getTestEntry().getDisplayname();
 			case results: {
 				if(session.getFinishTime() != null) {
 					return AssessmentHelper.getRoundedScore(session.getScore());
@@ -97,6 +98,7 @@ public class QTI21TestSessionTableModel extends DefaultFlexiTableDataModel<Asses
 	public enum TSCols implements FlexiColumnDef {
 		lastModified("table.header.lastModified"),
 		duration("table.header.duration"),
+		test("table.header.test"),
 		results("table.header.results"),
 		open("table.header.action"),
 		correction("table.header.action");
diff --git a/src/main/java/org/olat/ims/qti21/ui/ResourcesMapper.java b/src/main/java/org/olat/ims/qti21/ui/ResourcesMapper.java
index d8fec2027b116319aaeed6f0cae3145033d2d98f..b0bad54983e12c0f3a567180607d3a3b4d3c2a59 100644
--- a/src/main/java/org/olat/ims/qti21/ui/ResourcesMapper.java
+++ b/src/main/java/org/olat/ims/qti21/ui/ResourcesMapper.java
@@ -79,7 +79,6 @@ public class ResourcesMapper implements Mapper {
 			if(file.exists()) {
 				resource = new FileMediaResource(file);
 			} else {
-				
 				String submissionName = null;
 				File storage = null;
 				if(filename.startsWith("submissions/")) {
diff --git a/src/main/java/org/olat/ims/qti21/ui/_content/assessment_results.html b/src/main/java/org/olat/ims/qti21/ui/_content/assessment_results.html
index f92ca714a74807836a34d8898c18aa2420e731b2..6516e537c870b1a057a57f0433ad8cc91f72b791 100644
--- a/src/main/java/org/olat/ims/qti21/ui/_content/assessment_results.html
+++ b/src/main/java/org/olat/ims/qti21/ui/_content/assessment_results.html
@@ -68,6 +68,12 @@
 			</td>
 		</tr>
 		#end
+		#if($r.isNotNull($signatureUrl))
+		<tr>
+			<th>$r.translate("digital.signature.download")</th>
+			<td><a href="$signatureUrl" target="_blank"><i class="o_icon o_icon-fw o_icon_download"> </i> $r.translate("digital.signature.download.link")</a></td> 
+		</tr>
+		#end
 	</tbody></table>
 </div>
 
diff --git a/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_de.properties
index 9f03dc6b2042b3c7c51e6b10200545c889cc682b..744f7de806a62428d9b688d934dfae4f2cf02ec9 100644
--- a/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_de.properties
@@ -61,12 +61,27 @@ correct.solution=Korrekte L\u00F6sung
 correction=Korrigieren
 debug.outcomes=Output Daten
 debug.responses=Antworten Daten
+digital.signature=Digital signature of test results
+digital.signature.certificate=Zertifikat
+digital.signature.certificate.example=Ein Zertifikat im .pfx Format mit "Private Key".
+digital.signature.certificate.hint=Der Zertifikat muss in .pfx Format sein und den "Private Key" enthalten.
+digital.signature.certificate.password=Certificat password
+digital.signature.text=Digital signature of test results
+digital.signature.mail.subject=Best\u00E4tigung Test {0}
+digital.signature.mail.body=Best\u00E4tigung Test {0}
+digital.signature.download=Digital Unterschrift
+digital.signature.download.link=Herunterladen
+digital.signature.test.option=Erstellt ein Digital Unterschrift
+digital.signature.mail.test.option=Schickt Digital Unterschrift per Mail
 drawing.brushsize=Pinselgr\u00F6sse
 drawing.opacity=Deckkraft
 error.as.directed=Alle 4 Antwortm\u00F6glichkeiten m\u00FCssen entweder mit Richtig oder Falsch beantwortet werden.
 error.as.directed.kprim=Bitte beantworten Sie die Frage wie vorgegeben.
 error.assessment.item=Die Datei konnte nicht gelesen werden. Sie ist entweder korrupt oder mit dem falschen Format gespeichert.
 error.choice=Sie m\u00FCssen ein von den folgenden Optionen w\u00E4hlen.
+error.digital.certificate.noX509=Es wurde kein X509 Zertifikat gefunden.
+error.digital.certificate.noPrivateKey=Es wurde kein "Private key" im Zertifikat gefunden. Sie ist erforderlich.
+error.digital.certificate.cannotread=Zertifikat konnte nicht gelesen werden.
 error.double=Falsches Zahlenformat. Beispiele\: 15.0, 5.5, 10
 error.input.choice.max=W\u00E4hlen Sie die {0} zutreffenden Antworten.
 error.input.choice.min=W\u00E4hlen Sie mindestens {0} Antwort(en).
diff --git a/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_en.properties
index fb9172d795c1f0059a051e67764e72634c0c871a..075077fe7a5a6b7b51ead77a8197e624efdde846 100644
--- a/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_en.properties
@@ -61,12 +61,27 @@ correct.solution=Correct solution
 correction=Grade
 debug.outcomes=Output data
 debug.responses=Responses data
+digital.signature=Digital signature of test results
+digital.signature.certificate=Certificate
+digital.signature.certificate.example=A certificate in .pfx format with "private key".
+digital.signature.certificate.hint=The certificate must be saved in .pfx format and contain the "private key".
+digital.signature.certificate.password=Certificate password
+digital.signature.text=Digital signature of test results
+digital.signature.mail.subject=Confirmation test {0}
+digital.signature.mail.body=Confirmation test {0}
+digital.signature.download=Digital Signature
+digital.signature.download.link=Download
+digital.signature.test.option=Generate a digital signature
+digital.signature.mail.test.option=Send the signature per mail
 drawing.brushsize=Brush size
 drawing.opacity=Opacity
 error.as.directed=Please complete this interaction as directed.
 error.as.directed.kprim=Please complete this interaction as directed.
 error.assessment.item=The file cannot be interpreted. It seems corrupted or with the wrong format.
 error.choice=You must select one of the following options
+error.digital.certificate.noX509=The X509 certificate could not be found in the uploaded file.
+error.digital.certificate.noPrivateKey=The "private key" could not be found. It is mandatory.
+error.digital.certificate.cannotread=The certificate could not be read.
 error.double=Need to be a double
 error.input.choice.max=You must select at the most {0} choices.
 error.input.choice.min=You must select at least {0} choice(s).
diff --git a/src/main/resources/serviceconfig/olat.properties b/src/main/resources/serviceconfig/olat.properties
index 5e80a811280ea92385c2f41be074a5dd8eb08cb2..9266bbfb8bbd9abaa705bc471abb4b63a015dad3 100644
--- a/src/main/resources/serviceconfig/olat.properties
+++ b/src/main/resources/serviceconfig/olat.properties
@@ -326,6 +326,19 @@ onyx.plugin.exammodelocation=${onyx.plugin.wslocation}/onyxexamservices
 ### set or overwrite this switch to "true" if this olat-node is either a single-node or should be the controlling entity in a clustered-olat 
 exam.mode.masternode=true
 
+########################################################################
+# QTI 2.1 QtiWorks
+########################################################################
+
+# Enable or disable the Math Extension of QtiWorks (need Maxima)
+qti21.math.assessment.extension.enabled=false
+qti21.math.assessment.extension.enabled.values=true,false
+
+#Enable digital signature of the assessment results
+qti21.digital.signature.enabled=false
+#Path to a PFX certificate (with X509 certificate, private and public key)
+qti21.digital.signature.certificate=
+
 ########################################################################
 # Certificates
 ########################################################################
diff --git a/src/test/java/org/olat/core/util/xml/XMLDigitalSignatureUtilTest.java b/src/test/java/org/olat/core/util/xml/XMLDigitalSignatureUtilTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..c24d2b36287b4ca0d497799c23af1c798b33dc5e
--- /dev/null
+++ b/src/test/java/org/olat/core/util/xml/XMLDigitalSignatureUtilTest.java
@@ -0,0 +1,224 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.core.util.xml;
+
+import java.io.File;
+import java.net.URL;
+import java.nio.file.Files;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.olat.core.util.FileUtils;
+import org.olat.core.util.crypto.CryptoUtil;
+import org.olat.core.util.crypto.X509CertificatePrivateKeyPair;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+/**
+ * 
+ * Initial date: 16 févr. 2017<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class XMLDigitalSignatureUtilTest {
+	
+	
+	/**
+	 * Check if the cycle sign -> validation works
+	 * 
+	 * @throws Exception
+	 */
+	@Test
+	public void signDetachedAndValidate() throws Exception {
+		X509CertificatePrivateKeyPair certificateInfo = getCertificatePrivateKeyPair();
+		
+		URL xmlUrl = XMLDigitalSignatureUtilTest.class.getResource("assessmentResult.xml");
+		File xmlFile = new File(xmlUrl.toURI());
+		String xmlUri = xmlUrl.toURI().toString();
+
+		File xmlSignatureFile = File.createTempFile("assessment-result", "_signature.xml");
+		XMLDigitalSignatureUtil.signDetached(xmlUri, xmlFile, xmlSignatureFile, null,
+				null, certificateInfo.getX509Cert(), certificateInfo.getPrivateKey());
+
+		Assert.assertTrue(xmlSignatureFile.length() > 0);
+		
+		boolean valid = XMLDigitalSignatureUtil.validate(xmlUri, xmlFile, xmlSignatureFile,
+				certificateInfo.getX509Cert().getPublicKey());
+		Assert.assertTrue(valid);
+
+		//clean up
+		Files.deleteIfExists(xmlSignatureFile.toPath());
+	}
+	
+	@Test
+	public void signDetachedAndValidate_exoticUri() throws Exception {
+		X509CertificatePrivateKeyPair certificateInfo = getCertificatePrivateKeyPair();
+		
+		URL xmlUrl = XMLDigitalSignatureUtilTest.class.getResource("assessmentResult.xml");
+		File xmlFile = new File(xmlUrl.toURI());
+		String xmlUri = "http://localhost:8081/RepositoryEntry/688455680/CourseNode/95133178953589/TestSession/2693/assessmentResult.xml";
+
+		File xmlSignatureFile = File.createTempFile("assessment-result", "_signature.xml");
+		XMLDigitalSignatureUtil.signDetached(xmlUri, xmlFile, xmlSignatureFile, null,
+				null, certificateInfo.getX509Cert(), certificateInfo.getPrivateKey());
+
+		Assert.assertTrue(xmlSignatureFile.length() > 0);
+		
+		boolean valid = XMLDigitalSignatureUtil.validate(xmlUri, xmlFile, xmlSignatureFile,
+				certificateInfo.getX509Cert().getPublicKey());
+		Assert.assertTrue(valid);
+
+		//clean up
+		Files.deleteIfExists(xmlSignatureFile.toPath());
+	}
+	
+	/**
+	 * Test if the signature can be detached and imported in an other
+	 * DOM structure.
+	 * 
+	 * @throws Exception
+	 */
+	@Test
+	public void signDetachedAndValidate_containSignatureDocument() throws Exception {
+		X509CertificatePrivateKeyPair certificateInfo = getCertificatePrivateKeyPair();
+		
+		URL xmlUrl = XMLDigitalSignatureUtilTest.class.getResource("assessmentResult.xml");
+		File xmlFile = new File(xmlUrl.toURI());
+		String xmlUri = "http://localhost:8081/RepositoryEntry/688455680/CourseNode/95133178953589/TestSession/2693/assessmentResult.xml";
+		
+		Document signatureDocument =  XMLDigitalSignatureUtil.createDocument();
+		Node rootNode = signatureDocument.appendChild(signatureDocument.createElement("assessmentTestSignature"));
+		Node courseNode = rootNode.appendChild(signatureDocument.createElement("course"));
+		courseNode.appendChild(signatureDocument.createTextNode("Very difficult test"));
+
+		File xmlSignatureFile = File.createTempFile("assessment-result", "_signature.xml");
+		XMLDigitalSignatureUtil.signDetached(xmlUri, xmlFile, xmlSignatureFile, signatureDocument,
+				null, certificateInfo.getX509Cert(), certificateInfo.getPrivateKey());
+
+		Assert.assertTrue(xmlSignatureFile.length() > 0);
+		
+		boolean valid = XMLDigitalSignatureUtil.validate(xmlUri, xmlFile, xmlSignatureFile,
+				certificateInfo.getX509Cert().getPublicKey());
+		Assert.assertTrue(valid);
+		
+		//load the signature and check that the course info and the Signature is there
+		Document reloadSignatureDocument = XMLDigitalSignatureUtil.getDocument(xmlSignatureFile);
+		NodeList courseNl = reloadSignatureDocument.getElementsByTagName("course");
+		Assert.assertEquals(1, courseNl.getLength());
+		NodeList signatureNl = reloadSignatureDocument.getElementsByTagName("Signature");
+		Assert.assertEquals(1, signatureNl.getLength());
+
+		//clean up
+		Files.deleteIfExists(xmlSignatureFile.toPath());
+	}
+	
+	@Test
+	public void signDetachedAndValidate_notValid() throws Exception {
+		X509CertificatePrivateKeyPair certificateInfo = getCertificatePrivateKeyPair();
+		
+		URL xmlUrl = XMLDigitalSignatureUtilTest.class.getResource("assessmentResult.xml");
+		File xmlFile = new File(xmlUrl.toURI());
+		String xmlUri = xmlUrl.toURI().toString();
+
+		File xmlSignatureFile = File.createTempFile("assessment-result", "_signature.xml");
+		XMLDigitalSignatureUtil.signDetached(xmlUri, xmlFile, xmlSignatureFile, null,
+				null, certificateInfo.getX509Cert(), certificateInfo.getPrivateKey());
+		Assert.assertTrue(xmlSignatureFile.length() > 0);
+		
+		URL xmlTamperedUrl = XMLDigitalSignatureUtilTest.class.getResource("assessmentResult_tampered.xml");
+		File xmlTamperedFile = new File(xmlTamperedUrl.toURI());
+		boolean valid = XMLDigitalSignatureUtil.validate(xmlUri, xmlTamperedFile, xmlSignatureFile,
+				certificateInfo.getX509Cert().getPublicKey());
+		Assert.assertFalse(valid);
+
+		//clean up
+		Files.deleteIfExists(xmlSignatureFile.toPath());
+	}
+	
+	@Test
+	public void signAndValidate() throws Exception {
+		X509CertificatePrivateKeyPair certificateInfo = getCertificatePrivateKeyPair();
+		
+		URL xmlUrl = XMLDigitalSignatureUtilTest.class.getResource("assessmentResult.xml");
+		File xmlFile = new File(xmlUrl.toURI());
+
+		File xmlSignedFile = File.createTempFile("assessment-result", "_signed.xml");
+		XMLDigitalSignatureUtil.signEmbedded(xmlFile, xmlSignedFile,
+				certificateInfo.getX509Cert(), certificateInfo.getPrivateKey());
+
+		Assert.assertTrue(xmlSignedFile.length() > 0);
+		
+		boolean valid = XMLDigitalSignatureUtil.validate(xmlSignedFile,
+				certificateInfo.getX509Cert().getPublicKey());
+		Assert.assertTrue(valid);
+
+		//clean up
+		Files.deleteIfExists(xmlSignedFile.toPath());
+	}
+	
+	/**
+	 * Check that the signature validate the data too by slightly changing a value in
+	 * the signed XML file.
+	 * 
+	 * 
+	 * @throws Exception
+	 */
+	@Test
+	public void signAndValidate_notValid() throws Exception {
+		X509CertificatePrivateKeyPair certificateInfo = getCertificatePrivateKeyPair();
+		
+		URL xmlUrl = XMLDigitalSignatureUtilTest.class.getResource("assessmentResult.xml");
+		File xmlFile = new File(xmlUrl.toURI());
+
+		File xmlSignedFile = File.createTempFile("assessment-result", "_signed.xml");
+		XMLDigitalSignatureUtil.signEmbedded(xmlFile, xmlSignedFile,
+				certificateInfo.getX509Cert(), certificateInfo.getPrivateKey());
+
+		Assert.assertTrue(xmlSignedFile.length() > 0);
+		
+		//the xml is signed and valid
+		boolean valid = XMLDigitalSignatureUtil.validate(xmlSignedFile,
+				certificateInfo.getX509Cert().getPublicKey());
+		Assert.assertTrue(valid);
+		
+		//change it a little bit
+		String xml = FileUtils.load(xmlSignedFile, "UTF-8");
+		String rogueXml = xml.replace("test7501c21c-c3db-468d-b5b8-c40339aaf323.xml", "test7501c21c-468d-b5b8-c40339aaf323.xml");
+		Assert.assertNotEquals(xml, rogueXml);
+
+		File xmlRogueFile = File.createTempFile("assessment-result", "_rogue.xml");
+		FileUtils.save(xmlRogueFile, rogueXml, "UTF-8");
+		//the xml is not valid
+		boolean validity = XMLDigitalSignatureUtil.validate(xmlRogueFile,
+				certificateInfo.getX509Cert().getPublicKey());
+		Assert.assertFalse(validity);
+		
+		//clean up
+		Files.deleteIfExists(xmlSignedFile.toPath());
+		Files.deleteIfExists(xmlRogueFile.toPath());
+	}
+
+	private X509CertificatePrivateKeyPair getCertificatePrivateKeyPair() throws Exception {
+		URL certificateUrl = XMLDigitalSignatureUtilTest.class.getResource("certificate.pfx");
+		File certificate = new File(certificateUrl.toURI());
+		return CryptoUtil.getX509CertificatePrivateKeyPairPfx(certificate, "");
+	}
+}
diff --git a/src/test/java/org/olat/core/util/xml/assessmentResult.xml b/src/test/java/org/olat/core/util/xml/assessmentResult.xml
new file mode 100644
index 0000000000000000000000000000000000000000..1740fdbf074b917dd8f622fa50abf12a39fa0c88
--- /dev/null
+++ b/src/test/java/org/olat/core/util/xml/assessmentResult.xml
@@ -0,0 +1,215 @@
+<assessmentResult xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+                  xmlns="http://www.imsglobal.org/xsd/imsqti_result_v2p1"
+                  xsi:schemaLocation="http://www.imsglobal.org/xsd/imsqti_result_v2p1 http://www.imsglobal.org/xsd/imsqti_result_v2p1.xsd">
+  <context>
+    <sessionIdentifier sourceID="http://localhost:8081/auth/RepositoryEntry/720863232/TestSession/2038"
+                       identifier="testsession/2038"/>
+  </context>
+  <testResult identifier="test7501c21c-c3db-468d-b5b8-c40339aaf323.xml"
+              datestamp="2017-02-08T16:54:49">
+    <outcomeVariable identifier="SCORE" cardinality="single" baseType="float">
+      <value>0.0</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="PASS" cardinality="single" baseType="boolean">
+      <value>true</value>
+    </outcomeVariable>
+  </testResult>
+  <itemResult identifier="sc144e214a9f4cfe90d2ecf0781c291f" datestamp="2017-02-08T16:54:49"
+              sequenceIndex="1"
+              sessionStatus="final">
+    <outcomeVariable identifier="completionStatus" cardinality="single" baseType="identifier">
+      <value>unknown</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="FEEDBACKBASIC" cardinality="single" baseType="identifier"
+                     view="testConstructor">
+      <value>incorrect</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="FEEDBACKMODAL" cardinality="multiple" baseType="identifier"
+                     view="testConstructor">
+      <value>Feedback95140378493387</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="SOLUTIONMODAL" cardinality="single" baseType="identifier"
+                     view="testConstructor">
+      <value>Feedback95140378493385</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="SCORE" cardinality="single" baseType="float">
+      <value>0.0</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="MAXSCORE" cardinality="single" baseType="float">
+      <value>1.0</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="HINTFEEDBACKMODAL" cardinality="single" baseType="identifier"/>
+    <outcomeVariable identifier="MINSCORE" cardinality="single" baseType="float"
+                     view="testConstructor">
+      <value>0.0</value>
+    </outcomeVariable>
+    <responseVariable identifier="duration" cardinality="single" baseType="float">
+      <candidateResponse>
+        <value>701.727</value>
+      </candidateResponse>
+    </responseVariable>
+    <responseVariable identifier="numAttempts" cardinality="single" baseType="integer">
+      <candidateResponse>
+        <value>12</value>
+      </candidateResponse>
+    </responseVariable>
+    <responseVariable identifier="HINTREQUEST" cardinality="single" baseType="identifier">
+      <candidateResponse>
+        <value>false</value>
+      </candidateResponse>
+    </responseVariable>
+    <responseVariable identifier="RESPONSE_1" cardinality="single" baseType="identifier">
+      <correctResponse>
+        <value>sccde3ab0fdd487ea26a3f9594ebf40f</value>
+      </correctResponse>
+      <candidateResponse>
+        <value>sc3a3fd5e8cb4cdba7199e984202437d</value>
+      </candidateResponse>
+    </responseVariable>
+  </itemResult>
+  <itemResult identifier="mce58c219bae4156adf4e0e02803512f" datestamp="2017-02-08T16:54:49"
+              sequenceIndex="1"
+              sessionStatus="initial">
+    <outcomeVariable identifier="completionStatus" cardinality="single" baseType="identifier">
+      <value>unknown</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="FEEDBACKBASIC" cardinality="single" baseType="identifier"
+                     view="testConstructor">
+      <value>none</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="FEEDBACKMODAL" cardinality="multiple" baseType="identifier"
+                     view="testConstructor"/>
+    <outcomeVariable identifier="SOLUTIONMODAL" cardinality="single" baseType="identifier"
+                     view="testConstructor"/>
+    <outcomeVariable identifier="SCORE" cardinality="single" baseType="float">
+      <value>0.0</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="MAXSCORE" cardinality="single" baseType="float">
+      <value>1.0</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="HINTFEEDBACKMODAL" cardinality="single" baseType="identifier"/>
+    <outcomeVariable identifier="MINSCORE" cardinality="single" baseType="float"
+                     view="testConstructor">
+      <value>0.0</value>
+    </outcomeVariable>
+    <responseVariable identifier="duration" cardinality="single" baseType="float">
+      <candidateResponse>
+        <value>0.0</value>
+      </candidateResponse>
+    </responseVariable>
+    <responseVariable identifier="numAttempts" cardinality="single" baseType="integer">
+      <candidateResponse>
+        <value>0</value>
+      </candidateResponse>
+    </responseVariable>
+    <responseVariable identifier="HINTREQUEST" cardinality="single" baseType="identifier">
+      <candidateResponse/>
+    </responseVariable>
+    <responseVariable identifier="RESPONSE_1" cardinality="multiple" baseType="identifier">
+      <correctResponse>
+        <value>mca7d05a516a4d1c9dbe4963b766a517</value>
+        <value>mc746c767ff24f3891f93546cbe4ef51</value>
+      </correctResponse>
+      <candidateResponse/>
+    </responseVariable>
+  </itemResult>
+  <itemResult identifier="kprimeeceecc4d94b08c9d0ea6f0a9eb" datestamp="2017-02-08T16:54:49"
+              sequenceIndex="1"
+              sessionStatus="initial">
+    <outcomeVariable identifier="completionStatus" cardinality="single" baseType="identifier">
+      <value>unknown</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="FEEDBACKBASIC" cardinality="single" baseType="identifier"
+                     view="testConstructor">
+      <value>none</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="FEEDBACKMODAL" cardinality="multiple" baseType="identifier"
+                     view="testConstructor"/>
+    <outcomeVariable identifier="SOLUTIONMODAL" cardinality="single" baseType="identifier"
+                     view="testConstructor"/>
+    <outcomeVariable identifier="SCORE" cardinality="single" baseType="float">
+      <value>0.0</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="MAXSCORE" cardinality="single" baseType="float">
+      <value>1.0</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="HINTFEEDBACKMODAL" cardinality="single" baseType="identifier"/>
+    <outcomeVariable identifier="MINSCORE" cardinality="single" baseType="float"
+                     view="testConstructor">
+      <value>0.0</value>
+    </outcomeVariable>
+    <responseVariable identifier="duration" cardinality="single" baseType="float">
+      <candidateResponse>
+        <value>0.0</value>
+      </candidateResponse>
+    </responseVariable>
+    <responseVariable identifier="numAttempts" cardinality="single" baseType="integer">
+      <candidateResponse>
+        <value>0</value>
+      </candidateResponse>
+    </responseVariable>
+    <responseVariable identifier="HINTREQUEST" cardinality="single" baseType="identifier">
+      <candidateResponse/>
+    </responseVariable>
+    <responseVariable identifier="KPRIM_RESPONSE_1" cardinality="multiple" baseType="directedPair">
+      <correctResponse>
+        <value>d95140378493395 wrong</value>
+        <value>a95140378493392 wrong</value>
+        <value>b95140378493393 wrong</value>
+        <value>c95140378493394 wrong</value>
+      </correctResponse>
+      <candidateResponse/>
+    </responseVariable>
+  </itemResult>
+  <itemResult identifier="fib1e0afaf7f48bcbf2c7410df0a9062" datestamp="2017-02-08T16:54:49"
+              sequenceIndex="1"
+              sessionStatus="initial">
+    <outcomeVariable identifier="completionStatus" cardinality="single" baseType="identifier">
+      <value>unknown</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="FEEDBACKBASIC" cardinality="single" baseType="identifier"
+                     view="testConstructor">
+      <value>none</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="FEEDBACKMODAL" cardinality="multiple" baseType="identifier"
+                     view="testConstructor"/>
+    <outcomeVariable identifier="SOLUTIONMODAL" cardinality="single" baseType="identifier"
+                     view="testConstructor"/>
+    <outcomeVariable identifier="SCORE" cardinality="single" baseType="float">
+      <value>0.0</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="MAXSCORE" cardinality="single" baseType="float">
+      <value>1.0</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="MINSCORE_RESPONSE_1" cardinality="single" baseType="float">
+      <value>0.0</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="HINTFEEDBACKMODAL" cardinality="single" baseType="identifier"/>
+    <outcomeVariable identifier="MINSCORE" cardinality="single" baseType="float"
+                     view="testConstructor">
+      <value>0.0</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="SCORE_RESPONSE_1" cardinality="single" baseType="float">
+      <value>0.0</value>
+    </outcomeVariable>
+    <responseVariable identifier="duration" cardinality="single" baseType="float">
+      <candidateResponse>
+        <value>0.0</value>
+      </candidateResponse>
+    </responseVariable>
+    <responseVariable identifier="numAttempts" cardinality="single" baseType="integer">
+      <candidateResponse>
+        <value>0</value>
+      </candidateResponse>
+    </responseVariable>
+    <responseVariable identifier="HINTREQUEST" cardinality="single" baseType="identifier">
+      <candidateResponse/>
+    </responseVariable>
+    <responseVariable identifier="RESPONSE_1" cardinality="single" baseType="string">
+      <correctResponse>
+        <value>gap</value>
+      </correctResponse>
+      <candidateResponse/>
+    </responseVariable>
+  </itemResult>
+</assessmentResult>
\ No newline at end of file
diff --git a/src/test/java/org/olat/core/util/xml/assessmentResult_tampered.xml b/src/test/java/org/olat/core/util/xml/assessmentResult_tampered.xml
new file mode 100644
index 0000000000000000000000000000000000000000..01374444ddfaed3e4f074935fc65643aa3b23c0a
--- /dev/null
+++ b/src/test/java/org/olat/core/util/xml/assessmentResult_tampered.xml
@@ -0,0 +1,215 @@
+<assessmentResult xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+                  xmlns="http://www.imsglobal.org/xsd/imsqti_result_v2p1"
+                  xsi:schemaLocation="http://www.imsglobal.org/xsd/imsqti_result_v2p1 http://www.imsglobal.org/xsd/imsqti_result_v2p1.xsd">
+  <context>
+    <sessionIdentifier sourceID="http://localhost:8081/auth/RepositoryEntry/720863232/TestSession/2038"
+                       identifier="testsession/2038"/>
+  </context>
+  <testResult identifier="test7501c21c-c3db-468d-b5b8-c40339aaf323.xml"
+              datestamp="2017-02-08T16:54:49">
+    <outcomeVariable identifier="SCORE" cardinality="single" baseType="float">
+      <value>0.0</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="PASS" cardinality="single" baseType="boolean">
+      <value>true</value>
+    </outcomeVariable>
+  </testResult>
+  <itemResult identifier="sc1e214a9f4cfe90d2ecf0781c291f" datestamp="2017-02-08T16:54:49"
+              sequenceIndex="1"
+              sessionStatus="final">
+    <outcomeVariable identifier="completionStatus" cardinality="single" baseType="identifier">
+      <value>unknown</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="FEEDBACKBASIC" cardinality="single" baseType="identifier"
+                     view="testConstructor">
+      <value>incorrect</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="FEEDBACKMODAL" cardinality="multiple" baseType="identifier"
+                     view="testConstructor">
+      <value>Feedback95140378493387</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="SOLUTIONMODAL" cardinality="single" baseType="identifier"
+                     view="testConstructor">
+      <value>Feedback95140378493385</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="SCORE" cardinality="single" baseType="float">
+      <value>0.0</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="MAXSCORE" cardinality="single" baseType="float">
+      <value>1.0</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="HINTFEEDBACKMODAL" cardinality="single" baseType="identifier"/>
+    <outcomeVariable identifier="MINSCORE" cardinality="single" baseType="float"
+                     view="testConstructor">
+      <value>0.0</value>
+    </outcomeVariable>
+    <responseVariable identifier="duration" cardinality="single" baseType="float">
+      <candidateResponse>
+        <value>701.727</value>
+      </candidateResponse>
+    </responseVariable>
+    <responseVariable identifier="numAttempts" cardinality="single" baseType="integer">
+      <candidateResponse>
+        <value>12</value>
+      </candidateResponse>
+    </responseVariable>
+    <responseVariable identifier="HINTREQUEST" cardinality="single" baseType="identifier">
+      <candidateResponse>
+        <value>false</value>
+      </candidateResponse>
+    </responseVariable>
+    <responseVariable identifier="RESPONSE_1" cardinality="single" baseType="identifier">
+      <correctResponse>
+        <value>sccde3ab0fdd487ea26a3f9594ebf40f</value>
+      </correctResponse>
+      <candidateResponse>
+        <value>sc3a3fd5e8cb4cdba7199e984202437d</value>
+      </candidateResponse>
+    </responseVariable>
+  </itemResult>
+  <itemResult identifier="mce58c219bae4156adf4e0e02803512f" datestamp="2017-02-08T16:54:49"
+              sequenceIndex="1"
+              sessionStatus="initial">
+    <outcomeVariable identifier="completionStatus" cardinality="single" baseType="identifier">
+      <value>unknown</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="FEEDBACKBASIC" cardinality="single" baseType="identifier"
+                     view="testConstructor">
+      <value>none</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="FEEDBACKMODAL" cardinality="multiple" baseType="identifier"
+                     view="testConstructor"/>
+    <outcomeVariable identifier="SOLUTIONMODAL" cardinality="single" baseType="identifier"
+                     view="testConstructor"/>
+    <outcomeVariable identifier="SCORE" cardinality="single" baseType="float">
+      <value>0.0</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="MAXSCORE" cardinality="single" baseType="float">
+      <value>1.0</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="HINTFEEDBACKMODAL" cardinality="single" baseType="identifier"/>
+    <outcomeVariable identifier="MINSCORE" cardinality="single" baseType="float"
+                     view="testConstructor">
+      <value>0.0</value>
+    </outcomeVariable>
+    <responseVariable identifier="duration" cardinality="single" baseType="float">
+      <candidateResponse>
+        <value>0.0</value>
+      </candidateResponse>
+    </responseVariable>
+    <responseVariable identifier="numAttempts" cardinality="single" baseType="integer">
+      <candidateResponse>
+        <value>0</value>
+      </candidateResponse>
+    </responseVariable>
+    <responseVariable identifier="HINTREQUEST" cardinality="single" baseType="identifier">
+      <candidateResponse/>
+    </responseVariable>
+    <responseVariable identifier="RESPONSE_1" cardinality="multiple" baseType="identifier">
+      <correctResponse>
+        <value>mca7d05a516a4d1c9dbe4963b766a517</value>
+        <value>mc746c767ff24f3891f93546cbe4ef51</value>
+      </correctResponse>
+      <candidateResponse/>
+    </responseVariable>
+  </itemResult>
+  <itemResult identifier="kprimeeceecc4d94b08c9d0ea6f0a9eb" datestamp="2017-02-08T16:54:49"
+              sequenceIndex="1"
+              sessionStatus="initial">
+    <outcomeVariable identifier="completionStatus" cardinality="single" baseType="identifier">
+      <value>unknown</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="FEEDBACKBASIC" cardinality="single" baseType="identifier"
+                     view="testConstructor">
+      <value>none</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="FEEDBACKMODAL" cardinality="multiple" baseType="identifier"
+                     view="testConstructor"/>
+    <outcomeVariable identifier="SOLUTIONMODAL" cardinality="single" baseType="identifier"
+                     view="testConstructor"/>
+    <outcomeVariable identifier="SCORE" cardinality="single" baseType="float">
+      <value>0.0</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="MAXSCORE" cardinality="single" baseType="float">
+      <value>1.0</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="HINTFEEDBACKMODAL" cardinality="single" baseType="identifier"/>
+    <outcomeVariable identifier="MINSCORE" cardinality="single" baseType="float"
+                     view="testConstructor">
+      <value>0.0</value>
+    </outcomeVariable>
+    <responseVariable identifier="duration" cardinality="single" baseType="float">
+      <candidateResponse>
+        <value>0.0</value>
+      </candidateResponse>
+    </responseVariable>
+    <responseVariable identifier="numAttempts" cardinality="single" baseType="integer">
+      <candidateResponse>
+        <value>0</value>
+      </candidateResponse>
+    </responseVariable>
+    <responseVariable identifier="HINTREQUEST" cardinality="single" baseType="identifier">
+      <candidateResponse/>
+    </responseVariable>
+    <responseVariable identifier="KPRIM_RESPONSE_1" cardinality="multiple" baseType="directedPair">
+      <correctResponse>
+        <value>d95140378493395 wrong</value>
+        <value>a95140378493392 wrong</value>
+        <value>b95140378493393 wrong</value>
+        <value>c95140378493394 wrong</value>
+      </correctResponse>
+      <candidateResponse/>
+    </responseVariable>
+  </itemResult>
+  <itemResult identifier="fib1e0afaf7f48bcbf2c7410df0a9062" datestamp="2017-02-08T16:54:49"
+              sequenceIndex="1"
+              sessionStatus="initial">
+    <outcomeVariable identifier="completionStatus" cardinality="single" baseType="identifier">
+      <value>unknown</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="FEEDBACKBASIC" cardinality="single" baseType="identifier"
+                     view="testConstructor">
+      <value>none</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="FEEDBACKMODAL" cardinality="multiple" baseType="identifier"
+                     view="testConstructor"/>
+    <outcomeVariable identifier="SOLUTIONMODAL" cardinality="single" baseType="identifier"
+                     view="testConstructor"/>
+    <outcomeVariable identifier="SCORE" cardinality="single" baseType="float">
+      <value>0.0</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="MAXSCORE" cardinality="single" baseType="float">
+      <value>1.0</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="MINSCORE_RESPONSE_1" cardinality="single" baseType="float">
+      <value>0.0</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="HINTFEEDBACKMODAL" cardinality="single" baseType="identifier"/>
+    <outcomeVariable identifier="MINSCORE" cardinality="single" baseType="float"
+                     view="testConstructor">
+      <value>0.0</value>
+    </outcomeVariable>
+    <outcomeVariable identifier="SCORE_RESPONSE_1" cardinality="single" baseType="float">
+      <value>0.0</value>
+    </outcomeVariable>
+    <responseVariable identifier="duration" cardinality="single" baseType="float">
+      <candidateResponse>
+        <value>0.0</value>
+      </candidateResponse>
+    </responseVariable>
+    <responseVariable identifier="numAttempts" cardinality="single" baseType="integer">
+      <candidateResponse>
+        <value>0</value>
+      </candidateResponse>
+    </responseVariable>
+    <responseVariable identifier="HINTREQUEST" cardinality="single" baseType="identifier">
+      <candidateResponse/>
+    </responseVariable>
+    <responseVariable identifier="RESPONSE_1" cardinality="single" baseType="string">
+      <correctResponse>
+        <value>gap</value>
+      </correctResponse>
+      <candidateResponse/>
+    </responseVariable>
+  </itemResult>
+</assessmentResult>
\ No newline at end of file
diff --git a/src/test/java/org/olat/core/util/xml/certificate.pfx b/src/test/java/org/olat/core/util/xml/certificate.pfx
new file mode 100644
index 0000000000000000000000000000000000000000..cd0b20db56b6fdb260a84ae69ec882008b894856
Binary files /dev/null and b/src/test/java/org/olat/core/util/xml/certificate.pfx differ
diff --git a/src/test/java/org/olat/test/AllTestsJunit4.java b/src/test/java/org/olat/test/AllTestsJunit4.java
index e0959663c82ee01320f23f98fefc51b1640180f2..a8f044f2d7b2f33c8425ce248f81760b106bd58a 100644
--- a/src/test/java/org/olat/test/AllTestsJunit4.java
+++ b/src/test/java/org/olat/test/AllTestsJunit4.java
@@ -69,6 +69,7 @@ import org.junit.runners.Suite;
 	org.olat.core.util.mail.manager.MailManagerTest.class,
 	org.olat.core.util.openxml.OpenXmlWorkbookTest.class,
 	org.olat.core.util.openxml.OpenXMLDocumentTest.class,
+	org.olat.core.util.xml.XMLDigitalSignatureUtilTest.class,
 	org.olat.core.id.context.BusinessControlFactoryTest.class,
 	org.olat.core.id.context.HistoryManagerTest.class,
 	org.olat.core.id.IdentityEnvironmentTest.class,