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:
commit
c48044419e
11 changed files with 191 additions and 7 deletions
41
app/lib/certigna/api.rb
Normal file
41
app/lib/certigna/api.rb
Normal 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
|
58
app/lib/certigna/authorities.crt
Normal file
58
app/lib/certigna/authorities.crt
Normal 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-----
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'] %>
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
{"dossier1": "hash1", "dossier2": "hash2"}
|
{"1":"hash1","2":"hash2"}
|
BIN
spec/fixtures/files/bill_signature/signature.der
vendored
BIN
spec/fixtures/files/bill_signature/signature.der
vendored
Binary file not shown.
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 à l’empreinte/)
|
||||||
|
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
|
||||||
|
|
Loading…
Reference in a new issue