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 public void setURL(String tsaurl) { 062 setURLs(tsaurl); 063 } 064 065 /** 066 * @since 2.0 067 */ 068 public void setURLs(String... tsaurls) { 069 List<URL> urls = new ArrayList<>(); 070 for (String tsaurl : tsaurls) { 071 try { 072 urls.add(new URL(tsaurl)); 073 } catch (MalformedURLException e) { 074 throw new IllegalArgumentException("Invalid timestamping URL: " + tsaurl, e); 075 } 076 } 077 this.tsaurls = urls; 078 } 079 080 /** 081 * Set the number of retries. 082 */ 083 public void setRetries(int retries) { 084 this.retries = retries; 085 } 086 087 /** 088 * Set the number of seconds to wait between retries. 089 */ 090 public void setRetryWait(int retryWait) { 091 this.retryWait = retryWait; 092 } 093 094 /** 095 * Timestamp the specified signature. 096 * 097 * @param algo the digest algorithm used for the timestamp 098 * @param sigData the signed data to be timestamped 099 * @return the signed data with the timestamp added 100 */ 101 public CMSSignedData timestamp(DigestAlgorithm algo, CMSSignedData sigData) throws IOException, CMSException { 102 CMSSignedData token = null; 103 104 // Retry the timestamping and failover other services if a TSA is unavailable for a short period of time 105 int attempts = Math.max(retries, tsaurls.size()); 106 TimestampingException exception = new TimestampingException("Unable to complete the timestamping after " + attempts + " attempt" + (attempts > 1 ? "s" : "")); 107 int count = 0; 108 while (token == null && count < Math.max(retries, tsaurls.size())) { 109 try { 110 tsaurl = tsaurls.get(count % tsaurls.size()); 111 token = timestamp(algo, getEncryptedDigest(sigData)); 112 break; 113 } catch (TimestampingException | IOException e) { 114 exception.addSuppressed(e); 115 } 116 117 // pause before the next attempt 118 try { 119 Thread.sleep(retryWait * 1000); 120 count++; 121 } catch (InterruptedException ie) { 122 } 123 } 124 125 if (token == null) { 126 throw exception; 127 } 128 129 return modifySignedData(sigData, getUnsignedAttributes(token), getExtraCertificates(token)); 130 } 131 132 /** 133 * Return the encrypted digest of the specified signature. 134 */ 135 private byte[] getEncryptedDigest(CMSSignedData sigData) { 136 SignerInformation signerInformation = sigData.getSignerInfos().getSigners().iterator().next(); 137 return signerInformation.toASN1Structure().getEncryptedDigest().getOctets(); 138 } 139 140 /** 141 * Return the certificate chain of the timestamping authority if it isn't included 142 * with the counter signature in the unsigned attributes. 143 */ 144 protected Collection<X509CertificateHolder> getExtraCertificates(CMSSignedData token) { 145 return null; 146 } 147 148 /** 149 * Return the counter signature to be added as an unsigned attribute. 150 */ 151 protected abstract AttributeTable getUnsignedAttributes(CMSSignedData token); 152 153 protected CMSSignedData modifySignedData(CMSSignedData sigData, AttributeTable unsignedAttributes, Collection<X509CertificateHolder> extraCertificates) throws IOException, CMSException { 154 SignerInformation signerInformation = sigData.getSignerInfos().getSigners().iterator().next(); 155 signerInformation = SignerInformation.replaceUnsignedAttributes(signerInformation, unsignedAttributes); 156 157 Collection<X509CertificateHolder> certificates = new ArrayList<>(); 158 certificates.addAll(sigData.getCertificates().getMatches(null)); 159 if (extraCertificates != null) { 160 certificates.addAll(extraCertificates); 161 } 162 Store<X509CertificateHolder> certificateStore = new CollectionStore<>(certificates); 163 164 AuthenticodeSignedDataGenerator generator = new AuthenticodeSignedDataGenerator(); 165 generator.addCertificates(certificateStore); 166 generator.addSigners(new SignerInformationStore(signerInformation)); 167 168 ASN1ObjectIdentifier contentType = new ASN1ObjectIdentifier(sigData.getSignedContentTypeOID()); 169 ASN1Encodable content = ASN1Sequence.getInstance(sigData.getSignedContent().getContent()); 170 171 return generator.generate(contentType, content); 172 } 173 174 protected abstract CMSSignedData timestamp(DigestAlgorithm algo, byte[] encryptedDigest) throws IOException, CMSException, TimestampingException; 175 176 /** 177 * Returns the timestamper for the specified mode. 178 */ 179 public static Timestamper create(TimestampingMode mode) { 180 switch (mode) { 181 case AUTHENTICODE: 182 return new AuthenticodeTimestamper(); 183 case RFC3161: 184 return new RFC3161Timestamper(); 185 default: 186 throw new IllegalArgumentException("Unsupported timestamping mode: " + mode); 187 } 188 } 189}