From be9b7b65289213bec65bf5619c705be027b99c5d Mon Sep 17 00:00:00 2001 From: Emmanuel Bourg Date: Fri, 14 Jun 2024 15:39:22 +0200 Subject: [PATCH] Timestamp command implementation --- README.md | 1 + docs/index.html | 11 +- .../src/main/java/net/jsign/JsignCLI.java | 11 ++ .../src/test/java/net/jsign/JsignCLITest.java | 9 ++ .../java/net/jsign/AuthenticodeSigner.java | 41 +------ .../main/java/net/jsign/SignatureUtils.java | 92 +++++++++++++++- .../src/main/java/net/jsign/SignerHelper.java | 77 +++++++++++++ .../java/net/jsign/timestamp/Timestamper.java | 28 ++++- .../test/java/net/jsign/SignerHelperTest.java | 103 ++++++++++++++++++ 9 files changed, 323 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 15e8f2b9..480b8071 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ See https://ebourg.github.io/jsign for more information. * File list files prefixed with `@` are now supported with the command line tool to sign multiple files * Wildcard patterns are now accepted by the command line tool to scan directories for files to sign * Jsign now checks if the certificate subject matches the app manifest publisher before signing APPX/MSIX packages (with contributions from Scott Cooper) +* The `timestamp` command has been added to timestamp the signatures of a file * The `tag` command has been added to add unsigned data (such as user identification data) to signed files * The `extract` command has been added to extract the signature from a signed file, in DER or PEM format * The `remove` command has been added to remove the signature from a signed file diff --git a/docs/index.html b/docs/index.html index 1b62359e..cb3e88d8 100644 --- a/docs/index.html +++ b/docs/index.html @@ -521,7 +521,7 @@

Command Line Tool

files (CAB), Catalog files (CAT), Windows packages (APPX/MSIX), Microsoft Dynamics 365 extension packages, NuGet packages and scripts (PowerShell, VBScript, JScript, WSF) - commands: sign (default), extract, remove, tag + commands: sign (default), timestamp, extract, remove, tag sign: -s,--keystore <FILE> The keystore file, the SunPKCS11 configuration file, @@ -576,6 +576,15 @@

Command Line Tool

