/**
* DSS - Digital Signature Services
* Copyright (C) 2015 European Commission, provided under the CEF programme
*
* This file is part of the "DSS - Digital Signature Services" project.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package eu.europa.esig.dss.validation;
import java.io.IOException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import javax.security.auth.x500.X500Principal;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.cms.AttributeTable;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.Certificate;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cms.SignerInformationVerifier;
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.tsp.TSPException;
import org.bouncycastle.tsp.TSPValidationException;
import org.bouncycastle.tsp.TimeStampToken;
import org.bouncycastle.tsp.TimeStampTokenInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import eu.europa.esig.dss.DSSException;
import eu.europa.esig.dss.DSSUtils;
import eu.europa.esig.dss.DigestAlgorithm;
import eu.europa.esig.dss.EncryptionAlgorithm;
import eu.europa.esig.dss.SignatureAlgorithm;
import eu.europa.esig.dss.utils.Utils;
import eu.europa.esig.dss.x509.ArchiveTimestampType;
import eu.europa.esig.dss.x509.CertificatePool;
import eu.europa.esig.dss.x509.CertificateToken;
import eu.europa.esig.dss.x509.TimestampType;
import eu.europa.esig.dss.x509.TimestampValidation;
import eu.europa.esig.dss.x509.TimestampValidity;
import eu.europa.esig.dss.x509.Token;
import eu.europa.esig.dss.x509.TokenValidationExtraInfo;
/**
* SignedToken containing a TimeStamp.
*
*/
public class TimestampToken extends Token {
private static final Logger logger = LoggerFactory.getLogger(TimestampToken.class);
private final TimeStampToken timeStamp;
private TimestampType timeStampType;
private CAdESCertificateSource wrappedSource;
private X500Principal issuerX500Principal;
private boolean messageImprintData;
private Boolean messageImprintIntact = null;
private List<TimestampReference> timestampedReferences;
private List<TimestampInclude> timestampIncludes;
/**
* Defines for archive timestamp its type.
*/
private ArchiveTimestampType archiveTimestampType;
/**
* This attribute is used for XAdES timestamps. It indicates the canonicalization method used before creating the
* digest.
*/
private String canonicalizationMethod;
/**
* This attribute is used only with XAdES timestamps. It represents the hash code of the DOM element containing the
* timestamp. It's an internal attribute which allows to
* unambiguously identify a timestamp.
*/
private int hashCode;
/**
* Constructor with an indication of the timestamp type. The default constructor for {@code TimestampToken}.
*
* @param timeStamp
* {@code TimeStampToken}
* @param type
* {@code TimestampType}
* @param certPool
* {@code CertificatePool} which is used to identify the signing certificate of the timestamp
*/
public TimestampToken(final TimeStampToken timeStamp, final TimestampType type, final CertificatePool certPool) {
this.timeStamp = timeStamp;
this.timeStampType = type;
this.extraInfo = new TokenValidationExtraInfo();
wrappedSource = new CAdESCertificateSource(timeStamp, certPool);
final Collection<CertificateToken> certs = wrappedSource.getCertificates();
for (final CertificateToken certificateToken : certs) {
final byte[] encoded = certificateToken.getEncoded();
final Certificate certificate = Certificate.getInstance(encoded);
final X509CertificateHolder x509CertificateHolder = new X509CertificateHolder(certificate);
if (timeStamp.getSID().match(x509CertificateHolder)) {
boolean valid = isSignedBy(certificateToken);
if (valid) {
break;
}
}
}
}
@Override
public String getAbbreviation() {
return timeStampType.name() + ": " + getDSSIdAsString() + ": " + DSSUtils.formatInternal(timeStamp.getTimeStampInfo().getGenTime());
}
/**
* This method returns the issuing certificate's distinguished subject name.
*
* @return {@code X500Principal} representing the issuing certificate's distinguished subject name.
*/
@Override
public X500Principal getIssuerX500Principal() {
return issuerX500Principal;
}
@Override
public boolean isSignedBy(final CertificateToken issuerToken) {
if (this.issuerToken != null) {
return this.issuerToken.equals(issuerToken);
}
final TimestampValidation timestampValidation = validateTimestampToken(timeStamp, issuerToken);
final TimestampValidity timestampValidity = timestampValidation.getValidity();
signatureInvalidityReason = timestampValidity.name();
signatureValid = timestampValidation.isValid();
if (signatureValid) {
this.issuerToken = issuerToken;
issuerX500Principal = issuerToken.getSubjectX500Principal();
final String algorithm = issuerToken.getPublicKey().getAlgorithm();
final EncryptionAlgorithm encryptionAlgorithm = EncryptionAlgorithm.forName(algorithm);
final AlgorithmIdentifier hashAlgorithm = timeStamp.getTimeStampInfo().getHashAlgorithm();
final DigestAlgorithm digestAlgorithm = DigestAlgorithm.forOID(hashAlgorithm.getAlgorithm().getId());
signatureAlgorithm = SignatureAlgorithm.getAlgorithm(encryptionAlgorithm, digestAlgorithm);
}
return signatureValid;
}
private TimestampValidation validateTimestampToken(final TimeStampToken timeStampToken, final CertificateToken issuerToken) {
TimestampValidity timestampValidity;
try {
final JcaSimpleSignerInfoVerifierBuilder verifierBuilder = new JcaSimpleSignerInfoVerifierBuilder();
final X509Certificate x509Certificate = issuerToken.getCertificate();
final SignerInformationVerifier verifier = verifierBuilder.build(x509Certificate);
timeStampToken.validate(verifier);
timestampValidity = TimestampValidity.VALID;
} catch (IllegalArgumentException e) {
if (logger.isDebugEnabled()) {
logger.debug("No signing certificate for timestamp token: ", e);
} else {
logger.info("No signing certificate for timestamp token: {}", e.getMessage());
}
timestampValidity = TimestampValidity.NO_SIGNING_CERTIFICATE;
} catch (TSPValidationException e) {
if (logger.isDebugEnabled()) {
logger.debug("No valid signature for timestamp token: ", e);
} else {
logger.info("No valid signature for timestamp token: {}", e.getMessage());
}
timestampValidity = TimestampValidity.NOT_VALID_SIGNATURE;
} catch (TSPException e) {
if (logger.isDebugEnabled()) {
logger.debug("No valid structure for timestamp token: ", e);
} else {
logger.info("No valid structure for timestamp token: {}", e.getMessage());
}
timestampValidity = TimestampValidity.NOT_VALID_STRUCTURE;
} catch (OperatorCreationException e) {
if (logger.isDebugEnabled()) {
logger.debug("No valid structure for timestamp token: ", e);
} else {
logger.info("No valid structure for timestamp token: {}", e.getMessage());
}
timestampValidity = TimestampValidity.NOT_VALID_STRUCTURE;
}
final TimestampValidation timestampValidation = new TimestampValidation(timestampValidity);
return timestampValidation;
}
/**
* Checks if the {@code TimeStampToken} matches the signed data.
*
* @param data
* the array of {@code byte} representing the timestamped data
* @return true if the data is verified by the TimeStampToken
*/
public boolean matchData(final byte[] data) {
try {
messageImprintData = data != null;
final TimeStampTokenInfo timeStampInfo = timeStamp.getTimeStampInfo();
final ASN1ObjectIdentifier hashAlgorithm = timeStampInfo.getHashAlgorithm().getAlgorithm();
final DigestAlgorithm digestAlgorithm = DigestAlgorithm.forOID(hashAlgorithm.getId());
final byte[] computedDigest = DSSUtils.digest(digestAlgorithm, data);
final byte[] timestampDigest = timeStampInfo.getMessageImprintDigest();
messageImprintIntact = Arrays.equals(computedDigest, timestampDigest);
if (!messageImprintIntact) {
logger.error("Computed digest ({}) on the extracted data from the document : {}", digestAlgorithm, Utils.toHex(computedDigest));
logger.error("Digest present in TimestampToken: {}", Utils.toHex(timestampDigest));
logger.error("Digest in TimestampToken matches digest of extracted data from document: {}", messageImprintIntact);
}
} catch (DSSException e) {
messageImprintIntact = false;
}
return messageImprintIntact;
}
/**
* Retrieves the type of the timestamp token.
*
* @return {@code TimestampType}
*/
public TimestampType getTimeStampType() {
return timeStampType;
}
/**
* Retrieves the timestamp generation time.
*
* @return {@code Date}
*/
public Date getGenerationTime() {
return timeStamp.getTimeStampInfo().getGenTime();
}
/**
* Retrieves the {@code DigestAlgorithm} used to generate the digest value to timestamp.
*
* @return {@code DigestAlgorithm}
*/
public DigestAlgorithm getSignedDataDigestAlgo() {
final ASN1ObjectIdentifier oid = timeStamp.getTimeStampInfo().getHashAlgorithm().getAlgorithm();
return DigestAlgorithm.forOID(oid.getId());
}
/**
* Retrieves the encoded signed data digest value.
*
* @return base 64 encoded {@code String}
*/
public String getEncodedSignedDataDigestValue() {
final byte[] messageImprintDigest = timeStamp.getTimeStampInfo().getMessageImprintDigest();
return Utils.toBase64(messageImprintDigest);
}
/**
* @return true if the message imprint data was found, false otherwise
*/
public Boolean isMessageImprintDataFound() {
return messageImprintData;
}
/**
* The method {@code matchData} must be invoked previously.
*
* @return true if the message imprint data is intact, false otherwise
*/
public Boolean isMessageImprintDataIntact() {
if (messageImprintIntact == null) {
throw new DSSException("Invoke matchData(byte[] data) method before!");
}
return messageImprintIntact;
}
/**
* @return {@code List} of {@code TimestampReference}s
*/
public List<TimestampReference> getTimestampedReferences() {
return timestampedReferences;
}
/**
* This method is used to set the timestamped references. The reference can be the digest value of the certificate
* or of the revocation data. The same references can be
* timestamped by different timestamps.
*
* @param timestampedReferences
* {@code List} of {@code TimestampReference}
*/
public void setTimestampedReferences(final List<TimestampReference> timestampedReferences) {
this.timestampedReferences = timestampedReferences;
}
/**
* @return {@code ArchiveTimestampType} in the case of an archive timestamp, {@code null} otherwise
*/
public ArchiveTimestampType getArchiveTimestampType() {
return archiveTimestampType;
}
/**
* Archive timestamps can be of different sub type.
*
* @param archiveTimestampType
* {@code ArchiveTimestampType}
*/
public void setArchiveTimestampType(final ArchiveTimestampType archiveTimestampType) {
this.archiveTimestampType = archiveTimestampType;
}
/**
* Applies only fro XAdES timestamps
*
* @return {@code String} representing the canonicalization method used by the timestamp
*/
public String getCanonicalizationMethod() {
return canonicalizationMethod;
}
/**
* Allows to set the canonicalization method used by the timestamp. Applies only with XAdES timestamps.
*
* @param canonicalizationMethod
* {@code String} representing the canonicalization method
*/
public void setCanonicalizationMethod(final String canonicalizationMethod) {
this.canonicalizationMethod = canonicalizationMethod;
}
@Override
public byte[] getEncoded() {
try {
return timeStamp.getEncoded();
} catch (IOException e) {
throw new DSSException(e);
}
}
// TODO-Vin (12/09/2014): Comment!
public List<TimestampInclude> getTimestampIncludes() {
return timestampIncludes;
}
// TODO-Vin (12/09/2014): Comment!
public void setTimestampIncludes(List<TimestampInclude> timestampIncludes) {
this.timestampIncludes = timestampIncludes;
}
/**
* Returns the list of wrapped certificates.
*
* @return {@code List} of {@code CertificateToken}
*/
public List<CertificateToken> getCertificates() {
return wrappedSource.getCertificates();
}
public AttributeTable getUnsignedAttributes() {
return timeStamp.getUnsignedAttributes();
}
/**
* Used only with XAdES timestamps.
*
* @param hashCode
* the hash code of the DOM element containing the timestamp
*/
public void setHashCode(final int hashCode) {
this.hashCode = hashCode;
}
/**
* Used only with XAdES timestamps.
*
* @return the hash code of the DOM element containing the timestamp
*/
public int getHashCode() {
return hashCode;
}
@Override
public String toString(String indentStr) {
try {
final StringBuffer out = new StringBuffer();
out.append(indentStr).append("TimestampToken[signedBy=").append(issuerToken == null ? "?" : issuerToken.getDSSIdAsString());
out.append(", generated: ").append(DSSUtils.formatInternal(timeStamp.getTimeStampInfo().getGenTime()));
out.append(" / ").append(timeStampType).append('\n');
if (signatureValid) {
indentStr += "\t";
out.append(indentStr).append("Timestamp's signature validity: VALID").append('\n');
indentStr = indentStr.substring(1);
} else {
if (!signatureInvalidityReason.isEmpty()) {
indentStr += "\t";
out.append(indentStr).append("Timestamp's signature validity: INVALID").append(" - ").append(signatureInvalidityReason).append('\n');
indentStr = indentStr.substring(1);
}
}
indentStr += "\t";
if (messageImprintIntact != null) {
if (messageImprintIntact) {
out.append(indentStr).append("Timestamp MATCHES the signed data.").append('\n');
} else {
out.append(indentStr).append("Timestamp DOES NOT MATCH the signed data.").append('\n');
}
}
indentStr = indentStr.substring(1);
if (issuerToken != null) {
indentStr += "\t";
out.append(issuerToken.toString(indentStr)).append('\n');
indentStr = indentStr.substring(1);
out.append(indentStr);
}
out.append("]");
return out.toString();
} catch (Exception e) {
return getClass().getName();
}
}
}