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])
|
||||
if io.present?
|
||||
io.rewind
|
||||
io.read
|
||||
result = io.read
|
||||
io.rewind
|
||||
result
|
||||
end
|
||||
elsif serialized.attached?
|
||||
serialized.download
|
||||
|
|
|
@ -9,8 +9,41 @@ class BillSignatureService
|
|||
|
||||
def self.sign_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.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
|
||||
|
|
|
@ -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_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/")
|
||||
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")
|
||||
|
||||
# Internal URLs
|
||||
|
|
|
@ -76,6 +76,8 @@ defaults: &defaults
|
|||
client_key: <%= ENV['CRISP_CLIENT_KEY'] %>
|
||||
universign:
|
||||
userpwd: <%= ENV['UNIVERSIGN_USERPWD'] %>
|
||||
certigna:
|
||||
userpwd: <%= ENV['CERTIGNA_USERPWD'] %>
|
||||
autocomplete:
|
||||
api_geo_url: <%= ENV['API_GEO_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
|
||||
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
|
||||
|
||||
describe '.timestamp_signed_data' do
|
||||
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 }
|
||||
end
|
||||
|
|
|
@ -73,8 +73,8 @@ RSpec.describe BillSignature, type: :model do
|
|||
|
||||
describe 'check_signature_contents' do
|
||||
let(:signature) { File.open('spec/fixtures/files/bill_signature/signature.der') }
|
||||
let(:signature_date) { DateTime.parse('2019-04-30 15:30:20') }
|
||||
let(:signature_digest) { Digest::SHA256.hexdigest('CECI EST UN BLOB') }
|
||||
let(:signature_date) { DateTime.parse('2022-12-06 11:00:00') }
|
||||
let(:signature_digest) { Digest::SHA256.hexdigest('{"1":"hash1","2":"hash2"}') }
|
||||
let(:current_date) { Time.zone.now }
|
||||
|
||||
before do
|
||||
|
|
|
@ -32,4 +32,51 @@ describe BillSignatureService do
|
|||
it { is_expected.to eq 0 }
|
||||
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
|
||||
|
|
Loading…
Reference in a new issue