Merge pull request #8206 from demarches-simplifiees/certigna_timestamp

feat(timestamp): utilise l'horodatage de certigna et vérifie par openssl la validité du jeton
This commit is contained in:
LeSim 2022-12-07 19:46:13 +01:00 committed by GitHub
commit c48044419e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 191 additions and 7 deletions

41
app/lib/certigna/api.rb Normal file
View file

@ -0,0 +1,41 @@
class Certigna::API
## Certigna Timestamp POST API
# the CAfile used to controle the timestamp token is build:
# curl http://autorite.certigna.fr/ACcertigna.crt http://autorite.certigna.fr/entityca.crt > authorities.crt
def self.ensure_properly_configured!
if userpwd.blank?
raise StandardError, 'Certigna API is not properly configured'
end
end
def self.timestamp(data)
ensure_properly_configured!
response = Typhoeus.post(
CERTIGNA_API_URL,
userpwd: userpwd,
body: body(data)
)
if response.success?
response.body
else
raise StandardError, "Certigna timestamp query failed: #{response.status_message}"
end
end
private
def self.body(data)
{
'hashAlgorithm': 'SHA256',
'certReq': 'true',
'hashedMessage': data
}
end
def self.userpwd
Rails.application.secrets.certigna[:userpwd]
end
end

View file

@ -0,0 +1,58 @@
-----BEGIN CERTIFICATE-----
MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV
BAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4X
DTA3MDYyOTE1MTMwNVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQ
BgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3
DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4
QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyHGxny
gQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbw
zBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q
130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2
JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkw
DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEw
ZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYT
AkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzj
AQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG
9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8h
bV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFnc
fca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9qcEQCYsuu
HWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w
t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw
WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIGFTCCBP2gAwIBAgIRAPlPiLTzBzmpCdNvjtZMWaYwDQYJKoZIhvcNAQELBQAw
NDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwIQ2Vy
dGlnbmEwHhcNMTUxMTI1MTAyNDA3WhcNMjUxMTIyMTAyNDA3WjB7MQswCQYDVQQG
EwJGUjESMBAGA1UECgwJREhJTVlPVElTMRwwGgYDVQQLDBMwMDAyIDQ4MTQ2MzA4
MTAwMDM2MR0wGwYDVQRhDBROVFJGUi00ODE0NjMwODEwMDAzNjEbMBkGA1UEAwwS
Q2VydGlnbmEgRW50aXR5IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC
AgEA0lTrAwYaOJpvbeSPdj8l4DVUCP2td5ZpJ6YBETkj0DEyairmzh+QdoqJusE0
bPu7DeivGYLaIfZP53fVi+OM8/TfiQK61B53kgwI1tL1nEhdLd7G8G7SrKs0HGSo
fk6KMWqn4t2kVkkvotuH5RzxW/TrYyF9kUrBvqcAqUigpPtNv2w6JRmEN3QpYYDj
G4Bwz4J3RMRMtq5z6T1F55hX1hFP7FcavN+q4GiJ6m7LMhwiwh7nHU71lmLhUrYH
AIrygN/eQobCnV8HiSxBm0eFtnJDXWUd7KdkdJ02E67aclV625Vwzw+0OKH20XBq
O4tO8Yy0TuJnZQGfA/tSxdfp7hxNWlXFHvkaRGTfj9y6tv9WVZrgcNbaXR4c3gjE
/GuhGDCqt32ogTKu37cHMBxCRXCgMWEAyz4tBDVT8/UZzDbyFJOe63aroZdoohI4
XkEj50fYT6+AoEl0UoctswbPzq1PdIzXb+u8ki4FZK/2sTwCKJxifplzj9eFcv0m
cEVyYozVPAPzAo1aksK1/KWSCdHB2Odo5dhLlyiG/AmVcdA7NlQEPg6fSkJcbZV5
kNnmkI9bgQv0iVFO9y0s1AlzfFcIrFCLS/A1UsL6+59H1ifSubehmD4ms8At83/N
jyAjEJCs7UvIGkSFe54ifSnIdB03KZaz6cnFIwPy7ThPU4kCAwEAAaOCAdkwggHV
MBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSl
Px4kTGz4i9IbcphGUMrohlW52DBkBgNVHSMEXTBbgBQa7f5BOZC0JFm+AfJS1UX2
WjncEaE4pDYwNDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczERMA8G
A1UEAwwIQ2VydGlnbmGCCQD+3OMBD8lI/zBJBgNVHSAEQjBAMD4GCiqBegGBMQEA
AQIwMDAuBggrBgEFBQcCARYiaHR0cHM6Ly93d3cuY2VydGlnbmEuZnIvYXV0b3Jp
dGVzLzB8BggrBgEFBQcBAQRwMG4wNAYIKwYBBQUHMAKGKGh0dHA6Ly9hdXRvcml0
ZS5jZXJ0aWduYS5mci9jZXJ0aWduYS5kZXIwNgYIKwYBBQUHMAKGKmh0dHA6Ly9h
dXRvcml0ZS5kaGlteW90aXMuY29tL2NlcnRpZ25hLmRlcjBhBgNVHR8EWjBYMCmg
J6AlhiNodHRwOi8vY3JsLmNlcnRpZ25hLmZyL2NlcnRpZ25hLmNybDAroCmgJ4Yl
aHR0cDovL2NybC5kaGlteW90aXMuY29tL2NlcnRpZ25hLmNybDANBgkqhkiG9w0B
AQsFAAOCAQEAq05OtV0WmCyLpzxxcw98xPV8OiY56TQch4KEGotY1Dfw7H0nM7la
p/6kAZkUwfI4WJt4piRedJHgasQOj+aJbpxCYX7Hv7vCnGDKZFC6c/vY3B/stbQR
GTl//7+iKB2fgB/FoQOohrZqO77C0tqp1KuTfCSq/XAyZNtXoVn+YDaILMUHp2vs
lSHh131XPGjkloeXTMLf/+RcVVimMyAUjwdfcIUQEGU4Z5suQNWiwCU89l1UY7qa
WC9As/GqWo3vqQG9ALG+HBFBr1HSPnUjW88J6CYUVxOcTDfAj3eJtvUF/3dFjPl6
jM4rAJNeg+LqU6kQ+z/50yhrdgJ/A8cZiQ==
-----END CERTIFICATE-----

