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.AttributeTable; 030import org.bouncycastle.cert.X509CertificateHolder; 031import org.bouncycastle.cms.CMSException; 032import org.bouncycastle.cms.CMSSignedData; 033import org.bouncycastle.cms.SignerInformation; 034import org.bouncycastle.cms.SignerInformationStore; 035import org.bouncycastle.util.CollectionStore; 036import org.bouncycastle.util.Store; 037 038import net.jsign.DigestAlgorithm; 039import net.jsign.asn1.authenticode.AuthenticodeSignedDataGenerator; 040 041/** 042 * Interface for a timestamping service. 043 * 044 * @author Emmanuel Bourg 045 * @since 1.3 046 */ 047public abstract class Timestamper { 048 049 /** The URL of the current timestamping service */ 050 protected URL tsaurl; 051 052 /** The URLs of the timestamping services */ 053 protected List<URL> tsaurls; 054 055 /** The number of retries */ 056 protected int retries = 3; 057 058 /** Seconds to wait between retries */ 059 protected int retryWait = 10; 060 061 /** 062 * Set the URL of the timestamping service. 063 * 064 * @param tsaurl the URL of the timestamping service 065 */ 066 public void setURL(String tsaurl) { 067 setURLs(tsaurl); 068 } 069 070 /** 071 * Set the URLs of the timestamping services. 072 * 073 * @param tsaurls the URLs of the timestamping services 074 * @since 2.0 075 */ 076 public void setURLs(String... tsaurls) { 077 List<URL> urls = new ArrayList<>(); 078 for (String tsaurl : tsaurls) { 079 try { 080 urls.add(new URL(tsaurl)); 081 } catch (MalformedURLException e) { 082 throw new IllegalArgumentException("Invalid timestamping URL: " + tsaurl, e); 083 } 084 } 085 this.tsaurls = urls; 086 } 087 088 /** 089 * Set the number of retries. 090 * 091 * @param retries the number of retries 092 */ 093 public void setRetries(int retries) { 094 this.retries = retries; 095 } 096 097 /** 098 * Set the number of seconds to wait between retries. 099 * 100 * @param retryWait the wait time between retries (in seconds) 101 */ 102 public void setRetryWait(int retryWait) { 103 this.retryWait = retryWait; 104 } 105 106 /** 107 * Timestamp the specified signature. 108 * 109 * @param algo the digest algorithm used for the timestamp 110 * @param sigData the signed data to be timestamped 111 * @return the signed data with the timestamp added 112 * @throws IOException if an I/O error occurs 113 * @throws TimestampingException if the timestamping keeps failing after the configured number of attempts 114 * @throws CMSException if the signature cannot be generated 115 */ 116 public CMSSignedData timestamp(DigestAlgorithm algo, CMSSignedData sigData) throws TimestampingException, IOException, CMSException { 117 CMSSignedData token = null; 118 119 // Retry the timestamping and failover other services if a TSA is unavailable for a short period of time 120 int attempts = Math.max(retries, tsaurls.size()); 121 TimestampingException exception = new TimestampingException("Unable to complete the timestamping after " + attempts + " attempt" + (attempts > 1 ? "s" : "")); 122 int count = 0; 123 while (count < Math.max(retries, tsaurls.size())) { 124 try { 125 tsaurl = tsaurls.get(count % tsaurls.size()); 126 token = timestamp(algo, getEncryptedDigest(sigData)); 127 break; 128 } catch (TimestampingException | IOException e) { 129 exception.addSuppressed(e); 130 } 131 132 // pause before the next attempt 133 try { 134 Thread.sleep(retryWait * 1000L); 135 count++; 136 } catch (InterruptedException ie) { 137 } 138 } 139 140 if (token == null) { 141 throw exception; 142 } 143 144 return modifySignedData(sigData, getUnsignedAttributes(token), getExtraCertificates(token)); 145 } 146 147 /** 148 * Return the encrypted digest of the specified signature. 149 * 150 * @param sigData the signature 151 * @return the encrypted digest 152 */ 153 private byte[] getEncryptedDigest(CMSSignedData sigData) { 154 SignerInformation signerInformation = sigData.getSignerInfos().getSigners().iterator().next(); 155 return signerInformation.toASN1Structure().getEncryptedDigest().getOctets(); 156 } 157 158 /** 159 * Return the certificate chain of the timestamping authority if it isn't included 160 * with the counter signature in the unsigned attributes. 161 * 162 * @param token the timestamp 163 * @return the certificate chain of the timestamping authority 164 */ 165 protected Collection<X509CertificateHolder> getExtraCertificates(CMSSignedData token) { 166 return null; 167 } 168 169 /** 170 * Return the counter signature to be added as an unsigned attribute. 171 * 172 * @param token the timestamp 173 * @return the unsigned attribute wrapping the timestamp 174 */ 175 protected abstract AttributeTable getUnsignedAttributes(CMSSignedData token); 176 177 protected CMSSignedData modifySignedData(CMSSignedData sigData, AttributeTable unsignedAttributes, Collection<X509CertificateHolder> extraCertificates) throws IOException, CMSException { 178 SignerInformation signerInformation = sigData.getSignerInfos().getSigners().iterator().next(); 179 signerInformation = SignerInformation.replaceUnsignedAttributes(signerInformation, unsignedAttributes); 180 181 Collection<X509CertificateHolder> certificates = new ArrayList<>(); 182 certificates.addAll(sigData.getCertificates().getMatches(null)); 183 if (extraCertificates != null) { 184 certificates.addAll(extraCertificates); 185 } 186 Store<X509CertificateHolder> certificateStore = new CollectionStore<>(certificates); 187 188 AuthenticodeSignedDataGenerator generator = new AuthenticodeSignedDataGenerator(); 189 generator.addCertificates(certificateStore); 190 generator.addSigners(new SignerInformationStore(signerInformation)); 191 192 ASN1ObjectIdentifier contentType = new ASN1ObjectIdentifier(sigData.getSignedContentTypeOID()); 193 ASN1Encodable content = ASN1Sequence.getInstance(sigData.getSignedContent().getContent()); 194 195 return generator.generate(contentType, content); 196 } 197 198 protected abstract CMSSignedData timestamp(DigestAlgorithm algo, byte[] encryptedDigest) throws IOException, TimestampingException; 199 200 /** 201 * Returns the timestamper for the specified mode. 202 * 203 * @param mode the timestamping mode 204 * @return a new timestamper for the specified mode 205 */ 206 public static Timestamper create(TimestampingMode mode) { 207 switch (mode) { 208 case AUTHENTICODE: 209 return new AuthenticodeTimestamper(); 210 case RFC3161: 211 return new RFC3161Timestamper(); 212 default: 213 throw new IllegalArgumentException("Unsupported timestamping mode: " + mode); 214 } 215 } 216}