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.HttpURLConnection; 021import java.net.MalformedURLException; 022import java.net.URL; 023import java.util.ArrayList; 024import java.util.Collection; 025import java.util.List; 026import java.util.Map; 027import java.util.logging.Level; 028import java.util.logging.Logger; 029 030import org.apache.commons.io.HexDump; 031import org.apache.commons.io.IOUtils; 032import org.bouncycastle.asn1.ASN1InputStream; 033import org.bouncycastle.asn1.cms.Attribute; 034import org.bouncycastle.asn1.cms.AttributeTable; 035import org.bouncycastle.asn1.cms.ContentInfo; 036import org.bouncycastle.asn1.cms.SignedData; 037import org.bouncycastle.cert.X509CertificateHolder; 038import org.bouncycastle.cert.selector.X509CertificateHolderSelector; 039import org.bouncycastle.cms.CMSException; 040import org.bouncycastle.cms.CMSSignedData; 041import org.bouncycastle.cms.CMSSignedDataGenerator; 042import org.bouncycastle.cms.PKCS7ProcessableObject; 043import org.bouncycastle.cms.SignerInformation; 044import org.bouncycastle.cms.SignerInformationStore; 045import org.bouncycastle.util.CollectionStore; 046import org.bouncycastle.util.Store; 047 048import net.jsign.DigestAlgorithm; 049import net.jsign.asn1.authenticode.AuthenticodeObjectIdentifiers; 050import net.jsign.asn1.authenticode.AuthenticodeSignedDataGenerator; 051 052/** 053 * Interface for a timestamping service. 054 * 055 * @author Emmanuel Bourg 056 * @since 1.3 057 */ 058public abstract class Timestamper { 059 060 protected Logger log = Logger.getLogger(getClass().getName()); 061 062 /** The URL of the current timestamping service */ 063 protected URL tsaurl; 064 065 /** The URLs of the timestamping services */ 066 protected List<URL> tsaurls; 067 068 /** The number of retries */ 069 protected int retries = 3; 070 071 /** Seconds to wait between retries */ 072 protected int retryWait = 10; 073 074 /** 075 * Set the URL of the timestamping service. 076 * 077 * @param tsaurl the URL of the timestamping service 078 */ 079 public void setURL(String tsaurl) { 080 setURLs(tsaurl); 081 } 082 083 /** 084 * Set the URLs of the timestamping services. 085 * 086 * @param tsaurls the URLs of the timestamping services 087 * @since 2.0 088 */ 089 public void setURLs(String... tsaurls) { 090 List<URL> urls = new ArrayList<>(); 091 for (String tsaurl : tsaurls) { 092 try { 093 urls.add(new URL(tsaurl)); 094 } catch (MalformedURLException e) { 095 throw new IllegalArgumentException("Invalid timestamping URL: " + tsaurl, e); 096 } 097 } 098 this.tsaurls = urls; 099 } 100 101 /** 102 * Set the number of retries. 103 * 104 * @param retries the number of retries 105 */ 106 public void setRetries(int retries) { 107 this.retries = retries; 108 } 109 110 /** 111 * Set the number of seconds to wait between retries. 112 * 113 * @param retryWait the wait time between retries (in seconds) 114 */ 115 public void setRetryWait(int retryWait) { 116 this.retryWait = retryWait; 117 } 118 119 /** 120 * Timestamp the specified signature. 121 * 122 * @param algo the digest algorithm used for the timestamp 123 * @param sigData the signed data to be timestamped 124 * @return the signed data with the timestamp added 125 * @throws IOException if an I/O error occurs 126 * @throws TimestampingException if the timestamping keeps failing after the configured number of attempts 127 * @throws CMSException if the signature cannot be generated 128 */ 129 public CMSSignedData timestamp(DigestAlgorithm algo, CMSSignedData sigData) throws TimestampingException, IOException, CMSException { 130 CMSSignedData token = null; 131 132 // Retry the timestamping and failover other services if a TSA is unavailable for a short period of time 133 int attempts = Math.max(retries, tsaurls.size()); 134 TimestampingException exception = new TimestampingException("Unable to complete the timestamping after " + attempts + " attempt" + (attempts > 1 ? "s" : "")); 135 int count = 0; 136 while (count < Math.max(retries, tsaurls.size())) { 137 if (count > 0) { 138 // pause before the next attempt 139 try { 140 long pause = retryWait * 1000L; 141 log.fine("Timestamping failed, retrying in " + pause / 1000 + " seconds"); 142 Thread.sleep(pause); 143 } catch (InterruptedException ie) { 144 } 145 } 146 try { 147 tsaurl = tsaurls.get(count % tsaurls.size()); 148 log.fine("Timestamping with " + tsaurl); 149 long t0 = System.currentTimeMillis(); 150 token = timestamp(algo, getEncryptedDigest(sigData)); 151 long t1 = System.currentTimeMillis(); 152 log.fine("Timestamping completed in " + (t1 - t0) + " ms"); 153 break; 154 } catch (TimestampingException | IOException e) { 155 exception.addSuppressed(e); 156 } 157 count++; 158 } 159 160 if (token == null) { 161 throw exception; 162 } 163 164 return modifySignedData(sigData, getCounterSignature(token), getExtraCertificates(token)); 165 } 166 167 byte[] post(URL url, byte[] data, Map<String, String> headers) throws IOException { 168 log.finest("POST " + url); 169 170 HttpURLConnection conn = (HttpURLConnection) url.openConnection(); 171 conn.setConnectTimeout(10000); 172 conn.setReadTimeout(10000); 173 conn.setDoOutput(true); 174 conn.setDoInput(true); 175 conn.setUseCaches(false); 176 conn.setRequestMethod("POST"); 177 conn.setRequestProperty("Content-length", String.valueOf(data.length)); 178 conn.setRequestProperty("User-Agent", "Transport"); 179 for (Map.Entry<String, String> header : headers.entrySet()) { 180 conn.setRequestProperty(header.getKey(), header.getValue()); 181 } 182 183 if (log.isLoggable(Level.FINEST)) { 184 for (String header : conn.getRequestProperties().keySet()) { 185 log.finest(header + ": " + conn.getRequestProperty(header)); 186 } 187 log("Content", data); 188 } 189 190 conn.getOutputStream().write(data); 191 conn.getOutputStream().flush(); 192 193 for (String header : conn.getHeaderFields().keySet()) { 194 log.finest((header != null ? header + ": " : "") + conn.getHeaderField(header)); 195 } 196 if (conn.getResponseCode() >= 400) { 197 byte[] error = conn.getErrorStream() != null ? IOUtils.toByteArray(conn.getErrorStream()) : new byte[0]; 198 if (conn.getErrorStream() != null) { 199 log("Error", error); 200 } 201 throw new IOException("Unable to complete the timestamping due to HTTP error: " + conn.getResponseCode() + " - " + conn.getResponseMessage()); 202 } 203 204 byte[] content = IOUtils.toByteArray(conn.getInputStream()); 205 log("Content", content); 206 207 return content; 208 } 209 210 private void log(String description, byte[] data) throws IOException { 211 if (log.isLoggable(Level.FINEST)) { 212 log.finest(description + ":"); 213 StringBuffer out = new StringBuffer(); 214 HexDump.dump(data, 0, out, 0, data.length); 215 log.finest(out.toString()); 216 } 217 } 218 219 /** 220 * Return the encrypted digest of the specified signature. 221 * 222 * @param sigData the signature 223 * @return the encrypted digest 224 */ 225 private byte[] getEncryptedDigest(CMSSignedData sigData) { 226 SignerInformation signerInformation = sigData.getSignerInfos().getSigners().iterator().next(); 227 return signerInformation.toASN1Structure().getEncryptedDigest().getOctets(); 228 } 229 230 /** 231 * Return the certificate chain of the timestamping authority if it isn't included 232 * with the counter signature in the unsigned attributes. 233 * 234 * @param token the timestamp 235 * @return the certificate chain of the timestamping authority 236 */ 237 protected Collection<X509CertificateHolder> getExtraCertificates(CMSSignedData token) { 238 return null; 239 } 240 241 /** 242 * Return the counter signature to be added as an unsigned attribute. 243 * 244 * @param token the timestamp 245 * @return the attribute wrapping the timestamp 246 * @since 5.0 247 */ 248 protected abstract Attribute getCounterSignature(CMSSignedData token); 249 250 /** 251 * Return the counter signature to be added as an unsigned attribute. 252 * 253 * @param token the timestamp 254 * @return the attribute wrapping the timestamp 255 * @deprecated use {@link #getCounterSignature(CMSSignedData)} instead 256 */ 257 @Deprecated 258 protected AttributeTable getUnsignedAttributes(CMSSignedData token) { 259 return new AttributeTable(getCounterSignature(token)); 260 } 261 262 @Deprecated 263 protected CMSSignedData modifySignedData(CMSSignedData sigData, AttributeTable counterSignature, Collection<X509CertificateHolder> extraCertificates) throws IOException, CMSException { 264 return modifySignedData(sigData, Attribute.getInstance(counterSignature.toASN1EncodableVector().get(0)), extraCertificates); 265 } 266 267 protected CMSSignedData modifySignedData(CMSSignedData sigData, Attribute counterSignature, Collection<X509CertificateHolder> extraCertificates) throws IOException, CMSException { 268 SignerInformation signerInformation = sigData.getSignerInfos().getSigners().iterator().next(); 269 AttributeTable unsignedAttributes = signerInformation.getUnsignedAttributes(); 270 if (unsignedAttributes == null) { 271 unsignedAttributes = new AttributeTable(counterSignature); 272 } else { 273 unsignedAttributes = unsignedAttributes.add(counterSignature.getAttrType(), counterSignature.getAttrValues().getObjectAt(0)); 274 } 275 signerInformation = SignerInformation.replaceUnsignedAttributes(signerInformation, unsignedAttributes); 276 277 // add the new timestamping authority certificates 278 Collection<X509CertificateHolder> certificates = new ArrayList<>(sigData.getCertificates().getMatches(null)); 279 if (extraCertificates != null) { 280 for (X509CertificateHolder certificate : extraCertificates) { 281 X509CertificateHolderSelector selector = new X509CertificateHolderSelector(certificate.getIssuer(), certificate.getSerialNumber()); 282 if (sigData.getCertificates().getMatches(selector).isEmpty()) { 283 certificates.add(certificate); 284 } 285 } 286 } 287 Store<X509CertificateHolder> certificateStore = new CollectionStore<>(certificates); 288 289 // get the signed content (CMSSignedData.getSignedContent() has a null content when loading the signature back from the file) 290 byte[] encoded = sigData.toASN1Structure().getContent().toASN1Primitive().getEncoded("DER"); 291 SignedData signedData = SignedData.getInstance(new ASN1InputStream(encoded).readObject()); 292 ContentInfo content = signedData.getEncapContentInfo(); 293 PKCS7ProcessableObject signedContent = new PKCS7ProcessableObject(content.getContentType(), content.getContent()); 294 295 boolean authenticode = AuthenticodeObjectIdentifiers.isAuthenticode(sigData.getSignedContentTypeOID()); 296 CMSSignedDataGenerator generator = authenticode ? new AuthenticodeSignedDataGenerator() : new CMSSignedDataGenerator(); 297 generator.addCertificates(certificateStore); 298 generator.addSigners(new SignerInformationStore(signerInformation)); 299 300 return generator.generate(signedContent, true); 301 } 302 303 protected abstract CMSSignedData timestamp(DigestAlgorithm algo, byte[] encryptedDigest) throws IOException, TimestampingException; 304 305 /** 306 * Returns the timestamper for the specified mode. 307 * 308 * @param mode the timestamping mode 309 * @return a new timestamper for the specified mode 310 */ 311 public static Timestamper create(TimestampingMode mode) { 312 switch (mode) { 313 case AUTHENTICODE: 314 return new AuthenticodeTimestamper(); 315 case RFC3161: 316 return new RFC3161Timestamper(); 317 default: 318 throw new IllegalArgumentException("Unsupported timestamping mode: " + mode); 319 } 320 } 321}