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}