zatca-sdk 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +8 -0
  4. data/LICENSE +21 -0
  5. data/README.md +60 -0
  6. data/Rakefile +4 -0
  7. data/bin/console +15 -0
  8. data/bin/setup +8 -0
  9. data/lib/zatca/client.rb +211 -0
  10. data/lib/zatca/hacks.rb +45 -0
  11. data/lib/zatca/hashing.rb +18 -0
  12. data/lib/zatca/qr_code_extractor.rb +31 -0
  13. data/lib/zatca/qr_code_generator.rb +28 -0
  14. data/lib/zatca/signing/certificate.rb +78 -0
  15. data/lib/zatca/signing/csr.rb +220 -0
  16. data/lib/zatca/signing/ecdsa.rb +59 -0
  17. data/lib/zatca/tag.rb +44 -0
  18. data/lib/zatca/tags.rb +46 -0
  19. data/lib/zatca/tags_schema.rb +22 -0
  20. data/lib/zatca/types.rb +7 -0
  21. data/lib/zatca/ubl/base_component.rb +142 -0
  22. data/lib/zatca/ubl/builder.rb +166 -0
  23. data/lib/zatca/ubl/common_aggregate_components/allowance_charge.rb +64 -0
  24. data/lib/zatca/ubl/common_aggregate_components/classified_tax_category.rb +25 -0
  25. data/lib/zatca/ubl/common_aggregate_components/delivery.rb +27 -0
  26. data/lib/zatca/ubl/common_aggregate_components/invoice_line.rb +63 -0
  27. data/lib/zatca/ubl/common_aggregate_components/item.rb +21 -0
  28. data/lib/zatca/ubl/common_aggregate_components/legal_monetary_total.rb +59 -0
  29. data/lib/zatca/ubl/common_aggregate_components/party.rb +28 -0
  30. data/lib/zatca/ubl/common_aggregate_components/party_identification.rb +25 -0
  31. data/lib/zatca/ubl/common_aggregate_components/party_legal_entity.rb +19 -0
  32. data/lib/zatca/ubl/common_aggregate_components/party_tax_scheme.rb +30 -0
  33. data/lib/zatca/ubl/common_aggregate_components/postal_address.rb +59 -0
  34. data/lib/zatca/ubl/common_aggregate_components/price.rb +20 -0
  35. data/lib/zatca/ubl/common_aggregate_components/tax_category.rb +56 -0
  36. data/lib/zatca/ubl/common_aggregate_components/tax_total.rb +58 -0
  37. data/lib/zatca/ubl/common_aggregate_components.rb +2 -0
  38. data/lib/zatca/ubl/invoice.rb +481 -0
  39. data/lib/zatca/ubl/invoice_subtype_builder.rb +50 -0
  40. data/lib/zatca/ubl/signing/cert.rb +48 -0
  41. data/lib/zatca/ubl/signing/invoice_signed_data_reference.rb +44 -0
  42. data/lib/zatca/ubl/signing/key_info.rb +25 -0
  43. data/lib/zatca/ubl/signing/object.rb +20 -0
  44. data/lib/zatca/ubl/signing/qualifying_properties.rb +27 -0
  45. data/lib/zatca/ubl/signing/signature.rb +50 -0
  46. data/lib/zatca/ubl/signing/signature_information.rb +19 -0
  47. data/lib/zatca/ubl/signing/signature_properties_reference.rb +26 -0
  48. data/lib/zatca/ubl/signing/signed_info.rb +21 -0
  49. data/lib/zatca/ubl/signing/signed_properties.rb +81 -0
  50. data/lib/zatca/ubl/signing/signed_signature_properties.rb +23 -0
  51. data/lib/zatca/ubl/signing/ubl_document_signatures.rb +25 -0
  52. data/lib/zatca/ubl/signing/ubl_extension.rb +22 -0
  53. data/lib/zatca/ubl/signing/ubl_extensions.rb +17 -0
  54. data/lib/zatca/ubl/signing.rb +2 -0
  55. data/lib/zatca/ubl.rb +2 -0
  56. data/lib/zatca/version.rb +5 -0
  57. data/lib/zatca.rb +48 -0
  58. data/zatca_sdk.gemspec +52 -0
  59. metadata +301 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fdfd39a00f730f2ea0756c860af60ed558c3ca6b85d232cfa2aeb8dfddd82de3
