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}