Skip to content

Commit

Permalink
Timestamp command implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
ebourg committed Jun 14, 2024
1 parent b5ab1ea commit be9b7b6
Show file tree
Hide file tree
Showing 9 changed files with 323 additions and 50 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,7 @@ <h3 id="cli">Command Line Tool</h3>
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 &lt;FILE> The keystore file, the SunPKCS11 configuration file,
Expand Down Expand Up @@ -576,6 +576,15 @@ <h3 id="cli">Command Line Tool</h3>
--debug Print debugging information
-h,--help Print the help

timestamp:
-t,--tsaurl &lt;URL> The URL of the timestamping authority
-m,--tsmode &lt;MODE> The timestamping mode (RFC3161 or Authenticode)
-r,--tsretries &lt;NUMBER> The number of retries for timestamping
-w,--tsretrywait &lt;SECONDS> The number of seconds to wait between timestamping retries
-n,--name &lt;NAME> The name of the application
-u,--url &lt;URL> The URL of the application
--replace Tells if the previous timestamps should be replaced

extract:
--format &lt;FORMAT> The output format of the signature (DER or PEM)

Expand Down
11 changes: 11 additions & 0 deletions jsign-cli/src/main/java/net/jsign/JsignCLI.java
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down
9 changes: 9 additions & 0 deletions jsign-cli/src/test/java/net/jsign/JsignCLITest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
}
41 changes: 1 addition & 40 deletions jsign-core/src/main/java/net/jsign/AuthenticodeSigner.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -376,7 +373,7 @@ public void sign(Signable file) throws Exception {
List<CMSSignedData> signatures = file.getSignatures();
if (!signatures.isEmpty()) {
// append the nested signature
sigData = addNestedSignature(signatures.get(0), sigData);
sigData = SignatureUtils.addNestedSignature(signatures.get(0), false, sigData);
}
}

Expand Down Expand Up @@ -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
Expand Down
92 changes: 89 additions & 3 deletions jsign-core/src/main/java/net/jsign/SignatureUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -67,10 +72,10 @@ public static List<CMSSignedData> 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)));
Expand All @@ -84,4 +89,85 @@ public static List<CMSSignedData> 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));
}
}
77 changes: 77 additions & 0 deletions jsign-core/src/main/java/net/jsign/SignerHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

/**
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<CMSSignedData> 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<CMSSignedData> 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.
*
Expand Down
Loading

0 comments on commit be9b7b6

Please sign in to comment.