我正在尝试使用 PDFBox 1.8.9 在 PDF 文档上的签名中添加时间戳。我收到错误消息说无法验证时间戳,当我尝试检查时间戳颁发机构的证书时,它显示为“不可用”。
这是我在实现 SignatureInterface 的类中的方法签名,它调用方法 SignTimeStamps
public byte[] sign(java.io.InputStream content)
{
CMSProcessableInputStream input = new CMSProcessableInputStream(content);
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
// Certificate Chain
java.util.List certList = java.util.Arrays.asList(certificates);
java.security.cert.CertStore certStore = null;
try
{
certStore = java.security.cert.CertStore.getInstance("Collection", new java.security.cert.CollectionCertStoreParameters(certList), provider);
gen.addSigner(privateKey, (java.security.cert.X509Certificate)certList.get(0), CMSSignedGenerator.DIGEST_SHA256);
gen.addCertificatesAndCRLs(certStore);
CMSSignedData signedData = gen.generate(input, false, provider);
if (signatureResources.TsaUrl != null)
{
// signedData = AddTimeStamp(signedData);
signedData = SignTimeStamps(signedData);
}
return signedData.getEncoded();
}
catch (java.lang.Exception e)
{
// should be handled
System.Console.WriteLine("error while creating pkcs7 signature");
e.printStackTrace();
}
throw new java.lang.RuntimeException("problem while preparing signature");
}
SignTimeStamps 方法为每个 SignerInfo 调用方法 SignTimeStamp
private CMSSignedData SignTimeStamps(CMSSignedData signedData)
{
SignerInformationStore signerStore = signedData.getSignerInfos();
java.util.Iterator iterator = ((java.util.Collection)signerStore.getSigners()).iterator();
java.util.List newSigners = new java.util.ArrayList();
while (iterator.hasNext())
{
newSigners.add(SignTimeStamp((SignerInformation)iterator.next()));
}
return CMSSignedData.replaceSigners(signedData, new SignerInformationStore(newSigners));
}
然后我们从 SignerInfo 中获取签名,这是我们的 TimeStampToken 的消息印记,如 RFC 3161 的附录 A 中所述
private SignerInformation SignTimeStamp(SignerInformation signer)
{
AttributeTable unsignedAttributes = signer.getUnsignedAttributes();
ASN1EncodableVector vector = new ASN1EncodableVector();
if (unsignedAttributes != null)
{
vector = unsignedAttributes.toASN1EncodableVector();
}
TSAClientBouncyCastle tsaClient = new TSAClientBouncyCastle(signatureResources.TsaUrl);
byte[] token = tsaClient.GetTimeStampToken(signer.getSignature());
DERObjectIdentifier oid = PKCSObjectIdentifiers.id_aa_signatureTimeStampToken;
ASN1Encodable signatureTimeStamp = new org.bouncycastle.asn1.cms.Attribute(oid, new DERSet(ASN1Object.fromByteArray(token)));
vector.add(signatureTimeStamp);
SignerInformation newSigner = SignerInformation.replaceUnsignedAttributes(signer, new AttributeTable(vector));
if (newSigner == null) return signer;
return newSigner;
}
所以我不确定我做错了什么,我该怎么做才能验证时间戳并且时间戳权限显示为可用?
编辑 这是我签署的 PDF 文件的链接https://www.dropbox.com/s/al3h8lorqisi34q/dummy_signed.pdf?dl=1。TSA 是免费的 TSA ( http://time.certum.pl/ )
编辑 2
这是我的 TSAClientBouncyCastle.cs 文件
using System;
using System.IO;
using System.Collections;
using System.Net;
using System.Text;
using System.util;
using Org.BouncyCastle.X509;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Tsp;
using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Asn1.Cmp;
using Org.BouncyCastle.Asn1.Tsp;
using Org.BouncyCastle.Crypto;
namespace ProofOfConcept {
public class TSAClientBouncyCastle : ITSAClient {
/** The Logger instance. */
private static readonly ILogger LOGGER = LoggerFactory.GetLogger(typeof(TSAClientBouncyCastle));
/** URL of the Time Stamp Authority */
protected internal String tsaURL;
/** TSA Username */
protected internal String tsaUsername;
/** TSA password */
protected internal String tsaPassword;
/** An interface that allows you to inspect the timestamp info. */
protected ITSAInfoBouncyCastle tsaInfo;
/** The default value for the hash algorithm */
public const int DEFAULTTOKENSIZE = 4096;
/** Estimate of the received time stamp token */
protected internal int tokenSizeEstimate;
/** The default value for the hash algorithm */
public const String DEFAULTHASHALGORITHM = "SHA-256";
/** Hash algorithm */
protected internal String digestAlgorithm;
/**
* Creates an instance of a TSAClient that will use BouncyCastle.
* @param url String - Time Stamp Authority URL (i.e. "http://tsatest1.digistamp.com/TSA")
*/
public TSAClientBouncyCastle(String url)
: this(url, null, null, DEFAULTTOKENSIZE, DEFAULTHASHALGORITHM) {
}
/**
* Creates an instance of a TSAClient that will use BouncyCastle.
* @param url String - Time Stamp Authority URL (i.e. "http://tsatest1.digistamp.com/TSA")
* @param username String - user(account) name
* @param password String - password
*/
public TSAClientBouncyCastle(String url, String username, String password)
: this(url, username, password, DEFAULTTOKENSIZE, DEFAULTHASHALGORITHM) {
}
/**
* Constructor.
* Note the token size estimate is updated by each call, as the token
* size is not likely to change (as long as we call the same TSA using
* the same imprint length).
* @param url String - Time Stamp Authority URL (i.e. "http://tsatest1.digistamp.com/TSA")
* @param username String - user(account) name
* @param password String - password
* @param tokSzEstimate int - estimated size of received time stamp token (DER encoded)
*/
public TSAClientBouncyCastle(String url, String username, String password, int tokSzEstimate, String digestAlgorithm) {
this.tsaURL = url;
this.tsaUsername = username;
this.tsaPassword = password;
this.tokenSizeEstimate = tokSzEstimate;
this.digestAlgorithm = digestAlgorithm;
}
/**
* @param tsaInfo the tsaInfo to set
*/
public void SetTSAInfo(ITSAInfoBouncyCastle tsaInfo) {
this.tsaInfo = tsaInfo;
}
/**
* Get the token size estimate.
* Returned value reflects the result of the last succesfull call, padded
* @return an estimate of the token size
*/
public virtual int GetTokenSizeEstimate() {
return tokenSizeEstimate;
}
/**
* Gets the MessageDigest to digest the data imprint
* @return the digest algorithm name
*/
public IDigest GetMessageDigest() {
return DigestAlgorithms.GetMessageDigest(digestAlgorithm);
}
/**
* Get RFC 3161 timeStampToken.
* Method may return null indicating that timestamp should be skipped.
* @param imprint data imprint to be time-stamped
* @return encoded, TSA signed data of the timeStampToken
*/
public virtual byte[] GetTimeStampToken(byte[] imprint) {
byte[] respBytes = null;
// Setup the time stamp request
TimeStampRequestGenerator tsqGenerator = new TimeStampRequestGenerator();
tsqGenerator.SetCertReq(true);
// tsqGenerator.setReqPolicy("1.3.6.1.4.1.601.10.3.1");
BigInteger nonce = BigInteger.ValueOf(DateTime.Now.Ticks + Environment.TickCount);
TimeStampRequest request = tsqGenerator.Generate(DigestAlgorithms.GetAllowedDigests(digestAlgorithm), imprint, nonce);
byte[] requestBytes = request.GetEncoded();
// Call the communications layer
respBytes = GetTSAResponse(requestBytes);
// Handle the TSA response
TimeStampResponse response = new TimeStampResponse(respBytes);
// validate communication level attributes (RFC 3161 PKIStatus)
response.Validate(request);
PkiFailureInfo failure = response.GetFailInfo();
int value = (failure == null) ? 0 : failure.IntValue;
if (value != 0) {
// @todo: Translate value of 15 error codes defined by PKIFailureInfo to string
throw new IOException(MessageLocalization.GetComposedMessage("invalid.tsa.1.response.code.2", tsaURL, value));
}
// @todo: validate the time stap certificate chain (if we want
// assure we do not sign using an invalid timestamp).
// extract just the time stamp token (removes communication status info)
TimeStampToken tsToken = response.TimeStampToken;
if (tsToken == null) {
throw new IOException(MessageLocalization.GetComposedMessage("tsa.1.failed.to.return.time.stamp.token.2", tsaURL, response.GetStatusString()));
}
TimeStampTokenInfo tsTokenInfo = tsToken.TimeStampInfo; // to view details
byte[] encoded = tsToken.GetEncoded();
LOGGER.Info("Timestamp generated: " + tsTokenInfo.GenTime);
if (tsaInfo != null) {
tsaInfo.InspectTimeStampTokenInfo(tsTokenInfo);
}
// Update our token size estimate for the next call (padded to be safe)
this.tokenSizeEstimate = encoded.Length + 32;
return encoded;
}
/**
* Get timestamp token - communications layer
* @return - byte[] - TSA response, raw bytes (RFC 3161 encoded)
*/
protected internal virtual byte[] GetTSAResponse(byte[] requestBytes) {
HttpWebRequest con = (HttpWebRequest)WebRequest.Create(tsaURL);
con.ContentLength = requestBytes.Length;
con.ContentType = "application/timestamp-query";
con.Method = "POST";
if ((tsaUsername != null) && !tsaUsername.Equals("") ) {
string authInfo = tsaUsername + ":" + tsaPassword;
authInfo = Convert.ToBase64String(Encoding.Default.GetBytes(authInfo), Base64FormattingOptions.None);
con.Headers["Authorization"] = "Basic " + authInfo;
}
Stream outp = con.GetRequestStream();
outp.Write(requestBytes, 0, requestBytes.Length);
outp.Close();
HttpWebResponse response = (HttpWebResponse)con.GetResponse();
if (response.StatusCode != HttpStatusCode.OK)
throw new IOException(MessageLocalization.GetComposedMessage("invalid.http.response.1", (int)response.StatusCode));
Stream inp = response.GetResponseStream();
MemoryStream baos = new MemoryStream();
byte[] buffer = new byte[1024];
int bytesRead = 0;
while ((bytesRead = inp.Read(buffer, 0, buffer.Length)) > 0) {
baos.Write(buffer, 0, bytesRead);
}
inp.Close();
response.Close();
byte[] respBytes = baos.ToArray();
String encoding = response.ContentEncoding;
if (encoding != null && Util.EqualsIgnoreCase(encoding, "base64")) {
respBytes = Convert.FromBase64String(Encoding.ASCII.GetString(respBytes));
}
return respBytes;
}
}
编辑最终(解决方案)
感谢大家帮助我,正如 Tilman Hausherr 指出的那样,我根本没有对签名进行哈希处理。我换了
byte[] token = tsaClient.GetTimeStampToken(signer.getSignature());
在我的 SignTimeStamp 方法中
java.security.MessageDigest mda = java.security.MessageDigest.getInstance("SHA-256");
byte[] digest = mda.digest(signer.getSignature());
byte[] token = tsaClient.GetTimeStampToken(digest);
我现在可以看到时间戳权限。再次非常感谢您!