View file

@ -109,7 +109,9 @@ class BillSignature < ApplicationRecord
io = io_for_changes(attachment_changes[attachment]) io = io_for_changes(attachment_changes[attachment])
if io.present? if io.present?
io.rewind io.rewind
io.read result = io.read
io.rewind
result
end end
elsif serialized.attached? elsif serialized.attached?
serialized.download serialized.download

View file

@ -9,8 +9,41 @@ class BillSignatureService
def self.sign_operations(operations, day) def self.sign_operations(operations, day)
bill = BillSignature.build_with_operations(operations, day) bill = BillSignature.build_with_operations(operations, day)
signature = Universign::API.timestamp(bill.digest) signature = Certigna::API.timestamp(bill.digest)
bill.set_signature(signature, day) bill.set_signature(signature, day)
bill.save! bill.save!
ensure_valid_signature(bill.reload)
rescue => error
operations.each { |o| o.update(bill_signature: nil) }
bill&.destroy
raise error
end
def self.ensure_valid_signature(bill)
Dir.mktmpdir do |dir|
operations_path = File.join(dir, 'operations')
File.write(operations_path, bill.serialized.download, mode: 'wb')
signature_path = File.join(dir, 'signature')
File.write(signature_path, bill.signature.download, mode: 'wb')
authorities_path = Rails.application.config.root.join('app', 'lib', 'certigna', 'authorities.crt').to_s
verify_cmd = "openssl ts -verify -CAfile #{authorities_path.shellescape} -data #{operations_path.shellescape} -in #{signature_path.shellescape} -token_in"
openssl_errors = nil
openssl_output = nil
process_status = Open3.popen3(verify_cmd) do |_stdin, stdout, stderr, wait_thr|
openssl_errors = stderr.read
openssl_output = stdout.read
wait_thr.value
end
if !process_status.success? || openssl_output&.strip != 'Verification: OK'
raise StandardError, "openssl verification failed: #{openssl_errors}"
end
end
end end
end end

View file

