package org.bouncycastle.tsp; import java.io.IOException; import java.io.OutputStream; import java.math.BigInteger; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.SimpleTimeZone; import org.bouncycastle.asn1.ASN1Boolean; import org.bouncycastle.asn1.ASN1Encoding; import org.bouncycastle.asn1.ASN1GeneralizedTime; import org.bouncycastle.asn1.ASN1Integer; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.DERNull; import org.bouncycastle.asn1.cms.AttributeTable; import org.bouncycastle.asn1.ess.ESSCertID; import org.bouncycastle.asn1.ess.ESSCertIDv2; import org.bouncycastle.asn1.ess.SigningCertificate; import org.bouncycastle.asn1.ess.SigningCertificateV2; import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers; import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; import org.bouncycastle.asn1.tsp.Accuracy; import org.bouncycastle.asn1.tsp.MessageImprint; import org.bouncycastle.asn1.tsp.TSTInfo; import org.bouncycastle.asn1.x509.AlgorithmIdentifier; import org.bouncycastle.asn1.x509.Extensions; import org.bouncycastle.asn1.x509.ExtensionsGenerator; import org.bouncycastle.asn1.x509.GeneralName; import org.bouncycastle.asn1.x509.GeneralNames; import org.bouncycastle.asn1.x509.IssuerSerial; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cms.CMSAttributeTableGenerationException; import org.bouncycastle.cms.CMSAttributeTableGenerator; import org.bouncycastle.cms.CMSException; import org.bouncycastle.cms.CMSProcessableByteArray; import org.bouncycastle.cms.CMSSignedData; import org.bouncycastle.cms.CMSSignedDataGenerator; import org.bouncycastle.cms.SignerInfoGenerator; import org.bouncycastle.operator.DigestCalculator; import org.bouncycastle.util.CollectionStore; import org.bouncycastle.util.Store; /** * Currently the class supports ESSCertID by if a digest calculator based on SHA1 is passed in, otherwise it uses * ESSCertIDv2. In the event you need to pass both types, you will need to override the SignedAttributeGenerator * for the SignerInfoGeneratorBuilder you are using. For the default for ESSCertIDv2 the code will look something * like the following: * <pre> * final ESSCertID essCertid = new ESSCertID(certHashSha1, issuerSerial); * final ESSCertIDv2 essCertidV2 = new ESSCertIDv2(certHashSha256, issuerSerial); * * signerInfoGenBuilder.setSignedAttributeGenerator(new CMSAttributeTableGenerator() * { * public AttributeTable getAttributes(Map parameters) * throws CMSAttributeTableGenerationException * { * CMSAttributeTableGenerator attrGen = new DefaultSignedAttributeTableGenerator(); * * AttributeTable table = attrGen.getAttributes(parameters); * * table = table.add(PKCSObjectIdentifiers.id_aa_signingCertificate, new SigningCertificate(essCertid)); * table = table.add(PKCSObjectIdentifiers.id_aa_signingCertificateV2, new SigningCertificateV2(essCertidV2)); * * return table; * } * }); * </pre> */ public class TimeStampTokenGenerator { /** * Create time-stamps with a resolution of 1 second (the default). */ public static final int R_SECONDS = 0; /** * Create time-stamps with a resolution of 1 tenth of a second. */ public static final int R_TENTHS_OF_SECONDS = 1; /** * Create time-stamps with a resolution of 1 microsecond. */ public static final int R_MICROSECONDS = 2; /** * Create time-stamps with a resolution of 1 millisecond. */ public static final int R_MILLISECONDS = 3; private int resolution = R_SECONDS; private Locale locale = null; // default locale private int accuracySeconds = -1; private int accuracyMillis = -1; private int accuracyMicros = -1; boolean ordering = false; GeneralName tsa = null; private ASN1ObjectIdentifier tsaPolicyOID; private List certs = new ArrayList(); private List crls = new ArrayList(); private List attrCerts = new ArrayList(); private Map otherRevoc = new HashMap(); private SignerInfoGenerator signerInfoGen; /** * Basic Constructor - set up a calculator based on signerInfoGen with a ESSCertID calculated from * the signer's associated certificate using the sha1DigestCalculator. If alternate values are required * for id-aa-signingCertificate they should be added to the signerInfoGen object before it is passed in, * otherwise a standard digest based value will be added. * * @param signerInfoGen the generator for the signer we are using. * @param digestCalculator calculator for to use for digest of certificate. * @param tsaPolicy tasPolicy to send. * @throws IllegalArgumentException if calculator is not SHA-1 or there is no associated certificate for the signer, * @throws TSPException if the signer certificate cannot be processed. */ public TimeStampTokenGenerator( final SignerInfoGenerator signerInfoGen, DigestCalculator digestCalculator, ASN1ObjectIdentifier tsaPolicy) throws IllegalArgumentException, TSPException { this(signerInfoGen, digestCalculator, tsaPolicy, false); } /** * Basic Constructor - set up a calculator based on signerInfoGen with a ESSCertID calculated from * the signer's associated certificate using the sha1DigestCalculator. If alternate values are required * for id-aa-signingCertificate they should be added to the signerInfoGen object before it is passed in, * otherwise a standard digest based value will be added. * * @param signerInfoGen the generator for the signer we are using. * @param digestCalculator calculator for to use for digest of certificate. * @param tsaPolicy tasPolicy to send. * @param isIssuerSerialIncluded should issuerSerial be included in the ESSCertIDs, true if yes, by default false. * @throws IllegalArgumentException if calculator is not SHA-1 or there is no associated certificate for the signer, * @throws TSPException if the signer certificate cannot be processed. */ public TimeStampTokenGenerator( final SignerInfoGenerator signerInfoGen, DigestCalculator digestCalculator, ASN1ObjectIdentifier tsaPolicy, boolean isIssuerSerialIncluded) throws IllegalArgumentException, TSPException { this.signerInfoGen = signerInfoGen; this.tsaPolicyOID = tsaPolicy; if (!signerInfoGen.hasAssociatedCertificate()) { throw new IllegalArgumentException("SignerInfoGenerator must have an associated certificate"); } X509CertificateHolder assocCert = signerInfoGen.getAssociatedCertificate(); TSPUtil.validateCertificate(assocCert); try { OutputStream dOut = digestCalculator.getOutputStream(); dOut.write(assocCert.getEncoded()); dOut.close(); if (digestCalculator.getAlgorithmIdentifier().getAlgorithm().equals(OIWObjectIdentifiers.idSHA1)) { final ESSCertID essCertid = new ESSCertID(digestCalculator.getDigest(), isIssuerSerialIncluded ? new IssuerSerial(new GeneralNames(new GeneralName(assocCert.getIssuer())), assocCert.getSerialNumber()) : null); this.signerInfoGen = new SignerInfoGenerator(signerInfoGen, new CMSAttributeTableGenerator() { public AttributeTable getAttributes(Map parameters) throws CMSAttributeTableGenerationException { AttributeTable table = signerInfoGen.getSignedAttributeTableGenerator().getAttributes(parameters); if (table.get(PKCSObjectIdentifiers.id_aa_signingCertificate) == null) { return table.add(PKCSObjectIdentifiers.id_aa_signingCertificate, new SigningCertificate(essCertid)); } return table; } }, signerInfoGen.getUnsignedAttributeTableGenerator()); } else { AlgorithmIdentifier digAlgID = new AlgorithmIdentifier(digestCalculator.getAlgorithmIdentifier().getAlgorithm()); final ESSCertIDv2 essCertid = new ESSCertIDv2(digAlgID, digestCalculator.getDigest(), isIssuerSerialIncluded ? new IssuerSerial(new GeneralNames(new GeneralName(assocCert.getIssuer())), new ASN1Integer(assocCert.getSerialNumber())) : null); this.signerInfoGen = new SignerInfoGenerator(signerInfoGen, new CMSAttributeTableGenerator() { public AttributeTable getAttributes(Map parameters) throws CMSAttributeTableGenerationException { AttributeTable table = signerInfoGen.getSignedAttributeTableGenerator().getAttributes(parameters); if (table.get(PKCSObjectIdentifiers.id_aa_signingCertificateV2) == null) { return table.add(PKCSObjectIdentifiers.id_aa_signingCertificateV2, new SigningCertificateV2(essCertid)); } return table; } }, signerInfoGen.getUnsignedAttributeTableGenerator()); } } catch (IOException e) { throw new TSPException("Exception processing certificate.", e); } } /** * Add the store of X509 Certificates to the generator. * * @param certStore a Store containing X509CertificateHolder objects */ public void addCertificates( Store certStore) { certs.addAll(certStore.getMatches(null)); } /** * * @param crlStore a Store containing X509CRLHolder objects. */ public void addCRLs( Store crlStore) { crls.addAll(crlStore.getMatches(null)); } /** * * @param attrStore a Store containing X509AttributeCertificate objects. */ public void addAttributeCertificates( Store attrStore) { attrCerts.addAll(attrStore.getMatches(null)); } /** * Add a Store of otherRevocationData to the CRL set to be included with the generated TimeStampToken. * * @param otherRevocationInfoFormat the OID specifying the format of the otherRevocationInfo data. * @param otherRevocationInfos a Store of otherRevocationInfo data to add. */ public void addOtherRevocationInfo( ASN1ObjectIdentifier otherRevocationInfoFormat, Store otherRevocationInfos) { otherRevoc.put(otherRevocationInfoFormat, otherRevocationInfos.getMatches(null)); } /** * Set the resolution of the time stamp - R_SECONDS (the default), R_TENTH_OF_SECONDS, R_MICROSECONDS, R_MILLISECONDS * * @param resolution resolution of timestamps to be produced. */ public void setResolution(int resolution) { this.resolution = resolution; } /** * Set a Locale for time creation - you may need to use this if the default locale * doesn't use a Gregorian calender so that the GeneralizedTime produced is compatible with other ASN.1 implementations. * * @param locale a locale to use for converting system time into a GeneralizedTime. */ public void setLocale(Locale locale) { this.locale = locale; } public void setAccuracySeconds(int accuracySeconds) { this.accuracySeconds = accuracySeconds; } public void setAccuracyMillis(int accuracyMillis) { this.accuracyMillis = accuracyMillis; } public void setAccuracyMicros(int accuracyMicros) { this.accuracyMicros = accuracyMicros; } public void setOrdering(boolean ordering) { this.ordering = ordering; } public void setTSA(GeneralName tsa) { this.tsa = tsa; } /** * Generate a TimeStampToken for the passed in request and serialNumber marking it with the passed in genTime. * * @param request the originating request. * @param serialNumber serial number for the TimeStampToken * @param genTime token generation time. * @return a TimeStampToken * @throws TSPException */ public TimeStampToken generate( TimeStampRequest request, BigInteger serialNumber, Date genTime) throws TSPException { return generate(request, serialNumber, genTime, null); } /** * Generate a TimeStampToken for the passed in request and serialNumber marking it with the passed in genTime. * * @param request the originating request. * @param serialNumber serial number for the TimeStampToken * @param genTime token generation time. * @param additionalExtensions extra extensions to be added to the response token. * @return a TimeStampToken * @throws TSPException */ public TimeStampToken generate( TimeStampRequest request, BigInteger serialNumber, Date genTime, Extensions additionalExtensions) throws TSPException { ASN1ObjectIdentifier digestAlgOID = request.getMessageImprintAlgOID(); AlgorithmIdentifier algID = new AlgorithmIdentifier(digestAlgOID, DERNull.INSTANCE); MessageImprint messageImprint = new MessageImprint(algID, request.getMessageImprintDigest()); Accuracy accuracy = null; if (accuracySeconds > 0 || accuracyMillis > 0 || accuracyMicros > 0) { ASN1Integer seconds = null; if (accuracySeconds > 0) { seconds = new ASN1Integer(accuracySeconds); } ASN1Integer millis = null; if (accuracyMillis > 0) { millis = new ASN1Integer(accuracyMillis); } ASN1Integer micros = null; if (accuracyMicros > 0) { micros = new ASN1Integer(accuracyMicros); } accuracy = new Accuracy(seconds, millis, micros); } ASN1Boolean derOrdering = null; if (ordering) { derOrdering = ASN1Boolean.getInstance(ordering); } ASN1Integer nonce = null; if (request.getNonce() != null) { nonce = new ASN1Integer(request.getNonce()); } ASN1ObjectIdentifier tsaPolicy = tsaPolicyOID; if (request.getReqPolicy() != null) { tsaPolicy = request.getReqPolicy(); } Extensions respExtensions = request.getExtensions(); if (additionalExtensions != null) { ExtensionsGenerator extGen = new ExtensionsGenerator(); if (respExtensions != null) { for (Enumeration en = respExtensions.oids(); en.hasMoreElements(); ) { extGen.addExtension(respExtensions.getExtension(ASN1ObjectIdentifier.getInstance(en.nextElement()))); } } for (Enumeration en = additionalExtensions.oids(); en.hasMoreElements(); ) { extGen.addExtension(additionalExtensions.getExtension(ASN1ObjectIdentifier.getInstance(en.nextElement()))); } respExtensions = extGen.generate(); } ASN1GeneralizedTime timeStampTime; if (resolution == R_SECONDS) { timeStampTime = (locale == null) ? new ASN1GeneralizedTime(genTime) : new ASN1GeneralizedTime(genTime, locale); } else { timeStampTime = createGeneralizedTime(genTime); } TSTInfo tstInfo = new TSTInfo(tsaPolicy, messageImprint, new ASN1Integer(serialNumber), timeStampTime, accuracy, derOrdering, nonce, tsa, respExtensions); try { CMSSignedDataGenerator signedDataGenerator = new CMSSignedDataGenerator(); if (request.getCertReq()) { // TODO: do we need to check certs non-empty? signedDataGenerator.addCertificates(new CollectionStore(certs)); signedDataGenerator.addAttributeCertificates(new CollectionStore(attrCerts)); } signedDataGenerator.addCRLs(new CollectionStore(crls)); if (!otherRevoc.isEmpty()) { for (Iterator it = otherRevoc.keySet().iterator(); it.hasNext();) { ASN1ObjectIdentifier format = (ASN1ObjectIdentifier)it.next(); signedDataGenerator.addOtherRevocationInfo(format, new CollectionStore((Collection)otherRevoc.get(format))); } } signedDataGenerator.addSignerInfoGenerator(signerInfoGen); byte[] derEncodedTSTInfo = tstInfo.getEncoded(ASN1Encoding.DER); CMSSignedData signedData = signedDataGenerator.generate(new CMSProcessableByteArray(PKCSObjectIdentifiers.id_ct_TSTInfo, derEncodedTSTInfo), true); return new TimeStampToken(signedData); } catch (CMSException cmsEx) { throw new TSPException("Error generating time-stamp token", cmsEx); } catch (IOException e) { throw new TSPException("Exception encoding info", e); } } // we need to produce a correct DER encoding GeneralizedTime here as the BC ASN.1 library doesn't handle this properly yet. private ASN1GeneralizedTime createGeneralizedTime(Date time) throws TSPException { String format = "yyyyMMddHHmmss.SSS"; SimpleDateFormat dateF = (locale == null) ? new SimpleDateFormat(format) : new SimpleDateFormat(format, locale); dateF.setTimeZone(new SimpleTimeZone(0, "Z")); StringBuffer sBuild = new StringBuffer(dateF.format(time)); int dotIndex = 9; while (dotIndex != sBuild.length() && sBuild.charAt(dotIndex) != '.') { dotIndex++; } if (dotIndex == sBuild.length()) { // came back in seconds only, just return sBuild.append("Z"); return new ASN1GeneralizedTime(sBuild.toString()); } // trim to resolution switch (resolution) { case R_TENTHS_OF_SECONDS: if (sBuild.length() > dotIndex + 2) { sBuild.delete(dotIndex + 2, sBuild.length()); } break; case R_MICROSECONDS: if (sBuild.length() > dotIndex + 3) { sBuild.delete(dotIndex + 3, sBuild.length()); } break; case R_MILLISECONDS: // do nothing break; default: throw new TSPException("unknown time-stamp resolution: " + resolution); } // remove trailing zeros while (sBuild.charAt(sBuild.length() - 1) == '0') { sBuild.deleteCharAt(sBuild.length() - 1); } if (sBuild.length() - 1 == dotIndex) { sBuild.deleteCharAt(sBuild.length() - 1); } sBuild.append("Z"); return new ASN1GeneralizedTime(sBuild.toString()); } }