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}