diff options
Diffstat (limited to 'pkix/src/main/java/org/spongycastle/tsp')
20 files changed, 3184 insertions, 0 deletions
diff --git a/pkix/src/main/java/org/spongycastle/tsp/GenTimeAccuracy.java b/pkix/src/main/java/org/spongycastle/tsp/GenTimeAccuracy.java new file mode 100644 index 00000000..bb042e90 --- /dev/null +++ b/pkix/src/main/java/org/spongycastle/tsp/GenTimeAccuracy.java @@ -0,0 +1,60 @@ +package org.spongycastle.tsp; + +import org.spongycastle.asn1.ASN1Integer; +import org.spongycastle.asn1.tsp.Accuracy; + +public class GenTimeAccuracy +{ + private Accuracy accuracy; + + public GenTimeAccuracy(Accuracy accuracy) + { + this.accuracy = accuracy; + } + + public int getSeconds() + { + return getTimeComponent(accuracy.getSeconds()); + } + + public int getMillis() + { + return getTimeComponent(accuracy.getMillis()); + } + + public int getMicros() + { + return getTimeComponent(accuracy.getMicros()); + } + + private int getTimeComponent( + ASN1Integer time) + { + if (time != null) + { + return time.getValue().intValue(); + } + + return 0; + } + + public String toString() + { // digits + return getSeconds() + "." + format(getMillis()) + format(getMicros()); + } + + private String format(int v) + { + if (v < 10) + { + return "00" + v; + } + + if (v < 100) + { + return "0" + v; + } + + return Integer.toString(v); + } +} diff --git a/pkix/src/main/java/org/spongycastle/tsp/TSPAlgorithms.java b/pkix/src/main/java/org/spongycastle/tsp/TSPAlgorithms.java new file mode 100644 index 00000000..fa5c6f06 --- /dev/null +++ b/pkix/src/main/java/org/spongycastle/tsp/TSPAlgorithms.java @@ -0,0 +1,35 @@ +package org.spongycastle.tsp; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.spongycastle.asn1.ASN1ObjectIdentifier; +import org.spongycastle.asn1.cryptopro.CryptoProObjectIdentifiers; +import org.spongycastle.asn1.nist.NISTObjectIdentifiers; +import org.spongycastle.asn1.oiw.OIWObjectIdentifiers; +import org.spongycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.spongycastle.asn1.teletrust.TeleTrusTObjectIdentifiers; + +/** + * Recognised hash algorithms for the time stamp protocol. + */ +public interface TSPAlgorithms +{ + public static final ASN1ObjectIdentifier MD5 = PKCSObjectIdentifiers.md5; + + public static final ASN1ObjectIdentifier SHA1 = OIWObjectIdentifiers.idSHA1; + + public static final ASN1ObjectIdentifier SHA224 = NISTObjectIdentifiers.id_sha224; + public static final ASN1ObjectIdentifier SHA256 = NISTObjectIdentifiers.id_sha256; + public static final ASN1ObjectIdentifier SHA384 = NISTObjectIdentifiers.id_sha384; + public static final ASN1ObjectIdentifier SHA512 = NISTObjectIdentifiers.id_sha512; + + public static final ASN1ObjectIdentifier RIPEMD128 = TeleTrusTObjectIdentifiers.ripemd128; + public static final ASN1ObjectIdentifier RIPEMD160 = TeleTrusTObjectIdentifiers.ripemd160; + public static final ASN1ObjectIdentifier RIPEMD256 = TeleTrusTObjectIdentifiers.ripemd256; + + public static final ASN1ObjectIdentifier GOST3411 = CryptoProObjectIdentifiers.gostR3411; + + public static final Set ALLOWED = new HashSet(Arrays.asList(new ASN1ObjectIdentifier[] { GOST3411, MD5, SHA1, SHA224, SHA256, SHA384, SHA512, RIPEMD128, RIPEMD160, RIPEMD256 })); +} diff --git a/pkix/src/main/java/org/spongycastle/tsp/TSPException.java b/pkix/src/main/java/org/spongycastle/tsp/TSPException.java new file mode 100644 index 00000000..6125ceff --- /dev/null +++ b/pkix/src/main/java/org/spongycastle/tsp/TSPException.java @@ -0,0 +1,28 @@ +package org.spongycastle.tsp; + +public class TSPException + extends Exception +{ + Throwable underlyingException; + + public TSPException(String message) + { + super(message); + } + + public TSPException(String message, Throwable e) + { + super(message); + underlyingException = e; + } + + public Exception getUnderlyingException() + { + return (Exception)underlyingException; + } + + public Throwable getCause() + { + return underlyingException; + } +} diff --git a/pkix/src/main/java/org/spongycastle/tsp/TSPIOException.java b/pkix/src/main/java/org/spongycastle/tsp/TSPIOException.java new file mode 100644 index 00000000..418ff1b2 --- /dev/null +++ b/pkix/src/main/java/org/spongycastle/tsp/TSPIOException.java @@ -0,0 +1,30 @@ +package org.spongycastle.tsp; + +import java.io.IOException; + +public class TSPIOException + extends IOException +{ + Throwable underlyingException; + + public TSPIOException(String message) + { + super(message); + } + + public TSPIOException(String message, Throwable e) + { + super(message); + underlyingException = e; + } + + public Exception getUnderlyingException() + { + return (Exception)underlyingException; + } + + public Throwable getCause() + { + return underlyingException; + } +} diff --git a/pkix/src/main/java/org/spongycastle/tsp/TSPUtil.java b/pkix/src/main/java/org/spongycastle/tsp/TSPUtil.java new file mode 100644 index 00000000..23a0454c --- /dev/null +++ b/pkix/src/main/java/org/spongycastle/tsp/TSPUtil.java @@ -0,0 +1,209 @@ +package org.spongycastle.tsp; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.spongycastle.asn1.ASN1Encodable; +import org.spongycastle.asn1.ASN1EncodableVector; +import org.spongycastle.asn1.ASN1ObjectIdentifier; +import org.spongycastle.asn1.ASN1Set; +import org.spongycastle.asn1.cms.Attribute; +import org.spongycastle.asn1.cms.AttributeTable; +import org.spongycastle.asn1.cms.ContentInfo; +import org.spongycastle.asn1.cryptopro.CryptoProObjectIdentifiers; +import org.spongycastle.asn1.nist.NISTObjectIdentifiers; +import org.spongycastle.asn1.oiw.OIWObjectIdentifiers; +import org.spongycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.spongycastle.asn1.teletrust.TeleTrusTObjectIdentifiers; +import org.spongycastle.asn1.x509.ExtendedKeyUsage; +import org.spongycastle.asn1.x509.Extension; +import org.spongycastle.asn1.x509.Extensions; +import org.spongycastle.asn1.x509.ExtensionsGenerator; +import org.spongycastle.asn1.x509.KeyPurposeId; +import org.spongycastle.cert.X509CertificateHolder; +import org.spongycastle.cms.SignerInformation; +import org.spongycastle.operator.DigestCalculator; +import org.spongycastle.operator.DigestCalculatorProvider; +import org.spongycastle.operator.OperatorCreationException; +import org.spongycastle.util.Arrays; +import org.spongycastle.util.Integers; + +public class TSPUtil +{ + private static List EMPTY_LIST = Collections.unmodifiableList(new ArrayList()); + + private static final Map digestLengths = new HashMap(); + private static final Map digestNames = new HashMap(); + + static + { + digestLengths.put(PKCSObjectIdentifiers.md5.getId(), Integers.valueOf(16)); + digestLengths.put(OIWObjectIdentifiers.idSHA1.getId(), Integers.valueOf(20)); + digestLengths.put(NISTObjectIdentifiers.id_sha224.getId(), Integers.valueOf(28)); + digestLengths.put(NISTObjectIdentifiers.id_sha256.getId(), Integers.valueOf(32)); + digestLengths.put(NISTObjectIdentifiers.id_sha384.getId(), Integers.valueOf(48)); + digestLengths.put(NISTObjectIdentifiers.id_sha512.getId(), Integers.valueOf(64)); + digestLengths.put(TeleTrusTObjectIdentifiers.ripemd128.getId(), Integers.valueOf(16)); + digestLengths.put(TeleTrusTObjectIdentifiers.ripemd160.getId(), Integers.valueOf(20)); + digestLengths.put(TeleTrusTObjectIdentifiers.ripemd256.getId(), Integers.valueOf(32)); + digestLengths.put(CryptoProObjectIdentifiers.gostR3411.getId(), Integers.valueOf(32)); + + digestNames.put(PKCSObjectIdentifiers.md5.getId(), "MD5"); + digestNames.put(OIWObjectIdentifiers.idSHA1.getId(), "SHA1"); + digestNames.put(NISTObjectIdentifiers.id_sha224.getId(), "SHA224"); + digestNames.put(NISTObjectIdentifiers.id_sha256.getId(), "SHA256"); + digestNames.put(NISTObjectIdentifiers.id_sha384.getId(), "SHA384"); + digestNames.put(NISTObjectIdentifiers.id_sha512.getId(), "SHA512"); + digestNames.put(PKCSObjectIdentifiers.sha1WithRSAEncryption.getId(), "SHA1"); + digestNames.put(PKCSObjectIdentifiers.sha224WithRSAEncryption.getId(), "SHA224"); + digestNames.put(PKCSObjectIdentifiers.sha256WithRSAEncryption.getId(), "SHA256"); + digestNames.put(PKCSObjectIdentifiers.sha384WithRSAEncryption.getId(), "SHA384"); + digestNames.put(PKCSObjectIdentifiers.sha512WithRSAEncryption.getId(), "SHA512"); + digestNames.put(TeleTrusTObjectIdentifiers.ripemd128.getId(), "RIPEMD128"); + digestNames.put(TeleTrusTObjectIdentifiers.ripemd160.getId(), "RIPEMD160"); + digestNames.put(TeleTrusTObjectIdentifiers.ripemd256.getId(), "RIPEMD256"); + digestNames.put(CryptoProObjectIdentifiers.gostR3411.getId(), "GOST3411"); + } + + /** + * Fetches the signature time-stamp attributes from a SignerInformation object. + * Checks that the MessageImprint for each time-stamp matches the signature field. + * (see RFC 3161 Appendix A). + * + * @param signerInfo a SignerInformation to search for time-stamps + * @param digCalcProvider provider for digest calculators + * @return a collection of TimeStampToken objects + * @throws TSPValidationException + */ + public static Collection getSignatureTimestamps(SignerInformation signerInfo, DigestCalculatorProvider digCalcProvider) + throws TSPValidationException + { + List timestamps = new ArrayList(); + + AttributeTable unsignedAttrs = signerInfo.getUnsignedAttributes(); + if (unsignedAttrs != null) + { + ASN1EncodableVector allTSAttrs = unsignedAttrs.getAll( + PKCSObjectIdentifiers.id_aa_signatureTimeStampToken); + for (int i = 0; i < allTSAttrs.size(); ++i) + { + Attribute tsAttr = (Attribute)allTSAttrs.get(i); + ASN1Set tsAttrValues = tsAttr.getAttrValues(); + for (int j = 0; j < tsAttrValues.size(); ++j) + { + try + { + ContentInfo contentInfo = ContentInfo.getInstance(tsAttrValues.getObjectAt(j)); + TimeStampToken timeStampToken = new TimeStampToken(contentInfo); + TimeStampTokenInfo tstInfo = timeStampToken.getTimeStampInfo(); + + DigestCalculator digCalc = digCalcProvider.get(tstInfo.getHashAlgorithm()); + + OutputStream dOut = digCalc.getOutputStream(); + + dOut.write(signerInfo.getSignature()); + dOut.close(); + + byte[] expectedDigest = digCalc.getDigest(); + + if (!Arrays.constantTimeAreEqual(expectedDigest, tstInfo.getMessageImprintDigest())) + { + throw new TSPValidationException("Incorrect digest in message imprint"); + } + + timestamps.add(timeStampToken); + } + catch (OperatorCreationException e) + { + throw new TSPValidationException("Unknown hash algorithm specified in timestamp"); + } + catch (Exception e) + { + throw new TSPValidationException("Timestamp could not be parsed"); + } + } + } + } + + return timestamps; + } + + /** + * Validate the passed in certificate as being of the correct type to be used + * for time stamping. To be valid it must have an ExtendedKeyUsage extension + * which has a key purpose identifier of id-kp-timeStamping. + * + * @param cert the certificate of interest. + * @throws TSPValidationException if the certificate fails on one of the check points. + */ + public static void validateCertificate( + X509CertificateHolder cert) + throws TSPValidationException + { + if (cert.toASN1Structure().getVersionNumber() != 3) + { + throw new IllegalArgumentException("Certificate must have an ExtendedKeyUsage extension."); + } + + Extension ext = cert.getExtension(Extension.extendedKeyUsage); + if (ext == null) + { + throw new TSPValidationException("Certificate must have an ExtendedKeyUsage extension."); + } + + if (!ext.isCritical()) + { + throw new TSPValidationException("Certificate must have an ExtendedKeyUsage extension marked as critical."); + } + + ExtendedKeyUsage extKey = ExtendedKeyUsage.getInstance(ext.getParsedValue()); + + if (!extKey.hasKeyPurposeId(KeyPurposeId.id_kp_timeStamping) || extKey.size() != 1) + { + throw new TSPValidationException("ExtendedKeyUsage not solely time stamping."); + } + } + + static int getDigestLength( + String digestAlgOID) + throws TSPException + { + Integer length = (Integer)digestLengths.get(digestAlgOID); + + if (length != null) + { + return length.intValue(); + } + + throw new TSPException("digest algorithm cannot be found."); + } + + static List getExtensionOIDs(Extensions extensions) + { + if (extensions == null) + { + return EMPTY_LIST; + } + + return Collections.unmodifiableList(java.util.Arrays.asList(extensions.getExtensionOIDs())); + } + + static void addExtension(ExtensionsGenerator extGenerator, ASN1ObjectIdentifier oid, boolean isCritical, ASN1Encodable value) + throws TSPIOException + { + try + { + extGenerator.addExtension(oid, isCritical, value); + } + catch (IOException e) + { + throw new TSPIOException("cannot encode extension: " + e.getMessage(), e); + } + } +} diff --git a/pkix/src/main/java/org/spongycastle/tsp/TSPValidationException.java b/pkix/src/main/java/org/spongycastle/tsp/TSPValidationException.java new file mode 100644 index 00000000..f89ac6c1 --- /dev/null +++ b/pkix/src/main/java/org/spongycastle/tsp/TSPValidationException.java @@ -0,0 +1,34 @@ +package org.spongycastle.tsp; + +/** + * Exception thrown if a TSP request or response fails to validate. + * <p> + * If a failure code is associated with the exception it can be retrieved using + * the getFailureCode() method. + */ +public class TSPValidationException + extends TSPException +{ + private int failureCode = -1; + + public TSPValidationException(String message) + { + super(message); + } + + public TSPValidationException(String message, int failureCode) + { + super(message); + this.failureCode = failureCode; + } + + /** + * Return the failure code associated with this exception - if one is set. + * + * @return the failure code if set, -1 otherwise. + */ + public int getFailureCode() + { + return failureCode; + } +} diff --git a/pkix/src/main/java/org/spongycastle/tsp/TimeStampRequest.java b/pkix/src/main/java/org/spongycastle/tsp/TimeStampRequest.java new file mode 100644 index 00000000..54ed40ec --- /dev/null +++ b/pkix/src/main/java/org/spongycastle/tsp/TimeStampRequest.java @@ -0,0 +1,267 @@ +package org.spongycastle.tsp; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import org.spongycastle.asn1.ASN1InputStream; +import org.spongycastle.asn1.ASN1ObjectIdentifier; +import org.spongycastle.asn1.cmp.PKIFailureInfo; +import org.spongycastle.asn1.tsp.TimeStampReq; +import org.spongycastle.asn1.x509.Extension; +import org.spongycastle.asn1.x509.Extensions; + +/** + * Base class for an RFC 3161 Time Stamp Request. + */ +public class TimeStampRequest +{ + private static Set EMPTY_SET = Collections.unmodifiableSet(new HashSet()); + + private TimeStampReq req; + private Extensions extensions; + + public TimeStampRequest(TimeStampReq req) + { + this.req = req; + this.extensions = req.getExtensions(); + } + + /** + * Create a TimeStampRequest from the past in byte array. + * + * @param req byte array containing the request. + * @throws IOException if the request is malformed. + */ + public TimeStampRequest(byte[] req) + throws IOException + { + this(new ByteArrayInputStream(req)); + } + + /** + * Create a TimeStampRequest from the past in input stream. + * + * @param in input stream containing the request. + * @throws IOException if the request is malformed. + */ + public TimeStampRequest(InputStream in) + throws IOException + { + this(loadRequest(in)); + } + + private static TimeStampReq loadRequest(InputStream in) + throws IOException + { + try + { + return TimeStampReq.getInstance(new ASN1InputStream(in).readObject()); + } + catch (ClassCastException e) + { + throw new IOException("malformed request: " + e); + } + catch (IllegalArgumentException e) + { + throw new IOException("malformed request: " + e); + } + } + + public int getVersion() + { + return req.getVersion().getValue().intValue(); + } + + public ASN1ObjectIdentifier getMessageImprintAlgOID() + { + return req.getMessageImprint().getHashAlgorithm().getAlgorithm(); + } + + public byte[] getMessageImprintDigest() + { + return req.getMessageImprint().getHashedMessage(); + } + + public ASN1ObjectIdentifier getReqPolicy() + { + if (req.getReqPolicy() != null) + { + return req.getReqPolicy(); + } + else + { + return null; + } + } + + public BigInteger getNonce() + { + if (req.getNonce() != null) + { + return req.getNonce().getValue(); + } + else + { + return null; + } + } + + public boolean getCertReq() + { + if (req.getCertReq() != null) + { + return req.getCertReq().isTrue(); + } + else + { + return false; + } + } + + /** + * Validate the timestamp request, checking the digest to see if it is of an + * accepted type and whether it is of the correct length for the algorithm specified. + * + * @param algorithms a set of OIDs giving accepted algorithms. + * @param policies if non-null a set of policies OIDs we are willing to sign under. + * @param extensions if non-null a set of extensions OIDs we are willing to accept. + * @throws TSPException if the request is invalid, or processing fails. + */ + public void validate( + Set algorithms, + Set policies, + Set extensions) + throws TSPException + { + algorithms = convert(algorithms); + policies = convert(policies); + extensions = convert(extensions); + + if (!algorithms.contains(this.getMessageImprintAlgOID())) + { + throw new TSPValidationException("request contains unknown algorithm.", PKIFailureInfo.badAlg); + } + + if (policies != null && this.getReqPolicy() != null && !policies.contains(this.getReqPolicy())) + { + throw new TSPValidationException("request contains unknown policy.", PKIFailureInfo.unacceptedPolicy); + } + + if (this.getExtensions() != null && extensions != null) + { + Enumeration en = this.getExtensions().oids(); + while(en.hasMoreElements()) + { + String oid = ((ASN1ObjectIdentifier)en.nextElement()).getId(); + if (!extensions.contains(oid)) + { + throw new TSPValidationException("request contains unknown extension.", PKIFailureInfo.unacceptedExtension); + } + } + } + + int digestLength = TSPUtil.getDigestLength(this.getMessageImprintAlgOID().getId()); + + if (digestLength != this.getMessageImprintDigest().length) + { + throw new TSPValidationException("imprint digest the wrong length.", PKIFailureInfo.badDataFormat); + } + } + + /** + * return the ASN.1 encoded representation of this object. + * @return the default ASN,1 byte encoding for the object. + */ + public byte[] getEncoded() throws IOException + { + return req.getEncoded(); + } + + Extensions getExtensions() + { + return extensions; + } + + public boolean hasExtensions() + { + return extensions != null; + } + + public Extension getExtension(ASN1ObjectIdentifier oid) + { + if (extensions != null) + { + return extensions.getExtension(oid); + } + + return null; + } + + public List getExtensionOIDs() + { + return TSPUtil.getExtensionOIDs(extensions); + } + + /** + * Returns a set of ASN1ObjectIdentifiers giving the non-critical extensions. + * @return a set of ASN1ObjectIdentifiers. + */ + public Set getNonCriticalExtensionOIDs() + { + if (extensions == null) + { + return EMPTY_SET; + } + + return Collections.unmodifiableSet(new HashSet(Arrays.asList(extensions.getNonCriticalExtensionOIDs()))); + } + + /** + * Returns a set of ASN1ObjectIdentifiers giving the critical extensions. + * @return a set of ASN1ObjectIdentifiers. + */ + public Set getCriticalExtensionOIDs() + { + if (extensions == null) + { + return EMPTY_SET; + } + + return Collections.unmodifiableSet(new HashSet(Arrays.asList(extensions.getCriticalExtensionOIDs()))); + } + + private Set convert(Set orig) + { + if (orig == null) + { + return orig; + } + + Set con = new HashSet(orig.size()); + + for (Iterator it = orig.iterator(); it.hasNext();) + { + Object o = it.next(); + + if (o instanceof String) + { + con.add(new ASN1ObjectIdentifier((String)o)); + } + else + { + con.add(o); + } + } + + return con; + } +} diff --git a/pkix/src/main/java/org/spongycastle/tsp/TimeStampRequestGenerator.java b/pkix/src/main/java/org/spongycastle/tsp/TimeStampRequestGenerator.java new file mode 100644 index 00000000..382b366a --- /dev/null +++ b/pkix/src/main/java/org/spongycastle/tsp/TimeStampRequestGenerator.java @@ -0,0 +1,163 @@ +package org.spongycastle.tsp; + +import java.io.IOException; +import java.math.BigInteger; + +import org.spongycastle.asn1.ASN1Boolean; +import org.spongycastle.asn1.ASN1Encodable; +import org.spongycastle.asn1.ASN1Integer; +import org.spongycastle.asn1.ASN1ObjectIdentifier; +import org.spongycastle.asn1.DERNull; +import org.spongycastle.asn1.tsp.MessageImprint; +import org.spongycastle.asn1.tsp.TimeStampReq; +import org.spongycastle.asn1.x509.AlgorithmIdentifier; +import org.spongycastle.asn1.x509.Extensions; +import org.spongycastle.asn1.x509.ExtensionsGenerator; + +/** + * Generator for RFC 3161 Time Stamp Request objects. + */ +public class TimeStampRequestGenerator +{ + private ASN1ObjectIdentifier reqPolicy; + + private ASN1Boolean certReq; + private ExtensionsGenerator extGenerator = new ExtensionsGenerator(); + + public TimeStampRequestGenerator() + { + } + + /** + * @deprecated use method taking ASN1ObjectIdentifier + * @param reqPolicy + */ + public void setReqPolicy( + String reqPolicy) + { + this.reqPolicy= new ASN1ObjectIdentifier(reqPolicy); + } + + public void setReqPolicy( + ASN1ObjectIdentifier reqPolicy) + { + this.reqPolicy= reqPolicy; + } + + public void setCertReq( + boolean certReq) + { + this.certReq = ASN1Boolean.getInstance(certReq); + } + + /** + * add a given extension field for the standard extensions tag (tag 3) + * @throws IOException + * @deprecated use method taking ASN1ObjectIdentifier + */ + public void addExtension( + String OID, + boolean critical, + ASN1Encodable value) + throws IOException + { + this.addExtension(OID, critical, value.toASN1Primitive().getEncoded()); + } + + /** + * add a given extension field for the standard extensions tag + * The value parameter becomes the contents of the octet string associated + * with the extension. + * @deprecated use method taking ASN1ObjectIdentifier + */ + public void addExtension( + String OID, + boolean critical, + byte[] value) + { + extGenerator.addExtension(new ASN1ObjectIdentifier(OID), critical, value); + } + + /** + * add a given extension field for the standard extensions tag (tag 3) + * @throws TSPIOException + */ + public void addExtension( + ASN1ObjectIdentifier oid, + boolean isCritical, + ASN1Encodable value) + throws TSPIOException + { + TSPUtil.addExtension(extGenerator, oid, isCritical, value); + } + + /** + * add a given extension field for the standard extensions tag + * The value parameter becomes the contents of the octet string associated + * with the extension. + */ + public void addExtension( + ASN1ObjectIdentifier oid, + boolean isCritical, + byte[] value) + { + extGenerator.addExtension(oid, isCritical, value); + } + + /** + * @deprecated use method taking ANS1ObjectIdentifier + */ + public TimeStampRequest generate( + String digestAlgorithm, + byte[] digest) + { + return this.generate(digestAlgorithm, digest, null); + } + + /** + * @deprecated use method taking ANS1ObjectIdentifier + */ + public TimeStampRequest generate( + String digestAlgorithmOID, + byte[] digest, + BigInteger nonce) + { + if (digestAlgorithmOID == null) + { + throw new IllegalArgumentException("No digest algorithm specified"); + } + + ASN1ObjectIdentifier digestAlgOID = new ASN1ObjectIdentifier(digestAlgorithmOID); + + AlgorithmIdentifier algID = new AlgorithmIdentifier(digestAlgOID, DERNull.INSTANCE); + MessageImprint messageImprint = new MessageImprint(algID, digest); + + Extensions ext = null; + + if (!extGenerator.isEmpty()) + { + ext = extGenerator.generate(); + } + + if (nonce != null) + { + return new TimeStampRequest(new TimeStampReq(messageImprint, + reqPolicy, new ASN1Integer(nonce), certReq, ext)); + } + else + { + return new TimeStampRequest(new TimeStampReq(messageImprint, + reqPolicy, null, certReq, ext)); + } + } + + public TimeStampRequest generate(ASN1ObjectIdentifier digestAlgorithm, byte[] digest) + { + return generate(digestAlgorithm.getId(), digest); + } + + public TimeStampRequest generate(ASN1ObjectIdentifier digestAlgorithm, byte[] digest, BigInteger nonce) + { + return generate(digestAlgorithm.getId(), digest, nonce); + } +} diff --git a/pkix/src/main/java/org/spongycastle/tsp/TimeStampResponse.java b/pkix/src/main/java/org/spongycastle/tsp/TimeStampResponse.java new file mode 100644 index 00000000..cc327f45 --- /dev/null +++ b/pkix/src/main/java/org/spongycastle/tsp/TimeStampResponse.java @@ -0,0 +1,189 @@ +package org.spongycastle.tsp; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.spongycastle.asn1.ASN1InputStream; +import org.spongycastle.asn1.cmp.PKIFailureInfo; +import org.spongycastle.asn1.cmp.PKIFreeText; +import org.spongycastle.asn1.cmp.PKIStatus; +import org.spongycastle.asn1.cms.Attribute; +import org.spongycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.spongycastle.asn1.tsp.TimeStampResp; +import org.spongycastle.util.Arrays; + +/** + * Base class for an RFC 3161 Time Stamp Response object. + */ +public class TimeStampResponse +{ + TimeStampResp resp; + TimeStampToken timeStampToken; + + public TimeStampResponse(TimeStampResp resp) + throws TSPException, IOException + { + this.resp = resp; + + if (resp.getTimeStampToken() != null) + { + timeStampToken = new TimeStampToken(resp.getTimeStampToken()); + } + } + + /** + * Create a TimeStampResponse from a byte array containing an ASN.1 encoding. + * + * @param resp the byte array containing the encoded response. + * @throws TSPException if the response is malformed. + * @throws IOException if the byte array doesn't represent an ASN.1 encoding. + */ + public TimeStampResponse(byte[] resp) + throws TSPException, IOException + { + this(new ByteArrayInputStream(resp)); + } + + /** + * Create a TimeStampResponse from an input stream containing an ASN.1 encoding. + * + * @param in the input stream containing the encoded response. + * @throws TSPException if the response is malformed. + * @throws IOException if the stream doesn't represent an ASN.1 encoding. + */ + public TimeStampResponse(InputStream in) + throws TSPException, IOException + { + this(readTimeStampResp(in)); + } + + private static TimeStampResp readTimeStampResp( + InputStream in) + throws IOException, TSPException + { + try + { + return TimeStampResp.getInstance(new ASN1InputStream(in).readObject()); + } + catch (IllegalArgumentException e) + { + throw new TSPException("malformed timestamp response: " + e, e); + } + catch (ClassCastException e) + { + throw new TSPException("malformed timestamp response: " + e, e); + } + } + + public int getStatus() + { + return resp.getStatus().getStatus().intValue(); + } + + public String getStatusString() + { + if (resp.getStatus().getStatusString() != null) + { + StringBuffer statusStringBuf = new StringBuffer(); + PKIFreeText text = resp.getStatus().getStatusString(); + for (int i = 0; i != text.size(); i++) + { + statusStringBuf.append(text.getStringAt(i).getString()); + } + return statusStringBuf.toString(); + } + else + { + return null; + } + } + + public PKIFailureInfo getFailInfo() + { + if (resp.getStatus().getFailInfo() != null) + { + return new PKIFailureInfo(resp.getStatus().getFailInfo()); + } + + return null; + } + + public TimeStampToken getTimeStampToken() + { + return timeStampToken; + } + + /** + * Check this response against to see if it a well formed response for + * the passed in request. Validation will include checking the time stamp + * token if the response status is GRANTED or GRANTED_WITH_MODS. + * + * @param request the request to be checked against + * @throws TSPException if the request can not match this response. + */ + public void validate( + TimeStampRequest request) + throws TSPException + { + TimeStampToken tok = this.getTimeStampToken(); + + if (tok != null) + { + TimeStampTokenInfo tstInfo = tok.getTimeStampInfo(); + + if (request.getNonce() != null && !request.getNonce().equals(tstInfo.getNonce())) + { + throw new TSPValidationException("response contains wrong nonce value."); + } + + if (this.getStatus() != PKIStatus.GRANTED && this.getStatus() != PKIStatus.GRANTED_WITH_MODS) + { + throw new TSPValidationException("time stamp token found in failed request."); + } + + if (!Arrays.constantTimeAreEqual(request.getMessageImprintDigest(), tstInfo.getMessageImprintDigest())) + { + throw new TSPValidationException("response for different message imprint digest."); + } + + if (!tstInfo.getMessageImprintAlgOID().equals(request.getMessageImprintAlgOID())) + { + throw new TSPValidationException("response for different message imprint algorithm."); + } + + Attribute scV1 = tok.getSignedAttributes().get(PKCSObjectIdentifiers.id_aa_signingCertificate); + Attribute scV2 = tok.getSignedAttributes().get(PKCSObjectIdentifiers.id_aa_signingCertificateV2); + + if (scV1 == null && scV2 == null) + { + throw new TSPValidationException("no signing certificate attribute present."); + } + + if (scV1 != null && scV2 != null) + { + /* + * RFC 5035 5.4. If both attributes exist in a single message, + * they are independently evaluated. + */ + } + + if (request.getReqPolicy() != null && !request.getReqPolicy().equals(tstInfo.getPolicy())) + { + throw new TSPValidationException("TSA policy wrong for request."); + } + } + else if (this.getStatus() == PKIStatus.GRANTED || this.getStatus() == PKIStatus.GRANTED_WITH_MODS) + { + throw new TSPValidationException("no time stamp token found and one expected."); + } + } + + /** + * return the ASN.1 encoded representation of this object. + */ + public byte[] getEncoded() throws IOException + { + return resp.getEncoded(); + } +}
\ No newline at end of file diff --git a/pkix/src/main/java/org/spongycastle/tsp/TimeStampResponseGenerator.java b/pkix/src/main/java/org/spongycastle/tsp/TimeStampResponseGenerator.java new file mode 100644 index 00000000..57e83aa2 --- /dev/null +++ b/pkix/src/main/java/org/spongycastle/tsp/TimeStampResponseGenerator.java @@ -0,0 +1,353 @@ +package org.spongycastle.tsp; + +import java.io.IOException; +import java.math.BigInteger; +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import org.spongycastle.asn1.ASN1EncodableVector; +import org.spongycastle.asn1.ASN1Integer; +import org.spongycastle.asn1.ASN1ObjectIdentifier; +import org.spongycastle.asn1.DERBitString; +import org.spongycastle.asn1.DERSequence; +import org.spongycastle.asn1.DERUTF8String; +import org.spongycastle.asn1.cmp.PKIFailureInfo; +import org.spongycastle.asn1.cmp.PKIFreeText; +import org.spongycastle.asn1.cmp.PKIStatus; +import org.spongycastle.asn1.cmp.PKIStatusInfo; +import org.spongycastle.asn1.cms.ContentInfo; +import org.spongycastle.asn1.tsp.TimeStampResp; + +/** + * Generator for RFC 3161 Time Stamp Responses. + * <p> + * New generate methods have been introduced to give people more control over what ends up in the message. + * Unfortunately it turns out that in some cases fields like statusString must be left out otherwise a an + * otherwise valid timestamp will be rejected. + * </p> + * If you're after the most control with generating a response use: + * <pre> + * TimeStampResponse tsResp; + * + * try + * { + * tsResp = tsRespGen.generateGrantedResponse(request, new BigInteger("23"), new Date()); + * } + * catch (Exception e) + * { + * tsResp = tsRespGen.generateRejectedResponse(e); + * } + * </pre> + * The generate method does this, but provides a status string of "Operation Okay". + * <p> + * It should be pointed out that generateRejectedResponse() may also, on very rare occasions throw a TSPException. + * In the event that happens, there's a serious internal problem with your responder. + * </p> + */ +public class TimeStampResponseGenerator +{ + int status; + + ASN1EncodableVector statusStrings; + + int failInfo; + private TimeStampTokenGenerator tokenGenerator; + private Set acceptedAlgorithms; + private Set acceptedPolicies; + private Set acceptedExtensions; + + /** + * + * @param tokenGenerator + * @param acceptedAlgorithms a set of OIDs giving accepted algorithms. + */ + public TimeStampResponseGenerator( + TimeStampTokenGenerator tokenGenerator, + Set acceptedAlgorithms) + { + this(tokenGenerator, acceptedAlgorithms, null, null); + } + + /** + * + * @param tokenGenerator + * @param acceptedAlgorithms a set of OIDs giving accepted algorithms. + * @param acceptedPolicies if non-null a set of policies OIDs we are willing to sign under. + */ + public TimeStampResponseGenerator( + TimeStampTokenGenerator tokenGenerator, + Set acceptedAlgorithms, + Set acceptedPolicies) + { + this(tokenGenerator, acceptedAlgorithms, acceptedPolicies, null); + } + + /** + * + * @param tokenGenerator + * @param acceptedAlgorithms a set of OIDs giving accepted algorithms. + * @param acceptedPolicies if non-null a set of policies OIDs we are willing to sign under. + * @param acceptedExtensions if non-null a set of extensions OIDs we are willing to accept. + */ + public TimeStampResponseGenerator( + TimeStampTokenGenerator tokenGenerator, + Set acceptedAlgorithms, + Set acceptedPolicies, + Set acceptedExtensions) + { + this.tokenGenerator = tokenGenerator; + this.acceptedAlgorithms = convert(acceptedAlgorithms); + this.acceptedPolicies = convert(acceptedPolicies); + this.acceptedExtensions = convert(acceptedExtensions); + + statusStrings = new ASN1EncodableVector(); + } + + private void addStatusString(String statusString) + { + statusStrings.add(new DERUTF8String(statusString)); + } + + private void setFailInfoField(int field) + { + failInfo = failInfo | field; + } + + private PKIStatusInfo getPKIStatusInfo() + { + ASN1EncodableVector v = new ASN1EncodableVector(); + + v.add(new ASN1Integer(status)); + + if (statusStrings.size() > 0) + { + v.add(PKIFreeText.getInstance(new DERSequence(statusStrings))); + } + + if (failInfo != 0) + { + DERBitString failInfoBitString = new FailInfo(failInfo); + v.add(failInfoBitString); + } + + return PKIStatusInfo.getInstance(new DERSequence(v)); + } + + /** + * Return an appropriate TimeStampResponse. + * <p> + * If genTime is null a timeNotAvailable error response will be returned. Calling generate() is the + * equivalent of: + * <pre> + * TimeStampResponse tsResp; + * + * try + * { + * tsResp = tsRespGen.generateGrantedResponse(request, serialNumber, genTime, "Operation Okay"); + * } + * catch (Exception e) + * { + * tsResp = tsRespGen.generateRejectedResponse(e); + * } + * </pre> + * @param request the request this response is for. + * @param serialNumber serial number for the response token. + * @param genTime generation time for the response token. + * @return a TimeStampResponse. + * @throws TSPException + */ + public TimeStampResponse generate( + TimeStampRequest request, + BigInteger serialNumber, + Date genTime) + throws TSPException + { + try + { + return this.generateGrantedResponse(request, serialNumber, genTime, "Operation Okay"); + } + catch (Exception e) + { + return this.generateRejectedResponse(e); + } + } + + /** + * Return a granted response, if the passed in request passes validation. + * <p> + * If genTime is null a timeNotAvailable or a validation exception occurs a TSPValidationException will + * be thrown. The parent TSPException will only occur on some sort of system failure. + * </p> + * @param request the request this response is for. + * @param serialNumber serial number for the response token. + * @param genTime generation time for the response token. + * @return the TimeStampResponse with a status of PKIStatus.GRANTED + * @throws TSPException on validation exception or internal error. + */ + public TimeStampResponse generateGrantedResponse( + TimeStampRequest request, + BigInteger serialNumber, + Date genTime) + throws TSPException + { + return generateGrantedResponse(request, serialNumber, genTime, null); + } + + /** + * Return a granted response, if the passed in request passes validation with the passed in status string. + * <p> + * If genTime is null a timeNotAvailable or a validation exception occurs a TSPValidationException will + * be thrown. The parent TSPException will only occur on some sort of system failure. + * </p> + * @param request the request this response is for. + * @param serialNumber serial number for the response token. + * @param genTime generation time for the response token. + * @return the TimeStampResponse with a status of PKIStatus.GRANTED + * @throws TSPException on validation exception or internal error. + */ + public TimeStampResponse generateGrantedResponse( + TimeStampRequest request, + BigInteger serialNumber, + Date genTime, + String statusString) + throws TSPException + { + if (genTime == null) + { + throw new TSPValidationException("The time source is not available.", PKIFailureInfo.timeNotAvailable); + } + + request.validate(acceptedAlgorithms, acceptedPolicies, acceptedExtensions); + + status = PKIStatus.GRANTED; + statusStrings = new ASN1EncodableVector(); + + if (statusString != null) + { + this.addStatusString(statusString); + } + + PKIStatusInfo pkiStatusInfo = getPKIStatusInfo(); + + ContentInfo tstTokenContentInfo; + try + { + tstTokenContentInfo = tokenGenerator.generate(request, serialNumber, genTime).toCMSSignedData().toASN1Structure(); + } + catch (TSPException e) + { + throw e; + } + catch (Exception e) + { + throw new TSPException( + "Timestamp token received cannot be converted to ContentInfo", e); + } + + TimeStampResp resp = new TimeStampResp(pkiStatusInfo, tstTokenContentInfo); + + try + { + return new TimeStampResponse(resp); + } + catch (IOException e) + { + throw new TSPException("created badly formatted response!"); + } + } + + /** + * Generate a generic rejection response based on a TSPValidationException or + * an Exception. Exceptions which are not an instance of TSPValidationException + * will be treated as systemFailure. The return value of exception.getMessage() will + * be used as the status string for the response. + * + * @param exception the exception thrown on validating the request. + * @return a TimeStampResponse. + * @throws TSPException if a failure response cannot be generated. + */ + public TimeStampResponse generateRejectedResponse(Exception exception) + throws TSPException + { + if (exception instanceof TSPValidationException) + { + return generateFailResponse(PKIStatus.REJECTION, ((TSPValidationException)exception).getFailureCode(), exception.getMessage()); + } + else + { + return generateFailResponse(PKIStatus.REJECTION, PKIFailureInfo.systemFailure, exception.getMessage()); + } + } + + /** + * Generate a non-granted TimeStampResponse with chosen status and FailInfoField. + * + * @param status the PKIStatus to set. + * @param failInfoField the FailInfoField to set. + * @param statusString an optional string describing the failure. + * @return a TimeStampResponse with a failInfoField and optional statusString + * @throws TSPException in case the response could not be created + */ + public TimeStampResponse generateFailResponse(int status, int failInfoField, String statusString) + throws TSPException + { + this.status = status; + this.statusStrings = new ASN1EncodableVector(); + + this.setFailInfoField(failInfoField); + + if (statusString != null) + { + this.addStatusString(statusString); + } + + PKIStatusInfo pkiStatusInfo = getPKIStatusInfo(); + + TimeStampResp resp = new TimeStampResp(pkiStatusInfo, null); + + try + { + return new TimeStampResponse(resp); + } + catch (IOException e) + { + throw new TSPException("created badly formatted response!"); + } + } + + private Set convert(Set orig) + { + if (orig == null) + { + return orig; + } + + Set con = new HashSet(orig.size()); + + for (Iterator it = orig.iterator(); it.hasNext();) + { + Object o = it.next(); + + if (o instanceof String) + { + con.add(new ASN1ObjectIdentifier((String)o)); + } + else + { + con.add(o); + } + } + + return con; + } + + class FailInfo extends DERBitString + { + FailInfo(int failInfoValue) + { + super(getBytes(failInfoValue), getPadBits(failInfoValue)); + } + } +} diff --git a/pkix/src/main/java/org/spongycastle/tsp/TimeStampToken.java b/pkix/src/main/java/org/spongycastle/tsp/TimeStampToken.java new file mode 100644 index 00000000..00d2a903 --- /dev/null +++ b/pkix/src/main/java/org/spongycastle/tsp/TimeStampToken.java @@ -0,0 +1,393 @@ +package org.spongycastle.tsp; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Collection; +import java.util.Date; + +import org.spongycastle.asn1.ASN1InputStream; +import org.spongycastle.asn1.cms.Attribute; +import org.spongycastle.asn1.cms.AttributeTable; +import org.spongycastle.asn1.cms.ContentInfo; +import org.spongycastle.asn1.cms.IssuerAndSerialNumber; +import org.spongycastle.asn1.ess.ESSCertID; +import org.spongycastle.asn1.ess.ESSCertIDv2; +import org.spongycastle.asn1.ess.SigningCertificate; +import org.spongycastle.asn1.ess.SigningCertificateV2; +import org.spongycastle.asn1.nist.NISTObjectIdentifiers; +import org.spongycastle.asn1.oiw.OIWObjectIdentifiers; +import org.spongycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.spongycastle.asn1.tsp.TSTInfo; +import org.spongycastle.asn1.x500.X500Name; +import org.spongycastle.asn1.x509.AlgorithmIdentifier; +import org.spongycastle.asn1.x509.GeneralName; +import org.spongycastle.asn1.x509.IssuerSerial; +import org.spongycastle.cert.X509CertificateHolder; +import org.spongycastle.cms.CMSException; +import org.spongycastle.cms.CMSProcessable; +import org.spongycastle.cms.CMSSignedData; +import org.spongycastle.cms.SignerId; +import org.spongycastle.cms.SignerInformation; +import org.spongycastle.cms.SignerInformationVerifier; +import org.spongycastle.operator.DigestCalculator; +import org.spongycastle.operator.OperatorCreationException; +import org.spongycastle.util.Arrays; +import org.spongycastle.util.Store; + +/** + * Carrier class for a TimeStampToken. + */ +public class TimeStampToken +{ + CMSSignedData tsToken; + + SignerInformation tsaSignerInfo; + + Date genTime; + + TimeStampTokenInfo tstInfo; + + CertID certID; + + public TimeStampToken(ContentInfo contentInfo) + throws TSPException, IOException + { + this(getSignedData(contentInfo)); + } + + private static CMSSignedData getSignedData(ContentInfo contentInfo) + throws TSPException + { + try + { + return new CMSSignedData(contentInfo); + } + catch (CMSException e) + { + throw new TSPException("TSP parsing error: " + e.getMessage(), e.getCause()); + } + } + + public TimeStampToken(CMSSignedData signedData) + throws TSPException, IOException + { + this.tsToken = signedData; + + if (!this.tsToken.getSignedContentTypeOID().equals(PKCSObjectIdentifiers.id_ct_TSTInfo.getId())) + { + throw new TSPValidationException("ContentInfo object not for a time stamp."); + } + + Collection signers = tsToken.getSignerInfos().getSigners(); + + if (signers.size() != 1) + { + throw new IllegalArgumentException("Time-stamp token signed by " + + signers.size() + + " signers, but it must contain just the TSA signature."); + } + + tsaSignerInfo = (SignerInformation)signers.iterator().next(); + + try + { + CMSProcessable content = tsToken.getSignedContent(); + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + + content.write(bOut); + + ASN1InputStream aIn = new ASN1InputStream(new ByteArrayInputStream(bOut.toByteArray())); + + this.tstInfo = new TimeStampTokenInfo(TSTInfo.getInstance(aIn.readObject())); + + Attribute attr = tsaSignerInfo.getSignedAttributes().get(PKCSObjectIdentifiers.id_aa_signingCertificate); + + if (attr != null) + { + SigningCertificate signCert = SigningCertificate.getInstance(attr.getAttrValues().getObjectAt(0)); + + this.certID = new CertID(ESSCertID.getInstance(signCert.getCerts()[0])); + } + else + { + attr = tsaSignerInfo.getSignedAttributes().get(PKCSObjectIdentifiers.id_aa_signingCertificateV2); + + if (attr == null) + { + throw new TSPValidationException("no signing certificate attribute found, time stamp invalid."); + } + + SigningCertificateV2 signCertV2 = SigningCertificateV2.getInstance(attr.getAttrValues().getObjectAt(0)); + + this.certID = new CertID(ESSCertIDv2.getInstance(signCertV2.getCerts()[0])); + } + } + catch (CMSException e) + { + throw new TSPException(e.getMessage(), e.getUnderlyingException()); + } + } + + public TimeStampTokenInfo getTimeStampInfo() + { + return tstInfo; + } + + public SignerId getSID() + { + return tsaSignerInfo.getSID(); + } + + public AttributeTable getSignedAttributes() + { + return tsaSignerInfo.getSignedAttributes(); + } + + public AttributeTable getUnsignedAttributes() + { + return tsaSignerInfo.getUnsignedAttributes(); + } + + public Store getCertificates() + { + return tsToken.getCertificates(); + } + + public Store getCRLs() + { + return tsToken.getCRLs(); + } + + public Store getAttributeCertificates() + { + return tsToken.getAttributeCertificates(); + } + + /** + * Validate the time stamp token. + * <p> + * To be valid the token must be signed by the passed in certificate and + * the certificate must be the one referred to by the SigningCertificate + * attribute included in the hashed attributes of the token. The + * certificate must also have the ExtendedKeyUsageExtension with only + * KeyPurposeId.id_kp_timeStamping and have been valid at the time the + * timestamp was created. + * </p> + * <p> + * A successful call to validate means all the above are true. + * </p> + * + * @param sigVerifier the content verifier create the objects required to verify the CMS object in the timestamp. + * @throws TSPException if an exception occurs in processing the token. + * @throws TSPValidationException if the certificate or signature fail to be valid. + * @throws IllegalArgumentException if the sigVerifierProvider has no associated certificate. + */ + public void validate( + SignerInformationVerifier sigVerifier) + throws TSPException, TSPValidationException + { + if (!sigVerifier.hasAssociatedCertificate()) + { + throw new IllegalArgumentException("verifier provider needs an associated certificate"); + } + + try + { + X509CertificateHolder certHolder = sigVerifier.getAssociatedCertificate(); + DigestCalculator calc = sigVerifier.getDigestCalculator(certID.getHashAlgorithm()); + + OutputStream cOut = calc.getOutputStream(); + + cOut.write(certHolder.getEncoded()); + cOut.close(); + + if (!Arrays.constantTimeAreEqual(certID.getCertHash(), calc.getDigest())) + { + throw new TSPValidationException("certificate hash does not match certID hash."); + } + + if (certID.getIssuerSerial() != null) + { + IssuerAndSerialNumber issuerSerial = new IssuerAndSerialNumber(certHolder.toASN1Structure()); + + if (!certID.getIssuerSerial().getSerial().equals(issuerSerial.getSerialNumber())) + { + throw new TSPValidationException("certificate serial number does not match certID for signature."); + } + + GeneralName[] names = certID.getIssuerSerial().getIssuer().getNames(); + boolean found = false; + + for (int i = 0; i != names.length; i++) + { + if (names[i].getTagNo() == 4 && X500Name.getInstance(names[i].getName()).equals(X500Name.getInstance(issuerSerial.getName()))) + { + found = true; + break; + } + } + + if (!found) + { + throw new TSPValidationException("certificate name does not match certID for signature. "); + } + } + + TSPUtil.validateCertificate(certHolder); + + if (!certHolder.isValidOn(tstInfo.getGenTime())) + { + throw new TSPValidationException("certificate not valid when time stamp created."); + } + + if (!tsaSignerInfo.verify(sigVerifier)) + { + throw new TSPValidationException("signature not created by certificate."); + } + } + catch (CMSException e) + { + if (e.getUnderlyingException() != null) + { + throw new TSPException(e.getMessage(), e.getUnderlyingException()); + } + else + { + throw new TSPException("CMS exception: " + e, e); + } + } + catch (IOException e) + { + throw new TSPException("problem processing certificate: " + e, e); + } + catch (OperatorCreationException e) + { + throw new TSPException("unable to create digest: " + e.getMessage(), e); + } + } + + /** + * Return true if the signature on time stamp token is valid. + * <p> + * Note: this is a much weaker proof of correctness than calling validate(). + * </p> + * + * @param sigVerifier the content verifier create the objects required to verify the CMS object in the timestamp. + * @return true if the signature matches, false otherwise. + * @throws TSPException if the signature cannot be processed or the provider cannot match the algorithm. + */ + public boolean isSignatureValid( + SignerInformationVerifier sigVerifier) + throws TSPException + { + try + { + return tsaSignerInfo.verify(sigVerifier); + } + catch (CMSException e) + { + if (e.getUnderlyingException() != null) + { + throw new TSPException(e.getMessage(), e.getUnderlyingException()); + } + else + { + throw new TSPException("CMS exception: " + e, e); + } + } + } + + /** + * Return the underlying CMSSignedData object. + * + * @return the underlying CMS structure. + */ + public CMSSignedData toCMSSignedData() + { + return tsToken; + } + + /** + * Return a ASN.1 encoded byte stream representing the encoded object. + * + * @throws IOException if encoding fails. + */ + public byte[] getEncoded() + throws IOException + { + return tsToken.getEncoded(); + } + + // perhaps this should be done using an interface on the ASN.1 classes... + private class CertID + { + private ESSCertID certID; + private ESSCertIDv2 certIDv2; + + CertID(ESSCertID certID) + { + this.certID = certID; + this.certIDv2 = null; + } + + CertID(ESSCertIDv2 certID) + { + this.certIDv2 = certID; + this.certID = null; + } + + public String getHashAlgorithmName() + { + if (certID != null) + { + return "SHA-1"; + } + else + { + if (NISTObjectIdentifiers.id_sha256.equals(certIDv2.getHashAlgorithm().getAlgorithm())) + { + return "SHA-256"; + } + return certIDv2.getHashAlgorithm().getAlgorithm().getId(); + } + } + + public AlgorithmIdentifier getHashAlgorithm() + { + if (certID != null) + { + return new AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1); + } + else + { + return certIDv2.getHashAlgorithm(); + } + } + + public byte[] getCertHash() + { + if (certID != null) + { + return certID.getCertHash(); + } + else + { + return certIDv2.getCertHash(); + } + } + + public IssuerSerial getIssuerSerial() + { + if (certID != null) + { + return certID.getIssuerSerial(); + } + else + { + return certIDv2.getIssuerSerial(); + } + } + } +} diff --git a/pkix/src/main/java/org/spongycastle/tsp/TimeStampTokenGenerator.java b/pkix/src/main/java/org/spongycastle/tsp/TimeStampTokenGenerator.java new file mode 100644 index 00000000..f3783f80 --- /dev/null +++ b/pkix/src/main/java/org/spongycastle/tsp/TimeStampTokenGenerator.java @@ -0,0 +1,380 @@ +package org.spongycastle.tsp; + +import java.io.IOException; +import java.io.OutputStream; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.spongycastle.asn1.ASN1Boolean; +import org.spongycastle.asn1.ASN1Encoding; +import org.spongycastle.asn1.ASN1GeneralizedTime; +import org.spongycastle.asn1.ASN1Integer; +import org.spongycastle.asn1.ASN1ObjectIdentifier; +import org.spongycastle.asn1.DERNull; +import org.spongycastle.asn1.cms.AttributeTable; +import org.spongycastle.asn1.ess.ESSCertID; +import org.spongycastle.asn1.ess.ESSCertIDv2; +import org.spongycastle.asn1.ess.SigningCertificate; +import org.spongycastle.asn1.ess.SigningCertificateV2; +import org.spongycastle.asn1.oiw.OIWObjectIdentifiers; +import org.spongycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.spongycastle.asn1.tsp.Accuracy; +import org.spongycastle.asn1.tsp.MessageImprint; +import org.spongycastle.asn1.tsp.TSTInfo; +import org.spongycastle.asn1.x509.AlgorithmIdentifier; +import org.spongycastle.asn1.x509.GeneralName; +import org.spongycastle.asn1.x509.GeneralNames; +import org.spongycastle.asn1.x509.IssuerSerial; +import org.spongycastle.cert.X509CertificateHolder; +import org.spongycastle.cms.CMSAttributeTableGenerationException; +import org.spongycastle.cms.CMSAttributeTableGenerator; +import org.spongycastle.cms.CMSException; +import org.spongycastle.cms.CMSProcessableByteArray; +import org.spongycastle.cms.CMSSignedData; +import org.spongycastle.cms.CMSSignedDataGenerator; +import org.spongycastle.cms.SignerInfoGenerator; +import org.spongycastle.operator.DigestCalculator; +import org.spongycastle.util.CollectionStore; +import org.spongycastle.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 +{ + int accuracySeconds = -1; + + int accuracyMillis = -1; + + 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)); + } + + 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 + { + 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 = new ASN1Boolean(ordering); + } + + ASN1Integer nonce = null; + if (request.getNonce() != null) + { + nonce = new ASN1Integer(request.getNonce()); + } + + ASN1ObjectIdentifier tsaPolicy = tsaPolicyOID; + if (request.getReqPolicy() != null) + { + tsaPolicy = request.getReqPolicy(); + } + + TSTInfo tstInfo = new TSTInfo(tsaPolicy, + messageImprint, new ASN1Integer(serialNumber), + new ASN1GeneralizedTime(genTime), accuracy, derOrdering, + nonce, tsa, request.getExtensions()); + + 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); + } + } +} diff --git a/pkix/src/main/java/org/spongycastle/tsp/TimeStampTokenInfo.java b/pkix/src/main/java/org/spongycastle/tsp/TimeStampTokenInfo.java new file mode 100644 index 00000000..e9f70650 --- /dev/null +++ b/pkix/src/main/java/org/spongycastle/tsp/TimeStampTokenInfo.java @@ -0,0 +1,121 @@ +package org.spongycastle.tsp; + +import java.io.IOException; +import java.math.BigInteger; +import java.text.ParseException; +import java.util.Date; + +import org.spongycastle.asn1.ASN1ObjectIdentifier; +import org.spongycastle.asn1.tsp.Accuracy; +import org.spongycastle.asn1.tsp.TSTInfo; +import org.spongycastle.asn1.x509.AlgorithmIdentifier; +import org.spongycastle.asn1.x509.GeneralName; + +public class TimeStampTokenInfo +{ + TSTInfo tstInfo; + Date genTime; + + TimeStampTokenInfo(TSTInfo tstInfo) + throws TSPException, IOException + { + this.tstInfo = tstInfo; + + try + { + this.genTime = tstInfo.getGenTime().getDate(); + } + catch (ParseException e) + { + throw new TSPException("unable to parse genTime field"); + } + } + + public boolean isOrdered() + { + return tstInfo.getOrdering().isTrue(); + } + + public Accuracy getAccuracy() + { + return tstInfo.getAccuracy(); + } + + public Date getGenTime() + { + return genTime; + } + + public GenTimeAccuracy getGenTimeAccuracy() + { + if (this.getAccuracy() != null) + { + return new GenTimeAccuracy(this.getAccuracy()); + } + + return null; + } + + public ASN1ObjectIdentifier getPolicy() + { + return tstInfo.getPolicy(); + } + + public BigInteger getSerialNumber() + { + return tstInfo.getSerialNumber().getValue(); + } + + public GeneralName getTsa() + { + return tstInfo.getTsa(); + } + + /** + * @return the nonce value, null if there isn't one. + */ + public BigInteger getNonce() + { + if (tstInfo.getNonce() != null) + { + return tstInfo.getNonce().getValue(); + } + + return null; + } + + public AlgorithmIdentifier getHashAlgorithm() + { + return tstInfo.getMessageImprint().getHashAlgorithm(); + } + + public ASN1ObjectIdentifier getMessageImprintAlgOID() + { + return tstInfo.getMessageImprint().getHashAlgorithm().getAlgorithm(); + } + + public byte[] getMessageImprintDigest() + { + return tstInfo.getMessageImprint().getHashedMessage(); + } + + public byte[] getEncoded() + throws IOException + { + return tstInfo.getEncoded(); + } + + /** + * @deprecated use toASN1Structure + * @return + */ + public TSTInfo toTSTInfo() + { + return tstInfo; + } + + public TSTInfo toASN1Structure() + { + return tstInfo; + } +} diff --git a/pkix/src/main/java/org/spongycastle/tsp/cms/CMSTimeStampedData.java b/pkix/src/main/java/org/spongycastle/tsp/cms/CMSTimeStampedData.java new file mode 100644 index 00000000..844f123a --- /dev/null +++ b/pkix/src/main/java/org/spongycastle/tsp/cms/CMSTimeStampedData.java @@ -0,0 +1,204 @@ +package org.spongycastle.tsp.cms; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; + +import org.spongycastle.asn1.ASN1InputStream; +import org.spongycastle.asn1.DERIA5String; +import org.spongycastle.asn1.cms.AttributeTable; +import org.spongycastle.asn1.cms.CMSObjectIdentifiers; +import org.spongycastle.asn1.cms.ContentInfo; +import org.spongycastle.asn1.cms.Evidence; +import org.spongycastle.asn1.cms.TimeStampAndCRL; +import org.spongycastle.asn1.cms.TimeStampTokenEvidence; +import org.spongycastle.asn1.cms.TimeStampedData; +import org.spongycastle.cms.CMSException; +import org.spongycastle.operator.DigestCalculator; +import org.spongycastle.operator.DigestCalculatorProvider; +import org.spongycastle.operator.OperatorCreationException; +import org.spongycastle.tsp.TimeStampToken; + +public class CMSTimeStampedData +{ + private TimeStampedData timeStampedData; + private ContentInfo contentInfo; + private TimeStampDataUtil util; + + public CMSTimeStampedData(ContentInfo contentInfo) + { + this.initialize(contentInfo); + } + + public CMSTimeStampedData(InputStream in) + throws IOException + { + try + { + initialize(ContentInfo.getInstance(new ASN1InputStream(in).readObject())); + } + catch (ClassCastException e) + { + throw new IOException("Malformed content: " + e); + } + catch (IllegalArgumentException e) + { + throw new IOException("Malformed content: " + e); + } + } + + public CMSTimeStampedData(byte[] baseData) + throws IOException + { + this(new ByteArrayInputStream(baseData)); + } + + private void initialize(ContentInfo contentInfo) + { + this.contentInfo = contentInfo; + + if (CMSObjectIdentifiers.timestampedData.equals(contentInfo.getContentType())) + { + this.timeStampedData = TimeStampedData.getInstance(contentInfo.getContent()); + } + else + { + throw new IllegalArgumentException("Malformed content - type must be " + CMSObjectIdentifiers.timestampedData.getId()); + } + + util = new TimeStampDataUtil(this.timeStampedData); + } + + public byte[] calculateNextHash(DigestCalculator calculator) + throws CMSException + { + return util.calculateNextHash(calculator); + } + + /** + * Return a new timeStampedData object with the additional token attached. + * + * @throws CMSException + */ + public CMSTimeStampedData addTimeStamp(TimeStampToken token) + throws CMSException + { + TimeStampAndCRL[] timeStamps = util.getTimeStamps(); + TimeStampAndCRL[] newTimeStamps = new TimeStampAndCRL[timeStamps.length + 1]; + + System.arraycopy(timeStamps, 0, newTimeStamps, 0, timeStamps.length); + + newTimeStamps[timeStamps.length] = new TimeStampAndCRL(token.toCMSSignedData().toASN1Structure()); + + return new CMSTimeStampedData(new ContentInfo(CMSObjectIdentifiers.timestampedData, new TimeStampedData(timeStampedData.getDataUri(), timeStampedData.getMetaData(), timeStampedData.getContent(), new Evidence(new TimeStampTokenEvidence(newTimeStamps))))); + } + + public byte[] getContent() + { + if (timeStampedData.getContent() != null) + { + return timeStampedData.getContent().getOctets(); + } + + return null; + } + + public URI getDataUri() + throws URISyntaxException + { + DERIA5String dataURI = this.timeStampedData.getDataUri(); + + if (dataURI != null) + { + return new URI(dataURI.getString()); + } + + return null; + } + + public String getFileName() + { + return util.getFileName(); + } + + public String getMediaType() + { + return util.getMediaType(); + } + + public AttributeTable getOtherMetaData() + { + return util.getOtherMetaData(); + } + + public TimeStampToken[] getTimeStampTokens() + throws CMSException + { + return util.getTimeStampTokens(); + } + + /** + * Initialise the passed in calculator with the MetaData for this message, if it is + * required as part of the initial message imprint calculation. + * + * @param calculator the digest calculator to be initialised. + * @throws CMSException if the MetaData is required and cannot be processed + */ + public void initialiseMessageImprintDigestCalculator(DigestCalculator calculator) + throws CMSException + { + util.initialiseMessageImprintDigestCalculator(calculator); + } + + /** + * Returns an appropriately initialised digest calculator based on the message imprint algorithm + * described in the first time stamp in the TemporalData for this message. If the metadata is required + * to be included in the digest calculation, the returned calculator will be pre-initialised. + * + * @param calculatorProvider a provider of DigestCalculator objects. + * @return an initialised digest calculator. + * @throws OperatorCreationException if the provider is unable to create the calculator. + */ + public DigestCalculator getMessageImprintDigestCalculator(DigestCalculatorProvider calculatorProvider) + throws OperatorCreationException + { + return util.getMessageImprintDigestCalculator(calculatorProvider); + } + + /** + * Validate the digests present in the TimeStampTokens contained in the CMSTimeStampedData. + * + * @param calculatorProvider provider for digest calculators + * @param dataDigest the calculated data digest for the message + * @throws ImprintDigestInvalidException if an imprint digest fails to compare + * @throws CMSException if an exception occurs processing the message. + */ + public void validate(DigestCalculatorProvider calculatorProvider, byte[] dataDigest) + throws ImprintDigestInvalidException, CMSException + { + util.validate(calculatorProvider, dataDigest); + } + + /** + * Validate the passed in timestamp token against the tokens and data present in the message. + * + * @param calculatorProvider provider for digest calculators + * @param dataDigest the calculated data digest for the message. + * @param timeStampToken the timestamp token of interest. + * @throws ImprintDigestInvalidException if the token is not present in the message, or an imprint digest fails to compare. + * @throws CMSException if an exception occurs processing the message. + */ + public void validate(DigestCalculatorProvider calculatorProvider, byte[] dataDigest, TimeStampToken timeStampToken) + throws ImprintDigestInvalidException, CMSException + { + util.validate(calculatorProvider, dataDigest, timeStampToken); + } + + public byte[] getEncoded() + throws IOException + { + return contentInfo.getEncoded(); + } +} diff --git a/pkix/src/main/java/org/spongycastle/tsp/cms/CMSTimeStampedDataGenerator.java b/pkix/src/main/java/org/spongycastle/tsp/cms/CMSTimeStampedDataGenerator.java new file mode 100644 index 00000000..a1ad0c0f --- /dev/null +++ b/pkix/src/main/java/org/spongycastle/tsp/cms/CMSTimeStampedDataGenerator.java @@ -0,0 +1,70 @@ +package org.spongycastle.tsp.cms; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.spongycastle.asn1.ASN1OctetString; +import org.spongycastle.asn1.BEROctetString; +import org.spongycastle.asn1.DERIA5String; +import org.spongycastle.asn1.cms.CMSObjectIdentifiers; +import org.spongycastle.asn1.cms.ContentInfo; +import org.spongycastle.asn1.cms.Evidence; +import org.spongycastle.asn1.cms.TimeStampAndCRL; +import org.spongycastle.asn1.cms.TimeStampTokenEvidence; +import org.spongycastle.asn1.cms.TimeStampedData; +import org.spongycastle.cms.CMSException; +import org.spongycastle.tsp.TimeStampToken; +import org.spongycastle.util.io.Streams; + +public class CMSTimeStampedDataGenerator + extends CMSTimeStampedGenerator +{ + public CMSTimeStampedData generate(TimeStampToken timeStamp) throws CMSException + { + return generate(timeStamp, (InputStream)null); + } + + public CMSTimeStampedData generate(TimeStampToken timeStamp, byte[] content) throws CMSException + { + return generate(timeStamp, new ByteArrayInputStream(content)); + } + + public CMSTimeStampedData generate(TimeStampToken timeStamp, InputStream content) + throws CMSException + { + ByteArrayOutputStream contentOut = new ByteArrayOutputStream(); + + if (content != null) + { + try + { + Streams.pipeAll(content, contentOut); + } + catch (IOException e) + { + throw new CMSException("exception encapsulating content: " + e.getMessage(), e); + } + } + + ASN1OctetString encContent = null; + + if (contentOut.size() != 0) + { + encContent = new BEROctetString(contentOut.toByteArray()); + } + + TimeStampAndCRL stamp = new TimeStampAndCRL(timeStamp.toCMSSignedData().toASN1Structure()); + + DERIA5String asn1DataUri = null; + + if (dataUri != null) + { + asn1DataUri = new DERIA5String(dataUri.toString()); + } + + return new CMSTimeStampedData(new ContentInfo(CMSObjectIdentifiers.timestampedData, new TimeStampedData(asn1DataUri, metaData, encContent, new Evidence(new TimeStampTokenEvidence(stamp))))); + } +} + diff --git a/pkix/src/main/java/org/spongycastle/tsp/cms/CMSTimeStampedDataParser.java b/pkix/src/main/java/org/spongycastle/tsp/cms/CMSTimeStampedDataParser.java new file mode 100644 index 00000000..c3518b77 --- /dev/null +++ b/pkix/src/main/java/org/spongycastle/tsp/cms/CMSTimeStampedDataParser.java @@ -0,0 +1,207 @@ +package org.spongycastle.tsp.cms; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; + +import org.spongycastle.asn1.BERTags; +import org.spongycastle.asn1.DERIA5String; +import org.spongycastle.asn1.cms.AttributeTable; +import org.spongycastle.asn1.cms.CMSObjectIdentifiers; +import org.spongycastle.asn1.cms.ContentInfoParser; +import org.spongycastle.asn1.cms.TimeStampedDataParser; +import org.spongycastle.cms.CMSContentInfoParser; +import org.spongycastle.cms.CMSException; +import org.spongycastle.operator.DigestCalculator; +import org.spongycastle.operator.DigestCalculatorProvider; +import org.spongycastle.operator.OperatorCreationException; +import org.spongycastle.tsp.TimeStampToken; +import org.spongycastle.util.io.Streams; + +public class CMSTimeStampedDataParser + extends CMSContentInfoParser +{ + private TimeStampedDataParser timeStampedData; + private TimeStampDataUtil util; + + public CMSTimeStampedDataParser(InputStream in) + throws CMSException + { + super(in); + + initialize(_contentInfo); + } + + public CMSTimeStampedDataParser(byte[] baseData) + throws CMSException + { + this(new ByteArrayInputStream(baseData)); + } + + private void initialize(ContentInfoParser contentInfo) + throws CMSException + { + try + { + if (CMSObjectIdentifiers.timestampedData.equals(contentInfo.getContentType())) + { + this.timeStampedData = TimeStampedDataParser.getInstance(contentInfo.getContent(BERTags.SEQUENCE)); + } + else + { + throw new IllegalArgumentException("Malformed content - type must be " + CMSObjectIdentifiers.timestampedData.getId()); + } + } + catch (IOException e) + { + throw new CMSException("parsing exception: " + e.getMessage(), e); + } + } + + public byte[] calculateNextHash(DigestCalculator calculator) + throws CMSException + { + return util.calculateNextHash(calculator); + } + + public InputStream getContent() + { + if (timeStampedData.getContent() != null) + { + return timeStampedData.getContent().getOctetStream(); + } + + return null; + } + + public URI getDataUri() + throws URISyntaxException + { + DERIA5String dataURI = this.timeStampedData.getDataUri(); + + if (dataURI != null) + { + return new URI(dataURI.getString()); + } + + return null; + } + + public String getFileName() + { + return util.getFileName(); + } + + public String getMediaType() + { + return util.getMediaType(); + } + + public AttributeTable getOtherMetaData() + { + return util.getOtherMetaData(); + } + + /** + * Initialise the passed in calculator with the MetaData for this message, if it is + * required as part of the initial message imprint calculation. + * + * @param calculator the digest calculator to be initialised. + * @throws CMSException if the MetaData is required and cannot be processed + */ + public void initialiseMessageImprintDigestCalculator(DigestCalculator calculator) + throws CMSException + { + util.initialiseMessageImprintDigestCalculator(calculator); + } + + /** + * Returns an appropriately initialised digest calculator based on the message imprint algorithm + * described in the first time stamp in the TemporalData for this message. If the metadata is required + * to be included in the digest calculation, the returned calculator will be pre-initialised. + * + * @param calculatorProvider a provider of DigestCalculator objects. + * @return an initialised digest calculator. + * @throws OperatorCreationException if the provider is unable to create the calculator. + */ + public DigestCalculator getMessageImprintDigestCalculator(DigestCalculatorProvider calculatorProvider) + throws OperatorCreationException + { + try + { + parseTimeStamps(); + } + catch (CMSException e) + { + throw new OperatorCreationException("unable to extract algorithm ID: " + e.getMessage(), e); + } + + return util.getMessageImprintDigestCalculator(calculatorProvider); + } + + public TimeStampToken[] getTimeStampTokens() + throws CMSException + { + parseTimeStamps(); + + return util.getTimeStampTokens(); + } + + /** + * Validate the digests present in the TimeStampTokens contained in the CMSTimeStampedData. + * + * @param calculatorProvider provider for digest calculators + * @param dataDigest the calculated data digest for the message + * @throws ImprintDigestInvalidException if an imprint digest fails to compare + * @throws CMSException if an exception occurs processing the message. + */ + public void validate(DigestCalculatorProvider calculatorProvider, byte[] dataDigest) + throws ImprintDigestInvalidException, CMSException + { + parseTimeStamps(); + + util.validate(calculatorProvider, dataDigest); + } + + /** + * Validate the passed in timestamp token against the tokens and data present in the message. + * + * @param calculatorProvider provider for digest calculators + * @param dataDigest the calculated data digest for the message. + * @param timeStampToken the timestamp token of interest. + * @throws ImprintDigestInvalidException if the token is not present in the message, or an imprint digest fails to compare. + * @throws CMSException if an exception occurs processing the message. + */ + public void validate(DigestCalculatorProvider calculatorProvider, byte[] dataDigest, TimeStampToken timeStampToken) + throws ImprintDigestInvalidException, CMSException + { + parseTimeStamps(); + + util.validate(calculatorProvider, dataDigest, timeStampToken); + } + + private void parseTimeStamps() + throws CMSException + { + try + { + if (util == null) + { + InputStream cont = this.getContent(); + + if (cont != null) + { + Streams.drain(cont); + } + + util = new TimeStampDataUtil(timeStampedData); + } + } + catch (IOException e) + { + throw new CMSException("unable to parse evidence block: " + e.getMessage(), e); + } + } +} diff --git a/pkix/src/main/java/org/spongycastle/tsp/cms/CMSTimeStampedGenerator.java b/pkix/src/main/java/org/spongycastle/tsp/cms/CMSTimeStampedGenerator.java new file mode 100644 index 00000000..9dbeb97f --- /dev/null +++ b/pkix/src/main/java/org/spongycastle/tsp/cms/CMSTimeStampedGenerator.java @@ -0,0 +1,88 @@ +package org.spongycastle.tsp.cms; + +import java.net.URI; + +import org.spongycastle.asn1.ASN1Boolean; +import org.spongycastle.asn1.DERIA5String; +import org.spongycastle.asn1.DERUTF8String; +import org.spongycastle.asn1.cms.Attributes; +import org.spongycastle.asn1.cms.MetaData; +import org.spongycastle.cms.CMSException; +import org.spongycastle.operator.DigestCalculator; + +public class CMSTimeStampedGenerator +{ + protected MetaData metaData; + protected URI dataUri; + + /** + * Set the dataURI to be included in message. + * + * @param dataUri URI for the data the initial message imprint digest is based on. + */ + public void setDataUri(URI dataUri) + { + this.dataUri = dataUri; + } + + /** + * Set the MetaData for the generated message. + * + * @param hashProtected true if the MetaData should be included in first imprint calculation, false otherwise. + * @param fileName optional file name, may be null. + * @param mediaType optional media type, may be null. + */ + public void setMetaData(boolean hashProtected, String fileName, String mediaType) + { + setMetaData(hashProtected, fileName, mediaType, null); + } + + /** + * Set the MetaData for the generated message. + * + * @param hashProtected true if the MetaData should be included in first imprint calculation, false otherwise. + * @param fileName optional file name, may be null. + * @param mediaType optional media type, may be null. + * @param attributes optional attributes, may be null. + */ + public void setMetaData(boolean hashProtected, String fileName, String mediaType, Attributes attributes) + { + DERUTF8String asn1FileName = null; + + if (fileName != null) + { + asn1FileName = new DERUTF8String(fileName); + } + + DERIA5String asn1MediaType = null; + + if (mediaType != null) + { + asn1MediaType = new DERIA5String(mediaType); + } + + setMetaData(hashProtected, asn1FileName, asn1MediaType, attributes); + } + + private void setMetaData(boolean hashProtected, DERUTF8String fileName, DERIA5String mediaType, Attributes attributes) + { + this.metaData = new MetaData(ASN1Boolean.getInstance(hashProtected), fileName, mediaType, attributes); + } + + /** + * Initialise the passed in calculator with the MetaData for this message, if it is + * required as part of the initial message imprint calculation. After initialisation the + * calculator can then be used to calculate the initial message imprint digest for the first + * timestamp. + * + * @param calculator the digest calculator to be initialised. + * @throws CMSException if the MetaData is required and cannot be processed + */ + public void initialiseMessageImprintDigestCalculator(DigestCalculator calculator) + throws CMSException + { + MetaDataUtil util = new MetaDataUtil(metaData); + + util.initialiseMessageImprintDigestCalculator(calculator); + } +} diff --git a/pkix/src/main/java/org/spongycastle/tsp/cms/ImprintDigestInvalidException.java b/pkix/src/main/java/org/spongycastle/tsp/cms/ImprintDigestInvalidException.java new file mode 100644 index 00000000..1dfc2bb3 --- /dev/null +++ b/pkix/src/main/java/org/spongycastle/tsp/cms/ImprintDigestInvalidException.java @@ -0,0 +1,21 @@ +package org.spongycastle.tsp.cms; + +import org.spongycastle.tsp.TimeStampToken; + +public class ImprintDigestInvalidException + extends Exception +{ + private TimeStampToken token; + + public ImprintDigestInvalidException(String message, TimeStampToken token) + { + super(message); + + this.token = token; + } + + public TimeStampToken getTimeStampToken() + { + return token; + } +} diff --git a/pkix/src/main/java/org/spongycastle/tsp/cms/MetaDataUtil.java b/pkix/src/main/java/org/spongycastle/tsp/cms/MetaDataUtil.java new file mode 100644 index 00000000..f4ad579a --- /dev/null +++ b/pkix/src/main/java/org/spongycastle/tsp/cms/MetaDataUtil.java @@ -0,0 +1,76 @@ +package org.spongycastle.tsp.cms; + +import java.io.IOException; + +import org.spongycastle.asn1.ASN1Encoding; +import org.spongycastle.asn1.ASN1String; +import org.spongycastle.asn1.cms.Attributes; +import org.spongycastle.asn1.cms.MetaData; +import org.spongycastle.cms.CMSException; +import org.spongycastle.operator.DigestCalculator; + +class MetaDataUtil +{ + private final MetaData metaData; + + MetaDataUtil(MetaData metaData) + { + this.metaData = metaData; + } + + void initialiseMessageImprintDigestCalculator(DigestCalculator calculator) + throws CMSException + { + if (metaData != null && metaData.isHashProtected()) + { + try + { + calculator.getOutputStream().write(metaData.getEncoded(ASN1Encoding.DER)); + } + catch (IOException e) + { + throw new CMSException("unable to initialise calculator from metaData: " + e.getMessage(), e); + } + } + } + + String getFileName() + { + if (metaData != null) + { + return convertString(metaData.getFileName()); + } + + return null; + } + + String getMediaType() + { + if (metaData != null) + { + return convertString(metaData.getMediaType()); + } + + return null; + } + + Attributes getOtherMetaData() + { + if (metaData != null) + { + return metaData.getOtherMetaData(); + } + + return null; + } + + private String convertString(ASN1String s) + { + if (s != null) + { + return s.toString(); + } + + return null; + } +} diff --git a/pkix/src/main/java/org/spongycastle/tsp/cms/TimeStampDataUtil.java b/pkix/src/main/java/org/spongycastle/tsp/cms/TimeStampDataUtil.java new file mode 100644 index 00000000..fd1bcc8d --- /dev/null +++ b/pkix/src/main/java/org/spongycastle/tsp/cms/TimeStampDataUtil.java @@ -0,0 +1,256 @@ +package org.spongycastle.tsp.cms; + +import java.io.IOException; +import java.io.OutputStream; + +import org.spongycastle.asn1.ASN1Encoding; +import org.spongycastle.asn1.ASN1ObjectIdentifier; +import org.spongycastle.asn1.cms.AttributeTable; +import org.spongycastle.asn1.cms.ContentInfo; +import org.spongycastle.asn1.cms.Evidence; +import org.spongycastle.asn1.cms.TimeStampAndCRL; +import org.spongycastle.asn1.cms.TimeStampedData; +import org.spongycastle.asn1.cms.TimeStampedDataParser; +import org.spongycastle.asn1.x509.AlgorithmIdentifier; +import org.spongycastle.cms.CMSException; +import org.spongycastle.operator.DigestCalculator; +import org.spongycastle.operator.DigestCalculatorProvider; +import org.spongycastle.operator.OperatorCreationException; +import org.spongycastle.tsp.TSPException; +import org.spongycastle.tsp.TimeStampToken; +import org.spongycastle.tsp.TimeStampTokenInfo; +import org.spongycastle.util.Arrays; + +class TimeStampDataUtil +{ + private final TimeStampAndCRL[] timeStamps; + + private final MetaDataUtil metaDataUtil; + + TimeStampDataUtil(TimeStampedData timeStampedData) + { + this.metaDataUtil = new MetaDataUtil(timeStampedData.getMetaData()); + + Evidence evidence = timeStampedData.getTemporalEvidence(); + this.timeStamps = evidence.getTstEvidence().toTimeStampAndCRLArray(); + } + + TimeStampDataUtil(TimeStampedDataParser timeStampedData) + throws IOException + { + this.metaDataUtil = new MetaDataUtil(timeStampedData.getMetaData()); + + Evidence evidence = timeStampedData.getTemporalEvidence(); + this.timeStamps = evidence.getTstEvidence().toTimeStampAndCRLArray(); + } + + TimeStampToken getTimeStampToken(TimeStampAndCRL timeStampAndCRL) + throws CMSException + { + ContentInfo timeStampToken = timeStampAndCRL.getTimeStampToken(); + + try + { + TimeStampToken token = new TimeStampToken(timeStampToken); + return token; + } + catch (IOException e) + { + throw new CMSException("unable to parse token data: " + e.getMessage(), e); + } + catch (TSPException e) + { + if (e.getCause() instanceof CMSException) + { + throw (CMSException)e.getCause(); + } + + throw new CMSException("token data invalid: " + e.getMessage(), e); + } + catch (IllegalArgumentException e) + { + throw new CMSException("token data invalid: " + e.getMessage(), e); + } + } + + void initialiseMessageImprintDigestCalculator(DigestCalculator calculator) + throws CMSException + { + metaDataUtil.initialiseMessageImprintDigestCalculator(calculator); + } + + DigestCalculator getMessageImprintDigestCalculator(DigestCalculatorProvider calculatorProvider) + throws OperatorCreationException + { + TimeStampToken token; + + try + { + token = this.getTimeStampToken(timeStamps[0]); + + TimeStampTokenInfo info = token.getTimeStampInfo(); + ASN1ObjectIdentifier algOID = info.getMessageImprintAlgOID(); + + DigestCalculator calc = calculatorProvider.get(new AlgorithmIdentifier(algOID)); + + initialiseMessageImprintDigestCalculator(calc); + + return calc; + } + catch (CMSException e) + { + throw new OperatorCreationException("unable to extract algorithm ID: " + e.getMessage(), e); + } + } + + TimeStampToken[] getTimeStampTokens() + throws CMSException + { + TimeStampToken[] tokens = new TimeStampToken[timeStamps.length]; + for (int i = 0; i < timeStamps.length; i++) + { + tokens[i] = this.getTimeStampToken(timeStamps[i]); + } + + return tokens; + } + + TimeStampAndCRL[] getTimeStamps() + { + return timeStamps; + } + + byte[] calculateNextHash(DigestCalculator calculator) + throws CMSException + { + TimeStampAndCRL tspToken = timeStamps[timeStamps.length - 1]; + + OutputStream out = calculator.getOutputStream(); + + try + { + out.write(tspToken.getEncoded(ASN1Encoding.DER)); + + out.close(); + + return calculator.getDigest(); + } + catch (IOException e) + { + throw new CMSException("exception calculating hash: " + e.getMessage(), e); + } + } + + /** + * Validate the digests present in the TimeStampTokens contained in the CMSTimeStampedData. + */ + void validate(DigestCalculatorProvider calculatorProvider, byte[] dataDigest) + throws ImprintDigestInvalidException, CMSException + { + byte[] currentDigest = dataDigest; + + for (int i = 0; i < timeStamps.length; i++) + { + try + { + TimeStampToken token = this.getTimeStampToken(timeStamps[i]); + if (i > 0) + { + TimeStampTokenInfo info = token.getTimeStampInfo(); + DigestCalculator calculator = calculatorProvider.get(info.getHashAlgorithm()); + + calculator.getOutputStream().write(timeStamps[i - 1].getEncoded(ASN1Encoding.DER)); + + currentDigest = calculator.getDigest(); + } + + this.compareDigest(token, currentDigest); + } + catch (IOException e) + { + throw new CMSException("exception calculating hash: " + e.getMessage(), e); + } + catch (OperatorCreationException e) + { + throw new CMSException("cannot create digest: " + e.getMessage(), e); + } + } + } + + void validate(DigestCalculatorProvider calculatorProvider, byte[] dataDigest, TimeStampToken timeStampToken) + throws ImprintDigestInvalidException, CMSException + { + byte[] currentDigest = dataDigest; + byte[] encToken; + + try + { + encToken = timeStampToken.getEncoded(); + } + catch (IOException e) + { + throw new CMSException("exception encoding timeStampToken: " + e.getMessage(), e); + } + + for (int i = 0; i < timeStamps.length; i++) + { + try + { + TimeStampToken token = this.getTimeStampToken(timeStamps[i]); + if (i > 0) + { + TimeStampTokenInfo info = token.getTimeStampInfo(); + DigestCalculator calculator = calculatorProvider.get(info.getHashAlgorithm()); + + calculator.getOutputStream().write(timeStamps[i - 1].getEncoded(ASN1Encoding.DER)); + + currentDigest = calculator.getDigest(); + } + + this.compareDigest(token, currentDigest); + + if (Arrays.areEqual(token.getEncoded(), encToken)) + { + return; + } + } + catch (IOException e) + { + throw new CMSException("exception calculating hash: " + e.getMessage(), e); + } + catch (OperatorCreationException e) + { + throw new CMSException("cannot create digest: " + e.getMessage(), e); + } + } + + throw new ImprintDigestInvalidException("passed in token not associated with timestamps present", timeStampToken); + } + + private void compareDigest(TimeStampToken timeStampToken, byte[] digest) + throws ImprintDigestInvalidException + { + TimeStampTokenInfo info = timeStampToken.getTimeStampInfo(); + byte[] tsrMessageDigest = info.getMessageImprintDigest(); + + if (!Arrays.areEqual(digest, tsrMessageDigest)) + { + throw new ImprintDigestInvalidException("hash calculated is different from MessageImprintDigest found in TimeStampToken", timeStampToken); + } + } + + String getFileName() + { + return metaDataUtil.getFileName(); + } + + String getMediaType() + { + return metaDataUtil.getMediaType(); + } + + AttributeTable getOtherMetaData() + { + return new AttributeTable(metaDataUtil.getOtherMetaData()); + } +} |