class BillSignature < ApplicationRecord has_many :dossier_operation_logs has_one_attached :serialized has_one_attached :signature validate :check_bill_digest validate :check_serialized_bill_contents validate :check_signature_contents def self.build_with_operations(operations, day) bill = new(dossier_operation_logs: operations) bill.serialize_operations(day) bill end def serialize_operations(day) serialized.attach( io: StringIO.new(operations_bill_json), filename: "demarches-simplifiees-operations-#{day.to_date.iso8601}.json", content_type: 'application/json', # we don't want to run virus scanner on this file metadata: { virus_scan_result: ActiveStorage::VirusScanner::SAFE } ) self.digest = operations_bill_digest end def operations_bill dossier_operation_logs.map { |op| [op.id.to_s, op.digest] }.to_h end def operations_bill_json operations_bill.to_json end def operations_bill_digest Digest::SHA256.hexdigest(operations_bill_json) end def set_signature(signature, day) self.signature.attach( io: StringIO.new(signature), filename: "demarches-simplifiees-signature-#{day.to_date.iso8601}.der", content_type: 'application/x-x509-ca-cert', # we don't want to run virus scanner on this file metadata: { virus_scan_result: ActiveStorage::VirusScanner::SAFE } ) end # Validations def check_bill_digest if digest != operations_bill_digest errors.add(:digest) end end def check_serialized_bill_contents if !serialized.attached? errors.add(:serialized, :blank) return end if JSON.parse(read_serialized) != operations_bill errors.add(:serialized) end end def check_signature_contents if !signature.attached? errors.add(:signature, :blank) return end timestamp_signature_date = ASN1::Timestamp.signature_time(read_signature) if timestamp_signature_date > Time.zone.now errors.add(:signature, :invalid_date) end timestamp_signed_digest = ASN1::Timestamp.signed_digest(read_signature) if timestamp_signed_digest != digest errors.add(:signature) end end def read_signature read_attachment('signature') end def read_serialized read_attachment('serialized') end private def read_attachment(attachment) if attachment_changes[attachment] io = io_for_changes(attachment_changes[attachment]) if io.present? io.rewind result = io.read io.rewind result end elsif serialized.attached? serialized.download end end def io_for_changes(attachment_changes) attachable = attachment_changes.attachable case attachable when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile attachable.open when Hash attachable.fetch(:io) end end end