--debug Print debugging information -h,--help Print the help + timestamp: + -t,--tsaurl <URL> The URL of the timestamping authority + -m,--tsmode <MODE> The timestamping mode (RFC3161 or Authenticode) + -r,--tsretries <NUMBER> The number of retries for timestamping + -w,--tsretrywait <SECONDS> The number of seconds to wait between timestamping retries + -n,--name <NAME> The name of the application + -u,--url <URL> The URL of the application + --replace Tells if the previous timestamps should be replaced + extract: --format <FORMAT> The output format of the signature (DER or PEM) diff --git a/jsign-cli/src/main/java/net/jsign/JsignCLI.java b/jsign-cli/src/main/java/net/jsign/JsignCLI.java index b6e64a10..0740c921 100644 --- a/jsign-cli/src/main/java/net/jsign/JsignCLI.java +++ b/jsign-cli/src/main/java/net/jsign/JsignCLI.java @@ -121,6 +121,17 @@ public static void main(String... args) { this.options.put("sign", options); + options = new Options(); + options.addOption(Option.builder("t").hasArg().longOpt(PARAM_TSAURL).argName("URL").desc("The URL of the timestamping authority").build()); + options.addOption(Option.builder("m").hasArg().longOpt(PARAM_TSMODE).argName("MODE").desc("The timestamping mode (RFC3161 or Authenticode)").build()); + options.addOption(Option.builder("r").hasArg().longOpt(PARAM_TSRETRIES).argName("NUMBER").desc("The number of retries for timestamping").build()); + options.addOption(Option.builder("w").hasArg().longOpt(PARAM_TSRETRY_WAIT).argName("SECONDS").desc("The number of seconds to wait between timestamping retries").build()); + options.addOption(Option.builder("n").hasArg().longOpt(PARAM_NAME).argName("NAME").desc("The name of the application").build()); + options.addOption(Option.builder("u").hasArg().longOpt(PARAM_URL).argName("URL").desc("The URL of the application").build()); + options.addOption(Option.builder().longOpt(PARAM_REPLACE).desc("Tells if the previous timestamps should be replaced").build()); + + this.options.put("timestamp", options); + options = new Options(); options.addOption(Option.builder().hasArg().longOpt(PARAM_FORMAT).argName("FORMAT").desc(" The output format of the signature (DER or PEM)").build()); diff --git a/jsign-cli/src/test/java/net/jsign/JsignCLITest.java b/jsign-cli/src/test/java/net/jsign/JsignCLITest.java index 8154c2b9..c5463fb4 100644 --- a/jsign-cli/src/test/java/net/jsign/JsignCLITest.java +++ b/jsign-cli/src/test/java/net/jsign/JsignCLITest.java @@ -629,4 +629,13 @@ public void testTag() throws Exception { assertEquals("message", "No signature found in " + targetFile.getPath(), e.getMessage()); } } + + @Test + public void testTimestamp() throws Exception { + try { + cli.execute("timestamp", "" + targetFile); + } catch (SignerException e) { + assertEquals("message", "No signature found in " + targetFile.getPath(), e.getMessage()); + } + } } diff --git a/jsign-core/src/main/java/net/jsign/AuthenticodeSigner.java b/jsign-core/src/main/java/net/jsign/AuthenticodeSigner.java index 7c6be8f1..9b124f3e 100644 --- a/jsign-core/src/main/java/net/jsign/AuthenticodeSigner.java +++ b/jsign-core/src/main/java/net/jsign/AuthenticodeSigner.java @@ -34,7 +34,6 @@ import java.util.List; import org.bouncycastle.asn1.ASN1Encodable; -import org.bouncycastle.asn1.ASN1EncodableVector; import org.bouncycastle.asn1.DERNull; import org.bouncycastle.asn1.DERSet; import org.bouncycastle.asn1.cms.Attribute; @@ -54,8 +53,6 @@ import org.bouncycastle.cms.DefaultSignedAttributeTableGenerator; import org.bouncycastle.cms.SignerInfoGenerator; import org.bouncycastle.cms.SignerInfoGeneratorBuilder; -import org.bouncycastle.cms.SignerInformation; -import org.bouncycastle.cms.SignerInformationStore; import org.bouncycastle.cms.SignerInformationVerifier; import org.bouncycastle.cms.jcajce.JcaSignerInfoVerifierBuilder; import org.bouncycastle.operator.ContentSigner; @@ -376,7 +373,7 @@ public void sign(Signable file) throws Exception { List signatures = file.getSignatures(); if (!signatures.isEmpty()) { // append the nested signature - sigData = addNestedSignature(signatures.get(0), sigData); + sigData = SignatureUtils.addNestedSignature(signatures.get(0), false, sigData); } } @@ -516,42 +513,6 @@ private AttributeTable createAuthenticatedAttributes() { return new AttributeTable(new DERSet(attributes.toArray(new ASN1Encodable[0]))); } - /** - * Embed a signature as an unsigned attribute of an existing signature. - * - * @param primary the root signature hosting the nested secondary signature - * @param secondary the additional signature to nest inside the primary one - * @return the signature combining the specified signatures - */ - protected CMSSignedData addNestedSignature(CMSSignedData primary, CMSSignedData secondary) { - SignerInformation signerInformation = primary.getSignerInfos().getSigners().iterator().next(); - - AttributeTable unsignedAttributes = signerInformation.getUnsignedAttributes(); - if (unsignedAttributes == null) { - unsignedAttributes = new AttributeTable(new DERSet()); - } - Attribute nestedSignaturesAttribute = unsignedAttributes.get(AuthenticodeObjectIdentifiers.SPC_NESTED_SIGNATURE_OBJID); - if (nestedSignaturesAttribute == null) { - // first nested signature - unsignedAttributes = unsignedAttributes.add(AuthenticodeObjectIdentifiers.SPC_NESTED_SIGNATURE_OBJID, secondary.toASN1Structure()); - } else { - // append the signature to the previous nested signatures - ASN1EncodableVector nestedSignatures = new ASN1EncodableVector(); - for (ASN1Encodable nestedSignature : nestedSignaturesAttribute.getAttrValues()) { - nestedSignatures.add(nestedSignature); - } - nestedSignatures.add(secondary.toASN1Structure()); - - ASN1EncodableVector attributes = unsignedAttributes.remove(AuthenticodeObjectIdentifiers.SPC_NESTED_SIGNATURE_OBJID).toASN1EncodableVector(); - attributes.add(new Attribute(AuthenticodeObjectIdentifiers.SPC_NESTED_SIGNATURE_OBJID, new DERSet(nestedSignatures))); - - unsignedAttributes = new AttributeTable(attributes); - } - - signerInformation = SignerInformation.replaceUnsignedAttributes(signerInformation, unsignedAttributes); - return CMSSignedData.replaceSigners(primary, new SignerInformationStore(signerInformation)); - } - /** * Create the digest algorithm identifier to use as content digest. * By default looks up the default identifier but also makes sure it includes diff --git a/jsign-core/src/main/java/net/jsign/SignatureUtils.java b/jsign-core/src/main/java/net/jsign/SignatureUtils.java index 36089ea2..2e4db4fc 100644 --- a/jsign-core/src/main/java/net/jsign/SignatureUtils.java +++ b/jsign-core/src/main/java/net/jsign/SignatureUtils.java @@ -22,16 +22,21 @@ import java.util.NoSuchElementException; import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1EncodableVector; import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.DERSet; import org.bouncycastle.asn1.cms.Attribute; import org.bouncycastle.asn1.cms.AttributeTable; +import org.bouncycastle.asn1.cms.CMSAttributes; import org.bouncycastle.asn1.cms.ContentInfo; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; import org.bouncycastle.cms.CMSException; import org.bouncycastle.cms.CMSProcessable; import org.bouncycastle.cms.CMSSignedData; import org.bouncycastle.cms.SignerInformation; +import org.bouncycastle.cms.SignerInformationStore; -import net.jsign.asn1.authenticode.AuthenticodeObjectIdentifiers; +import static net.jsign.asn1.authenticode.AuthenticodeObjectIdentifiers.*; /** * Helper class for working with signatures. @@ -67,10 +72,10 @@ public static List getSignatures(CMSSignedData signature) throws signatures.add(signature); // look for nested signatures - SignerInformation signerInformation = signature.getSignerInfos().getSigners().iterator().next(); + SignerInformation signerInformation = signature.getSignerInfos().iterator().next(); AttributeTable unsignedAttributes = signerInformation.getUnsignedAttributes(); if (unsignedAttributes != null) { - Attribute nestedSignatures = unsignedAttributes.get(AuthenticodeObjectIdentifiers.SPC_NESTED_SIGNATURE_OBJID); + Attribute nestedSignatures = unsignedAttributes.get(SPC_NESTED_SIGNATURE_OBJID); if (nestedSignatures != null) { for (ASN1Encodable nestedSignature : nestedSignatures.getAttrValues()) { signatures.add(new CMSSignedData((CMSProcessable) null, ContentInfo.getInstance(nestedSignature))); @@ -84,4 +89,85 @@ public static List getSignatures(CMSSignedData signature) throws return signatures; } + + /** + * Embed a signature as an unsigned attribute of an existing signature. + * + * @param parent the root signature hosting the nested secondary signature + * @param children the additional signature to nest inside the root signature + * @return the signature combining the specified signatures + */ + static CMSSignedData addNestedSignature(CMSSignedData parent, boolean replace, CMSSignedData... children) { + SignerInformation signerInformation = parent.getSignerInfos().iterator().next(); + + AttributeTable unsignedAttributes = signerInformation.getUnsignedAttributes(); + if (unsignedAttributes == null) { + unsignedAttributes = new AttributeTable(new DERSet()); + } + + Attribute nestedSignaturesAttribute = unsignedAttributes.get(SPC_NESTED_SIGNATURE_OBJID); + ASN1EncodableVector nestedSignatures = new ASN1EncodableVector(); + if (nestedSignaturesAttribute != null && !replace) { + // keep the previous nested signatures + for (ASN1Encodable nestedSignature : nestedSignaturesAttribute.getAttrValues()) { + nestedSignatures.add(nestedSignature); + } + } + + // append the new signatures + for (CMSSignedData nestedSignature : children) { + nestedSignatures.add(nestedSignature.toASN1Structure()); + } + + // replace the nested signatures attribute + ASN1EncodableVector attributes = unsignedAttributes.remove(SPC_NESTED_SIGNATURE_OBJID).toASN1EncodableVector(); + attributes.add(new Attribute(SPC_NESTED_SIGNATURE_OBJID, new DERSet(nestedSignatures))); + + unsignedAttributes = new AttributeTable(attributes); + + signerInformation = SignerInformation.replaceUnsignedAttributes(signerInformation, unsignedAttributes); + return CMSSignedData.replaceSigners(parent, new SignerInformationStore(signerInformation)); + } + + /** + * Tells if the specified signature is timestamped. + * + * @param signature the signature to check + */ + static boolean isTimestamped(CMSSignedData signature) { + SignerInformation signerInformation = signature.getSignerInfos().iterator().next(); + + AttributeTable unsignedAttributes = signerInformation.getUnsignedAttributes(); + if (unsignedAttributes == null) { + return false; + } + + boolean authenticode = isAuthenticode(signature.getSignedContentTypeOID()); + Attribute authenticodeTimestampAttribute = unsignedAttributes.get(CMSAttributes.counterSignature); + Attribute rfc3161TimestampAttribute = unsignedAttributes.get(authenticode ? SPC_RFC3161_OBJID : PKCSObjectIdentifiers.id_aa_signatureTimeStampToken); + return authenticodeTimestampAttribute != null || rfc3161TimestampAttribute != null; + } + + /** + * Removes the timestamp from the specified signature. + * + * @param signature the signature to modify + */ + static CMSSignedData removeTimestamp(CMSSignedData signature) { + SignerInformation signerInformation = signature.getSignerInfos().iterator().next(); + + AttributeTable unsignedAttributes = signerInformation.getUnsignedAttributes(); + if (unsignedAttributes == null) { + return signature; + } + + unsignedAttributes = unsignedAttributes.remove(CMSAttributes.counterSignature); + unsignedAttributes = unsignedAttributes.remove(PKCSObjectIdentifiers.id_aa_signatureTimeStampToken); + unsignedAttributes = unsignedAttributes.remove(SPC_RFC3161_OBJID); + + // todo remove the TSA certificates from the certificate store + + signerInformation = SignerInformation.replaceUnsignedAttributes(signerInformation, unsignedAttributes); + return CMSSignedData.replaceSigners(signature, new SignerInformationStore(signerInformation)); + } } diff --git a/jsign-core/src/main/java/net/jsign/SignerHelper.java b/jsign-core/src/main/java/net/jsign/SignerHelper.java index e99732ba..42220647 100644 --- a/jsign-core/src/main/java/net/jsign/SignerHelper.java +++ b/jsign-core/src/main/java/net/jsign/SignerHelper.java @@ -36,9 +36,11 @@ import java.security.PrivateKey; import java.security.Provider; import java.security.cert.Certificate; +import java.util.ArrayList; import java.util.Base64; import java.util.Collection; import java.util.Collections; +import java.util.Date; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -55,14 +57,19 @@ import org.bouncycastle.asn1.DERUTF8String; import org.bouncycastle.asn1.cms.AttributeTable; import org.bouncycastle.asn1.cms.ContentInfo; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cms.CMSException; import org.bouncycastle.cms.CMSProcessable; import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.SignerId; import org.bouncycastle.cms.SignerInformation; import org.bouncycastle.cms.SignerInformationStore; +import org.bouncycastle.operator.DefaultAlgorithmNameFinder; import org.bouncycastle.util.encoders.Hex; import net.jsign.asn1.authenticode.AuthenticodeObjectIdentifiers; +import net.jsign.timestamp.Timestamper; import net.jsign.timestamp.TimestampingMode; /** @@ -102,7 +109,9 @@ class SignerHelper { /** The name used to refer to a configuration parameter */ private final String parameterName; + /** The command to execute */ private String command = "sign"; + private final KeyStoreBuilder ksparams; private String alias; private String tsaurl; @@ -300,6 +309,9 @@ public void execute(File file) throws SignerException { case "sign": sign(file); break; + case "timestamp": + timestamp(file); + break; case "extract": extract(file); break; @@ -634,6 +646,71 @@ private ASN1Encodable getTagValue() throws IOException { } } + private void timestamp(File file) throws SignerException { + if (!file.exists()) { + throw new SignerException("Couldn't find " + file); + } + + try { + initializeProxy(proxyUrl, proxyUser, proxyPass); + } catch (Exception e) { + throw new SignerException("Couldn't initialize proxy", e); + } + + try (Signable signable = Signable.of(file)) { + if (signable.getSignatures().isEmpty()) { + throw new SignerException("No signature found in " + file); + } + + Timestamper timestamper = Timestamper.create(tsmode != null ? TimestampingMode.of(tsmode) : TimestampingMode.AUTHENTICODE); + timestamper.setRetries(tsretries); + timestamper.setRetryWait(tsretrywait); + if (tsaurl != null) { + timestamper.setURLs(tsaurl.split(",")); + } + DigestAlgorithm digestAlgorithm = alg != null ? DigestAlgorithm.of(alg) : DigestAlgorithm.getDefault(); + + List signatures = new ArrayList<>(); + for (CMSSignedData signature : signable.getSignatures()) { + SignerInformation signerInformation = signature.getSignerInfos().iterator().next(); + SignerId signerId = signerInformation.getSID(); + X509CertificateHolder certificate = (X509CertificateHolder) signature.getCertificates().getMatches(signerId).iterator().next(); + + String digestAlgorithmName = new DefaultAlgorithmNameFinder().getAlgorithmName(signerInformation.getDigestAlgorithmID()); + String keyAlgorithmName = new DefaultAlgorithmNameFinder().getAlgorithmName(new ASN1ObjectIdentifier(signerInformation.getEncryptionAlgOID())); + String name = digestAlgorithmName + "/" + keyAlgorithmName + " signature from '" + certificate.getSubject() + "'"; + + if (SignatureUtils.isTimestamped(signature) && !replace) { + log.fine(name + " already timestamped"); + signatures.add(signature); + continue; + } + + boolean expired = certificate.getNotAfter().before(new Date()); + if (expired) { + log.fine(name + " is expired, skipping"); + signatures.add(signature); + continue; + } + + log.info("Adding timestamp to " + name); + signature = SignatureUtils.removeTimestamp(signature); + signature = timestamper.timestamp(digestAlgorithm, signature); + + signatures.add(signature); + } + + CMSSignedData signature = signatures.get(0); + if (signatures.size() > 1) { + Collection nestedSignatures = signatures.subList(1, signatures.size()); + signature = SignatureUtils.addNestedSignature(signature, true, nestedSignatures.toArray(new CMSSignedData[0])); + } + signable.setSignature(signature); + } catch (IOException | CMSException e) { + throw new SignerException("Couldn't timestamp " + file, e); + } + } + /** * Initializes the proxy. * diff --git a/jsign-core/src/main/java/net/jsign/timestamp/Timestamper.java b/jsign-core/src/main/java/net/jsign/timestamp/Timestamper.java index 42390630..b39d020c 100644 --- a/jsign-core/src/main/java/net/jsign/timestamp/Timestamper.java +++ b/jsign-core/src/main/java/net/jsign/timestamp/Timestamper.java @@ -29,12 +29,17 @@ import org.apache.commons.io.HexDump; import org.apache.commons.io.IOUtils; +import org.bouncycastle.asn1.ASN1InputStream; import org.bouncycastle.asn1.cms.Attribute; import org.bouncycastle.asn1.cms.AttributeTable; +import org.bouncycastle.asn1.cms.ContentInfo; +import org.bouncycastle.asn1.cms.SignedData; import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.selector.X509CertificateHolderSelector; import org.bouncycastle.cms.CMSException; import org.bouncycastle.cms.CMSSignedData; import org.bouncycastle.cms.CMSSignedDataGenerator; +import org.bouncycastle.cms.PKCS7ProcessableObject; import org.bouncycastle.cms.SignerInformation; import org.bouncycastle.cms.SignerInformationStore; import org.bouncycastle.util.CollectionStore; @@ -255,33 +260,44 @@ protected AttributeTable getUnsignedAttributes(CMSSignedData token) { } @Deprecated - protected CMSSignedData modifySignedData(CMSSignedData sigData, AttributeTable counterSignature, Collection extraCertificates) throws CMSException { + protected CMSSignedData modifySignedData(CMSSignedData sigData, AttributeTable counterSignature, Collection extraCertificates) throws IOException, CMSException { return modifySignedData(sigData, Attribute.getInstance(counterSignature.toASN1EncodableVector().get(0)), extraCertificates); } - protected CMSSignedData modifySignedData(CMSSignedData sigData, Attribute counterSignature, Collection extraCertificates) throws CMSException { + protected CMSSignedData modifySignedData(CMSSignedData sigData, Attribute counterSignature, Collection extraCertificates) throws IOException, CMSException { SignerInformation signerInformation = sigData.getSignerInfos().getSigners().iterator().next(); AttributeTable unsignedAttributes = signerInformation.getUnsignedAttributes(); if (unsignedAttributes == null) { unsignedAttributes = new AttributeTable(counterSignature); } else { - unsignedAttributes = unsignedAttributes.add(counterSignature.getAttrType(), counterSignature.getAttrValues()); + unsignedAttributes = unsignedAttributes.add(counterSignature.getAttrType(), counterSignature.getAttrValues().getObjectAt(0)); } signerInformation = SignerInformation.replaceUnsignedAttributes(signerInformation, unsignedAttributes); - // add the certificates from the timestamping authority + // add the new timestamping authority certificates Collection certificates = new ArrayList<>(sigData.getCertificates().getMatches(null)); if (extraCertificates != null) { - certificates.addAll(extraCertificates); + for (X509CertificateHolder certificate : extraCertificates) { + X509CertificateHolderSelector selector = new X509CertificateHolderSelector(certificate.getIssuer(), certificate.getSerialNumber()); + if (sigData.getCertificates().getMatches(selector).isEmpty()) { + certificates.add(certificate); + } + } } Store certificateStore = new CollectionStore<>(certificates); + // get the signed content (CMSSignedData.getSignedContent() has a null content when loading the signature back from the file) + byte[] encoded = sigData.toASN1Structure().getContent().toASN1Primitive().getEncoded("DER"); + SignedData signedData = SignedData.getInstance(new ASN1InputStream(encoded).readObject()); + ContentInfo content = signedData.getEncapContentInfo(); + PKCS7ProcessableObject signedContent = new PKCS7ProcessableObject(content.getContentType(), content.getContent()); + boolean authenticode = AuthenticodeObjectIdentifiers.isAuthenticode(sigData.getSignedContentTypeOID()); CMSSignedDataGenerator generator = authenticode ? new AuthenticodeSignedDataGenerator() : new CMSSignedDataGenerator(); generator.addCertificates(certificateStore); generator.addSigners(new SignerInformationStore(signerInformation)); - return generator.generate(sigData.getSignedContent(), true); + return generator.generate(signedContent, true); } protected abstract CMSSignedData timestamp(DigestAlgorithm algo, byte[] encryptedDigest) throws IOException, TimestampingException; diff --git a/jsign-core/src/test/java/net/jsign/SignerHelperTest.java b/jsign-core/src/test/java/net/jsign/SignerHelperTest.java index a9945204..ff4c1e63 100644 --- a/jsign-core/src/test/java/net/jsign/SignerHelperTest.java +++ b/jsign-core/src/test/java/net/jsign/SignerHelperTest.java @@ -26,6 +26,7 @@ import org.apache.commons.io.FileUtils; import org.bouncycastle.asn1.DEROctetString; import org.bouncycastle.asn1.cms.Attribute; +import org.bouncycastle.asn1.cms.CMSAttributes; import org.bouncycastle.cms.CMSSignedData; import org.bouncycastle.cms.SignerInformation; import org.junit.Assume; @@ -39,6 +40,7 @@ import net.jsign.jca.OracleCloudCredentials; import net.jsign.jca.PIVCardTest; import net.jsign.pe.PEFile; +import net.jsign.timestamp.TimestampingMode; import static net.jsign.DigestAlgorithm.*; import static org.junit.Assert.*; @@ -764,4 +766,105 @@ public void testTagMissingFile() { assertEquals("message", "Couldn't find target/test-classes/xeyes.exe", e.getMessage().replace('\\', '/')); } } + + @Test + public void testTimestamp() throws Exception { + File sourceFile = new File("target/test-classes/wineyes.exe"); + File targetFile = new File("target/test-classes/wineyes-signed-then-timestamped.exe"); + + FileUtils.copyFile(sourceFile, targetFile); + + SignerHelper signer = new SignerHelper("parameter") + .keystore("target/test-classes/keystores/keystore.jks") + .keypass("password") + .tsmode(TimestampingMode.AUTHENTICODE.name()); + + signer.execute(targetFile); + + signer = new SignerHelper("parameter") + .keystore("target/test-classes/keystores/keystore.jks") + .keypass("password"); + + signer.execute(targetFile); + signer.execute(targetFile); + + try (Signable signable = Signable.of(targetFile)) { + SignatureAssert.assertTimestamped("Invalid timestamp", signable.getSignatures().get(0)); + SignatureAssert.assertNotTimestamped("Unexpected timestamp", signable.getSignatures().get(1)); + SignatureAssert.assertNotTimestamped("Unexpected timestamp", signable.getSignatures().get(2)); + } + + signer.command("timestamp"); + signer.execute(targetFile); + + try (Signable signable = Signable.of(targetFile)) { + SignatureAssert.assertTimestamped("Invalid timestamp", signable.getSignatures().get(0)); + SignatureAssert.assertTimestamped("Invalid timestamp", signable.getSignatures().get(1)); + SignatureAssert.assertTimestamped("Invalid timestamp", signable.getSignatures().get(2)); + } + } + + @Test + public void testReplaceTimestamp() throws Exception { + File sourceFile = new File("target/test-classes/wineyes.exe"); + File targetFile = new File("target/test-classes/wineyes-timestamp-replaced.exe"); + + FileUtils.copyFile(sourceFile, targetFile); + + SignerHelper signer = new SignerHelper("parameter") + .keystore("target/test-classes/keystores/keystore.jks") + .keypass("password") + .tsaurl("http://timestamp.sectigo.com") + .tsmode(TimestampingMode.AUTHENTICODE.name()); + + signer.execute(targetFile); + try (Signable signable = Signable.of(targetFile)) { + SignatureAssert.assertTimestamped("Invalid timestamp", signable.getSignatures().get(0)); + } + + signer = new SignerHelper("parameter"); + signer.command("timestamp"); + signer.tsaurl("http://timestamp.sectigo.com"); + signer.tsmode(TimestampingMode.AUTHENTICODE.name()); + signer.replace(true); + signer.execute(targetFile); + + try (Signable signable = Signable.of(targetFile)) { + CMSSignedData signature = signable.getSignatures().get(0); + SignatureAssert.assertTimestamped("Invalid timestamp", signature); + SignerInformation signerInformation = signature.getSignerInfos().iterator().next(); + assertNull("old timestamp not removed", signerInformation.getUnsignedAttributes().get(AuthenticodeObjectIdentifiers.SPC_RFC3161_OBJID)); + assertNotNull("missing new timestamp", signerInformation.getUnsignedAttributes().get(CMSAttributes.counterSignature)); + } + } + + @Test + public void testTimestampUnsignedFile() { + File file = new File("target/test-classes/wineyes.exe"); + + SignerHelper signer = new SignerHelper("parameter"); + signer.command("timestamp"); + + try { + signer.execute(file); + fail("Exception not thrown"); + } catch (SignerException e) { + assertEquals("message", "No signature found in target/test-classes/wineyes.exe", e.getMessage().replace('\\', '/')); + } + } + + @Test + public void testTimestampMissingFile() { + File file = new File("target/test-classes/xeyes.exe"); + + SignerHelper signer = new SignerHelper("parameter"); + signer.command("timestamp"); + + try { + signer.execute(file); + fail("Exception not thrown"); + } catch (SignerException e) { + assertEquals("message", "Couldn't find target/test-classes/xeyes.exe", e.getMessage().replace('\\', '/')); + } + } }