Skip to content

Instantly share code, notes, and snippets.

@maxfindel
Created June 23, 2023 18:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save maxfindel/af4afccc6261d7053946dbe2dd837b14 to your computer and use it in GitHub Desktop.
Save maxfindel/af4afccc6261d7053946dbe2dd837b14 to your computer and use it in GitHub Desktop.
Adding LTV data to document signed with HexaPDF ruby gem
# 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