As it turned out in this comment
i want is Adobe LTV-enable
the task is less PAdES related (even though mechanisms introduced in PAdES are used) but focused on an Adobe proprietary signature profile, "LTV enabled" signatures.
Unfortunately, this proprietary signature profile is not properly specified. All Adobe tells us is
LTV enabled means that all information necessary to validate the file (minus root certs) is contained within.
(for details and backgrounds read this answer)
Thus, implementing a way to LTV enable the example signature involved some trial and error, and I cannot guarantee Adobe will accept the outputs of this code as "LTV enabled" in Adobe Acrobat versions to come.
Furthermore, the current iText 5 signature APIs do not suffice out of the box for the task because (as it turned out) Adobe requires certain otherwise optional structures which the iText code does not create (but see the PPS below). The most simple way to fix this was to update the iText class LtvVerification
in two aspects, so I'll describe that way here. Alternatively one could have used Java reflection or copied and tweaked quite a bit of code; if you cannot update iText as shown below, you'll have to chose one such alternative approach.
LTV enabling the signatures of a signed PDF
This section shows the code additions and changes with which one can LTV enable documents like the OP's example PDF sign_without_LTV.pdf
.
An approach using iText's LtvVerification
class
This is the original code which makes use of the LtvVerification
class from iText's signature API. Unfortunately for this a functionality has to be added to that class.
Patching LtvVerification
The iText 5 LtvVerification
class only offers addVerification
methods accepting a signature field name. We need the functionality of these methods also for signatures not bound to a form field, e.g. for OCSP response signatures. For this I added the following overload of that method:
public boolean addVerification(PdfName signatureHash, Collection<byte[]> ocsps, Collection<byte[]> crls, Collection<byte[]> certs) throws IOException, GeneralSecurityException {
if (used)
throw new IllegalStateException(MessageLocalization.getComposedMessage("verification.already.output"));
ValidationData vd = new ValidationData();
if (ocsps != null) {
for (byte[] ocsp : ocsps) {
vd.ocsps.add(buildOCSPResponse(ocsp));
}
}
if (crls != null) {
for (byte[] crl : crls) {
vd.crls.add(crl);
}
}
if (certs != null) {
for (byte[] cert : certs) {
vd.certs.add(cert);
}
}
validated.put(signatureHash, vd);
return true;
}
Furthermore, a (per the specification optional) time entry in the final VRI dictionaries is required (but see the PPS below). Thus, I added the a line in the outputDss
method as follows:
...
if (ocsp.size() > 0)
vri.put(PdfName.OCSP, writer.addToBody(ocsp, false).getIndirectReference());
if (crl.size() > 0)
vri.put(PdfName.CRL, writer.addToBody(crl, false).getIndirectReference());
if (cert.size() > 0)
vri.put(PdfName.CERT, writer.addToBody(cert, false).getIndirectReference());
// v--- added line
vri.put(PdfName.TU, new PdfDate());
// ^--- added line
vrim.put(vkey, writer.addToBody(vri, false).getIndirectReference());
...
Some low level helper methods
Some helper methods operating on security primitives is required. These methods mostly have been collected from existing iText classes (which could not be used as is because they are private) or derived from code there:
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 PdfName getOcspSignatureKey(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(Utilities.convertToHex(octetHash));
return octetName;
}
static PdfName getCrlSignatureKey(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(Utilities.convertToHex(octetHash));
return octetName;
}
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 IOException(MessageLocalization.getComposedMessage("invalid.http.response.1", con.getResponseCode()));
}
InputStream inp = (InputStream) con.getContent();
byte[] buf = new byte[1024];
ByteArrayOutputStream bout = new ByteArrayOutputStream();
while (true) {
int n = inp.read(buf, 0, buf.length);
if (n <= 0)
break;
bout.write(buf, 0, n);
}
inp.close();
X509CertParser parser = new X509CertParser();
parser.engineInit(new ByteArrayInputStream(bout.toByteArray()));
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");
}
static byte[] hashBytesSha1(byte[] b) throws NoSuchAlgorithmException {
MessageDigest sh = MessageDigest.getInstance("SHA1");
return sh.digest(b);
}
(as in MakeLtvEnabled)
They aren't optimized yet, one certainly can make them more performant and more elegant.
Adding LTV information
Based on these additions and helpers one can add the LTV information required for LTV enabled signatures with this method makeLtvEnabled
:
public void makeLtvEnabled(PdfStamper stp, OcspClient ocspClient, CrlClient crlClient) throws IOException, GeneralSecurityException, StreamParsingException, OperatorCreationException, OCSPException {
stp.getWriter().addDeveloperExtension(new PdfDeveloperExtension(PdfName.ADBE, new PdfName("1.7"), 8));
LtvVerification v = stp.getLtvVerification();
AcroFields fields = stp.getAcroFields();
Map<PdfName, X509Certificate> moreToCheck = new HashMap<>();
ArrayList<String> names = fields.getSignatureNames();
for (String name : names)
{
PdfPKCS7 pdfPKCS7 = fields.verifySignature(name);
List<X509Certificate> certificatesToCheck = new ArrayList<>();
certificatesToCheck.add(pdfPKCS7.getSigningCertificate());
while (!certificatesToCheck.isEmpty()) {
X509Certificate certificate = certificatesToCheck.remove(0);
addLtvForChain(certificate, ocspClient, crlClient,
(ocsps, crls, certs) -> {
try {
v.addVerification(name, ocsps, crls, certs);
} catch (IOException | GeneralSecurityException e) {
e.printStackTrace();
}
},
moreToCheck::put
);
}
}
while (!moreToCheck.isEmpty()) {
PdfName key = moreToCheck.keySet().iterator().next();
X509Certificate certificate = moreToCheck.remove(key);
addLtvForChain(certificate, ocspClient, crlClient,
(ocsps, crls, certs) -> {
try {
v.addVerification(key, ocsps, crls, certs);