I have to extract signature fields from PDF signed document to create a printed signature version. Until now I've been able to recover signer certificate, reason, signing date and other fields using this iText code:
PdfReader reader = new PdfReader(signedPdf);
AcroFields af = reader.getAcroFields();
ArrayList<String> names = af.getSignatureNames();
SimpleDateFormat sdf = new SimpleDateFormat(
"dd/MM/yyyy 'a las' HH:mm:ss");
for (int i = 0; i < names.size(); ++i) {
StringBuilder sb = new StringBuilder();
String name = names.get(i);
PdfPKCS7 pk = af.verifySignature(name);
String firmante = CertificateInfo.getSubjectFields(
pk.getSigningCertificate()).getField("CN");
sb.append("Nombre del firmante: " + firmante + "\n");
Date signDate = pk.getSignDate().getTime();
String sdate = sdf.format(signDate);
sb.append("Fecha y hora de la firma: " + sdate + "\n");
String razon = pk.getReason();
sb.append("proposito: " + razon + "\n");
}
As far as I know, the PDF signature was made with iText PdfPkcs7 class using setExternalDigest method to add a PKCS1 byte array created in an external application. The file looks correctly signed and validated by external tools.
// Create the signature
PdfPKCS7 sgn = new PdfPKCS7(null, chain, "SHA1", "BC", null, false);
//pkcs1Bytes is a byte array with the signed document hash
sgn.setExternalDigest(pkcs1Bytes, null, "RSA");
However, one of the required fields for printed version is a "signature digital stamp" which is a base 64 string of signed document hash or PKCS1.
It's possible to extract that PKCS1 bytes from the signed PDF document?
EDITED: I forgot to mention that when I use PdfPKCS7.getEncodedPKCS1() method after verifying signature it throws ExceptionConverter: java.security.SignatureException: object not initialized for signing
I have reviewed the code and seems the class PdfPKCS7 does not allow to access the digest. But, the content is stored in a private member PdfPKCS7.digest. So using reflection will allow you to extract it.
I have found a similar example here and here (is basically the same)
PdfPKCS7 pdfPkcs7 = acroFields.verifySignature(name);
pdfPkcs7.verify();
Field digestField = PdfPKCS7.class.getDeclaredField("digest");
digestField.setAccessible(true);
byte[] digest = (byte[]) digestField.get(pdfPkcs7);
I think the variable you need is digest because the value is assigned in getEncodedPKCS1 when performing the signature
public byte[] getEncodedPKCS1() {
try {
if (externalDigest != null)
digest = externalDigest;
else
digest = sig.sign();
//skipped content
And is used in verify() in the following way verifyResult = sig.verify(digest);
Note that digest is a private variable, so the name or content could depend on the version. Review the code of your specific version.
Considering your code I assume you are using a 5.x iText version, not a 7.x.
You can either use reflection (cf. this older answer or pedrofb's answer here) or you can simply extract the CMS signature container using iText and then analyze that container using BouncyCastle; a version of BC usually already is present anyways if you use signature related functionality of iText.
As the OP has already observed, PdfPKCS7.getEncodedPKCS7() fails with "ExceptionConverter: java.security.SignatureException: object not initialized for signing". The reason is that this method is meant for retrieving a signature container newly constructed by the PdfPKCS7 instance.
To extract the CMS signature container using iText you can use this code instead:
AcroFields fields = reader.getAcroFields();
PdfDictionary sigDict = fields.getSignatureDictionary(name);
PdfString contents = sigDict.getAsString(PdfName.CONTENTS);
byte[] contentBytes = contents.getOriginalBytes();
contentBytes now contains the encoded CMS container (plus some trailing bytes, usually null bytes, as the Contents value usually is larger than required for the signature container).
Analyzing this container using BouncyCastle is not difficult but the details may depend on the exact BouncyCastle version you use.
Related
MongoDB offers a feature called Client-Side Field Level Encryption (CSFLE) which allows applications to encrypt data before storing it in the database.
As the name suggests you can control the encryption for each field of a document individually using a different Data Encryption Key (DEK) for every field if needed.
These DEKs are stored in a collection called Key Vault where the actual key material is encrypted with a key named Customer Master Key (CMK).
The CMK has a fixed size of 96 bytes.
To read an encrypted field from a document, an application has to
grab the document from the database
fetch the corresponding DEK from the Key Vault
decrypt the DEK using the CMK
decrypt the encrypted field from the document using the decrypted DEK
Storing an encrypted field uses the same mechanism.
Key element is the CMK, especially regarding security as MongoDB points out.
My understanding of encryption so far is that larger keys usually provide more security and that you have to use the same key to decrypt something you've encrypted.
However, while working on a Java-project using CSFLE I noticed MongoDB behaving differently from what I'd expect: I can use different CMKs and still get a valid decryption.
To be more precise, any change in the last 32 bytes of the original CMK does not throw an exception.
How is this possible? Is this intended?
I don't understand this so I'm looking forward to any kind of explanation.
How to replicate
You can copy the example code from MongoDB to replicate this.
I've made some adjustments to the code (e.g. authentication)
MongoClientSettings clientSettings = MongoClientSettings.builder()
.uuidRepresentation(UuidRepresentation.STANDARD)
.applyToClusterSettings(builder -> builder.hosts(Arrays.asList(new ServerAddress("localhost", 27017))))
.credential(...).build();
ClientEncryptionSettings clientEncryptionSettings = ClientEncryptionSettings.builder()
.keyVaultMongoClientSettings(MongoClientSettings.builder()
.applyToClusterSettings(builder -> builder.hosts(Arrays.asList(new ServerAddress("localhost", 27017))))
.credential(...).build())
.keyVaultNamespace(keyVaultNamespace.getFullName()).kmsProviders(kmsProviders).build();
to keep the DEK over subsequent runs
BsonBinary dataKeyId = null;
List<String> keyAltNames = List.of("testCMK");
MongoCursor<Document> cursor = keyVaultCollection.find().iterator();
try {
while (cursor.hasNext() && dataKeyId == null) {
Document key = cursor.next();
List<String> altNames = key.getList("keyAltNames", String.class);
if (altNames != null) {
for (String name : altNames) {
if (name.contentEquals("testCMK")) {
dataKeyId = new BsonBinary(key.get("_id", UUID.class));
break;
}
}
}
}
if (dataKeyId == null)
dataKeyId = clientEncryption.createDataKey("local", new DataKeyOptions().keyAltNames(keyAltNames));
} finally {
cursor.close();
}
and only insert the document once
if (collection.find().first() == null)
collection.insertOne(new Document("encryptedField", encryptedFieldValue));
Document doc = collection.find().first();
// Explicitly decrypt the field
BsonValue decrypted = clientEncryption.decrypt(
new BsonBinary(BsonBinarySubType.ENCRYPTED, doc.get("encryptedField", Binary.class).getData()));
System.out.println("decrypted: " + decrypted);
The original example code doesn't set the BsonBinarySubType which causes clientEncryption.decrypt() to do nothing because the passed BsonBinary is of type 0 (generic binary) instead of 6 (encrypted).
Generating the master key
The CMK is always generated as follows
final byte[] localMasterKey = new byte[96];
for (int i = 0; i < localMasterKey.length; i++)
localMasterKey[i] = (byte) (i + 1);
Running the program multiple times will always produce the same output:
raw value: BsonString{value='123456789'}
decrypted: BsonString{value='123456789'}
Now change the CMK by adding localMasterKey[0] = 0; after the for-loop which generates the key.
Running the program will result in an java.lang.reflect.InvocationTargetException caused by com.mongodb.crypt.capi.MongoCryptException: HMAC validation failure which is expected because the DEK cannot be decrypted with a different CMK.
However, if we instead add localMasterKey[localMasterKey.length - 1] = 0;, the program will again produce
raw value: BsonString{value='123456789'}
decrypted: BsonString{value='123456789'}
I would've expected an exception to be thrown but it decrypts the field with the correct value.
To summarize, the encryption/decryption fails if the generated master key differs from the original (first) master key in any of the first 64 bytes but succeeds when the difference is in the last 32 bytes.
This is not limited to a one byte difference - decryption works even when all 32 bytes are different.
Why is that?
When verifying a signature using Signature.verify I receive an "Invalid encoding for signature" exception.
When verifying same signature using Azure service, the signature is verified.
I have a hash-data (SHA-256), a public key, and a signature that I'm trying to verify.
The signature was received using com.microsoft.azure.keyvault.KeyVaultClient.sign method, with signing algorithm "ES256".
This works (using ES256 algorithm) :
com.microsoft.azure.keyvault.KeyVaultClient keyVaultClient;
String keyPairIdentifier;
boolean verify(byte[] hashData, byte[] signature, JsonWebKeySignatureAlgorithm signingAlgorithm) {
com.microsoft.azure.keyvault.models.KeyVerifyResult result = keyVaultClient.verify(keyPairIdentifier, signingAlgorithm, hashData, signature);
return result.value().booleanValue();
}
This fails (certificate holds same public key that is stored in Azure keyvault):
Signature ecdsaSign = Signature.getInstance("SHA256withECDSA");
ecdsaSign.initVerify(certificate.getPublicKey());
ecdsaSign.update(hashData);
ecdsaSign.verify(signature)
Expected result - true (signature is verified)
Actual result:
java.security.SignatureException: Could not verify signature
at sun.security.ec.ECDSASignature.engineVerify(ECDSASignature.java:325)
at java.security.Signature$Delegate.engineVerify(Signature.java:1222)
at java.security.Signature.verify(Signature.java:655)
at TestKV.KeyVault.VerifyDPSignature.verifySignatureUsingCertificate(VerifyDPSignature.java:143)
at TestKV.KeyVault.VerifyDPSignature.main(VerifyDPSignature.java:104)
Caused by: java.security.SignatureException: Invalid encoding for signature
at sun.security.ec.ECDSASignature.decodeSignature(ECDSASignature.java:400)
at sun.security.ec.ECDSASignature.engineVerify(ECDSASignature.java:322)
... 4 more
Caused by: java.io.IOException: Sequence tag error
at sun.security.util.DerInputStream.getSequence(DerInputStream.java:330)
at sun.security.ec.ECDSASignature.decodeSignature (ECDSASignature.java:376)
dave_thompson_085 - thanks!
There were a few mistakes in the code you attached, the tags of the signature parts should be 0x02, not 0x30, and you didn't increase o after copying the first part.
This is the code after the changes:
byte[] r = new BigInteger(1,Arrays.copyOfRange(signature,0,32)).toByteArray();
byte[] s = new BigInteger(1,Arrays.copyOfRange(signature,32,64)).toByteArray();
byte[] der = new byte[6+r.length+s.length];
der[0] = 0x30; // Tag of signature object
der[1] = (byte)(der.length-2); // Length of signature object
int o = 2;
der[o++] = 0x02; // Tag of ASN1 Integer
der[o++] = (byte)r.length; // Length of first signature part
System.arraycopy (r,0, der,o, r.length);
o += r.length;
der[o++] = 0x02; // Tag of ASN1 Integer
der[o++] = (byte)s.length; // Length of second signature part
System.arraycopy (s,0, der,o, s.length);
After the format change I didn't get the "Sequence tag error" exception. But the verification still failed.
Thanks!
Azure does JWS which simply concatenates fixed-size I2OSP of r and s but Java JCE, like most but not all standards, uses ASN.1 DER encoding e.g. rfc3279 (caveat: there are now OIDs for ECDSA with other hashes).
To convert JWS/plain to DER, see my (cross) https://security.stackexchange.com/questions/174095/convert-ecdsa-signature-from-plain-to-der-format for a C approach, but Java makes it easier because BigInteger does half the work for you:
// byte[64] plain contains the JWS-style r,s (de-base64-ed if necessary)
byte[] r = new BigInteger(1,Arrays.copyOfRange(plain,0,32)).toByteArray();
byte[] s = new BigInteger(1,Arrays.copyOfRange(plain,32,64)).toByteArray();
byte[] der = new byte[6+r.length+s.length]; der[0] = 0x30; der[1] = der.length-2; int o = 2;
der[o++] = 2; der[o++] = (byte)r.length; System.arraycopy (r,0, der,o, r.length); o+=r.length;
der[o++] = 2; der[o++] = (byte)s.length; System.arraycopy (s,0, der,o, s.length); //o+=s.length;
2020-05 corrected and added: also Java 9 up handles this directly using algoirthm names like SHA256withECDSAinP1363format, and on all versions of Java if you add BouncyCastle it does so using names like SHA256withPLAIN-ECDSA or SHA256withCVC-ECDSA. See how to specify signature length for java.security.Signature sign method and Java ECDSAwithSHA256 signature with inconsistent length .
I just had to first decode the signature's raw bytes to Base64 in my case.
byte[] signatureBytes = Base64.getDecoder().decode(signature.getBytes());
byte[] r = new BigInteger(1,Arrays.copyOfRange(signatureBytes,0,32)).toByteArray();
byte[] s = new BigInteger(1,Arrays.copyOfRange(signatureBytes,32,64)).toByteArray();
I am trying to implement ECDH encryption/decryption along with JWE in Android (Java).
I have found the jose4j and Nimbus JOSE libraries that aim to do everything I need but appears that it's more challenging than I thought.
If anybody is familiar then it's for 3D Secure 2.0...
In the spec below:
SDK = the local side
DS = Directory Server (the other side)
Next is the spec:
Given: P(DS) - an EC public key (provided in PEM format, can be transformed to PublicKey or to JWK)
Generate a fresh ephemeral key pair (Q(SDK), d(SDK))
Conduct a Diffie-Hellman key exchange process according to JWA (RFC7518) in Direct Key Agreement mode using curve P-256, d(SDK) and P(DS) to produce a CEK. The parameter values supported in this version of the specification are:
"alg":ECDH-ES
"apv":DirectoryServerID
"epk":P(DS),inJSONWebKey(JWK)format {"kty":"EC", "crv":"P-256"}
All other parameters: not present
CEK:"kty":oct-256bits
Generate 128-bit random data as IV
Encrypt the JSON object according to JWE (RFC7516) using the CEK and JWE Compact Serialization. The parameter values supported in this version of the specification are:
"alg":dir
"epk":Q(SDK) as {"kty": "EC", "crv": "P-256"}
"enc":either"A128CBC-HS256"or"A128GCM"
All other parameters: not present
If the algorithm is A128CBC-HS256 use the full CEK or if the algorithm is A128GCM use the leftmost 128 bits of the CEK.
Delete the ephemeral key pair (Q(SDK),d(SDK))
Makes the resulting JWE available to the 3DS Server as SDK Encrypted Data
If someone has implemented this exact spec and can share the code this would be brilliant!!
There's an example of creating JWT using ECDH in the examples of jose4j:
https://bitbucket.org/b_c/jose4j/wiki/JWT%20Examples (the last example, titled as "Producing and consuming a nested (signed and encrypted) JWT").
But this example is not exactly what I need. It creates a token while I need to encrypt a text.
Starting from "CEK:"kty":oct-256bits" in the spec above I don't understand what to do.
Here's my code (so far) using Nimbus lib:
public String nimbus_encrypt(String plainJson, ECPublicKey otherPublicKey, String directoryServerId) throws JOSEException {
JWEHeader jweHeader = new JWEHeader(
JWEAlgorithm.ECDH_ES,
EncryptionMethod.A128CBC_HS256,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
Base64URL.encode(directoryServerId),
null,
0,
null,
null,
null,
null);
JWEObject jwe = new JWEObject(jweHeader, new Payload(plainJson));
jwe.encrypt(new ECDHEncrypter(otherPublicKey));
String serializedJwe = jwe.serialize();
Log.d("[ENCRYPTION]", "nimbus_encrypt: jwe = " + jwe.getHeader());
Log.d("[ENCRYPTION]", "nimbus_encrypt: serializedJwe = " + serializedJwe);
return serializedJwe;
}
This is the nimbus output:
nimbus_encrypt: jwe = {"epk":{"kty":"EC","crv":"P-256","x":"AS0GRfAOWIDONXxaPR_4IuNHcDIUJPHbACjG5L7x-nQ","y":"xonFn1vRASKUTdCkFTwsl16LRmSe-bAF8EO4-mh1NYw"},"apv":"RjAwMDAwMDAwMQ","enc":"A128CBC-HS256","alg":"ECDH-ES"}
nimbus_encrypt: serializedJwe = eyJlcGsiOnsia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsIngiOiJBUzBHUmZBT1dJRE9OWHhhUFJfNEl1TkhjRElVSlBIYkFDakc1TDd4LW5RIiwieSI6InhvbkZuMXZSQVNLVVRkQ2tGVHdzbDE2TFJtU2UtYkFGOEVPNC1taDFOWXcifSwiYXB2IjoiUmpBd01EQXdNREF3TVEiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiRUNESC1FUyJ9..Pi48b7uj3UilvVXKewFacg.0sx9OkHxxtZvkVm-IENRFw.bu5GvOAwcZxdxaDKWIBqwA
Here's my code (so far, using #Brian-Campbell's answer) using jose4j lib:
public String jose4j_encrypt(String plainJson, PublicKey otherPublicKey, String directoryServerId) throws JoseException {
JsonWebEncryption jwe = new JsonWebEncryption();
jwe.setAlgorithmHeaderValue(KeyManagementAlgorithmIdentifiers.ECDH_ES);
jwe.setEncryptionMethodHeaderParameter(ContentEncryptionAlgorithmIdentifiers.AES_128_CBC_HMAC_SHA_256);
jwe.setHeader(HeaderParameterNames.AGREEMENT_PARTY_V_INFO, Base64Url.encodeUtf8ByteRepresentation(directoryServerId));
jwe.setKey(otherPublicKey);
jwe.setPayload(plainJson);
String serializedJwe = jwe.getCompactSerialization();
Log.d("[ENCRYPTION]", "jose4j_encrypt: jwe = " + jwe);
Log.d("[ENCRYPTION]", "jose4j_encrypt: serializedJwe = " + serializedJwe);
return serializedJwe;
}
This is the jose4j output:
jose4j_encrypt: jwe = JsonWebEncryption{"alg":"ECDH-ES","enc":"A128CBC-HS256","apv":"RjAwMDAwMDAwMQ","epk":{"kty":"EC","x":"prvyhexJXDWvPQmPA1xBjY8mkHEbrEiJ4Dr-7_5YfdQ","y":"fPjw8UdfzgkVTppPSN5o_wprItKLwecoia9yrWi38yo","crv":"P-256"}}
jose4j_encrypt: serializedJwe = eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTEyOENCQy1IUzI1NiIsImFwdiI6IlJqQXdNREF3TURBd01RIiwiZXBrIjp7Imt0eSI6IkVDIiwieCI6InBydnloZXhKWERXdlBRbVBBMXhCalk4bWtIRWJyRWlKNERyLTdfNVlmZFEiLCJ5IjoiZlBqdzhVZGZ6Z2tWVHBwUFNONW9fd3BySXRLTHdlY29pYTl5cldpMzh5byIsImNydiI6IlAtMjU2In19..gxWYwFQSOqLk5HAgs7acdA.mUIHBiWpWSlQaEOJ_EZGYA.eiTe-88fw-Jfuhji_W0rtg
As can be seen the "alg" header in the final result is "ECDH-ES" and not "dir" as required.
If I would implement both sides of the communication then it would be enough, but with this spec seems like many configurations are missing here...
Code using jose4j is longer and seems more configurable but I couldn't construct something valuable enough to post here.
The main missing part for me is how to generate the CEK from the spec above.
Thank you.
EDIT
Added jose4j code above and added the outputs...
Below is some example code using jose4j that I think does what you're looking for. The example you pointed to is similar with the plaintext of the JWE being a JWS/JWT but it can be any arbitrary content. The details of the CEK generation/derivation are taken care of by the underlying JWE functionality. Please note that this only encrypts the content and doesn't provide integrity protection or sender authentication.
String encodedCert = "MIIBRjCB7KADAgECAgYBaqxRCjswDAYIKoZIzj0EAwIFADApMQswCQYDVQQGEwJDQTEMMAoGA1UE\n" +
"ChMDbWVoMQwwCgYDVQQDEwNtZWgwHhcNMTkwNTEyMTM1MjMzWhcNMjAwNTExMTM1MjMzWjApMQsw\n" +
"CQYDVQQGEwJDQTEMMAoGA1UEChMDbWVoMQwwCgYDVQQDEwNtZWgwWTATBgcqhkjOPQIBBggqhkjO\n" +
"PQMBBwNCAAQH83AhYHCehKj7M5+UTNshwLFqqqJWGrJPNj9Kr7xvxtcZnyjq+AKLGMLfdk/G7yb8\n" +
"4vIh0cJwtVs70WgIXT8xMAwGCCqGSM49BAMCBQADRwAwRAIgO0PJRzan2msHpcvcqhybzeualDea\n" +
"/X2QGAWCYT+sNiwCIDMrfhrzUQ6uIX4vnB8AYqb85Ssl7Qcl9nYtjHb08NR8";
X509Util x509Util = new X509Util();
X509Certificate x509Certificate = x509Util.fromBase64Der(encodedCert);
// the JWE object
JsonWebEncryption jwe = new JsonWebEncryption();
// The output of the ECDH-ES key agreement will be used as the content encryption key
jwe.setAlgorithmHeaderValue(KeyManagementAlgorithmIdentifiers.ECDH_ES);
// The content encryption key is used to encrypt the payload
// with a composite AES-CBC / HMAC SHA2 encryption algorithm
jwe.setEncryptionMethodHeaderParameter(ContentEncryptionAlgorithmIdentifiers.AES_128_CBC_HMAC_SHA_256);
// don't think you really need this but you had ""apv":DirectoryServerID" in the question so...
jwe.setHeader(HeaderParameterNames.AGREEMENT_PARTY_V_INFO, Base64Url.encodeUtf8ByteRepresentation("<<DirectoryServerID>>"));
// We encrypt to the receiver using their public key
jwe.setKey(x509Certificate.getPublicKey());
// and maybe put x5t to help the receiver know which key to use in decryption
jwe.setX509CertSha1ThumbprintHeaderValue(x509Certificate);
// What is going to be encrypted
jwe.setPayload("Your text here. It can be JSON or whatever.");
// Produce the JWE compact serialization, which is a string consisting of five dot ('.') separated
// base64url-encoded parts in the form Header..IV.Ciphertext.AuthenticationTag
String serializedJwe = jwe.getCompactSerialization();
System.out.println(serializedJwe);
I'm trying to obtain the byte[] that represents the signature that is in a String but keep receiving "java.security.SignatureException: invalid encoding for signature"
What i'm trying to do is to send a signed string and veryfying it at server, here is my code:
Here is where i sign my string to send it via Web Service:
Signature signer = Signature.getInstance("DSA");
signer.initSign(signerKey);
signer.update(someString)
byte[] signature = signer.sign();
newToken = someString + Base64.getEncoder().encodeToString(signature);
Here is the server-side code to obtain the string and verify the whole thing with it:
byte [] sig = Base64.getDecoder().decode(stringSignature);
Signature verifier = Signature.getInstance("DSA");
verifier.initVerify(verifierPubKey);
verifier.update(token);
verified = verifier.verify(signature);
So, what's the best way to obtain a signature from a string that actually represents the signature i'm trying to verify?
Thanks.
I think this:
verified = verifier.verify(signature);
should be changed to this:
verified = verifier.verify(sig);
I'm guessing signature is the Base64 encoding or something, rather than the raw bytes you need.
I have a valid PKCS7 file loaded into a CMSSignedData object.
This PKCS7 file includes a plain text message and a valid attached digital signature (all in the same file).
Now I want to timestamp this file. This is the code I'm using (source):
private static CMSSignedData addTimestamp(CMSSignedData signedData)
throws Exception {
Collection ss = signedData.getSignerInfos().getSigners();
SignerInformation si = (SignerInformation) ss.iterator().next();
TimeStampToken tok = getTimeStampToken();
ASN1InputStream asn1InputStream = new ASN1InputStream
(tok.getEncoded());
DERObject tstDER = asn1InputStream.readObject();
DERSet ds = new DERSet(tstDER);
Attribute a = new Attribute(new
DERObjectIdentifier("1.2.840.113549.1.9.16.2.14"), ds);
DEREncodableVector dv = new DEREncodableVector();
dv.add(a);
AttributeTable at = new AttributeTable(dv);
si = SignerInformation.replaceUnsignedAttributes(si, at);
ss.clear();
ss.add(si);
SignerInformationStore sis = new SignerInformationStore(ss);
signedData = CMSSignedData.replaceSigners(signedData, sis);
return signedData;
}
private static TimeStampToken getTimeStampToken() throws
Exception {
Security.addProvider (new
org.bouncycastle.jce.provider.BouncyCastleProvider());
PostMethod post = new PostMethod("http://My-TrustedTimeStampProvier.com");
// I'm omitting the part where I pass the user and password
TimeStampRequestGenerator reqGen = new TimeStampRequestGenerator();
//request TSA to return certificate
reqGen.setCertReq (true); // In my case this works
//make a TSP request this is a dummy sha1 hash (20 zero bytes)
TimeStampRequest request =
reqGen.generate(TSPAlgorithms.SHA1, new byte[20], BigInteger.valueOf(100));
byte[] enc_req = request.getEncoded();
ByteArrayInputStream bais = new ByteArrayInputStream(enc_req);
post.setRequestBody(bais);
post.setRequestContentLength (enc_req.length);
post.setRequestHeader("Content-type","application/timestamp-query");
HttpClient http_client = new HttpClient();
http_client.executeMethod(post);
InputStream in = post.getResponseBodyAsStream();
//read TSP response
TimeStampResponse resp = new TimeStampResponse (in);
resp.validate(request);
TimeStampToken tsToken = resp.getTimeStampToken();
return tsToken;
}
I can get a valid TimeStamp, and I could put it into my CMSSignedData object and save it to a file writting the bytes from signedData.getEncoded() to the harddisk. But when I validate my new shinny timestamped file with a third party software, this software tells the original signature is ok, but the Timestamp doesn't correspond with the signature. This software also can show me the original plain text message.
I think the problem is in this line:
TimeStampRequest request =
reqGen.generate(TSPAlgorithms.SHA1, new byte[20], BigInteger.valueOf(100));
I think I have to pass a digest instead of a dummy byte array, but I don't know which digest, or what are the right bytes I have to timeStamp.
I successfully could get and verify a SignerInformation object from my signedData. Then I tried to pass to the reqGen.generate() function the bytes from mySignerInformation.getSignature(). The timestamp verification failed. Then I passed a Sha1 digest of mySignerInformation.getSignature(), but my timestamp verification failed again.
The RFC3161 specification says:
2.4.1. Request Format
A time-stamping request is as follows:
TimeStampReq ::= SEQUENCE { version INTEGER
{ v1(1) }, messageImprint MessageImprint,
--a hash algorithm OID and the hash value of the data to be
(...)
The messageImprint field SHOULD contain the hash of the datum to be
time-stamped. The hash is represented as an OCTET STRING. Its
length MUST match the length of the hash value for that algorithm
(e.g., 20 bytes for SHA-1 or 16 bytes for MD5).
MessageImprint ::= SEQUENCE {
hashAlgorithm AlgorithmIdentifier,
hashedMessage OCTET STRING }
But it doesn't tell me where or how I get the MessageImprint data if I want to TimeStamp the bytes inside a CMSSignedData object.
I'm a newbie in this digital signature stuff.
You're right, the problem is that you're timestamping the incorrect data. The rest of the code seems correct to me.
So the thing is that you've to timestamp the hash of the signature. To get the signature from your CMSSignedData and hash it; you can use the follow code (supposing that you've only one signer in your PKCS7 and you're using SHA1 hash algorithm):
CMSSignedData signedData = ...
// get the signers of your CMSSignedData signedData
Collection ss = signedData.getSignerInfos().getSigners();
SignerInformation si = (SignerInformation) ss.iterator().next();
// hash the signature
byte[] signDigest = MessageDigest
.getInstance(TSPAlgorithms.SHA1, new BouncyCastleProvider())
.digest(si.getSignature()); // since you're adding the bc provider with Security.addProvider you can use "BC" instead of passing the new BouncyCastleProvider()
TimeStampRequestGenerator reqGen = new TimeStampRequestGenerator();
// generate the TSRequest
TimeStampRequest request =
reqGen.generate(TSPAlgorithms.SHA1, signDigest, BigInteger.valueOf(100));
...
Hope this helps,