@ -10,6 +10,7 @@ PIPEDRIVE_API_URL = ENV.fetch("PIPEDRIVE_API_URL", "https://api.pipedrive.com/v1
SENDINBLUE_API_URL = ENV.fetch("SENDINBLUE_API_URL", "https://in-automate.sendinblue.com/api/v2") SENDINBLUE_API_URL = ENV.fetch("SENDINBLUE_API_URL", "https://in-automate.sendinblue.com/api/v2")
SENDINBLUE_API_V3_URL = ENV.fetch("SENDINBLUE_API_V3_URL", "https://api.sendinblue.com/v3") SENDINBLUE_API_V3_URL = ENV.fetch("SENDINBLUE_API_V3_URL", "https://api.sendinblue.com/v3")
UNIVERSIGN_API_URL = ENV.fetch("UNIVERSIGN_API_URL", "https://ws.universign.eu/tsa/post/") UNIVERSIGN_API_URL = ENV.fetch("UNIVERSIGN_API_URL", "https://ws.universign.eu/tsa/post/")
CERTIGNA_API_URL = ENV.fetch("CERTIGNA_API_URL", "https://timestamp.dhimyotis.com/api/v1/")
FEATURE_UPVOTE_URL = ENV.fetch("FEATURE_UPVOTE_URL", "https://demarches-simplifiees.featureupvote.com") FEATURE_UPVOTE_URL = ENV.fetch("FEATURE_UPVOTE_URL", "https://demarches-simplifiees.featureupvote.com")
# Internal URLs # Internal URLs

View file

@ -76,6 +76,8 @@ defaults: &defaults
client_key: <%= ENV['CRISP_CLIENT_KEY'] %> client_key: <%= ENV['CRISP_CLIENT_KEY'] %>
universign: universign:
userpwd: <%= ENV['UNIVERSIGN_USERPWD'] %> userpwd: <%= ENV['UNIVERSIGN_USERPWD'] %>
certigna:
userpwd: <%= ENV['CERTIGNA_USERPWD'] %>
autocomplete: autocomplete:
api_geo_url: <%= ENV['API_GEO_URL'] %> api_geo_url: <%= ENV['API_GEO_URL'] %>
api_adresse_url: <%= ENV['API_ADRESSE_URL'] %> api_adresse_url: <%= ENV['API_ADRESSE_URL'] %>

View file

@ -1 +1 @@
{"dossier1": "hash1", "dossier2": "hash2"} {"1":"hash1","2":"hash2"}

Binary file not shown.

View file

@ -4,13 +4,13 @@ describe ASN1::Timestamp do
describe '.timestamp_time' do describe '.timestamp_time' do
subject { described_class.signature_time(asn1timestamp) } subject { described_class.signature_time(asn1timestamp) }
it { is_expected.to eq Time.zone.parse('2019-04-30 15:30:20 UTC') } it { is_expected.to eq Time.zone.parse('2022-12-06 09:11:17Z') }
end end
describe '.timestamp_signed_data' do describe '.timestamp_signed_data' do
subject { described_class.signed_digest(asn1timestamp) } subject { described_class.signed_digest(asn1timestamp) }
let(:data) { Digest::SHA256.hexdigest('CECI EST UN BLOB') } let(:data) { Digest::SHA256.hexdigest('{"1":"hash1","2":"hash2"}') }
it { is_expected.to eq data } it { is_expected.to eq data }
end end

View file

@ -73,8 +73,8 @@ RSpec.describe BillSignature, type: :model do
describe 'check_signature_contents' do describe 'check_signature_contents' do
let(:signature) { File.open('spec/fixtures/files/bill_signature/signature.der') } let(:signature) { File.open('spec/fixtures/files/bill_signature/signature.der') }
let(:signature_date) { DateTime.parse('2019-04-30 15:30:20') } let(:signature_date) { DateTime.parse('2022-12-06 11:00:00') }
let(:signature_digest) { Digest::SHA256.hexdigest('CECI EST UN BLOB') } let(:signature_digest) { Digest::SHA256.hexdigest('{"1":"hash1","2":"hash2"}') }
let(:current_date) { Time.zone.now } let(:current_date) { Time.zone.now }
before do before do

View file

@ -32,4 +32,51 @@ describe BillSignatureService do
it { is_expected.to eq 0 } it { is_expected.to eq 0 }
end end
end end
describe ".sign_operations" do
let(:date) { Date.today }
let(:operations_hash) { [['1', 'hash1'], ['2', 'hash2']] }
let(:operations) do
operations_hash
.map { |id, digest| DossierOperationLog.new(id:, digest:, operation: 'accepter') }
end
let(:timestamp) { File.read('spec/fixtures/files/bill_signature/signature.der') }
subject { BillSignatureService.sign_operations(operations, date) }
before do
DossierOperationLog.where(id: [1, 2]).destroy_all
expect(Certigna::API).to receive(:timestamp).and_return(timestamp)
end
context "when everything is fine" do
it do
expect { subject }.not_to raise_error
expect(BillSignature.count).to eq(1)
end
end
context "when the digest does not match with the pre recorded timestamp token" do
let(:operations_hash) { [['1', 'hash1'], ['2', 'hash3']] }
it do
expect { subject }.to raise_error(/La validation a échoué : signature ne correspond pas à lempreinte/)
expect(BillSignature.count).to eq(0)
end
end
context "when the timestamp token cannot be verified by openssl" do
let(:timestamp) do
File.read('spec/fixtures/files/bill_signature/signature.der').tap { |s| s[-1] = 'd' }
end
it do
expect { subject }.to raise_error(/openssl verification failed/)
expect(BillSignature.count).to eq(0)
end
end
end
end end