We are trying to make the signed signature LTV enabled. I am using the below code to add verification. When signature.isTsp() is false, the PDF says Signature is not LTV enabled, though in the other case (signature.isTsp() is true) it shows as valid.
When we open the PDF and try to manually add verification info by right clicking on the signature it enables LTV without any issue. Not sure what we are missing here.
Any input will be highly helpful.
// Adds LTV-enabled information to the PDF document.
private ByteArrayOutputStream addLtv(final IOcspClient ocspClient,
final ByteArrayOutputStream docStream)
throws IOException, GeneralSecurityException {
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
final InputStream signedStream = new ByteArrayInputStream(docStream.toByteArray());
final PdfReader reader = new PdfReader(signedStream);
final PdfDocument document =
new PdfDocument(reader, new PdfWriter(outputStream), new StampingProperties().useAppendMode());
final LtvVerification verification = new LtvVerification(document);
final SignatureUtil signatureUtil = new SignatureUtil(document);
final List<String> signatureNames = signatureUtil.getSignatureNames();
final String sigName = signatureNames.get(signatureNames.size() - 1);
final PdfPKCS7 signature = signatureUtil.verifySignature(sigName);
final CrlClientOnline crl = new CrlClientOnline();
if (!signature.isTsp()) {
for (final String name: signatureNames) {
addVerificationInfo(ocspClient, verification, crl, name);
}
} else {
addVerificationInfo(ocspClient, verification, crl, sigName);
}
document.close();
return outputStream;
}
private void addVerificationInfo(final IOcspClient ocspClient, final LtvVerification verification,
final CrlClientOnline crl,
final String name) throws IOException, GeneralSecurityException {
verification.addVerification(
name, ocspClient, crl,
LtvVerification.CertificateOption.WHOLE_CHAIN,
LtvVerification.Level.OCSP_CRL,
LtvVerification.CertificateInclusion.NO);
}
The main reason why your code does not always LTV-enable PDFs is that it does not add validation information related to OCSP response signatures.
It doesn't add validation information for CRL signatures, either. As CRLs usually are signed by the issuer certificate of the signer certificate, though, and as validation information for that issuer certificate have already been added in the context of the main signature, LTV-enabling usually does not fail because of missing CRL signature validation information. So if you can use CRLs only, chances are that your code indeed already does LTV-enable PDFs.
In the context of this answer (in particular its section "An approach using an own utility class") I created an utility class AdobeLtvEnabling for iText 5 allowing to LTV-enable PDFs, mostly using bits and pieces found in iText 5 itself. In contrast to your code it does add validation information for OCSP response signatures (and also for CRL signatures).
Here you can find the port of that class to iText 7.
The utility class AdobeLtvEnabling
This utility class bundles the code required for LTV enabling the signatures in a signed PDF document. The code pieces mostly have been taken from existing iText code. The main reason why this class has not been designed to derive from LtvVerification is that required variables and methods from that class are private. As the class originally has been written for iText 5, some iText-5-isms probably can be found in it...
public class AdobeLtvEnabling {
/**
* Use this constructor with a {#link PdfDocument} in append mode. Otherwise
* the existing signatures will be damaged.
*/
public AdobeLtvEnabling(PdfDocument pdfDocument) {
this.pdfDocument = pdfDocument;
}
/**
* Call this method to have LTV information added to the {#link PdfDocument}
* given in the constructor.
*/
public void enable(IOcspClient ocspClient, ICrlClient crlClient) throws OperatorException, GeneralSecurityException, IOException, StreamParsingException, OCSPException {
SignatureUtil signatureUtil = new SignatureUtil(pdfDocument);
List<String> names = signatureUtil.getSignatureNames();
for (String name : names) {
PdfPKCS7 pdfPKCS7 = signatureUtil.verifySignature(name, BouncyCastleProvider.PROVIDER_NAME);
PdfSignature sig = signatureUtil.getSignature(name);
List<X509Certificate> certificatesToCheck = new ArrayList<>();
certificatesToCheck.add(pdfPKCS7.getSigningCertificate());
while (!certificatesToCheck.isEmpty()) {
X509Certificate certificate = certificatesToCheck.remove(0);
addLtvForChain(certificate, ocspClient, crlClient, getSignatureHashKey(sig));
}
}
outputDss();
}
//
// the actual LTV enabling methods
//
void addLtvForChain(X509Certificate certificate, IOcspClient ocspClient, ICrlClient crlClient, PdfName key) throws GeneralSecurityException, IOException, StreamParsingException, OperatorCreationException, OCSPException {
ValidationData validationData = new ValidationData();
while (certificate != null) {
System.out.println(certificate.getSubjectX500Principal().getName());
X509Certificate issuer = getIssuerCertificate(certificate);
validationData.certs.add(certificate.getEncoded());
byte[] ocspResponse = ocspClient.getEncoded(certificate, issuer, null);
if (ocspResponse != null) {
System.out.println(" with OCSP response");
validationData.ocsps.add(ocspResponse);
X509Certificate ocspSigner = getOcspSignerCertificate(ocspResponse);
if (ocspSigner != null) {
System.out.printf(" signed by %s\n", ocspSigner.getSubjectX500Principal().getName());
}
addLtvForChain(ocspSigner, ocspClient, crlClient, getOcspHashKey(ocspResponse));
} else {
Collection<byte[]> crl = crlClient.getEncoded(certificate, null);
if (crl != null && !crl.isEmpty()) {
System.out.printf(" with %s CRLs\n", crl.size());
validationData.crls.addAll(crl);
for (byte[] crlBytes : crl) {
addLtvForChain(null, ocspClient, crlClient, getCrlHashKey(crlBytes));
}
}
}
certificate = issuer;
}
validated.put(key, validationData);
}
void outputDss() throws IOException {
PdfDictionary dss = new PdfDictionary();
PdfDictionary vrim = new PdfDictionary();
PdfArray ocsps = new PdfArray();
PdfArray crls = new PdfArray();
PdfArray certs = new PdfArray();
PdfCatalog catalog = pdfDocument.getCatalog();
if (pdfDocument.getPdfVersion().compareTo(PdfVersion.PDF_2_0) < 0) {
catalog.addDeveloperExtension(PdfDeveloperExtension.ESIC_1_7_EXTENSIONLEVEL5);
catalog.addDeveloperExtension(new PdfDeveloperExtension(PdfName.ADBE, new PdfName("1.7"), 8));
}
for (PdfName vkey : validated.keySet()) {
PdfArray ocsp = new PdfArray();
PdfArray crl = new PdfArray();
PdfArray cert = new PdfArray();
PdfDictionary vri = new PdfDictionary();
for (byte[] b : validated.get(vkey).crls) {
PdfStream ps = new PdfStream(b);
ps.setCompressionLevel(CompressionConstants.DEFAULT_COMPRESSION);
ps.makeIndirect(pdfDocument);
crl.add(ps);
crls.add(ps);
crls.setModified();
}
for (byte[] b : validated.get(vkey).ocsps) {
b = buildOCSPResponse(b);
PdfStream ps = new PdfStream(b);
ps.setCompressionLevel(CompressionConstants.DEFAULT_COMPRESSION);
ps.makeIndirect(pdfDocument);
ocsp.add(ps);
ocsps.add(ps);
ocsps.setModified();
}
for (byte[] b : validated.get(vkey).certs) {
PdfStream ps = new PdfStream(b);
ps.setCompressionLevel(CompressionConstants.DEFAULT_COMPRESSION);
ps.makeIndirect(pdfDocument);
cert.add(ps);
certs.add(ps);
certs.setModified();
}
if (ocsp.size() > 0) {
ocsp.makeIndirect(pdfDocument);
vri.put(PdfName.OCSP, ocsp);
}
if (crl.size() > 0) {
crl.makeIndirect(pdfDocument);
vri.put(PdfName.CRL, crl);
}
if (cert.size() > 0) {
cert.makeIndirect(pdfDocument);
vri.put(PdfName.Cert, cert);
}
vri.put(PdfName.TU, new PdfDate().getPdfObject());
vri.makeIndirect(pdfDocument);
vrim.put(vkey, vri);
}
vrim.makeIndirect(pdfDocument);
vrim.setModified();
dss.put(PdfName.VRI, vrim);
if (ocsps.size() > 0) {
ocsps.makeIndirect(pdfDocument);
dss.put(PdfName.OCSPs, ocsps);
}
if (crls.size() > 0) {
crls.makeIndirect(pdfDocument);
dss.put(PdfName.CRLs, crls);
}
if (certs.size() > 0) {
certs.makeIndirect(pdfDocument);
dss.put(PdfName.Certs, certs);
}
dss.makeIndirect(pdfDocument);
dss.setModified();
catalog.put(PdfName.DSS, dss);
}
//
// VRI signature hash key calculation
//
static PdfName getCrlHashKey(byte[] crlBytes) throws NoSuchAlgorithmException, IOException, CRLException, CertificateException {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509CRL crl = (X509CRL)cf.generateCRL(new ByteArrayInputStream(crlBytes));
byte[] signatureBytes = crl.getSignature();
DEROctetString octetString = new DEROctetString(signatureBytes);
byte[] octetBytes = octetString.getEncoded();
byte[] octetHash = hashBytesSha1(octetBytes);
PdfName octetName = new PdfName(convertToHex(octetHash));
return octetName;
}
static PdfName getOcspHashKey(byte[] basicResponseBytes) throws NoSuchAlgorithmException, IOException {
BasicOCSPResponse basicResponse = BasicOCSPResponse.getInstance(basicResponseBytes);
byte[] signatureBytes = basicResponse.getSignature().getBytes();
DEROctetString octetString = new DEROctetString(signatureBytes);
byte[] octetBytes = octetString.getEncoded();
byte[] octetHash = hashBytesSha1(octetBytes);
PdfName octetName = new PdfName(convertToHex(octetHash));
return octetName;
}
static PdfName getSignatureHashKey(PdfSignature sig) throws NoSuchAlgorithmException, IOException {
PdfString contents = sig.getContents();
byte[] bc = PdfEncodings.convertToBytes(contents.getValue(), null);
if (PdfName.ETSI_RFC3161.equals(sig.getSubFilter())) {
try ( ASN1InputStream din = new ASN1InputStream(new ByteArrayInputStream(bc)) ) {
ASN1Primitive pkcs = din.readObject();
bc = pkcs.getEncoded();
}
}
byte[] bt = hashBytesSha1(bc);
return new PdfName(convertToHex(bt));
}
static byte[] hashBytesSha1(byte[] b) throws NoSuchAlgorithmException {
MessageDigest sh = MessageDigest.getInstance("SHA1");
return sh.digest(b);
}
static String convertToHex(byte[] bytes) {
ByteBuffer buf = new ByteBuffer();
for (byte b : bytes) {
buf.appendHex(b);
}
return PdfEncodings.convertToString(buf.toByteArray(), null).toUpperCase();
}
//
// OCSP response helpers
//
static X509Certificate getOcspSignerCertificate(byte[] basicResponseBytes) throws CertificateException, OCSPException, OperatorCreationException {
JcaX509CertificateConverter converter = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME);
BasicOCSPResponse borRaw = BasicOCSPResponse.getInstance(basicResponseBytes);
BasicOCSPResp bor = new BasicOCSPResp(borRaw);
for (final X509CertificateHolder x509CertificateHolder : bor.getCerts()) {
X509Certificate x509Certificate = converter.getCertificate(x509CertificateHolder);
JcaContentVerifierProviderBuilder jcaContentVerifierProviderBuilder = new JcaContentVerifierProviderBuilder();
jcaContentVerifierProviderBuilder.setProvider(BouncyCastleProvider.PROVIDER_NAME);
final PublicKey publicKey = x509Certificate.getPublicKey();
ContentVerifierProvider contentVerifierProvider = jcaContentVerifierProviderBuilder.build(publicKey);
if (bor.isSignatureValid(contentVerifierProvider))
return x509Certificate;
}
return null;
}
static byte[] buildOCSPResponse(byte[] BasicOCSPResponse) throws IOException {
DEROctetString doctet = new DEROctetString(BasicOCSPResponse);
ASN1EncodableVector v2 = new ASN1EncodableVector();
v2.add(OCSPObjectIdentifiers.id_pkix_ocsp_basic);
v2.add(doctet);
ASN1Enumerated den = new ASN1Enumerated(0);
ASN1EncodableVector v3 = new ASN1EncodableVector();
v3.add(den);
v3.add(new DERTaggedObject(true, 0, new DERSequence(v2)));
DERSequence seq = new DERSequence(v3);
return seq.getEncoded();
}
//
// X509 certificate related helpers
//
static X509Certificate getIssuerCertificate(X509Certificate certificate) throws IOException, StreamParsingException {
String url = getCACURL(certificate);
if (url != null && url.length() > 0) {
HttpURLConnection con = (HttpURLConnection)new URL(url).openConnection();
if (con.getResponseCode() / 100 != 2) {
throw new PdfException(PdfException.InvalidHttpResponse1).setMessageParams(con.getResponseCode());
}
InputStream inp = (InputStream) con.getContent();
X509CertParser parser = new X509CertParser();
parser.engineInit(new ByteArrayInputStream(StreamUtil.inputStreamToArray(inp)));
return (X509Certificate) parser.engineRead();
}
return null;
}
static String getCACURL(X509Certificate certificate) {
ASN1Primitive obj;
try {
obj = getExtensionValue(certificate, Extension.authorityInfoAccess.getId());
if (obj == null) {
return null;
}
ASN1Sequence AccessDescriptions = (ASN1Sequence) obj;
for (int i = 0; i < AccessDescriptions.size(); i++) {
ASN1Sequence AccessDescription = (ASN1Sequence) AccessDescriptions.getObjectAt(i);
if ( AccessDescription.size() != 2 ) {
continue;
}
else if (AccessDescription.getObjectAt(0) instanceof ASN1ObjectIdentifier) {
ASN1ObjectIdentifier id = (ASN1ObjectIdentifier)AccessDescription.getObjectAt(0);
if ("1.3.6.1.5.5.7.48.2".equals(id.getId())) {
ASN1Primitive description = (ASN1Primitive)AccessDescription.getObjectAt(1);
String AccessLocation = getStringFromGeneralName(description);
if (AccessLocation == null) {
return "" ;
}
else {
return AccessLocation ;
}
}
}
}
} catch (IOException e) {
return null;
}
return null;
}
static ASN1Primitive getExtensionValue(X509Certificate certificate, String oid) throws IOException {
byte[] bytes = certificate.getExtensionValue(oid);
if (bytes == null) {
return null;
}
ASN1InputStream aIn = new ASN1InputStream(new ByteArrayInputStream(bytes));
ASN1OctetString octs = (ASN1OctetString) aIn.readObject();
aIn = new ASN1InputStream(new ByteArrayInputStream(octs.getOctets()));
return aIn.readObject();
}
static String getStringFromGeneralName(ASN1Primitive names) throws IOException {
ASN1TaggedObject taggedObject = (ASN1TaggedObject) names ;
return new String(ASN1OctetString.getInstance(taggedObject, false).getOctets(), "ISO-8859-1");
}
//
// inner class
//
static class ValidationData {
final List<byte[]> crls = new ArrayList<byte[]>();
final List<byte[]> ocsps = new ArrayList<byte[]>();
final List<byte[]> certs = new ArrayList<byte[]>();
}
//
// member variables
//
final PdfDocument pdfDocument;
final Map<PdfName,ValidationData> validated = new HashMap<PdfName,ValidationData>();
}
(AdobeLtvEnabling.java)
Example use
You can use the AdobeLtvEnabling class like this:
try ( PdfReader pdfReader = new PdfReader(SOURCE);
PdfWriter pdfWriter = new PdfWriter(TARGET);
PdfDocument pdfDocument = new PdfDocument(pdfReader, pdfWriter,
new StampingProperties().preserveEncryption().useAppendMode())) {
AdobeLtvEnabling adobeLtvEnabling = new AdobeLtvEnabling(pdfDocument);
IOcspClient ocsp = new OcspClientBouncyCastle(null);
ICrlClient crl = new CrlClientOnline();
adobeLtvEnabling.enable(ocsp, crl);
}
(MakeLtvEnabled test testLtvEnableSignWithoutLtv)
Limitations
As this code essentially is ported from the iText 5 code from the referenced answer, it also inherits the limitations listed in that answer:
The code works under some simplifying restrictions, in particular:
signature time stamps are ignored,
retrieved CRLs are assumed to be direct and complete,
the complete certificate chains are assumed to be buildable using AIA entries.
You can improve the code accordingly if these restrictions are not acceptable for you.
Related
I try to sign pdf document using itext7, certificate and external signature returned from external web service say Sign Service:
I did the following steps:
Got the orginal pdf, added last page (sign page) with 2 signatures filelds on it and created temp pdf
Calculated hash from created temp pdf
Exchanhed with Sign Service my Base64 encoded hash with encoded Base64 signed hash (I'm not sure is this raw or CMS signature - I treat it as CMS container)
Decoded and put obtained signed hash along with certificate from Sign Company to one of my Sig field on temp pdf file. I will need sign subseqent field/fields in this way in the furure.
Unfortunately i got validation errors in Adobe Reader: “the document has been altered or corrupted since the signature was applied”:
link to Adobe validation result
Below the code fragment where I create sign page:
private void createPdfDocument(Document doc, int iteration) {
//Add last sign page to doc
doc.add(new AreaBreak(AreaBreakType.LAST_PAGE));
doc.add(new AreaBreak(AreaBreakType.NEXT_PAGE));
PdfPage lastPage = doc.getPdfDocument().getLastPage();
float width = lastPage.getPageSize().getWidth();
float height = lastPage.getPageSize().getHeight();
createTitle(doc);
PdfAcroForm form = PdfAcroForm.getAcroForm(doc.getPdfDocument(), true);
for (int i = 1; i <= iteration; i++) {
addSignArea(doc, form, VERTICAL_RECTANGLE_START - (i - 1) * VERTICAL_MARGIN,
VERTICAL_TEXT_START - (i - 1) * VERTICAL_MARGIN, i);
}
System.out.println("Creating sign page finished");
}
private void addSignArea(Document doc, PdfAcroForm form, int verticalRectPosition, int verticalFieldPosition, int iteration) {
Color color = new DeviceRgb(46, 66, 148);
//Create sign area frame
new PdfCanvas(doc.getPdfDocument().getLastPage())
.roundRectangle(50, verticalRectPosition, 495, 50, 5)
.setLineWidth(0.5f)
.setStrokeColor(color)
.stroke();
//Create text fields inside frame
PdfSignatureFormField signField = PdfSignatureFormField.createSignature(doc.getPdfDocument(),
new Rectangle(50, verticalRectPosition, 495, 50));
signField.setFieldName(getFieldCountedName("Signature", iteration));
form.addField(signField);
}
I calculate document hash that way:
public String getDocumentHash() {
try (FileInputStream is = new FileInputStream(DOC)) {
byte[] hash = DigestAlgorithms.digest(is, DigestAlgorithms.SHA256, null);
String encodeToString = Base64.getEncoder().encodeToString(hash);
System.out.println(encodeToString);
return encodeToString;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
And finally sign pdf file:
public class DocumentSigner {
public static final String DEST = "";
private static final String SOURCE = "";
private static final String DOC_HASH = "6XsoKhEXVMu8e0R7BGtaKvghwL0GBrqTGAivFpct6J4=";
public static final String[] RESULT_FILES = new String[]{
"sign_doc_result1.pdf"
};
public static void main(String[] args) throws GeneralSecurityException, IOException {
File file = new File(DEST);
file.mkdirs();
Certificate[] chain = new Certificate[1];
chain[0] = CertLoadTest.getPublicCert(); //load cert from path
String encodedExternalHash = getExternalSignedHash(); //get the signded hash returned from the Sign Service
new DocumentSigner().sign(SOURCE, DEST + RESULT_FILES[0], chain, PdfSigner.CryptoStandard.CMS,
encodedExternalHash, DOC_HASH, "Signature1");
}
public void sign(String src, String dest, Certificate[] chain, PdfSigner.CryptoStandard subfilter,
String encodedExternalHash, String documentHash, String fieldName) throws GeneralSecurityException, IOException {
try (FileOutputStream os = new FileOutputStream(dest); InputStream is = new FileInputStream(src)) {
PdfReader reader = new PdfReader(is);
PdfSigner signer = new PdfSigner(reader, os, new StampingProperties());
signer.setFieldName(fieldName);
IExternalDigest digest = new BouncyCastleDigest();
IExternalSignature signature = new CustomSignature(Base64.getDecoder().decode(encodedExternalHash),
Base64.getDecoder().decode(documentHash), chain);
signer.signDetached(digest, signature, chain, null, null, null,
8096, subfilter);
}
}
public class CustomSignature implements IExternalSignature {
private byte[] signedHash;
private byte[] documentHash;
private Certificate[] chain;
public CustomSignature(byte[] signedHash, byte[] documentHash, Certificate[] chain) {
this.signedHash = signedHash;
this.documentHash = documentHash;
this.chain = chain;
}
public String getHashAlgorithm() {
return DigestAlgorithms.SHA256;
}
public String getEncryptionAlgorithm() {
return "RSA";
}
public byte[] sign(byte[] message) throws GeneralSecurityException {
return signedHash;
}
}
private static String getExternalSignedHash() {
//mocked Sign Service result - documentHash is exchanged with signedHash
return "3BLqVMOLSFXEfCy++n0DmRqcfCGCqSLy9Nzpn1IpAn6iTqr+h78+yOomGMAL0La77IB08Tou9gkxbwSXPHrdN5+EPm7HCXeI/z3fzj711H9OH6P9tWtVHgieKUFOVhrm/PTeypSC/vy7RJQLNmL5+/+Moby5Bdo/KaaN2h9Jj41w1i6CwL/7wzCZ0h+AU9sI+IC0i/UbWFFz7VMfN5barcF1vP+ECLiX3qtZrGbFZNZfrr+28ytNTdUR4iZJRLKL2nXeg0CqxsTjnAyUsFMTCro1qv0QkQO8Cv6AJFhWlUFGUkt+pIUKhIticlypB+WdzwmISOsRK0IUiKgrJI6E3g==";
}
A also tried to treat returned from Sign Service hash as a raw signature - this is what sign method in CustomSignature class looks like then:
BouncyCastleDigest digest = new BouncyCastleDigest();
PdfPKCS7 sgn = new PdfPKCS7(null, chain, "SHA256", null, digest, false);
byte[] sh = sgn.getAuthenticatedAttributeBytes(documentHash, PdfSigner.CryptoStandard.CMS, null, null);
sgn.setExternalDigest(signedHash, null, "RSA");
byte[] encodedSig = sgn.getEncodedPKCS7(documentHash, PdfSigner.CryptoStandard.CMS, null, null, null);
return encodedSig;
But in this case i get formmatting signature errors in Adobe Reader
Is my flow correct or maybe i need another approach to properly sign document.
According to advice posted in the comment, I still use custom IExternalSignature implementation with external call in sign method:
public void sign(Certificate[] chain, PdfSigner.CryptoStandard subfilter, String fieldName) throws GeneralSecurityException, IOException {
try (InputStream is = new FileInputStream(src); FileOutputStream os = new FileOutputStream(dest)) {
PdfReader reader = new PdfReader(is);
PdfSigner signer = new PdfSigner(reader, os, new StampingProperties());
signer.setFieldName(fieldName); //My signature fields
IExternalDigest digest = new BouncyCastleDigest();
IExternalSignature signature = new CustomSignature(chain);
signer.signDetached(digest, signature, chain, null, null, null,
8196, subfilter);
}
}
public class CustomSignature implements IExternalSignature {
private Certificate[] chain;
public CustomSignature(Certificate[] chain) {
this.chain = chain;
}
public String getHashAlgorithm() {
return DigestAlgorithms.SHA256;
}
public String getEncryptionAlgorithm() {
return "RSA";
}
public byte[] sign(byte[] message) throws GeneralSecurityException {
BouncyCastleDigest digest = new BouncyCastleDigest();
byte[] hash = digest.getMessageDigest("SHA256").digest(message);
return Base64.getDecoder().decode(client.getSignedHash(Base64.getEncoder().encodeToString(hash))); // call externall service here
}
}
And for the first call validation error disappeared, Signature1 seems to be ok, but a problem occurred when I tried to sign second sig field using pdf generetaed in first call and takes another file as output. Now newly created Signature2 is ok, but the first one failed with a broken byte range:
new DocumentSigner(SOURCE, DEST1).sign(chain,PdfSigner.CryptoStandard.CMS, "Signature1");
new DocumentSigner(DEST1, DEST2).sign(chain, PdfSigner.CryptoStandard.CMS, "Signature2");
I will be grateful for any ideas what can I do to sign multiple fields without broke previous ones
Here is Adobe validation output after the second call
UPDATE:
I used append mode on PdfSigner's StampingProperties and now everything is ok:
StampingProperties stampingProperties = new StampingProperties();
stampingProperties.useAppendMode();
PdfSigner signer = new PdfSigner(reader, os, stampingProperties);
I have an API that creates Base64 digest of a PDF Document.
Now I want to create another API that takes this digest and PFX and creates an ETSI.CAdES.detached signature and takes LTV informations(Certs chain,OCSP response,CRL) that I want to embed in my PDF to obtain a PAdES-LTV signature using 3rd API(My 3rd API will take CAdES signature and LTV informations obtained from this API and will embed them in my PDF).I dont know how to create this ETSI.CAdES.detached signature using that digest and a PFX with Java and Bouncy Castle.I try to follow this github tutorial.
As you have declared, you have your own code for preparing a PDF for signing and for injecting the signature container into it. Thus, your question essentially burns down to
How to create a CAdES signature container with BouncyCastle that can be used to create a PAdES BASELINE B or T PDF signature?
Implementation in the iText 7 Signing Framework
As I do not have your existing code, I had to use a different framework for my tests. I used the iText 7 signing framework for that.
BouncyCastle does contain a CMSSignedDataGenerator to generate CMS signature containers.
The default implementation of the SignerInfo generation therein unfortunately is not CAdES/PAdES compatible as it does not create signed ESSCertID[v2] attributes. Fortunately, though, the implementation is designed to allow plugging in custom attributes sets.
Thus, you can create the CAdES containers required for PAdES BASELINE signatures with a customized CMSSignedDataGenerator.
So when you have prepared the PDF for signing, you can proceed like this:
InputStream data = [InputStream containing the PDF byte ranges to sign];
ContentSigner contentSigner = [BouncyCastle ContentSigner for your private key];
X509CertificateHolder x509CertificateHolder = [BouncyCastle X509CertificateHolder for your X.509 signer certificate];
DigestCalculatorProvider digestCalculatorProvider = new JcaDigestCalculatorProviderBuilder().setProvider("BC").build();
CMSTypedData msg = new CMSTypedDataInputStream(data);
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
gen.addSignerInfoGenerator(
new JcaSignerInfoGeneratorBuilder(digestCalculatorProvider)
.setSignedAttributeGenerator(new PadesSignedAttributeGenerator())
.setUnsignedAttributeGenerator(new PadesUnsignedAttributeGenerator())
.build(contentSigner, x509CertificateHolder));
gen.addCertificates(new JcaCertStore(Collections.singleton(x509CertificateHolder)));
CMSSignedData sigData = gen.generate(msg, false);
byte[] cmsBytes = sigData.getEncoded();
(PadesSignatureContainerBc method sign)
The byte[] cmsBytes contains the bytes to inject into the prepared PDF signature placeholder.
The following helper classes are needed:
First of all a wrapper for the InputStream containing the PDF ranges to sign to process by BouncyCastle.
class CMSTypedDataInputStream implements CMSTypedData {
InputStream in;
public CMSTypedDataInputStream(InputStream is) {
in = is;
}
#Override
public ASN1ObjectIdentifier getContentType() {
return PKCSObjectIdentifiers.data;
}
#Override
public Object getContent() {
return in;
}
#Override
public void write(OutputStream out) throws IOException,
CMSException {
byte[] buffer = new byte[8 * 1024];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
in.close();
}
}
(PadesSignatureContainerBc helper class CMSTypedDataInputStream)
Then a customized signed attributes generator for PAdES:
class PadesSignedAttributeGenerator implements CMSAttributeTableGenerator {
#Override
public AttributeTable getAttributes(#SuppressWarnings("rawtypes") Map params) throws CMSAttributeTableGenerationException {
String currentAttribute = null;
try {
ASN1EncodableVector signedAttributes = new ASN1EncodableVector();
currentAttribute = "SigningCertificateAttribute";
AlgorithmIdentifier digAlgId = (AlgorithmIdentifier) params.get(CMSAttributeTableGenerator.DIGEST_ALGORITHM_IDENTIFIER);
signedAttributes.add(createSigningCertificateAttribute(digAlgId));
currentAttribute = "ContentTypeAttribute";
ASN1ObjectIdentifier contentType = ASN1ObjectIdentifier.getInstance(params.get(CMSAttributeTableGenerator.CONTENT_TYPE));
signedAttributes.add(new Attribute(CMSAttributes.contentType, new DERSet(contentType)));
currentAttribute = "MessageDigestAttribute";
byte[] messageDigest = (byte[])params.get(CMSAttributeTableGenerator.DIGEST);
signedAttributes.add(new Attribute(CMSAttributes.messageDigest, new DERSet(new DEROctetString(messageDigest))));
return new AttributeTable(signedAttributes);
} catch (Exception e) {
throw new CMSAttributeTableGenerationException(currentAttribute, e);
}
}
Attribute createSigningCertificateAttribute(AlgorithmIdentifier digAlg) throws IOException, OperatorCreationException {
final IssuerSerial issuerSerial = getIssuerSerial();
DigestCalculator digestCalculator = digestCalculatorProvider.get(digAlg);
digestCalculator.getOutputStream().write(x509CertificateHolder.getEncoded());
final byte[] certHash = digestCalculator.getDigest();
if (OIWObjectIdentifiers.idSHA1.equals(digAlg.getAlgorithm())) {
final ESSCertID essCertID = new ESSCertID(certHash, issuerSerial);
SigningCertificate signingCertificate = new SigningCertificate(essCertID);
return new Attribute(id_aa_signingCertificate, new DERSet(signingCertificate));
} else {
ESSCertIDv2 essCertIdv2;
if (NISTObjectIdentifiers.id_sha256.equals(digAlg.getAlgorithm())) {
// SHA-256 is default
essCertIdv2 = new ESSCertIDv2(null, certHash, issuerSerial);
} else {
essCertIdv2 = new ESSCertIDv2(digAlg, certHash, issuerSerial);
}
SigningCertificateV2 signingCertificateV2 = new SigningCertificateV2(essCertIdv2);
return new Attribute(id_aa_signingCertificateV2, new DERSet(signingCertificateV2));
}
}
IssuerSerial getIssuerSerial() {
final X500Name issuerX500Name = x509CertificateHolder.getIssuer();
final GeneralName generalName = new GeneralName(issuerX500Name);
final GeneralNames generalNames = new GeneralNames(generalName);
final BigInteger serialNumber = x509CertificateHolder.getSerialNumber();
return new IssuerSerial(generalNames, serialNumber);
}
}
(PadesSignatureContainerBc helper class PadesSignedAttributeGenerator )
And finally a customized unsigned attributes generator for a signature timestamp:
class PadesUnsignedAttributeGenerator implements CMSAttributeTableGenerator {
#Override
public AttributeTable getAttributes(#SuppressWarnings("rawtypes") Map params) throws CMSAttributeTableGenerationException {
if (tsaClient == null)
return null;
try {
ASN1EncodableVector unsignedAttributes = new ASN1EncodableVector();
byte[] signature = (byte[])params.get(CMSAttributeTableGenerator.SIGNATURE);
byte[] timestamp = tsaClient.getTimeStampToken(tsaClient.getMessageDigest().digest(signature));
unsignedAttributes.add(new Attribute(id_aa_signatureTimeStampToken, new DERSet(ASN1Primitive.fromByteArray(timestamp))));
return new AttributeTable(unsignedAttributes);
} catch (Exception e) {
throw new CMSAttributeTableGenerationException("", e);
}
}
}
(PadesSignatureContainerBc helper class PadesUnsignedAttributeGenerator)
Here I assume a ITSAClient tsaClient, an iText 7 time stamp request client. You can of course use an arbitrary RFC 3161 time stamp request client of your choice.
If you have read your private key into a JCA/JCE PrivateKey pk, you can simply create the needed ContentSigner contentSigner using the BouncyCastle JcaContentSignerBuilder, e.g. like this:
ContentSigner contentSigner = new JcaContentSignerBuilder("SHA512withRSA").build(pk);
(compare the test testSignPadesBaselineT in SignPadesBc)
Implementation in the PDFBox 3 Signing Framework
You meanwhile indicated in comments that you're looking into using PDFBox to sign. Fortunately the code presented above can nearly without a change be used with PDFBox.
To use the code above with PDFBox, one merely has to wrap it into a PDFBox SignatureInterface frame:
public class PadesSignatureContainerBc implements SignatureInterface {
public PadesSignatureContainerBc(X509CertificateHolder x509CertificateHolder, ContentSigner contentSigner, TSAClient tsaClient) throws OperatorCreationException {
this.contentSigner = contentSigner;
this.tsaClient = tsaClient;
this.x509CertificateHolder = x509CertificateHolder;
digestCalculatorProvider = new JcaDigestCalculatorProviderBuilder().setProvider("BC").build();
}
#Override
public byte[] sign(InputStream content) throws IOException {
try {
CMSTypedData msg = new CMSTypedDataInputStream(content);
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
gen.addSignerInfoGenerator(
new JcaSignerInfoGeneratorBuilder(digestCalculatorProvider)
.setSignedAttributeGenerator(new PadesSignedAttributeGenerator())
.setUnsignedAttributeGenerator(new PadesUnsignedAttributeGenerator())
.build(contentSigner, x509CertificateHolder));
gen.addCertificates(new JcaCertStore(Collections.singleton(x509CertificateHolder)));
CMSSignedData sigData = gen.generate(msg, false);
return sigData.getEncoded();
} catch (OperatorCreationException | GeneralSecurityException | CMSException e) {
throw new IOException(e);
}
}
final ContentSigner contentSigner;
final X509CertificateHolder x509CertificateHolder;
final TSAClient tsaClient;
final DigestCalculatorProvider digestCalculatorProvider;
class CMSTypedDataInputStream implements CMSTypedData {
InputStream in;
public CMSTypedDataInputStream(InputStream is) {
in = is;
}
#Override
public ASN1ObjectIdentifier getContentType() {
return PKCSObjectIdentifiers.data;
}
#Override
public Object getContent() {
return in;
}
#Override
public void write(OutputStream out) throws IOException,
CMSException {
byte[] buffer = new byte[8 * 1024];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
in.close();
}
}
class PadesSignedAttributeGenerator implements CMSAttributeTableGenerator {
#Override
public AttributeTable getAttributes(#SuppressWarnings("rawtypes") Map params) throws CMSAttributeTableGenerationException {
String currentAttribute = null;
try {
ASN1EncodableVector signedAttributes = new ASN1EncodableVector();
currentAttribute = "SigningCertificateAttribute";
AlgorithmIdentifier digAlgId = (AlgorithmIdentifier) params.get(CMSAttributeTableGenerator.DIGEST_ALGORITHM_IDENTIFIER);
signedAttributes.add(createSigningCertificateAttribute(digAlgId));
currentAttribute = "ContentType";
ASN1ObjectIdentifier contentType = ASN1ObjectIdentifier.getInstance(params.get(CMSAttributeTableGenerator.CONTENT_TYPE));
signedAttributes.add(new Attribute(CMSAttributes.contentType, new DERSet(contentType)));
currentAttribute = "MessageDigest";
byte[] messageDigest = (byte[])params.get(CMSAttributeTableGenerator.DIGEST);
signedAttributes.add(new Attribute(CMSAttributes.messageDigest, new DERSet(new DEROctetString(messageDigest))));
return new AttributeTable(signedAttributes);
} catch (Exception e) {
throw new CMSAttributeTableGenerationException(currentAttribute, e);
}
}
Attribute createSigningCertificateAttribute(AlgorithmIdentifier digAlg) throws IOException, OperatorCreationException {
final IssuerSerial issuerSerial = getIssuerSerial();
DigestCalculator digestCalculator = digestCalculatorProvider.get(digAlg);
digestCalculator.getOutputStream().write(x509CertificateHolder.getEncoded());
final byte[] certHash = digestCalculator.getDigest();
if (OIWObjectIdentifiers.idSHA1.equals(digAlg.getAlgorithm())) {
final ESSCertID essCertID = new ESSCertID(certHash, issuerSerial);
SigningCertificate signingCertificate = new SigningCertificate(essCertID);
return new Attribute(id_aa_signingCertificate, new DERSet(signingCertificate));
} else {
ESSCertIDv2 essCertIdv2;
if (NISTObjectIdentifiers.id_sha256.equals(digAlg.getAlgorithm())) {
// SHA-256 is default
essCertIdv2 = new ESSCertIDv2(null, certHash, issuerSerial);
} else {
essCertIdv2 = new ESSCertIDv2(digAlg, certHash, issuerSerial);
}
SigningCertificateV2 signingCertificateV2 = new SigningCertificateV2(essCertIdv2);
return new Attribute(id_aa_signingCertificateV2, new DERSet(signingCertificateV2));
}
}
public IssuerSerial getIssuerSerial() {
final X500Name issuerX500Name = x509CertificateHolder.getIssuer();
final GeneralName generalName = new GeneralName(issuerX500Name);
final GeneralNames generalNames = new GeneralNames(generalName);
final BigInteger serialNumber = x509CertificateHolder.getSerialNumber();
return new IssuerSerial(generalNames, serialNumber);
}
}
class PadesUnsignedAttributeGenerator implements CMSAttributeTableGenerator {
#Override
public AttributeTable getAttributes(#SuppressWarnings("rawtypes") Map params) throws CMSAttributeTableGenerationException {
if (tsaClient == null)
return null;
try {
ASN1EncodableVector unsignedAttributes = new ASN1EncodableVector();
byte[] signature = (byte[])params.get(CMSAttributeTableGenerator.SIGNATURE);
byte[] timestamp = tsaClient.getTimeStampToken(new ByteArrayInputStream(signature)).getEncoded();
unsignedAttributes.add(new Attribute(id_aa_signatureTimeStampToken, new DERSet(ASN1Primitive.fromByteArray(timestamp))));
return new AttributeTable(unsignedAttributes);
} catch (Exception e) {
throw new CMSAttributeTableGenerationException("", e);
}
}
}
}
(PDFBox PadesSignatureContainerBc implementation of SignatureInterface)
You can use it like this
try ( PDDocument pdDocument = Loader.loadPDF(SOURCE_PDF) )
{
SignatureInterface signatureInterface = new PadesSignatureContainerBc(new X509CertificateHolder(chain[0].getEncoded()),
new JcaContentSignerBuilder("SHA512withRSA").build(pk),
new TSAClient(new URL("http://timestamp.server/rfc3161endpoint"), null, null, MessageDigest.getInstance("SHA-256")));
PDSignature signature = new PDSignature();
signature.setFilter(COSName.getPDFName("MKLx_PAdES_SIGNER"));
signature.setSubFilter(COSName.getPDFName("ETSI.CAdES.detached"));
signature.setName("Example User");
signature.setLocation("Los Angeles, CA");
signature.setReason("Testing");
signature.setSignDate(Calendar.getInstance());
pdDocument.addSignature(signature);
ExternalSigningSupport externalSigning = pdDocument.saveIncrementalForExternalSigning(RESULT_OUTPUT);
// invoke external signature service
byte[] cmsSignature = signatureInterface.sign(externalSigning.getContent());
// set signature bytes received from the service
externalSigning.setSignature(cmsSignature);
}
(PDFBox SignPadesBc test testSignPadesBaselineT)
I have a digitally signed pdf file and I am trying to verify the signature in that. But Signature is not getting verified. Below is the code
public static void main(String[] args) throws Exception {
HashMap<String, Object> keyPair = new HashMap<>();
String password = "123";
KeyStore keyStore = KeyStore.getInstance("pkcs12"); //, "BC");
keyStore.load(new FileInputStream("/Users/shivam/Documents/Projects/label-processor/src/main/resources/DigitalSignatureFile.pfx"), password.toCharArray());
Enumeration<String> keyStoreAliasEnum = keyStore.aliases();
String alias = null;
PublicKey publicKey = null;
PrivateKey privateKey = null;
byte[] signature = null;
while ( keyStoreAliasEnum.hasMoreElements() ) {
alias = keyStoreAliasEnum.nextElement();
if (password != null) {
privateKey = (PrivateKey) keyStore.getKey(alias, password.toCharArray());
X509Certificate x509Certificate = (X509Certificate) keyStore.getCertificate(alias);
publicKey = x509Certificate.getPublicKey();
signature = x509Certificate.getSignature();
keyPair.put("Alias", alias);
keyPair.put("PublicKey", publicKey);
keyPair.put("PrivateKey", privateKey);
keyPair.put("X509Certificate", x509Certificate);
}
}
verify(publicKey, signature);
}
public static void verify(PublicKey publicKey, byte[] sigBytes) throws Exception {
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initVerify(publicKey);
FileInputStream datafis = new FileInputStream("/Users/shivam/Desktop/DigitallySignedFile.pdf");
byte[] data = IOUtils.toByteArray(datafis);
sig.update(data);
boolean verifies = sig.verify(sigBytes);
System.out.println("signature verifies: " + verifies);
FileUtils.writeByteArrayToFile(
new File("/Users/shivam/Documents/Projects/label-processor/src/main/resources/VerifiedFile.pdf"),
data
);
}
Above program is giving "Signature verifies: false". And In VerifiedFile.pdf I am still seeing signature not verified. But when I am trying to verify in Adobe Reader it is verifying correctly.
I tried verifying with the help of itext library also. It is verifying signature But In pdf it still shows signature not verified.
public static void main(String[] args) throws Exception {
byte[] pdfBytes = Files.readAllBytes(
Paths.get("/Users/shivam/Desktop/DigitallySignedFile.pdf")
);
PdfReader pdfReader = new PdfReader(pdfBytes);
AcroFields acroFields = pdfReader.getAcroFields();
List<String> signatures = acroFields.getSignatureNames();
boolean valid = false;
if (!signatures.isEmpty()) {
for (String signature : signatures) {
if (acroFields.signatureCoversWholeDocument(signature)) {
PdfPKCS7 pkcs7 = acroFields.verifySignature(signature);
valid = pkcs7.verify();
String reason = pkcs7.getReason();
Calendar signedAt = pkcs7.getSignDate();
X509Certificate x509Certificate = pkcs7.getSigningCertificate();
Principal issuerDN = x509Certificate.getIssuerDN();
Principal subjectDN = x509Certificate.getSubjectDN();
logger.info("valid = {}, date = {}, reason = '{}', issuer = '{}', subject = '{}'",
valid, signedAt.getTime(), reason, issuerDN, subjectDN);
break;
}
}
}
FileUtils.writeByteArrayToFile(
new File("/Users/shivam/Documents/Projects/label-processor/src/main/resources/VerifiedFile.pdf"),
pdfBytes
);
}
I appreciate any help.
I am trying to sign a pdf document with a signature that comes from the entire client in format base 64.
the service makes a request to calculate the hash from the document
I take the content from the pdf of the document, calculate the hash from it according to the algorithm.
service takes the received hash and signs it, sends the received signature along with the bytes of the document to be signed
I get a string in base 64 and pdf bytes to be signed
Is it possiple case? I give a code example
public byte[] insertSignature(byte[] document, String signature) {
try (InputStream inputStream = new ByteArrayInputStream(document);
ByteArrayOutputStream os = new ByteArrayOutputStream();
ByteArrayOutputStream result = new ByteArrayOutputStream()) {
byte[] decodeSignature = Base64.decodeBase64(signature);
CAdESSignature cades = new CAdESSignature(decodeSignature, null, null);
var certificate = cades.getCAdESSignerInfo(0).getSignerCertificate();
var subject = new Subject(certificate.getSubjectX500Principal().getEncoded());
List<String> names = getSignaturesFields(document);
String sigFieldName = String.format("Signature %s", names.size() + 1);
PdfName filter = PdfName.Adobe_PPKLite;
PdfName subFilter = PdfName.ETSI_CAdES_DETACHED;
int estimatedSize = 8192;
PdfReader reader = new PdfReader(inputStream);
StampingProperties stampingProperties = new StampingProperties();
if (names.size() > 1) {
stampingProperties.useAppendMode();
}
PdfSigner signer = new PdfSigner(reader, os, stampingProperties);
signer.setCertificationLevel(PdfSigner.CERTIFIED_NO_CHANGES_ALLOWED);
PdfSignatureAppearance appearance = signer.getSignatureAppearance();
appearance
.setContact(subject.email().orElse(""))
.setSignatureCreator(subject.organizationName().orElse(""))
.setLocation(subject.country())
.setReuseAppearance(false)
.setPageNumber(1);
signer.setFieldName(sigFieldName);
ContainerForPrepareSignedDocument external = new ContainerForPrepareSignedDocument(filter, subFilter);
signer.signExternalContainer(external, estimatedSize);
byte[] preSignedBytes = os.toByteArray();
ContainerReadyToSignedDocument extSigContainer = new ContainerReadyToSignedDocument(decodeSignature);
PdfDocument docToSign = new PdfDocument(new PdfReader(new ByteArrayInputStream(preSignedBytes)));
PdfSigner.signDeferred(docToSign, sigFieldName, result, extSigContainer);
docToSign.close();
return result.toByteArray();
}
catch (IOException e) {
throw new InternalException("IO exception by insert signature to document:", e);
}
catch (GeneralSecurityException e) {
throw new InternalException("General security by insert signature to document:", e);
}
catch (CAdESException e) {
throw new InternalException("CAdESException by insert signature to document:", e);
}
}
private List<String> getSignaturesFields(byte[] document)
throws IOException {
try (InputStream inputStream = new ByteArrayInputStream(document);
PdfReader reader = new PdfReader(inputStream);
PdfDocument pdfDocument = new PdfDocument(reader)) {
SignatureUtil signUtil = new SignatureUtil(pdfDocument);
return signUtil.getSignatureNames();
}
}
static class ContainerForPrepareSignedDocument implements IExternalSignatureContainer {
private final PdfName filter;
private final PdfName subFilter;
public ContainerForPrepareSignedDocument(PdfName filter,
PdfName subFilter) {
this.filter = filter;
this.subFilter = subFilter;
}
public byte[] sign(InputStream docBytes) {
return new byte[0];
}
public void modifySigningDictionary(PdfDictionary signDic) {
signDic.put(PdfName.Filter, filter);
signDic.put(PdfName.SubFilter, subFilter);
}
}
static class ContainerReadyToSignedDocument implements IExternalSignatureContainer {
private byte[] cmsSignatureContents;
public ContainerReadyToSignedDocument(byte[] cmsSignatureContents) {
this.cmsSignatureContents = cmsSignatureContents;
}
public byte[] sign(InputStream docBytes) {
return cmsSignatureContents;
}
public void modifySigningDictionary(PdfDictionary signDic) {
}
}
I have written the below code to verify the signature of a file using a certificate that is there in my certificate store. But when I try to get its signature and pass it to the SignedData method, I am getting the below exception.
org.bouncycastle.cms.CMSException: Malformed content.
at org.bouncycastle.cms.CMSUtils.readContentInfo(Unknown Source)
at org.bouncycastle.cms.CMSUtils.readContentInfo(Unknown Source)
at org.bouncycastle.cms.CMSSignedData.<init>(Unknown Source)
at VerifyFinal.main(VerifyFinal.java:65)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:483)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
Caused by: java.lang.IllegalArgumentException: unknown object in getInstance: org.bouncycastle.asn1.DERApplicationSpecific
at org.bouncycastle.asn1.ASN1Sequence.getInstance(Unknown Source)
at org.bouncycastle.asn1.cms.ContentInfo.getInstance(Unknown Source)
... 9 more
Below is the code I used to verify the signature of the file.
Security.addProvider(new BouncyCastleProvider());
KeyStore msCertStore = KeyStore.getInstance("Windows-MY", "SunMSCAPI");
msCertStore.load(null, null);
X509Certificate cer = ((X509Certificate) msCertStore.getCertificate("Software View Certificate Authority"));
PublicKey pubKey = cer.getPublicKey();
byte[] sigToVerify = cer.getSignature();
Signature signature = Signature.getInstance("SHA1WithRSA", "BC");
signature.initVerify(pubKey);
CMSSignedData cms = new CMSSignedData(cer.getSignature());
Store store = cms.getCertificates();
SignerInformationStore signers = cms.getSignerInfos();
Collection c = signers.getSigners();
Iterator it = c.iterator();
while (it.hasNext()) {
SignerInformation signer = (SignerInformation) it.next();
Collection certCollection = store.getMatches(signer.getSID());
Iterator certIt = certCollection.iterator();
X509CertificateHolder certHolder = (X509CertificateHolder) certIt.next();
X509Certificate cert = new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder);
if (signer.verify(new JcaSimpleSignerInfoVerifierBuilder().setProvider("BC").build(cert))) {
System.out.println("verified");
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
In case you need, below is how I signed the file.
File file = new File("G:\\Projects\\test.zip");
fin = new FileInputStream(file);
byte fileContent[] = new byte[(int) file.length()];
Security.addProvider(new BouncyCastleProvider());
KeyStore ks = KeyStore.getInstance(KEYSTORE_INSTANCE);
ks.load(new FileInputStream(KEYSTORE_FILE), KEYSTORE_PWD.toCharArray());
Key key = ks.getKey(KEYSTORE_ALIAS, KEYSTORE_PWD.toCharArray());
//Sign
PrivateKey privKey = (PrivateKey) key;
Signature signature = Signature.getInstance("SHA1WithRSA", "BC");
signature.initSign(privKey);
signature.update(fileContent);
//Build CMS
X509Certificate cert = (X509Certificate) ks.getCertificate(KEYSTORE_ALIAS);
List certList = new ArrayList();
CMSTypedData msg = new CMSProcessableByteArray(signature.sign());
certList.add(cert);
Store certs = new JcaCertStore(certList);
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
ContentSigner sha1Signer = new JcaContentSignerBuilder("SHA1withRSA").setProvider("BC").build(privKey);
gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().setProvider("BC").build()).build(sha1Signer, cert));
gen.addCertificates(certs);
CMSSignedData sigData = gen.generate(msg, true);
BASE64Encoder encoder = new BASE64Encoder();
String signedContent = encoder.encode((byte[]) sigData.getSignedContent().getContent());
System.out.println("Signed content: " + signedContent + "\n");
String envelopedData = encoder.encode(sigData.getEncoded());
System.out.println("Enveloped data: " + envelopedData);
AFTER COMMENTS FROM VOLKERK :
How I generate the signature+data file :
public static void main(String[] args) throws Exception {
// String text = "This is a message";
// File file = new
// File("C:\\Users\\mayooranM\\Desktop\\SignatureVerificationTest\\ProcessExplorer.zip");
// fin = new FileInputStream(file);
// byte fileContent[] = new byte[(int) file.length()];
Path filepath = Paths.get("G:\\IntelliJTestProjects\\googleplaces.zip");
byte[] fileContent = Files.readAllBytes(filepath);
Security.addProvider(new BouncyCastleProvider());
KeyStore ks = KeyStore.getInstance(KEYSTORE_INSTANCE);
ks.load(new FileInputStream(KEYSTORE_FILE), KEYSTORE_PWD.toCharArray());
Key key = ks.getKey(KEYSTORE_ALIAS, KEYSTORE_PWD.toCharArray());
// Sign
PrivateKey privKey = (PrivateKey) key;
Signature signature = Signature.getInstance("SHA1WithRSA", "BC");
signature.initSign(privKey);
signature.update(fileContent);
// Build CMS
X509Certificate cert = (X509Certificate) ks.getCertificate(KEYSTORE_ALIAS);
List certList = new ArrayList();
CMSTypedData msg = new CMSProcessableByteArray(signature.sign());
certList.add(cert);
Store certs = new JcaCertStore(certList);
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
ContentSigner sha1Signer = new JcaContentSignerBuilder("SHA1withRSA").setProvider("BC").build(privKey);
gen.addSignerInfoGenerator(
new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().setProvider("BC").build())
.build(sha1Signer, cert));
gen.addCertificates(certs);
CMSSignedData sigData = gen.generate(msg, true);
BASE64Encoder encoder = new BASE64Encoder();
String signedContent = encoder.encode((byte[]) sigData.getSignedContent().getContent());
System.out.println("Signed content: " + signedContent + "\n");
String envelopedData = encoder.encode(sigData.getEncoded());
System.out.println("Enveloped data: " + envelopedData);
FileOutputStream fos = new FileOutputStream(
"G:\\IntelliJTestProjects\\SignedZip.zip");
fos.write(envelopedData.getBytes());
fos.close();
}
How I verify data:
public static void main(String[] args) {
try {
Security.addProvider(new BouncyCastleProvider());
Path path = Paths
.get("G:\\IntelliJTestProjects\\SignedZip.zip");
byte[] signedContent = Files.readAllBytes(path);
String output = new String(signedContent);
System.out.println("output: " + output);
CMSSignedData cms = new CMSSignedData(Base64.decode(signedContent));
Store store = cms.getCertificates();
SignerInformationStore signers = cms.getSignerInfos();
Collection c = signers.getSigners();
Iterator it = c.iterator();
while (it.hasNext()) {
SignerInformation signer = (SignerInformation) it.next();
Collection certCollection = store.getMatches(signer.getSID());
Iterator certIt = certCollection.iterator();
X509CertificateHolder certHolder = (X509CertificateHolder) certIt.next();
X509Certificate cert = new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder);
if (signer.verify(new JcaSimpleSignerInfoVerifierBuilder().setProvider("BC").build(cert))) {
System.out.println("verified");
}
}
CMSProcessable origData = cms.getSignedContent() ;
byte[] originalContent = (byte[]) origData.getContent();
ZipInputStream zipStream = new ZipInputStream(new ByteArrayInputStream(originalContent));
ZipEntry entry = null;
while ((entry = zipStream.getNextEntry()) != null) {
String entryName = entry.getName();
FileOutputStream out = new FileOutputStream(entryName);
byte[] byteBuff = new byte[4096];
int bytesRead = 0;
while ((bytesRead = zipStream.read(byteBuff)) != -1)
{
out.write(byteBuff, 0, bytesRead);
}
out.close();
zipStream.closeEntry();
}
zipStream.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
Part 2
Ok, now that you've got a file containing the pkcs7 signed data, let's try to retrieve the contents and verify the integrity ...and validity.
The goal is again not to load the whole thing into memory. Looks like CMSSignedDataParser can do that.
Since the documentation says
Note: that because we are in a streaming mode [...] it is important that the methods on the parser are called in the appropriate order.
So, let's first look at what we actual got so far. For that I used a text file containing the line Mary had a little lamb as the input file, instead of the .m4v (or the .zip in your case) and passed the result to http://lapo.it/asn1js/ (you got to love this tool....)
So, the actual contents comes before the signature data and we have to read the entries in the order they appear in the file. Would be easier the other way round, but ...
The idea is to write the contents to the target file regardless of whether it checks out or not. If it doesn't just delete file. (Drawback: If it contains e.g. a virus a virus scanner might be triggered ...too bad. I leave handling that up to you.)
public class SignedDataTest {
... see Part 1
private static void verify(Path signedFile, Path extractToFile) throws Exception {
FileInputStream fis = new FileInputStream(signedFile.toFile());
DigestCalculatorProvider build = new JcaDigestCalculatorProviderBuilder().setProvider("BC").build();
CMSSignedDataParser sp = new CMSSignedDataParser(build, fis);
// we have to read the whole stream sp.getSignedContent().getContentStream()
// just copy it to the target file
Files.copy(sp.getSignedContent().getContentStream(), extractToFile, StandardCopyOption.REPLACE_EXISTING);
// now we can go on with the other stuff.....
Store certStore = sp.getCertificates();
// the examples create a new instance of this for each certificate.
// I don't think that's necessary, but you might want to look into that...
JcaSimpleSignerInfoVerifierBuilder verifier = new JcaSimpleSignerInfoVerifierBuilder().setProvider("BC");
for (Object objSigner : sp.getSignerInfos().getSigners()) {
SignerInformation signer = (SignerInformation) objSigner;
// as I understand it, there should be only one match ....but anyways....
for (Object objMatch : certStore.getMatches(signer.getSID())) {
X509CertificateHolder certHolder = (X509CertificateHolder) objMatch;
System.out.print("verifying against " + certHolder.getSubject().toString());
if (signer.verify(verifier.build(certHolder))) {
System.out.println(": verified");
} else {
System.out.println(": no match");
}
}
}
}
}
So, what does this actually do/test for? It fetches the signer info from the pkcs7 signedData and then checks the hash and the signature againt the certificates that are contained in the signedData. Not good enough, I and any other attacker can put any certificate in there; so I create a new KeyPiar generate a selfsigned certificate for that key pair and put just any zip file in there I like, preferably a nasty phishing tool.
That's most likely the reason why you've used KeyStore.getInstance("Windows-MY", "SunMSCAPI") in your code; a KeyStore which you implictly trust. So, let's do just that.
Instead of building the SignerInformationVerifier from the data in the signedData file, we pass a ready-made verfier to the method. And this verifier is primed with the certificate from the windows "KeyStore". Btw: you cannot mix the BC and the SunMSCAPI providers arbitrarily; but you can mix them this way, i.e. have BC check the data integrity and SunMSCAPI check whether the hash has been signed by something considered trustworthy.
(sorry, got to go. I'll post just the complete sample class; there's a lot to say about it though ....actually one could write books about that ...actually actually books have been written about that topic ;-) )
public class SignedDataTest {
private static final File KEYSTORE_FILE = new File("c:\\temp\\Software_View_Certificate_Authority.p12");
private static final String KEYSTORE_TYPE = "pkcs12";
private static final char[] KEYSTORE_PWD = "foobar".toCharArray();
private static final String KEYSTORE_ALIAS = "Software View Certificate Authority";
private static final Path CONTENT_SRC_PATH = Paths.get("c:\\temp\\test.txt");
private static final Path CONTENT_TARGET_PATH = Paths.get("c:\\temp\\test-retrieved.txt");
private static final Path SIGNEDDATA_TARGET_PATH = Paths.get("c:\\temp\\test.txt.signed.pkcs7");
public static void main(String[] args) throws Exception {
Security.addProvider(new BouncyCastleProvider());
doForth();
andBack();
}
private static void doForth() throws Exception {
KeyStore ks = KeyStore.getInstance(KEYSTORE_TYPE, "BC");
ks.load(new FileInputStream(KEYSTORE_FILE), KEYSTORE_PWD);
X500PrivateCredential creds = new X500PrivateCredential(
(X509Certificate) ks.getCertificate(KEYSTORE_ALIAS),
(PrivateKey) ks.getKey(KEYSTORE_ALIAS, KEYSTORE_PWD)
);
createSignature(CONTENT_SRC_PATH, creds, new FileOutputStream(SIGNEDDATA_TARGET_PATH.toFile()));
}
private static void andBack() throws Exception {
KeyStore msCertStore = KeyStore.getInstance("Windows-MY", "SunMSCAPI");
msCertStore.load(null, null);
SignerInformationVerifier verifier = new JcaSimpleSignerInfoVerifierBuilder().setProvider("SunMSCAPI")
.build(((X509Certificate) msCertStore.getCertificate("Software View Certificate Authority")));
verify(SIGNEDDATA_TARGET_PATH, CONTENT_TARGET_PATH, verifier);
}
private static void verify(Path signedFile, Path extractToFile, SignerInformationVerifier verifier) throws Exception {
FileInputStream fis = new FileInputStream(signedFile.toFile());
DigestCalculatorProvider build = new JcaDigestCalculatorProviderBuilder().setProvider("BC").build();
CMSSignedDataParser sp = new CMSSignedDataParser(build, fis);
// we have to read the whole stream sp.getSignedContent().getContentStream()
// just copy it to the target file
Files.copy(sp.getSignedContent().getContentStream(), extractToFile, StandardCopyOption.REPLACE_EXISTING);
// now we can go on with the other stuff.....
Store certStore = sp.getCertificates();
// the examples create a new instance of this for each certificate.
// I don't think that's necessary, but you might want to look into that...
for (Object objSigner : sp.getSignerInfos().getSigners()) {
SignerInformation signer = (SignerInformation) objSigner;
if (signer.verify(verifier)) {
System.out.println("verified");
// now(!) you want to keep the target content file
} else {
// actually a "org.bouncycastle.cms.CMSSignerDigestMismatchException: message-digest attribute value does not match calculated value"
// exception will be thrown in case the contents has been altered
// So, you will need a try-catch(-finally?) construct to delete the target contents file in such cases....
System.out.println("no match");
}
}
}
private static void createSignature(Path srcfile, X500PrivateCredential creds, FileOutputStream target) throws Exception {
CMSSignedDataStreamGenerator gen = new CMSSignedDataStreamGenerator() {
{
addSignerInfoGenerator(
new JcaSignerInfoGeneratorBuilder(
new JcaDigestCalculatorProviderBuilder().setProvider("BC").build()
).build(
new JcaContentSignerBuilder("SHA1withRSA").setProvider("BC").build(creds.getPrivateKey()),
creds.getCertificate()
)
);
addCertificates(new JcaCertStore(new ArrayList<X509Certificate>() {
{
add(creds.getCertificate());
}
}));
}
};
try (OutputStream sigOut = gen.open(target, true)) {
Files.copy(srcfile, sigOut);
}
}
}
Part 1
Let's start with some transformations of the code. (I wonder how long an answer can be....)
Step 1: Not much going on here; just setting up the "framework" - and as you can see: yes, I'm really running/testing the code ;-)
public class SignedDataTest {
private static final File KEYSTORE_FILE = new File("c:\\temp\\Software_View_Certificate_Authority.p12");
private static final String KEYSTORE_TYPE = "pkcs12";
private static final char[] KEYSTORE_PWD = "foobar".toCharArray();
private static final String KEYSTORE_ALIAS = "Software View Certificate Authority";
private static final Path CONTENT_SRC_PATH = Paths.get("c:\\temp\\Londo Buttons are melting.m4v");
private static final Path CONTENT_TARGET_PATH = Paths.get("c:\\temp\\Londo Buttons are melting-retrieved.m4v");
private static final Path SIGNEDDATA_TARGET_PATH = Paths.get("c:\\temp\\Londo Buttons are melting-retrieved.m4v.signed.pkcs7");
public static void main(String[] args) throws Exception {
Security.addProvider(new BouncyCastleProvider());
createSignature();
}
private static void createSignature() throws Exception {
byte[] fileContent = Files.readAllBytes(CONTENT_SRC_PATH);
KeyStore ks = KeyStore.getInstance(KEYSTORE_TYPE, "BC");
ks.load(new FileInputStream(KEYSTORE_FILE), KEYSTORE_PWD);
Key key = ks.getKey(KEYSTORE_ALIAS, KEYSTORE_PWD);
// Sign
PrivateKey privKey = (PrivateKey)key;
Signature signature = Signature.getInstance("SHA1WithRSA", "BC");
signature.initSign(privKey);
signature.update(fileContent);
// Build CMS
X509Certificate cert = (X509Certificate) ks.getCertificate(KEYSTORE_ALIAS);
List certList = new ArrayList();
CMSTypedData msg = new CMSProcessableByteArray(signature.sign());
certList.add(cert);
Store certs = new JcaCertStore(certList);
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
ContentSigner sha1Signer = new JcaContentSignerBuilder("SHA1withRSA").setProvider("BC").build(privKey);
gen.addSignerInfoGenerator(
new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().setProvider("BC").build())
.build(sha1Signer, cert));
gen.addCertificates(certs);
CMSSignedData sigData = gen.generate(msg, true);
BASE64Encoder encoder = new BASE64Encoder();
String signedContent = encoder.encode((byte[]) sigData.getSignedContent().getContent());
System.out.println("Signed content: " + signedContent + "\n");
String envelopedData = encoder.encode(sigData.getEncoded());
System.out.println("Enveloped data: " + envelopedData);
FileOutputStream fos = new FileOutputStream(SIGNEDDATA_TARGET_PATH.toString());
fos.write(envelopedData.getBytes());
fos.close();
}
}
Step 2: This is probably the hardest; the transformation that makes the code look unlike your code the most. Take your time to understand what I'm doing here. I want to get rid of some of the unecessary stuff (e.g. the Base64 encoder) and get a bit more condensed code. This makes debugging a bit harder, since I removed most of the temporary variables, "hiding" them in the initializer block - doh, what's the proper name of that feature?)
public class SignedDataTest {
private static final File KEYSTORE_FILE = new File("c:\\temp\\Software_View_Certificate_Authority.p12");
private static final String KEYSTORE_TYPE = "pkcs12";
private static final char[] KEYSTORE_PWD = "foobar".toCharArray();
private static final String KEYSTORE_ALIAS = "Software View Certificate Authority";
private static final Path CONTENT_SRC_PATH = Paths.get("c:\\temp\\Londo Buttons are melting.m4v");
private static final Path CONTENT_TARGET_PATH = Paths.get("c:\\temp\\Londo Buttons are melting-retrieved.m4v");
private static final Path SIGNEDDATA_TARGET_PATH = Paths.get("c:\\temp\\Londo Buttons are melting-retrieved.m4v.signed.pkcs7");
public static void main(String[] args) throws Exception {
Security.addProvider(new BouncyCastleProvider());
doForth();
// doBack();
}
private static void doForth() throws Exception {
KeyStore ks = KeyStore.getInstance(KEYSTORE_TYPE, "BC");
ks.load(new FileInputStream(KEYSTORE_FILE), KEYSTORE_PWD);
X500PrivateCredential creds = new X500PrivateCredential(
(X509Certificate) ks.getCertificate(KEYSTORE_ALIAS),
(PrivateKey) ks.getKey(KEYSTORE_ALIAS, KEYSTORE_PWD)
);
createSignature(CONTENT_SRC_PATH, creds, new FileOutputStream(SIGNEDDATA_TARGET_PATH.toFile()));
}
private static void createSignature(Path srcfile, X500PrivateCredential creds, FileOutputStream target) throws Exception {
byte[] fileContent = Files.readAllBytes(CONTENT_SRC_PATH);
// Sign
Signature signature = Signature.getInstance("SHA1WithRSA", "BC");
signature.initSign(creds.getPrivateKey());
signature.update(fileContent);
CMSSignedDataGenerator gen = new CMSSignedDataGenerator() {
{
addSignerInfoGenerator(
new JcaSignerInfoGeneratorBuilder(
new JcaDigestCalculatorProviderBuilder().setProvider("BC").build()
).build(
new JcaContentSignerBuilder("SHA1withRSA").setProvider("BC").build(creds.getPrivateKey()),
creds.getCertificate()
)
);
addCertificates(new JcaCertStore(new ArrayList<X509Certificate>() {
{
add(creds.getCertificate());
}
}));
}
};
// Build CMS
CMSTypedData msg = new CMSProcessableByteArray(signature.sign());
CMSSignedData sigData = gen.generate(msg, true);
// write raw data instead of base64
target.write(sigData.getEncoded());
}
}
Step 3: This is probably the most important step: It changes the code from "cannot work as intended on a fundamental level" to "in principle this might work". You're creating a signature manually and then pass that signature to the CMSSignedDataGenerator as the message. In effect you're creating a signature of a signature; the "real" contents is lost. What you actually want to do is create a signature of the (file) contents:
private static void createSignature(Path srcfile, X500PrivateCredential creds, FileOutputStream target) throws Exception {
byte[] fileContent = Files.readAllBytes(CONTENT_SRC_PATH);
CMSSignedDataGenerator gen = new CMSSignedDataGenerator() {
...
};
// Build CMS
CMSTypedData msg = new CMSProcessableByteArray(fileContent);
CMSSignedData sigData = gen.generate(msg, true);
// write raw data instead of base64
target.write(sigData.getEncoded());
}
Step 4: You read the complete contents into memory. That might not be a good idea for a large input file.
private static void createSignature(Path srcfile, X500PrivateCredential creds, FileOutputStream target) throws Exception {
CMSSignedDataGenerator gen = new CMSSignedDataGenerator() {
...
};
// see https://www.bouncycastle.org/docs/pkixdocs1.4/org/bouncycastle/cms/CMSProcessableFile.html
CMSProcessableFile msg = new CMSProcessableFile(srcfile.toFile());
CMSSignedData sigData = gen.generate(msg, true);
// write raw data instead of base64
target.write(sigData.getEncoded());
}
Step 5: Again memory usage: gen.generate(msg, true): The true parameter means that the complete msg is included in the asn1-structure. When you call .getEncoded() you get a byte array of the complete asn1-structure, i.e. you have the complete file in memory again. RAM is cheap, but let's try to avoid that anyway. There's another generator called CMSSignedDataStreamGenerator which seems to offer what we need. Instead of working on byte arrays, you give it an OutputStream it can write the result to, and you get OutputStream you can write the contents to:
private static void createSignature(Path srcfile, X500PrivateCredential creds, FileOutputStream target) throws Exception {
CMSSignedDataStreamGenerator gen = new CMSSignedDataStreamGenerator() {
{
addSignerInfoGenerator(
new JcaSignerInfoGeneratorBuilder(
new JcaDigestCalculatorProviderBuilder().setProvider("BC").build()
).build(
new JcaContentSignerBuilder("SHA1withRSA").setProvider("BC").build(creds.getPrivateKey()),
creds.getCertificate()
)
);
addCertificates(new JcaCertStore(new ArrayList<X509Certificate>() {
{
add(creds.getCertificate());
}
}));
}
};
try (OutputStream sigOut = gen.open(target, true)) {
Files.copy(srcfile, sigOut);
}
}
So much for creating the signed message. I will post the verify-part in another answer - but have to work for real/for a living for a while.....
edit: There's probably still room to post the complete/final sample class
public class SignedDataTest {
private static final File KEYSTORE_FILE = new File("c:\\temp\\Software_View_Certificate_Authority.p12");
private static final String KEYSTORE_TYPE = "pkcs12";
private static final char[] KEYSTORE_PWD = "foobar".toCharArray();
private static final String KEYSTORE_ALIAS = "Software View Certificate Authority";
private static final Path CONTENT_SRC_PATH = Paths.get("c:\\temp\\Londo Buttons are melting.m4v");
private static final Path CONTENT_TARGET_PATH = Paths.get("c:\\temp\\Londo Buttons are melting-retrieved.m4v");
private static final Path SIGNEDDATA_TARGET_PATH = Paths.get("c:\\temp\\Londo Buttons are melting-retrieved.m4v.signed.pkcs7");
public static void main(String[] args) throws Exception {
Security.addProvider(new BouncyCastleProvider());
doForth();
//doBack();
}
private static void doForth() throws Exception {
KeyStore ks = KeyStore.getInstance(KEYSTORE_TYPE, "BC");
ks.load(new FileInputStream(KEYSTORE_FILE), KEYSTORE_PWD);
X500PrivateCredential creds = new X500PrivateCredential(
(X509Certificate) ks.getCertificate(KEYSTORE_ALIAS),
(PrivateKey) ks.getKey(KEYSTORE_ALIAS, KEYSTORE_PWD)
);
createSignature(CONTENT_SRC_PATH, creds, new FileOutputStream(SIGNEDDATA_TARGET_PATH.toFile()));
}
private static void createSignature(Path srcfile, X500PrivateCredential creds, FileOutputStream target) throws Exception {
CMSSignedDataStreamGenerator gen = new CMSSignedDataStreamGenerator() {
{
addSignerInfoGenerator(
new JcaSignerInfoGeneratorBuilder(
new JcaDigestCalculatorProviderBuilder().setProvider("BC").build()
).build(
new JcaContentSignerBuilder("SHA1withRSA").setProvider("BC").build(creds.getPrivateKey()),
creds.getCertificate()
)
);
addCertificates(new JcaCertStore(new ArrayList<X509Certificate>() {
{
add(creds.getCertificate());
}
}));
}
};
try (OutputStream sigOut = gen.open(target, true)) {
Files.copy(srcfile, sigOut);
}
}
}