Created
June 23, 2023 18:52
-
-
Save maxfindel/af4afccc6261d7053946dbe2dd837b14 to your computer and use it in GitHub Desktop.
Adding LTV data to document signed with HexaPDF ruby gem
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# This gist assumes you have a signed document, the certificate chain used to sign it and the certificate chain of a TSA (if you used one) | |
# signed_doc = HexaPDF::Document.open(File.join(base_path, 'signed-document.pdf')) | |
# sig_cert_chain = [end_user_cert, intermediate_cert, root_cert] | |
# tsa_cert_chain = [tsa_cert, tsa_intermediate_cert, tsa_root_cert] | |
# optional: output_path = File.join(base_path, 'signed-document-with-ltv.pdf') | |
# STEP 1: The base structure is added as indirect references all | |
signed_doc.catalog[:DSS] = signed_doc.add({}) | |
signed_doc.catalog[:DSS][:CRLs] = signed_doc.add([]) | |
signed_doc.catalog[:DSS][:Certs] = signed_doc.add([]) | |
signed_doc.catalog[:DSS][:OCSPs] = signed_doc.add([]) | |
signed_doc.catalog[:DSS][:VRI] = signed_doc.add({}) | |
# STEP 2: The VRI dictionary keys are determined according to standard | |
# Note from the ISO 32000-2:2020 standard: The key of each entry in this dictionary is the base-16-encoded (uppercase) SHA-1 digest of the signature to which it applies. For a document signature or document timestamp signatures, the bytes that are hashed are those of the complete hexadecimal string, including zero padding, in the Contents entry of the associated signature dictionary, containing the signature's DER-encoded binary data object (e.g. CMS or CAdES objects). | |
# Note: The output should look like this 0A3E031BF1038653D740FA303E96A8ABB7B0ADD9 | |
signature = signed_doc.signatures.first | |
sig_contents = signature[:Contents] | |
sig_key = OpenSSL::Digest.new('SHA1').hexdigest(sig_contents).upcase | |
signed_doc.catalog[:DSS][:VRI][sig_key.to_sym] = signed_doc.add({}) | |
# STEP 3: Store the end-user cert, the root cert and then all the TSA certs (if applies) | |
certs_list = sig_cert_chain + tsa_cert_chain | |
certs_list.each do |loaded_cert_file| | |
data = loaded_cert_file.to_der | |
cert_ref = signed_doc.add({ Filter: [:FlateDecode], Length: data.size }, stream: data) | |
signed_doc.catalog[:DSS][:Certs].insert(signed_doc.catalog[:DSS][:Certs].length, cert_ref) | |
end | |
# STEP 4: The DER-encoded CRL responses are stored, then referenced if needed for validation | |
crls_to_validate = [sig_cert_chain.first, tsa_cert_chain. first] | |
crl_vlidation_ref = nil | |
crls_to_validate.each do |loaded_cert_file| | |
(loaded_cert_file.crl_uris || []).each do |crl_uri| | |
# Note: This request uses HTTP (SSL: false) and will fail if you have a rule forcing HTTPS | |
crl_tempfile = URI.parse(crl_uri).open.read rescue nil | |
next if crl_tempfile.blank? | |
crl = OpenSSL::X509::CRL::new(crl_tempfile) rescue nil | |
# This validation is needed because there can malformed responses with 200 status | |
next if crl.blank? | |
data = crl.to_der | |
crl_ref = signed_doc.add({ Filter: [:FlateDecode], Length: data.size }, stream: data) | |
crl_vlidation_ref ||= crl_ref | |
signed_doc.catalog[:DSS][:CRLs].insert(signed_doc.catalog[:DSS][:CRLs].length, crl_ref) | |
end | |
end | |
# Note: Here I use the first successful CRL validation, but could be improved to make sure it's sufficient | |
if crl_vlidation_ref.present? | |
signed_doc.catalog[:DSS][:VRI][sig_key.to_sym][:CRL] = signed_doc.add([]) | |
signed_doc.catalog[:DSS][:VRI][sig_key.to_sym][:CRL].insert(0, crl_vlidation_ref) | |
end | |
# STEP 5: The DER-encoded CRL OCSP responses are stored, then referenced if needed for validationdigest = OpenSSL::Digest.new('SHA1') | |
ocsps_to_validate = [sig_cert_chain, [tsa_cert_chain.second, tsa_cert_chain.fourth]] | |
ocsp_vlidation_ref = nil | |
ocsps_to_validate.each do |certificate_chain| | |
# An OCSP Certificate ID is created using the end-cert as subject and the next one as issuer | |
certificate_id = OpenSSL::OCSP::CertificateId.new(certificate_chain.first, certificate_chain.second, digest) | |
(certificate_chain.first.ocsp_uris || []).each do |ocsp_str| | |
# An OCSP Request is created using the previous certificate_id | |
ocsp_request = OpenSSL::OCSP::Request.new | |
ocsp_request.add_certid certificate_id | |
ocsp_request.add_nonce | |
# Note: This request uses HTTP (SSL: false) and will fail if you have a rule forcing HTTPS | |
ocsp_uri = URI(ocsp_str) | |
http_response = Net::HTTP.post( | |
ocsp_uri, | |
ocsp_request.to_der, | |
'content-type' => 'application/ocsp-request' | |
) rescue nil | |
# If there's no valid response, we can't add it to the DSS dictionary | |
next if !http_response.kind_of?(Net::HTTPOK) | |
ocsp = OpenSSL::OCSP::Response.new(http_response.body) rescue nil | |
# This validation is needed because there can malformed responses with 200 status | |
next if ocsp.blank? || ocsp.status_string != 'successful' | |
data = ocsp.to_der | |
ocsp_ref = signed_doc.add({ Filter: [:FlateDecode], Length: data.size }, stream: data) | |
ocsp_vlidation_ref ||= ocsp_ref | |
signed_doc.catalog[:DSS][:OCSPs].insert(signed_doc.catalog[:DSS][:OCSPs].length, ocsp_ref) | |
end | |
end | |
# The OCSP validation is used only of no successful CRL validation was found | |
if crl_vlidation_ref.blank? && ocsp_vlidation_ref.present? | |
signed_doc.catalog[:DSS][:VRI][sig_key.to_sym][:OCSP] = signed_doc.add([]) | |
signed_doc.catalog[:DSS][:VRI][sig_key.to_sym][:OCSP].insert(0, ocsp_vlidation_ref) | |
end | |
# STEP 6 (optional): If you write the result to file, make sure to make the changes incremental | |
signed_doc.write(output_path, incremental: true) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment