package no.difi.sdp.client2.asice.signature;
import no.difi.sdp.client2.ObjectMother;
import no.difi.sdp.client2.asice.AsicEAttachable;
import no.difi.sdp.client2.domain.Noekkelpar;
import no.digipost.time.ControllableClock;
import org.apache.commons.io.IOUtils;
import org.apache.jcp.xml.dsig.internal.dom.DOMSubTreeData;
import org.etsi.uri._01903.v1_3.DataObjectFormat;
import org.etsi.uri._01903.v1_3.DigestAlgAndValueType;
import org.etsi.uri._01903.v1_3.QualifyingProperties;
import org.etsi.uri._01903.v1_3.SignedDataObjectProperties;
import org.etsi.uri._01903.v1_3.SigningCertificate;
import org.etsi.uri._2918.v1_2.XAdESSignatures;
import org.junit.Before;
import org.junit.Test;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;
import org.w3.xmldsig.Reference;
import org.w3.xmldsig.SignedInfo;
import org.w3.xmldsig.X509IssuerSerialType;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.bootstrap.DOMImplementationRegistry;
import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSSerializer;
import javax.xml.crypto.Data;
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.XMLSignature;
import javax.xml.crypto.dsig.XMLSignatureFactory;
import javax.xml.crypto.dsig.dom.DOMValidateContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.stream.StreamSource;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.math.BigInteger;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.asList;
import static no.difi.sdp.client2.internal.SdpTimeConstants.UTC;
import static no.digipost.DiggBase.nonNull;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
public class CreateSignatureTest {
private static final Jaxb2Marshaller marshaller;
static {
marshaller = new Jaxb2Marshaller();
marshaller.setClassesToBeBound(XAdESSignatures.class, QualifyingProperties.class);
}
private final ControllableClock clock = ControllableClock.freezedAt(Instant.now(), UTC);
/**
* SHA256 hash of "hoveddokument-innhold"
*/
private final byte[] expectedHovedDokumentHash = new byte[]{93, -36, 99, 92, -27, 39, 21, 31, 33, -127, 30, 77, 6, 49, 92, -48, -114, -61, -100, -126, -64, -70, 70, -38, 67, 93, -126, 62, -125, -7, -115, 123};
private CreateSignature sut;
private Noekkelpar noekkelpar;
private List<AsicEAttachable> files;
@Before
public void setUp() throws Exception {
noekkelpar = ObjectMother.selvsignertNoekkelparUtenTrustStore();
files = asList(
file("hoveddokument.pdf", "hoveddokument-innhold".getBytes(), "application/pdf"),
file("manifest.xml", "manifest-innhold".getBytes(), "application/xml")
);
sut = new CreateSignature(new CreateXAdESProperties(clock));
}
@Test
public void test_generated_signatures() {
Signature signature = sut.createSignature(noekkelpar, files);
XAdESSignatures xAdESSignatures = (XAdESSignatures) marshaller.unmarshal(new StreamSource(new ByteArrayInputStream(signature.getBytes())));
assertThat(xAdESSignatures.getSignatures(), hasSize(1));
org.w3.xmldsig.Signature dSignature = xAdESSignatures.getSignatures().get(0);
verify_signed_info(dSignature.getSignedInfo());
assertThat(dSignature.getSignatureValue(), notNullValue());
assertThat(dSignature.getKeyInfo(), notNullValue());
}
@Test
public void multithreaded_signing() throws Exception {
List<Thread> threads = new ArrayList<Thread>();
final AtomicInteger fails = new AtomicInteger(0);
for (int i = 0; i < 50; i++) {
Thread t = new Thread() {
@Override
public void run() {
for (int j = 0; j < 20; j++) {
Signature signature = sut.createSignature(noekkelpar, files);
if (!verify_signature(signature)) {
fails.incrementAndGet();
}
if (fails.get() > 0) {
break;
}
}
}
};
threads.add(t);
t.start();
}
for (Thread t : threads) {
t.join();
}
if (fails.get() > 0) {
fail("Signature validation failed");
}
}
@Test
public void test_xades_signed_properties() {
Signature signature = sut.createSignature(noekkelpar, files);
XAdESSignatures xAdESSignatures = (XAdESSignatures) marshaller.unmarshal(new StreamSource(new ByteArrayInputStream(signature.getBytes())));
org.w3.xmldsig.Object object = xAdESSignatures.getSignatures().get(0).getObjects().get(0);
QualifyingProperties xadesProperties = (QualifyingProperties) object.getContent().get(0);
SigningCertificate signingCertificate = xadesProperties.getSignedProperties().getSignedSignatureProperties().getSigningCertificate();
verify_signing_certificate(signingCertificate);
SignedDataObjectProperties signedDataObjectProperties = xadesProperties.getSignedProperties().getSignedDataObjectProperties();
verify_signed_data_object_properties(signedDataObjectProperties);
}
@Test
public void should_support_filenames_with_spaces_and_other_characters() {
List<AsicEAttachable> otherFiles = asList(
file("hoveddokument (2).pdf", "hoveddokument-innhold".getBytes(), "application/pdf"),
file("manifest.xml", "manifest-innhold".getBytes(), "application/xml")
);
Signature signature = sut.createSignature(noekkelpar, otherFiles);
XAdESSignatures xAdESSignatures = (XAdESSignatures) marshaller.unmarshal(new StreamSource(new ByteArrayInputStream(signature.getBytes())));
String uri = xAdESSignatures.getSignatures().get(0).getSignedInfo().getReferences().get(0).getURI();
assertEquals("hoveddokument+%282%29.pdf", uri);
}
@Test
public void test_pregenerated_xml() throws Exception {
// Note: this is a very brittle test. it is meant to be guiding. If it fails, manually check if the changes to the XML makes sense. If they do, just update the expected XML.
String expected;
try (InputStream expectedStream = nonNull("/asic/expected-asic-signature.xml", getClass()::getResourceAsStream)) {
expected = IOUtils.toString(expectedStream, UTF_8);
}
// The signature partly depends on the exact time the original message was signed
clock.set(ZonedDateTime.of(2014, 5, 21, 15, 7, 15, 756_000_000, UTC));
Signature signature = sut.createSignature(noekkelpar, files);
String actual = prettyPrint(signature);
assertEquals(expected, actual);
}
private void verify_signed_data_object_properties(final SignedDataObjectProperties signedDataObjectProperties) {
assertThat(signedDataObjectProperties.getDataObjectFormats(), hasSize(2)); // One per file
DataObjectFormat hoveddokumentDataObjectFormat = signedDataObjectProperties.getDataObjectFormats().get(0);
assertThat(hoveddokumentDataObjectFormat.getObjectReference(), equalTo("#ID_0"));
assertThat(hoveddokumentDataObjectFormat.getMimeType(), equalTo("application/pdf"));
DataObjectFormat manifestDataObjectFormat = signedDataObjectProperties.getDataObjectFormats().get(1);
assertThat(manifestDataObjectFormat.getObjectReference(), equalTo("#ID_1"));
assertThat(manifestDataObjectFormat.getMimeType(), equalTo("application/xml"));
}
private void verify_signing_certificate(final SigningCertificate signingCertificate) {
assertThat(signingCertificate.getCerts(), hasSize(1));
DigestAlgAndValueType certDigest = signingCertificate.getCerts().get(0).getCertDigest();
assertThat(certDigest.getDigestMethod().getAlgorithm(), equalTo("http://www.w3.org/2000/09/xmldsig#sha1"));
assertThat(certDigest.getDigestValue().length, is(20)); // SHA1 is 160 bits => 20 bytes
X509IssuerSerialType issuerSerial = signingCertificate.getCerts().get(0).getIssuerSerial();
assertThat(issuerSerial.getX509IssuerName(), equalTo("CN=Avsender, OU=Avsender, O=Avsender, L=Oslo, ST=NO, C=NO"));
assertThat(issuerSerial.getX509SerialNumber(), equalTo(new BigInteger("589725471")));
}
private void verify_signed_info(final SignedInfo signedInfo) {
assertThat(signedInfo.getCanonicalizationMethod().getAlgorithm(), equalTo("http://www.w3.org/TR/2001/REC-xml-c14n-20010315"));
assertThat(signedInfo.getSignatureMethod().getAlgorithm(), equalTo("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"));
List<Reference> references = signedInfo.getReferences();
assertThat(references, hasSize(3));
assert_hovedokument_reference(references.get(0));
assertThat(references.get(1).getURI(), equalTo("manifest.xml"));
verify_signed_properties_reference(references.get(2));
}
private boolean verify_signature(final Signature signature2) {
try {
signature2.getBytes();
DocumentBuilderFactory fac = DocumentBuilderFactory.newInstance();
fac.setNamespaceAware(true);
DocumentBuilder builder = fac.newDocumentBuilder();
final Document doc = builder.parse(new ByteArrayInputStream(signature2.getBytes()));
//System.err.println(new String(signature2.getBytes()));
NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
DOMValidateContext valContext = new DOMValidateContext
(noekkelpar.getVirksomhetssertifikat().getX509Certificate().getPublicKey(), nl.item(0));
valContext.setURIDereferencer(new URIDereferencer() {
@Override
public Data dereference(final URIReference uriReference, final XMLCryptoContext context) throws URIReferenceException {
//System.out.println("$$$$ " + uriReference.getURI());
for (AsicEAttachable file : files) {
if (file.getFileName().equals(uriReference.getURI().toString())) {
return new OctetStreamData(new ByteArrayInputStream(file.getBytes()));
}
}
uriReference.getURI().toString().replace("#", "");
Node element = doc.getElementsByTagName("SignedProperties").item(0);
return new DOMSubTreeData(element, false);
}
});
XMLSignatureFactory fact = XMLSignatureFactory.getInstance("DOM");
XMLSignature signature = fact.unmarshalXMLSignature(valContext);
boolean coreValidity = signature.validate(valContext);
if (coreValidity == false) {
System.err.println("Signature failed core validation");
boolean sv = signature.getSignatureValue().validate(valContext);
System.out.println("signature validation status: " + sv);
if (sv == false) {
// Check the validation status of each Reference.
@SuppressWarnings("unchecked")
Iterator<javax.xml.crypto.dsig.Reference> i = signature.getSignedInfo().getReferences().iterator();
for (int j = 0; i.hasNext(); j++) {
boolean refValid = i.next().validate(valContext);
System.out.println("ref[" + j + "] validity status: " + refValid);
}
}
}
return coreValidity;
} catch (Exception ex) {
ex.printStackTrace(System.err);
return false;
}
}
private void verify_signed_properties_reference(final Reference signedPropertiesReference) {
assertThat(signedPropertiesReference.getURI(), equalTo("#SignedProperties"));
assertThat(signedPropertiesReference.getType(), equalTo("http://uri.etsi.org/01903#SignedProperties"));
assertThat(signedPropertiesReference.getDigestMethod().getAlgorithm(), equalTo("http://www.w3.org/2001/04/xmlenc#sha256"));
assertThat(signedPropertiesReference.getDigestValue().length, is(32)); // SHA256 is 256 bits => 32 bytes
assertThat(signedPropertiesReference.getTransforms().getTransforms().get(0).getAlgorithm(), equalTo("http://www.w3.org/TR/2001/REC-xml-c14n-20010315"));
}
private void assert_hovedokument_reference(final Reference hovedDokumentReference) {
assertThat(hovedDokumentReference.getURI(), equalTo("hoveddokument.pdf"));
assertThat(hovedDokumentReference.getDigestValue(), equalTo(expectedHovedDokumentHash));
assertThat(hovedDokumentReference.getDigestMethod().getAlgorithm(), equalTo("http://www.w3.org/2001/04/xmlenc#sha256"));
}
private AsicEAttachable file(final String fileName, final byte[] contents, final String mimeType) {
return new AsicEAttachable() {
@Override
public String getFileName() {
return fileName;
}
@Override
public byte[] getBytes() {
return contents;
}
@Override
public String getMimeType() {
return mimeType;
}
};
}
private String prettyPrint(final Signature signature) throws TransformerException, ClassNotFoundException, InstantiationException, IllegalAccessException {
StreamSource xmlSource = new StreamSource(new ByteArrayInputStream(signature.getBytes()));
Transformer transformer = TransformerFactory.newInstance().newTransformer();
DOMResult outputTarget = new DOMResult();
transformer.transform(xmlSource, outputTarget);
final DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance();
final DOMImplementationLS impl = (DOMImplementationLS) registry.getDOMImplementation("LS");
final LSSerializer writer = impl.createLSSerializer();
writer.getDomConfig().setParameter("format-pretty-print", Boolean.TRUE);
writer.getDomConfig().setParameter("xml-declaration", Boolean.FALSE);
return writer.writeToString(outputTarget.getNode());
}
}