このコメントで判明したように
私が欲しいのはAdobe LTV対応です
このタスクは、PAdES にあまり関連していませんが (PAdES で導入されたメカニズムが使用されていますが)、アドビ独自の署名プロファイルである「LTV 対応」署名に焦点を当てています。
残念ながら、この独自の署名プロファイルは適切に指定されていません。アドビが教えてくれるのは、
LTV が有効になっているということは、ファイルの検証に必要なすべての情報 (ルート証明書を除く) が含まれていることを意味します。
(詳細と背景については、この回答をお読みください)
したがって、LTV を有効にする方法を実装してサンプルの署名を試行錯誤する必要があり、Adobe が今後の Adobe Acrobat バージョンでこのコードの出力を「LTV 有効」として受け入れることを保証することはできません。
さらに、現在の iText 5 シグネチャ API は、すぐに使用できるタスクには十分ではありません。これは、(判明したように) Adobe が、iText コードが作成しない特定のオプションの構造を必要とするためです (ただし、以下の PPS を参照してください)。これを修正する最も簡単な方法は、iText クラスLtvVerificationを 2 つの側面で更新することだったので、ここではその方法について説明します。あるいは、Java リフレクションを使用するか、かなりの量のコードをコピーして微調整することもできます。以下に示すように iText を更新できない場合は、そのような代替アプローチのいずれかを選択する必要があります。
署名済み PDF の署名を有効にする LTV
このセクションでは、OP のサンプル PDF のようなドキュメントを LTV で有効にするためのコードの追加と変更を示しますsign_without_LTV.pdf。
iTextのLtvVerificationクラスを使ったアプローチ
これは、LtvVerificationiText の署名 API のクラスを使用する元のコードです。残念ながら、これには機能をそのクラスに追加する必要があります。
パッチ適用LtvVerification
iText 5LtvVerificationクラスはaddVerification、署名フィールド名を受け入れるメソッドのみを提供します。これらのメソッドの機能は、フォーム フィールドにバインドされていない署名 (OCSP 応答署名など) にも必要です。このために、そのメソッドの次のオーバーロードを追加しました。
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;
}
さらに、最終的な VRI 辞書への (仕様によるとオプションの) 時間エントリが必要です (ただし、以下の PPS を参照してください)。outputDssしたがって、次のようにメソッドに a 行を追加しました。
...
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());
...
いくつかの低レベルのヘルパー メソッド
セキュリティ プリミティブで動作するいくつかのヘルパー メソッドが必要です。これらのメソッドの大部分は、既存の iText クラス (プライベートであるためそのままでは使用できません) から収集されたか、そこにあるコードから派生したものです。
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);
}
( MakeLtvEnabledのように)
それらはまだ最適化されていませんが、確実にパフォーマンスとエレガントさを向上させることができます。
LTV情報の追加
これらの追加とヘルパーに基づいて、LTV が有効な署名に必要な LTV 情報をこのメソッドで追加できます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);
} catch (IOException | GeneralSecurityException e) {
e.printStackTrace();
}
},
moreToCheck::put
);
}
}
void addLtvForChain(X509Certificate certificate, OcspClient ocspClient, CrlClient crlClient, VriAdder vriAdder,
BiConsumer<PdfName, X509Certificate> moreSignersAndCertificates) throws GeneralSecurityException, IOException, StreamParsingException, OperatorCreationException, OCSPException {
List<byte[]> ocspResponses = new ArrayList<>();
List<byte[]> crls = new ArrayList<>();
List<byte[]> certs = new ArrayList<>();
while (certificate != null) {
System.out.println(certificate.getSubjectX500Principal().getName());
X509Certificate issuer = getIssuerCertificate(certificate);
certs.add(certificate.getEncoded());
byte[] ocspResponse = ocspClient.getEncoded(certificate, issuer, null);
if (ocspResponse != null) {
System.out.println(" with OCSP response");
ocspResponses.add(ocspResponse);
X509Certificate ocspSigner = getOcspSignerCertificate(ocspResponse);
if (ocspSigner != null) {
System.out.printf(" signed by %s\n", ocspSigner.getSubjectX500Principal().getName());
}
moreSignersAndCertificates.accept(getOcspSignatureKey(ocspResponse), ocspSigner);
} else {
Collection<byte[]> crl = crlClient.getEncoded(certificate, null);
if (crl != null && !crl.isEmpty()) {
System.out.printf(" with %s CRLs\n", crl.size());
crls.addAll(crl);
for (byte[] crlBytes : crl) {
moreSignersAndCertificates.accept(getCrlSignatureKey(crlBytes), null);
}
}
}
certificate = issuer;
}
vriAdder.accept(ocspResponses, crls, certs);
}
interface VriAdder {
void accept(Collection<byte[]> ocsps, Collection<byte[]> crls, Collection<byte[]> certs);
}
( MakeLtvEnabled as makeLtvEnabledV2)
使用例
署名済み PDFINPUT_PDFと結果出力ストリームRESULT_STREAMの場合、上記の方法を次のように使用できます。
PdfReader pdfReader = new PdfReader(INPUT_PDF);
PdfStamper pdfStamper = new PdfStamper(pdfReader, RESULT_STREAM, (char)0, true);
OcspClient ocsp = new OcspClientBouncyCastle();
CrlClient crl = new CrlClientOnline();
makeLtvEnabledV2(pdfStamper, ocsp, crl);
pdfStamper.close();
( MakeLtvEnabledテスト方法testV2)
制限事項
上記のメソッドは、いくつかの単純化の制限の下でのみ機能します。特に:
- 署名のタイム スタンプは無視されます。
- 取得された CRL は、直接的かつ完全であると想定されます。
- 完全な証明書チェーンは、AIA エントリを使用して構築できると想定されています。
これらの制限が受け入れられない場合は、それに応じてコードを改善できます。
独自のユーティリティ クラスを使用したアプローチ
iText クラスにパッチを適用する必要がないように、このアプローチでは、上記のメソッドから必要なコードを取得し、LtvVerificationiText の署名 API からクラスを取得して、すべてを新しいユーティリティ クラスにマージします。このクラスは、パッチを適用した iText バージョンを必要とせずにドキュメントを LTV 対応にすることができます。
AdobeLtvEnablingクラス_
このクラスは、上記のコードといくつかのコードを組み合わせて、LtvVerificationLTV を有効にするドキュメントのユーティリティ クラスにします。
残念ながら、ここにコピーすると、メッセージ サイズがスタック オーバーフローの 30000 文字の制限を超えてしまいます。ただし、github からコードを取得できます。
AdobeLtvEnabling.java
使用例
署名済み PDFINPUT_PDFと結果出力ストリームRESULT_STREAMの場合、上記のクラスを次のように使用できます。
PdfReader pdfReader = new PdfReader(INPUT_PDF);
PdfStamper pdfStamper = new PdfStamper(pdfReader, RESULT_STREAM, (char)0, true);
AdobeLtvEnabling adobeLtvEnabling = new AdobeLtvEnabling(pdfStamper);
OcspClient ocsp = new OcspClientBouncyCastle();
CrlClient crl = new CrlClientOnline();
adobeLtvEnabling.enable(ocsp, crl);
pdfStamper.close();
( MakeLtvEnabledテスト方法testV3)
制限事項
このユーティリティ クラスは最初のアプローチのコードを再パッケージ化するだけなので、同じ制限が適用されます。
舞台裏
最初に述べたように、アドビが「LTV 対応」署名プロファイルについて私たちに伝えているのは、
LTV が有効になっているということは、ファイルの検証に必要なすべての情報 (ルート証明書を除く) が含まれていることを意味します。
しかし、情報がファイル内にどの程度正確に埋め込まれているかについては教えてくれません。
最初は、この情報をすべて収集し、該当する PDF の Document Security Store 辞書 ( Certs 、OCSPs 、および CRLs ) に追加されていることを確認しました。
しかし、ファイルの検証に必要なすべての情報 (ルート証明書を除く) が に含まれていたにもかかわらず、Adobe Acrobat はファイルが「LTV 対応」であるとは見なしませんでした。
次に、LTV は Adobe Acrobat を使用してドキュメントを有効にし、相違点を分析しました。結局のところ、次の追加データも必要でした (ただし、以下の PPS を参照してください)。
各 OCSP 応答の署名について、Adobe Acrobat では、それぞれのVRIディクショナリが存在する必要があります。OP のサンプル PDF では、この VRI ディクショナリに証明書、CRL、または OCSP 応答を含める必要はまったくありませんが、VRIディクショナリが存在する必要があります。
対照的に、これはCRL の署名には必要ありません。これは少し恣意的に見えます。
ISO 32000-2 と ETSI EN 319 142-1 の仕様によると、これらのVRI辞書の使用は完全にオプションです。PAdES BASELINE 署名については、VRI辞書を使用しないようにという勧告もあります!
Adobe Acrobat は、VRIディクショナリに、それぞれのVRIディクショナリの作成時刻を文書化したTUエントリが含まれていることを想定しています。(おそらくTSもそうするでしょう、私はそれをテストしていません)。
ISO 32000-2 と ETSI EN 319 142-1 の仕様によると、これらのTUエントリの使用は完全にオプションです。PAdES 署名については、TUまたはTSエントリを使用しないようにという勧告もあります!
したがって、PDF 仕様に従ってアプリケーションによって追加されたデフォルトの LTV 情報が、Adobe Acrobat によって報告された「LTV 対応」署名にならないことは驚くべきことではありません。
PS
明らかに、OPのドキュメント「LTV有効」に対する上記のコードの結果をまったく考慮させるために、Adobe Acrobatで証明書の信頼を追加する必要がありました。ルート証明書「CA RAIZ NACIONAL - COSTA RICA v2」を選択しました。
PPS (2020-03-02)
一方、Adobe Acrobat では、LTV が有効な状態をテストする際に DSS で失効情報を考慮するために、 VRI辞書 ( TUタイム スタンプはもちろん)の存在が不要になったようです。この回答の「DSS のオプション要素」セクションを参照してください。
したがって、上記のソリューションは多少単純化できる可能性が非常に高いです。