/* * Copyright (c) 2007, 2011, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code 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 General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */package sun.security.tools;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.math.BigInteger;import java.net.URI;import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;import java.security.Principal;import java.security.SecureRandom;import java.security.cert.CertificateException;import java.security.cert.X509Certificate;import java.util.List;import com.sun.jarsigner.*;import java.util.Arrays;import sun.security.pkcs.*;import sun.security.timestamp.*;import sun.security.util.*;import sun.security.x509.*;/** * This class implements a content signing service. * It generates a timestamped signature for a given content according to * <a href="http://www.ietf.org/rfc/rfc3161.txt">RFC 3161</a>. * The signature along with a trusted timestamp and the signer's certificate * are all packaged into a standard PKCS #7 Signed Data message. * * @author Vincent Ryan */public final class TimestampedSigner extends ContentSigner { /* * Random number generator for creating nonce values */ private static final SecureRandom RANDOM; static { SecureRandom tmp = null; try { tmp = SecureRandom.getInstance("SHA1PRNG"); } catch (NoSuchAlgorithmException e) { // should not happen } RANDOM = tmp; } /* * Object identifier for the subject information access X.509 certificate * extension. */ private static final String SUBJECT_INFO_ACCESS_OID = "1.3.6.1.5.5.7.1.11"; /* * Object identifier for the timestamping key purpose. */ private static final String KP_TIMESTAMPING_OID = "1.3.6.1.5.5.7.3.8"; /* * Object identifier for extendedKeyUsage extension */ private static final String EXTENDED_KEY_USAGE_OID = "2.5.29.37"; /* * Object identifier for the timestamping access descriptors. */ private static final ObjectIdentifier AD_TIMESTAMPING_Id; static { ObjectIdentifier tmp = null; try { tmp = new ObjectIdentifier("1.3.6.1.5.5.7.48.3"); } catch (IOException e) { // ignore } AD_TIMESTAMPING_Id = tmp; } /* * Location of the TSA. */ private String tsaUrl = null; /* * TSA's X.509 certificate. */ private X509Certificate tsaCertificate = null; /* * Generates an SHA-1 hash value for the data to be timestamped. */ private MessageDigest messageDigest = null; /* * Parameters for the timestamping protocol. */ private boolean tsRequestCertificate = true; /** * Instantiates a content signer that supports timestamped signatures. */ public TimestampedSigner() { } /** * Generates a PKCS #7 signed data message that includes a signature * timestamp. * This method is used when a signature has already been generated. * The signature, a signature timestamp, the signer's certificate chain, * and optionally the content that was signed, are packaged into a PKCS #7 * signed data message. * * @param parameters The non-null input parameters. * @param omitContent true if the content should be omitted from the * signed data message. Otherwise the content is included. * @param applyTimestamp true if the signature should be timestamped. * Otherwise timestamping is not performed. * @return A PKCS #7 signed data message including a signature timestamp. * @throws NoSuchAlgorithmException The exception is thrown if the signature * algorithm is unrecognised. * @throws CertificateException The exception is thrown if an error occurs * while processing the signer's certificate or the TSA's * certificate. * @throws IOException The exception is thrown if an error occurs while * generating the signature timestamp or while generating the signed * data message. * @throws NullPointerException The exception is thrown if parameters is * null. */ public byte[] generateSignedData(ContentSignerParameters parameters, boolean omitContent, boolean applyTimestamp) throws NoSuchAlgorithmException, CertificateException, IOException { if (parameters == null) { throw new NullPointerException(); } // Parse the signature algorithm to extract the digest and key // algorithms. The expected format is: // "<digest>with<encryption>" // or "<digest>with<encryption>and<mgf>" String signatureAlgorithm = parameters.getSignatureAlgorithm(); String keyAlgorithm = AlgorithmId.getEncAlgFromSigAlg(signatureAlgorithm); String digestAlgorithm = AlgorithmId.getDigAlgFromSigAlg(signatureAlgorithm); AlgorithmId digestAlgorithmId = AlgorithmId.get(digestAlgorithm); // Examine signer's certificate X509Certificate[] signerCertificateChain = parameters.getSignerCertificateChain(); Principal issuerName = signerCertificateChain[0].getIssuerDN(); if (!(issuerName instanceof X500Name)) { // must extract the original encoded form of DN for subsequent // name comparison checks (converting to a String and back to // an encoded DN could cause the types of String attribute // values to be changed) X509CertInfo tbsCert = new X509CertInfo(signerCertificateChain[0].getTBSCertificate()); issuerName = (Principal) tbsCert.get(CertificateIssuerName.NAME + "." + CertificateIssuerName.DN_NAME); } BigInteger serialNumber = signerCertificateChain[0].getSerialNumber(); // Include or exclude content byte[] content = parameters.getContent(); ContentInfo contentInfo; if (omitContent) { contentInfo = new ContentInfo(ContentInfo.DATA_OID, null); } else { contentInfo = new ContentInfo(content); } // Generate the timestamp token byte[] signature = parameters.getSignature(); SignerInfo signerInfo = null; if (applyTimestamp) { tsaCertificate = parameters.getTimestampingAuthorityCertificate(); URI tsaUri = parameters.getTimestampingAuthority(); if (tsaUri != null) { tsaUrl = tsaUri.toString(); } else { // Examine TSA cert String certUrl = getTimestampingUrl(tsaCertificate); if (certUrl == null) { throw new CertificateException( "Subject Information Access extension not found"); } tsaUrl = certUrl; } // Timestamp the signature byte[] tsToken = generateTimestampToken(signature); // Insert the timestamp token into the PKCS #7 signer info element // (as an unsigned attribute) PKCS9Attributes unsignedAttrs = new PKCS9Attributes(new PKCS9Attribute[]{ new PKCS9Attribute( PKCS9Attribute.SIGNATURE_TIMESTAMP_TOKEN_STR, tsToken)}); signerInfo = new SignerInfo((X500Name)issuerName, serialNumber, digestAlgorithmId, null, AlgorithmId.get(keyAlgorithm), signature, unsignedAttrs); } else { signerInfo = new SignerInfo((X500Name)issuerName, serialNumber, digestAlgorithmId, AlgorithmId.get(keyAlgorithm), signature); } SignerInfo[] signerInfos = {signerInfo}; AlgorithmId[] algorithms = {digestAlgorithmId}; // Create the PKCS #7 signed data message PKCS7 p7 = new PKCS7(algorithms, contentInfo, signerCertificateChain, null, signerInfos); ByteArrayOutputStream p7out = new ByteArrayOutputStream(); p7.encodeSignedData(p7out); return p7out.toByteArray(); } /** * Examine the certificate for a Subject Information Access extension * (<a href="http://www.ietf.org/rfc/rfc3280.txt">RFC 3280</a>). * The extension's <tt>accessMethod</tt> field should contain the object * identifier defined for timestamping: 1.3.6.1.5.5.7.48.3 and its * <tt>accessLocation</tt> field should contain an HTTP or HTTPS URL. * * @param tsaCertificate An X.509 certificate for the TSA. * @return An HTTP or HTTPS URL or null if none was found. */ public static String getTimestampingUrl(X509Certificate tsaCertificate) { if (tsaCertificate == null) { return null; } // Parse the extensions try { byte[] extensionValue = tsaCertificate.getExtensionValue(SUBJECT_INFO_ACCESS_OID); if (extensionValue == null) { return null; } DerInputStream der = new DerInputStream(extensionValue); der = new DerInputStream(der.getOctetString()); DerValue[] derValue = der.getSequence(5); AccessDescription description; GeneralName location; URIName uri; for (int i = 0; i < derValue.length; i++) { description = new AccessDescription(derValue[i]); if (description.getAccessMethod().equals(AD_TIMESTAMPING_Id)) { location = description.getAccessLocation(); if (location.getType() == GeneralNameInterface.NAME_URI) { uri = (URIName) location.getName(); if (uri.getScheme().equalsIgnoreCase("http") || uri.getScheme().equalsIgnoreCase("https")) { return uri.getName(); } } } } } catch (IOException ioe) { // ignore } return null; } /* * Returns a timestamp token from a TSA for the given content. * Performs a basic check on the token to confirm that it has been signed * by a certificate that is permitted to sign timestamps. * * @param toBeTimestamped The data to be timestamped. * @throws IOException The exception is throw if an error occurs while * communicating with the TSA. * @throws CertificateException The exception is throw if the TSA's * certificate is not permitted for timestamping. */ private byte[] generateTimestampToken(byte[] toBeTimestamped) throws CertificateException, IOException { // Generate hash value for the data to be timestamped // SHA-1 is always used. if (messageDigest == null) { try { messageDigest = MessageDigest.getInstance("SHA-1"); } catch (NoSuchAlgorithmException e) { // ignore } } byte[] digest = messageDigest.digest(toBeTimestamped); // Generate a timestamp TSRequest tsQuery = new TSRequest(digest, "SHA-1"); // Generate a nonce BigInteger nonce = null; if (RANDOM != null) { nonce = new BigInteger(64, RANDOM); tsQuery.setNonce(nonce); } tsQuery.requestCertificate(tsRequestCertificate); Timestamper tsa = new HttpTimestamper(tsaUrl); // use supplied TSA TSResponse tsReply = tsa.generateTimestamp(tsQuery); int status = tsReply.getStatusCode(); // Handle TSP error if (status != 0 && status != 1) { int failureCode = tsReply.getFailureCode(); if (failureCode == -1) { throw new IOException("Error generating timestamp: " + tsReply.getStatusCodeAsText()); } else { throw new IOException("Error generating timestamp: " + tsReply.getStatusCodeAsText() + " " + tsReply.getFailureCodeAsText()); } } PKCS7 tsToken = tsReply.getToken(); TimestampToken tst = new TimestampToken(tsToken.getContentInfo().getData()); if (!tst.getHashAlgorithm().equals( new AlgorithmId(new ObjectIdentifier("1.3.14.3.2.26")))) { throw new IOException("Digest algorithm not SHA-1 in timestamp token"); } if (!Arrays.equals(tst.getHashedMessage(), digest)) { throw new IOException("Digest octets changed in timestamp token"); }; BigInteger replyNonce = tst.getNonce(); if (replyNonce == null && nonce != null) { throw new IOException("Nonce missing in timestamp token"); } if (replyNonce != null && !replyNonce.equals(nonce)) { throw new IOException("Nonce changed in timestamp token"); } // Examine the TSA's certificate (if present) for (SignerInfo si: tsToken.getSignerInfos()) { X509Certificate cert = si.getCertificate(tsToken); if (cert == null) { // Error, we've already set tsRequestCertificate = true throw new CertificateException( "Certificate not included in timestamp token"); } else { if (!cert.getCriticalExtensionOIDs().contains( EXTENDED_KEY_USAGE_OID)) { throw new CertificateException( "Certificate is not valid for timestamping"); } List keyPurposes = cert.getExtendedKeyUsage(); if (keyPurposes == null || ! keyPurposes.contains(KP_TIMESTAMPING_OID)) { throw new CertificateException( "Certificate is not valid for timestamping"); } } } return tsReply.getEncodedToken(); }}
/*
* Copyright (c) 2007, 2011, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code 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 General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package sun.security.tools;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.net.URI;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.List;
import com.sun.jarsigner.*;
import java.util.Arrays;
import sun.security.pkcs.*;
import sun.security.timestamp.*;
import sun.security.util.*;
import sun.security.x509.*;
/**
* This class implements a content signing service.
* It generates a timestamped signature for a given content according to
* <a href="http://www.ietf.org/rfc/rfc3161.txt">RFC 3161</a>.
* The signature along with a trusted timestamp and the signer's certificate
* are all packaged into a standard PKCS #7 Signed Data message.
*
* @author Vincent Ryan
*/
public final class TimestampedSigner extends ContentSigner {
/*
* Random number generator for creating nonce values
*/
private static final SecureRandom RANDOM;
static {
SecureRandom tmp = null;
try {
tmp = SecureRandom.getInstance("SHA1PRNG");
} catch (NoSuchAlgorithmException e) {
// should not happen
}
RANDOM = tmp;
}
/*
* Object identifier for the subject information access X.509 certificate
* extension.
*/
private static final String SUBJECT_INFO_ACCESS_OID = "1.3.6.1.5.5.7.1.11";
/*
* Object identifier for the timestamping key purpose.
*/
private static final String KP_TIMESTAMPING_OID = "1.3.6.1.5.5.7.3.8";
/*
* Object identifier for extendedKeyUsage extension
*/
private static final String EXTENDED_KEY_USAGE_OID = "2.5.29.37";
/*
* Object identifier for the timestamping access descriptors.
*/
private static final ObjectIdentifier AD_TIMESTAMPING_Id;
static {
ObjectIdentifier tmp = null;
try {
tmp = new ObjectIdentifier("1.3.6.1.5.5.7.48.3");
} catch (IOException e) {
// ignore
}
AD_TIMESTAMPING_Id = tmp;
}
/*
* Location of the TSA.
*/
private String tsaUrl = null;
/*
* TSA's X.509 certificate.
*/
private X509Certificate tsaCertificate = null;
/*
* Generates an SHA-1 hash value for the data to be timestamped.
*/
private MessageDigest messageDigest = null;
/*
* Parameters for the timestamping protocol.
*/
private boolean tsRequestCertificate = true;
/**
* Instantiates a content signer that supports timestamped signatures.
*/
public TimestampedSigner() {
}
/**
* Generates a PKCS #7 signed data message that includes a signature
* timestamp.
* This method is used when a signature has already been generated.
* The signature, a signature timestamp, the signer's certificate chain,
* and optionally the content that was signed, are packaged into a PKCS #7
* signed data message.
*
* @param parameters The non-null input parameters.
* @param omitContent true if the content should be omitted from the
* signed data message. Otherwise the content is included.
* @param applyTimestamp true if the signature should be timestamped.
* Otherwise timestamping is not performed.
* @return A PKCS #7 signed data message including a signature timestamp.
* @throws NoSuchAlgorithmException The exception is thrown if the signature
* algorithm is unrecognised.
* @throws CertificateException The exception is thrown if an error occurs
* while processing the signer's certificate or the TSA's
* certificate.
* @throws IOException The exception is thrown if an error occurs while
* generating the signature timestamp or while generating the signed
* data message.
* @throws NullPointerException The exception is thrown if parameters is
* null.
*/
public byte[] generateSignedData(ContentSignerParameters parameters,
boolean omitContent, boolean applyTimestamp)
throws NoSuchAlgorithmException, CertificateException, IOException {
if (parameters == null) {
throw new NullPointerException();
}
// Parse the signature algorithm to extract the digest and key
// algorithms. The expected format is:
// "<digest>with<encryption>"
// or "<digest>with<encryption>and<mgf>"
String signatureAlgorithm = parameters.getSignatureAlgorithm();
String keyAlgorithm =
AlgorithmId.getEncAlgFromSigAlg(signatureAlgorithm);
String digestAlgorithm =
AlgorithmId.getDigAlgFromSigAlg(signatureAlgorithm);
AlgorithmId digestAlgorithmId = AlgorithmId.get(digestAlgorithm);
// Examine signer's certificate
X509Certificate[] signerCertificateChain =
parameters.getSignerCertificateChain();
Principal issuerName = signerCertificateChain[0].getIssuerDN();
if (!(issuerName instanceof X500Name)) {
// must extract the original encoded form of DN for subsequent
// name comparison checks (converting to a String and back to
// an encoded DN could cause the types of String attribute
// values to be changed)
X509CertInfo tbsCert = new
X509CertInfo(signerCertificateChain[0].getTBSCertificate());
issuerName = (Principal)
tbsCert.get(CertificateIssuerName.NAME + "." +
CertificateIssuerName.DN_NAME);
}
BigInteger serialNumber = signerCertificateChain[0].getSerialNumber();
// Include or exclude content
byte[] content = parameters.getContent();
ContentInfo contentInfo;
if (omitContent) {
contentInfo = new ContentInfo(ContentInfo.DATA_OID, null);
} else {
contentInfo = new ContentInfo(content);
}
// Generate the timestamp token
byte[] signature = parameters.getSignature();
SignerInfo signerInfo = null;
if (applyTimestamp) {
tsaCertificate = parameters.getTimestampingAuthorityCertificate();
URI tsaUri = parameters.getTimestampingAuthority();
if (tsaUri != null) {
tsaUrl = tsaUri.toString();
} else {
// Examine TSA cert
String certUrl = getTimestampingUrl(tsaCertificate);
if (certUrl == null) {
throw new CertificateException(
"Subject Information Access extension not found");
}
tsaUrl = certUrl;
}
// Timestamp the signature
byte[] tsToken = generateTimestampToken(signature);
// Insert the timestamp token into the PKCS #7 signer info element
// (as an unsigned attribute)
PKCS9Attributes unsignedAttrs =
new PKCS9Attributes(new PKCS9Attribute[]{
new PKCS9Attribute(
PKCS9Attribute.SIGNATURE_TIMESTAMP_TOKEN_STR,
tsToken)});
signerInfo = new SignerInfo((X500Name)issuerName, serialNumber,
digestAlgorithmId, null, AlgorithmId.get(keyAlgorithm),
signature, unsignedAttrs);
} else {
signerInfo = new SignerInfo((X500Name)issuerName, serialNumber,
digestAlgorithmId, AlgorithmId.get(keyAlgorithm), signature);
}
SignerInfo[] signerInfos = {signerInfo};
AlgorithmId[] algorithms = {digestAlgorithmId};
// Create the PKCS #7 signed data message
PKCS7 p7 = new PKCS7(algorithms, contentInfo, signerCertificateChain,
null, signerInfos);
ByteArrayOutputStream p7out = new ByteArrayOutputStream();
p7.encodeSignedData(p7out);
return p7out.toByteArray();
}
/**
* Examine the certificate for a Subject Information Access extension
* (<a href="http://www.ietf.org/rfc/rfc3280.txt">RFC 3280</a>).
* The extension's <tt>accessMethod</tt> field should contain the object
* identifier defined for timestamping: 1.3.6.1.5.5.7.48.3 and its
* <tt>accessLocation</tt> field should contain an HTTP or HTTPS URL.
*
* @param tsaCertificate An X.509 certificate for the TSA.
* @return An HTTP or HTTPS URL or null if none was found.
*/
public static String getTimestampingUrl(X509Certificate tsaCertificate) {
if (tsaCertificate == null) {
return null;
}
// Parse the extensions
try {
byte[] extensionValue =
tsaCertificate.getExtensionValue(SUBJECT_INFO_ACCESS_OID);
if (extensionValue == null) {
return null;
}
DerInputStream der = new DerInputStream(extensionValue);
der = new DerInputStream(der.getOctetString());
DerValue[] derValue = der.getSequence(5);
AccessDescription description;
GeneralName location;
URIName uri;
for (int i = 0; i < derValue.length; i++) {
description = new AccessDescription(derValue[i]);
if (description.getAccessMethod().equals(AD_TIMESTAMPING_Id)) {
location = description.getAccessLocation();
if (location.getType() == GeneralNameInterface.NAME_URI) {
uri = (URIName) location.getName();
if (uri.getScheme().equalsIgnoreCase("http") ||
uri.getScheme().equalsIgnoreCase("https")) {
return uri.getName();
}
}
}
}
} catch (IOException ioe) {
// ignore
}
return null;
}
/*
* Returns a timestamp token from a TSA for the given content.
* Performs a basic check on the token to confirm that it has been signed
* by a certificate that is permitted to sign timestamps.
*
* @param toBeTimestamped The data to be timestamped.
* @throws IOException The exception is throw if an error occurs while
* communicating with the TSA.
* @throws CertificateException The exception is throw if the TSA's
* certificate is not permitted for timestamping.
*/
private byte[] generateTimestampToken(byte[] toBeTimestamped)
throws CertificateException, IOException {
// Generate hash value for the data to be timestamped
// SHA-1 is always used.
if (messageDigest == null) {
try {
messageDigest = MessageDigest.getInstance("SHA-1");
} catch (NoSuchAlgorithmException e) {
// ignore
}
}
byte[] digest = messageDigest.digest(toBeTimestamped);
// Generate a timestamp
TSRequest tsQuery = new TSRequest(digest, "SHA-1");
// Generate a nonce
BigInteger nonce = null;
if (RANDOM != null) {
nonce = new BigInteger(64, RANDOM);
tsQuery.setNonce(nonce);
}
tsQuery.requestCertificate(tsRequestCertificate);
Timestamper tsa = new HttpTimestamper(tsaUrl); // use supplied TSA
TSResponse tsReply = tsa.generateTimestamp(tsQuery);
int status = tsReply.getStatusCode();
// Handle TSP error
if (status != 0 && status != 1) {
int failureCode = tsReply.getFailureCode();
if (failureCode == -1) {
throw new IOException("Error generating timestamp: " +
tsReply.getStatusCodeAsText());
} else {
throw new IOException("Error generating timestamp: " +
tsReply.getStatusCodeAsText() + " " +
tsReply.getFailureCodeAsText());
}
}
PKCS7 tsToken = tsReply.getToken();
TimestampToken tst = new TimestampToken(tsToken.getContentInfo().getData());
if (!tst.getHashAlgorithm().equals(
new AlgorithmId(new ObjectIdentifier("1.3.14.3.2.26")))) {
throw new IOException("Digest algorithm not SHA-1 in timestamp token");
}
if (!Arrays.equals(tst.getHashedMessage(), digest)) {
throw new IOException("Digest octets changed in timestamp token");
};
BigInteger replyNonce = tst.getNonce();
if (replyNonce == null && nonce != null) {
throw new IOException("Nonce missing in timestamp token");
}
if (replyNonce != null && !replyNonce.equals(nonce)) {
throw new IOException("Nonce changed in timestamp token");
}
// Examine the TSA's certificate (if present)
for (SignerInfo si: tsToken.getSignerInfos()) {
X509Certificate cert = si.getCertificate(tsToken);
if (cert == null) {
// Error, we've already set tsRequestCertificate = true
throw new CertificateException(
"Certificate not included in timestamp token");
} else {
if (!cert.getCriticalExtensionOIDs().contains(
EXTENDED_KEY_USAGE_OID)) {
throw new CertificateException(
"Certificate is not valid for timestamping");
}
List keyPurposes = cert.getExtendedKeyUsage();
if (keyPurposes == null ||
! keyPurposes.contains(KP_TIMESTAMPING_OID)) {
throw new CertificateException(
"Certificate is not valid for timestamping");
}
}
}
return tsReply.getEncodedToken();
}
}