4
+ data.tar.gz: 232904047e0134193d9d7af053eb541274088fa2de5e990e3053e3c804a5fbf9
5
+ SHA512:
6
+ metadata.gz: 40979739078ed6468c27df72ad6814a89b96f40595e9df89862a8d08d11515656afcab048922e55f666a14fc5d8f3202ecd5c42d5d62d64c04ed3cbbab4e848e
7
+ data.tar.gz: 7774f40179b60921c575d081a9b633ef7fddea630b61a827d89a2bb75c9c8b4a1be997508ab65345aa43afcc924091721774facbb07139c286e9ecb3481b4c46
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --require spec_helper
2
+ -I lib/zatca
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in zatca.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Mrsool
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # zatca
2
+ ![](https://img.shields.io/gem/v/zatca) ![](https://img.shields.io/github/actions/workflow/status/obahareth/zatca/test.yml?branch=main)
3
+
4
+ A Ruby library for generating QR Codes and e-invoices according to the standard created by ZATCA in Saudi Arabia.
5
+
6
+ This library supports both Phase 1 and Phase 2. Phase 2 support is still new so there may be bugs. Please [report any issues](https://github.com/obahareth/zatca/issues/new) you find.
7
+
8
+ # Installation
9
+
10
+ ## Rubygems
11
+ ```sh
12
+ gem install zatca
13
+ ```
14
+
15
+ ## Bundler
16
+ ```sh
17
+ bundle add zatca
18
+ ```
19
+
20
+ # Usage
21
+
22
+ ## Phase 1
23
+ ```rb
24
+ require "zatca"
25
+
26
+ tags = {
27
+ seller_name: "Mrsool",
28
+ vat_registration_number: "310228833400003",
29
+ timestamp: "2021-10-20T19:29:32+03:00",
30
+ vat_total: "15",
31
+ invoice_total: "115",
32
+ }
33
+
34
+ ZATCA.render_qr_code(tags: tags)
35
+ # => data:image/png;base64,...
36
+ # Hint (Try pasting the above into your web browser's address bar)
37
+ ```
38
+
39
+ If you'd like to customize the size of the QR Code you can manually use the generator like so:
40
+
41
+ ```rb
42
+ require "zatca"
43
+
44
+ tags = ZATCA::Tags.new({
45
+ seller_name: "Mrsool",
46
+ vat_registration_number: "310228833400003",
47
+ timestamp: "2021-10-20T19:29:32+03:00",
48
+ vat_total: "15",
49
+ invoice_total: "115",
50
+ })
51
+
52
+ generator = ZATCA::QRCodeGenerator.new(tags: tags)
53
+ generator.render(size: 512)
54
+ ```
55
+
56
+ ## Phase 2
57
+ Documentation lives in the [wiki](https://github.com/obahareth/zatca/wiki)
58
+
59
+ # Notice of Non-Affiliation and Disclaimer
60
+ This library is not affiliated, associated, authorized, endorsed by, or in any way officially connected with ZATCA (Zakat, Tax and Customs Authority), or any of its subsidiaries or its affiliates. The official ZATCA website can be found at https://zatca.gov.sa.
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ task default: %i[]
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require_relative "../lib/zatca.rb"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,211 @@
1
+ require "httpx"
2
+ require "json"
3
+
4
+ # This wraps the API described here:
5
+ # https://sandbox.zatca.gov.sa/IntegrationSandbox
6
+ class ZATCA::Client
7
+ attr_accessor :before_submitting_request, :before_parsing_response
8
+
9
+ # API URLs are not present in developer portal, they can only be found in a PDF
10
+ # called Fatoora Portal User Manual, here:
11
+ # https://zatca.gov.sa/en/E-Invoicing/Introduction/Guidelines/Documents/Fatoora%20portal%20user%20manual.pdf
12
+ PRODUCTION_BASE_URL = "https://gw-fatoora.zatca.gov.sa/e-invoicing/core".freeze
13
+ SANDBOX_BASE_URL = "https://gw-apic-gov.gazt.gov.sa/e-invoicing/developer-portal".freeze
14
+ SIMULATION_BASE_URL = "https://gw-fatoora.zatca.gov.sa/e-invoicing/simulation".freeze
15
+
16
+ ENVIRONMENTS_TO_URLS_MAP = {
17
+ production: PRODUCTION_BASE_URL,
18
+ sandbox: SANDBOX_BASE_URL,
19
+ simulation: SIMULATION_BASE_URL
20
+ }.freeze
21
+
22
+ DEFAULT_API_VERSION = "V2".freeze
23
+ LANGUAGES = %w[ar en].freeze
24
+
25
+ def initialize(
26
+ username:,
27
+ password:,
28
+ language: "ar",
29
+ version: DEFAULT_API_VERSION,
30
+ environment: :production,
31
+ verbose: false,
32
+ before_submitting_request: nil,
33
+ before_parsing_response: nil
34
+ )
35
+ raise "Invalid language: #{language}, Please use one of: #{LANGUAGES}" unless LANGUAGES.include?(language)
36
+
37
+ @username = username
38
+ @password = password
39
+ @language = language
40
+ @version = version
41
+ @verbose = verbose
42
+
43
+ @base_url = ENVIRONMENTS_TO_URLS_MAP[environment.to_sym] || PRODUCTION_BASE_URL
44
+
45
+ @before_submitting_request = before_submitting_request
46
+ @before_parsing_response = before_parsing_response
47
+ end
48
+
49
+ # Reporting API
50
+ def report_invoice(uuid:, invoice_hash:, invoice:, cleared:)
51
+ request(
52
+ path: "invoices/reporting/single",
53
+ method: :post,
54
+ body: {
55
+ uuid: uuid,
56
+ invoiceHash: invoice_hash,
57
+ invoice: invoice
58
+ },
59
+ headers: {
60
+ "Clearance-Status" => cleared ? "1" : "0"
61
+ }
62
+ )
63
+ end
64
+
65
+ # Clearance API
66
+ def clear_invoice(uuid:, invoice_hash:, invoice:, cleared:)
67
+ request(
68
+ path: "invoices/clearance/single",
69
+ method: :post,
70
+ body: {
71
+ uuid: uuid,
72
+ invoiceHash: invoice_hash,
73
+ invoice: invoice
74
+ },
75
+ headers: {
76
+ "Clearance-Status" => cleared ? "1" : "0"
77
+ }
78
+ )
79
+ end
80
+
81
+ # Compliance CSID API
82
+ # This should be used to obtain credentials to issue a certificate in the next
83
+ # request (issue_production_csid)
84
+ #
85
+ # csid stands for Cryptographic Stamp Identifier
86
+ #
87
+ # csr stands for Certificate Signing Request
88
+ # You should generate this via the ZATCA::Signing::CSR class
89
+ #
90
+ # otp stands for One Time Password.
91
+ # You can get this from the fatoora portal
92
+ # Returns:
93
+ # {
94
+ # "binarySecurityToken": "string" # To be used as a username in next request
95
+ # "secret": "string" # To be used as a password in next request
96
+ # }
97
+ def issue_csid(csr:, otp:)
98
+ request(
99
+ path: "compliance",
100
+ method: :post,
101
+ body: {csr: csr},
102
+ headers: {"OTP" => otp},
103
+ authenticated: false
104
+ )
105
+ end
106
+
107
+ # Compliance Invoice API
108
+ def compliance_check(uuid:, invoice_hash:, invoice:)
109
+ request(
110
+ path: "compliance/invoices",
111
+ method: :post,
112
+ body: {
113
+ uuid: uuid,
114
+ invoiceHash: invoice_hash,
115
+ invoice: invoice
116
+ }
117
+ )
118
+ end
119
+
120
+ # Production CSID (Onboarding) API
121
+ # This endpoint gives you the Base64-encoded certificate back
122
+ # compliance_request_id is retrieved from the issue_csid request, and is
123
+ # in the response as responseID
124
+ def issue_production_csid(compliance_request_id:)
125
+ request(
126
+ path: "production/csids",
127
+ method: :post,
128
+ body: {compliance_request_id: compliance_request_id}
129
+ )
130
+ end
131
+
132
+ # Production CSID (Renewal) API
133
+ # csr stands for Certificate Signing Request
134
+ # otp stands for One Time Password
135
+ def renew_production_csid(otp:, csr:)
136
+ request(
137
+ path: "production/csids",
138
+ method: :patch,
139
+ body: {csr: csr},
140
+ headers: {"OTP" => otp}
141
+ )
142
+ end
143
+
144
+ private
145
+
146
+ def request(method:, path:, body: {}, headers: {}, authenticated: true)
147
+ url = "#{@base_url}/#{path}"
148
+ headers = default_headers.merge(headers)
149
+
150
+ before_submitting_request&.call(method, url, body, headers)
151
+ log("Requesting #{method} #{url} with\n\nbody: #{body}\n\nheaders: #{headers}\n")
152
+
153
+ client = if authenticated
154
+ authenticated_request_cilent
155
+ else
156
+ unauthenticated_request_client
157
+ end
158
+
159
+ response = client.send(method, url, json: body, headers: headers)
160
+ before_parsing_response&.call(response)
161
+ log("Raw response: #{response}")
162
+
163
+ if response.instance_of?(HTTPX::ErrorResponse)
164
+ return {
165
+ message: response.error&.message,
166
+ details: response.to_s
167
+ }
168
+ end
169
+
170
+ response_body = response.body.to_s
171
+
172
+ parsed_body = if response.headers["Content-Type"] == "application/json"
173
+ parse_json_or_return_string(response_body)
174
+ else
175
+ response_body
176
+ end
177
+
178
+ log("Response body: #{parsed_body}")
179
+
180
+ parsed_body
181
+ end
182
+
183
+ def authenticated_request_cilent
184
+ HTTPX.plugin(:basic_authentication).basic_auth(@username, @password)
185
+ end
186
+
187
+ def unauthenticated_request_client
188
+ HTTPX
189
+ end
190
+
191
+ def default_headers
192
+ {
193
+ "Accept-Language" => @language,
194
+ "Content-Type" => "application/json",
195
+ "Accept-Version" => @version
196
+ }
197
+ end
198
+
199
+ def parse_json_or_return_string(json)
200
+ JSON.parse(json)
201
+ rescue JSON::ParserError
202
+ json
203
+ end
204
+
205
+ def log(message)
206
+ return unless @verbose
207
+ message = "\n\n---------------------\n\n#{message}\n\n"
208
+
209
+ puts message
210
+ end
211
+ end
@@ -0,0 +1,45 @@
1
+ module ZATCA::Hacks
2
+ extend self
3
+
4
+ # rubocop:disable Layout/HeredocIndentation
5
+ # rubocop:disable Layout/ClosingHeredocIndentation
6
+ # ZATCA also hashes serverside to ensure our signed properties hash is correct.
7
+ # However ZATCA does not format the XML to use the same whitespace needed for
8
+ # hashing. They generate the hash using the whitespace as you sent it, so to
9
+ # account for that we need to ensure we use the same exact whitespace as them.
10
+ #
11
+ # Due to the way our SDK works, we will sadly not be able to use the same
12
+ # generated XML, we need to use ZATCA's specific spacing.
13
+ # So we will generate the entire XML first then replace the qualifying
14
+ # properties block to account for this.
15
+ def zatca_indented_qualifying_properties(signing_time:, cert_digest_value:, cert_issuer_name:, cert_serial_number:)
16
+ <<-XML.chomp
17
+ <xades:QualifyingProperties xmlns:xades="http://uri.etsi.org/01903/v1.3.2#" Target="signature">
18
+ <xades:SignedProperties Id="xadesSignedProperties">
19
+ <xades:SignedSignatureProperties>
20
+ <xades:SigningTime>#{signing_time}</xades:SigningTime>
21
+ <xades:SigningCertificate>
22
+ <xades:Cert>
23
+ <xades:CertDigest>
24
+ <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
25
+ <ds:DigestValue>#{cert_digest_value}</ds:DigestValue>
26
+ </xades:CertDigest>
27
+ <xades:IssuerSerial>
28
+ <ds:X509IssuerName>#{cert_issuer_name}</ds:X509IssuerName>
29
+ <ds:X509SerialNumber>#{cert_serial_number}</ds:X509SerialNumber>
30
+ </xades:IssuerSerial>
31
+ </xades:Cert>
32
+ </xades:SigningCertificate>
33
+ </xades:SignedSignatureProperties>
34
+ </xades:SignedProperties>
35
+ </xades:QualifyingProperties>
36
+ XML
37
+ end
38
+
39
+ # rubocop:enable Layout/HeredocIndentation
40
+ # rubocop:enable Layout/ClosingHeredocIndentation
41
+
42
+ def qualifying_properties_regex
43
+ /[ ]*<xades:QualifyingProperties.*?<\/xades:QualifyingProperties>/m
44
+ end
45
+ end
@@ -0,0 +1,18 @@
1
+ class ZATCA::Hashing
2
+ # Returns the content as:
3
+ # - hash - SHA256 digest (bytes)
4
+ # - hexdigest - SHA256 digest (hex)
5
+ # - base64 - SHA256 digest (bytes) then Base64 encoded
6
+ # - hexdigest_base64 - SHA256 digest (hex) then Base64 encoded
7
+ def self.generate_hashes(content)
8
+ sha256 = Digest::SHA256.digest(content)
9
+ sha256_hex = Digest::SHA256.hexdigest(content)
10
+
11
+ {
12
+ base64: Base64.strict_encode64(sha256),
13
+ hexdigest_base64: Base64.strict_encode64(sha256_hex),
14
+ hexdigest: sha256_hex,
15
+ hash: sha256
16
+ }
17
+ end
18
+ end
@@ -0,0 +1,31 @@
1
+ require "nokogiri"
2
+ require "base64"
3
+
4
+ class ZATCA::QRCodeExtractor
5
+ attr_reader :invoice_base64
6
+
7
+ def initialize(invoice_base64:)
8
+ @invoice_base64 = invoice_base64
9
+ end
10
+
11
+ def extract
12
+ xml_invoice = Base64.strict_decode64(invoice_base64)
13
+ extract_qr_code_base64_from_xml(xml_invoice)
14
+ end
15
+
16
+ private
17
+
18
+ def extract_qr_code_base64_from_xml(xml)
19
+ # Read Invoice
20
+ doc = Nokogiri::XML(xml)
21
+
22
+ # Extract QR Code by XPath
23
+ qr_code_node = doc.xpath(qr_code_xpath)&.first
24
+
25
+ qr_code_node.present? ? qr_code_node.text : nil
26
+ end
27
+
28
+ def qr_code_xpath
29
+ "//cac:AdditionalDocumentReference[cbc:ID='QR']/cac:Attachment/cbc:EmbeddedDocumentBinaryObject"
30
+ end
31
+ end
@@ -0,0 +1,28 @@
1
+ require "rqrcode"
2
+
3
+ module ZATCA
4
+ class QRCodeGenerator
5
+ def initialize(tags: nil, base64: nil)
6
+ @tags = tags
7
+ @base64 = base64
8
+ end
9
+
10
+ def render(size: 256)
11
+ qr_code = generate
12
+
13
+ qr_code.as_png(size: size, border_modules: 2)&.to_data_url
14
+ end
15
+
16
+ private
17
+
18
+ def generate
19
+ if @tags.present?
20
+ RQRCode::QRCode.new(@tags.to_base64)
21
+ elsif @base64.present?
22
+ RQRCode::QRCode.new(@base64)
23
+ else
24
+ raise ArgumentError, "Either tags or base64 must be provided"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,78 @@
1
+ class ZATCA::Signing::Certificate
2
+ attr_accessor :serial_number, :issuer_name, :cert_content_without_headers,
3
+ :hash, :public_key, :public_key_without_headers, :signature,
4
+ :public_key_bytes
5
+
6
+ # Returns the certificate hashed with SHA256 then Base64 encoded
7
+ def self.generate_base64_hash(base64_certificate)
8
+ ZATCA::Hashing.generate_hashes(base64_certificate)[:hexdigest_base64]
9
+ end
10
+
11
+ def self.read_certificate(certificate_path)
12
+ certificate = OpenSSL::X509::Certificate.new(File.read(certificate_path))
13
+
14
+ new(openssl_certificate: certificate)
15
+ end
16
+
17
+ def initialize(openssl_certificate:)
18
+ super()
19
+
20
+ @serial_number = nil
21
+ @issuer_name = nil
22
+ @cert_content_without_headers = nil
23
+ @hash = nil
24
+ @public_key = nil
25
+ @public_key_without_headers = nil
26
+ @public_key_bytes = nil
27
+ @signature = nil
28
+
29
+ @openssl_certificate = openssl_certificate
30
+
31
+ parse_certificate
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :openssl_certificate
37
+
38
+ def parse_certificate
39
+ @cert_content_without_headers = openssl_certificate
40
+ .to_pem
41
+ .gsub("-----BEGIN CERTIFICATE-----", "")
42
+ .gsub("-----END CERTIFICATE-----", "")
43
+ .delete("\n")
44
+
45
+ @hash = self.class.generate_base64_hash(cert_content_without_headers)
46
+
47
+ # ZATCA expects the issuer name to have spaces after commas, the issue name
48
+ # looks like "CN=TSZEINVOICE-SubCA-1,DC=extgazt,DC=gov,DC=local"
49
+ # but ZATCA wants it to be "CN=TSZEINVOICE-SubCA-1, DC=extgazt, DC=gov, DC=local"
50
+ @issuer_name = openssl_certificate.issuer.to_utf8.gsub(",", ", ")
51
+
52
+ @serial_number = openssl_certificate.serial.to_s
53
+ @cert_content_without_headers = cert_content_without_headers
54
+ @public_key = openssl_certificate.public_key.to_pem
55
+ @public_key_without_headers = @public_key
56
+ .gsub("-----BEGIN PUBLIC KEY-----", "")
57
+ .gsub("-----END PUBLIC KEY-----", "")
58
+ .delete("\n")
59
+
60
+ @public_key_bytes = parse_public_key_bytes
61
+
62
+ parse_signature
63
+ end
64
+
65
+ def parse_public_key_bytes
66
+ openssl_certificate.public_key.to_der
67
+ end
68
+
69
+ def parse_signature
70
+ der = openssl_certificate.to_der
71
+ asn1 = OpenSSL::ASN1.decode(der)
72
+
73
+ # The last element of the ASN1 structure is always the signature
74
+ # The signature would look like so:
75
+ # "0F\x02!\x00\xEEa\xD3\xEB(<\xE6;P\x19jw3\xBBOO\xB2d\xDB\xEC\xEC\xBDQ\xC6\xB3v\xD4\xE5\x9E\xD8\x13\xAF\x02!\x00\xFA\xD1\xE6\xD0jf#b\xF7^nqc5\xFCx_\x87h\xA7\xB2\xEC\x10\x11B5+\vcB\x05i"
76
+ @signature = asn1.value[-1].value
77
+ end
78
+ end