001/** 002 * Copyright 2014 Emmanuel Bourg 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package net.jsign.timestamp; 018 019import java.io.IOException; 020import java.net.MalformedURLException; 021import java.net.URL; 022import java.util.ArrayList; 023import java.util.Collection; 024import java.util.List; 025 026import org.bouncycastle.asn1.ASN1Encodable; 027import org.bouncycastle.asn1.ASN1ObjectIdentifier; 028import org.bouncycastle.asn1.ASN1Sequence; 029import org.bouncycastle.asn1.cms.Attribute; 030import org.bouncycastle.asn1.cms.AttributeTable; 031import org.bouncycastle.cert.X509CertificateHolder; 032import org.bouncycastle.cms.CMSException; 033import org.bouncycastle.cms.CMSSignedData; 034import org.bouncycastle.cms.SignerInformation; 035import org.bouncycastle.cms.SignerInformationStore; 036import org.bouncycastle.util.CollectionStore; 037import org.bouncycastle.util.Store; 038 039import net.jsign.DigestAlgorithm; 040import net.jsign.asn1.authenticode.AuthenticodeSignedDataGenerator; 041 042/** 043 * Interface for a timestamping service. 044 * 045 * @author Emmanuel Bourg 046 * @since 1.3 047 */ 048public abstract class Timestamper { 049 050 /** The URL of the current timestamping service */ 051 protected URL tsaurl; 052 053 /** The URLs of the timestamping services */ 054 protected List<URL> tsaurls; 055 056 /** The number of retries */ 057 protected int retries = 3; 058 059 /** Seconds to wait between retries */ 060 protected int retryWait = 10; 061 062 /** 063 * Set the URL of the timestamping service. 064 * 065 * @param tsaurl the URL of the timestamping service 066 */ 067 public void setURL(String tsaurl) { 068 setURLs(tsaurl); 069 } 070 071 /** 072 * Set the URLs of the timestamping services. 073 * 074 * @param tsaurls the URLs of the timestamping services 075 * @since 2.0 076 */ 077 public void setURLs(String... tsaurls) { 078 List<URL> urls = new ArrayList<>(); 079 for (String tsaurl : tsaurls) { 080 try { 081 urls.add(new URL(tsaurl)); 082 } catch (MalformedURLException e) { 083 throw new IllegalArgumentException("Invalid timestamping URL: " + tsaurl, e); 084 } 085 } 086 this.tsaurls = urls; 087 } 088 089 /** 090 * Set the number of retries. 091 * 092 * @param retries the number of retries 093 */ 094 public void setRetries(int retries) { 095 this.retries = retries; 096 } 097 098 /** 099 * Set the number of seconds to wait between retries. 100 * 101 * @param retryWait the wait time between retries (in seconds) 102 */ 103 public void setRetryWait(int retryWait) { 104 this.retryWait = retryWait; 105 } 106 107 /** 108 * Timestamp the specified signature. 109 * 110 * @param algo the digest algorithm used for the timestamp 111 * @param sigData the signed data to be timestamped 112 * @return the signed data with the timestamp added 113 * @throws IOException if an I/O error occurs 114 * @throws TimestampingException if the timestamping keeps failing after the configured number of attempts 115 * @throws CMSException if the signature cannot be generated 116 */ 117 public CMSSignedData timestamp(DigestAlgorithm algo, CMSSignedData sigData) throws TimestampingException, IOException, CMSException { 118 CMSSignedData token = null; 119 120 // Retry the timestamping and failover other services if a TSA is unavailable for a short period of time 121 int attempts = Math.max(retries, tsaurls.size()); 122 TimestampingException exception = new TimestampingException("Unable to complete the timestamping after " + attempts + " attempt" + (attempts > 1 ? "s" : "")); 123 int count = 0; 124 while (count < Math.max(retries, tsaurls.size())) { 125 try { 126 tsaurl = tsaurls.get(count % tsaurls.size()); 127 token = timestamp(algo, getEncryptedDigest(sigData)); 128 break; 129 } catch (TimestampingException | IOException e) { 130 exception.addSuppressed(e); 131 } 132 133 // pause before the next attempt 134 try { 135 Thread.sleep(retryWait * 1000L); 136 count++; 137 } catch (InterruptedException ie) { 138 } 139 } 140 141 if (token == null) { 142 throw exception; 143 } 144 145 return modifySignedData(sigData, getCounterSignature(token), getExtraCertificates(token)); 146 } 147 148 /** 149 * Return the encrypted digest of the specified signature. 150 * 151 * @param sigData the signature 152 * @return the encrypted digest 153 */ 154 private byte[] getEncryptedDigest(CMSSignedData sigData) { 155 SignerInformation signerInformation = sigData.getSignerInfos().getSigners().iterator().next(); 156 return signerInformation.toASN1Structure().getEncryptedDigest().getOctets(); 157 } 158 159 /** 160 * Return the certificate chain of the timestamping authority if it isn't included 161 * with the counter signature in the unsigned attributes. 162 * 163 * @param token the timestamp 164 * @return the certificate chain of the timestamping authority 165 */ 166 protected Collection<X509CertificateHolder> getExtraCertificates(CMSSignedData token) { 167 return null; 168 } 169 170 /** 171 * Return the counter signature to be added as an unsigned attribute. 172 * 173 * @param token the timestamp 174 * @return the attribute wrapping the timestamp 175 * @since 5.0 176 */ 177 protected abstract Attribute getCounterSignature(CMSSignedData token); 178 179 /** 180 * Return the counter signature to be added as an unsigned attribute. 181 * 182 * @param token the timestamp 183 * @return the attribute wrapping the timestamp 184 * @deprecated use {@link #getCounterSignature(CMSSignedData)} instead 185 */ 186 @Deprecated 187 protected AttributeTable getUnsignedAttributes(CMSSignedData token) { 188 return new AttributeTable(getCounterSignature(token)); 189 } 190 191 @Deprecated 192 protected CMSSignedData modifySignedData(CMSSignedData sigData, AttributeTable counterSignature, Collection<X509CertificateHolder> extraCertificates) throws IOException, CMSException { 193 return modifySignedData(sigData, Attribute.getInstance(counterSignature.toASN1EncodableVector().get(0)), extraCertificates); 194 } 195 196 protected CMSSignedData modifySignedData(CMSSignedData sigData, Attribute counterSignature, Collection<X509CertificateHolder> extraCertificates) throws IOException, CMSException { 197 SignerInformation signerInformation = sigData.getSignerInfos().getSigners().iterator().next(); 198 AttributeTable unsignedAttributes = signerInformation.getUnsignedAttributes(); 199 if (unsignedAttributes == null) { 200 unsignedAttributes = new AttributeTable(counterSignature); 201 } else { 202 unsignedAttributes = unsignedAttributes.add(counterSignature.getAttrType(), counterSignature.getAttrValues()); 203 } 204 signerInformation = SignerInformation.replaceUnsignedAttributes(signerInformation, unsignedAttributes); 205 206 Collection<X509CertificateHolder> certificates = new ArrayList<>(); 207 certificates.addAll(sigData.getCertificates().getMatches(null)); 208 if (extraCertificates != null) { 209 certificates.addAll(extraCertificates); 210 } 211 Store<X509CertificateHolder> certificateStore = new CollectionStore<>(certificates); 212 213 AuthenticodeSignedDataGenerator generator = new AuthenticodeSignedDataGenerator(); 214 generator.addCertificates(certificateStore); 215 generator.addSigners(new SignerInformationStore(signerInformation)); 216 217 ASN1ObjectIdentifier contentType = new ASN1ObjectIdentifier(sigData.getSignedContentTypeOID()); 218 ASN1Encodable content = ASN1Sequence.getInstance(sigData.getSignedContent().getContent()); 219 220 return generator.generate(contentType, content); 221 } 222 223 protected abstract CMSSignedData timestamp(DigestAlgorithm algo, byte[] encryptedDigest) throws IOException, TimestampingException; 224 225 /** 226 * Returns the timestamper for the specified mode. 227 * 228 * @param mode the timestamping mode 229 * @return a new timestamper for the specified mode 230 */ 231 public static Timestamper create(TimestampingMode mode) { 232 switch (mode) { 233 case AUTHENTICODE: 234 return new AuthenticodeTimestamper(); 235 case RFC3161: 236 return new RFC3161Timestamper(); 237 default: 238 throw new IllegalArgumentException("Unsupported timestamping mode: " + mode); 239 } 240 } 241}