saro-dat 4.3.4 → 4.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 87e1f1949bc6ed97274fa1141ca2d76915b75a09e6010f111f4bfce321316396
4
- data.tar.gz: '05139251e2a28e03dd44cbe709b6612267373046c67c789052679dfbd13aec23'
3
+ metadata.gz: b6d5127dce2e1d7ca632f94e2fa8d6b6a1434a37ab285bbf43dfc589282fccbb
4
+ data.tar.gz: e26c938570bfe16eae48746935a541e6ed552653b302cf7d15c10b7304dbc0af
5
5
  SHA512:
6
- metadata.gz: 6b43afc3cac0deb85eb1bbd5174049d91d07a033319078d7f0af2ecc5b3e290e8a0dd768e7a2de9db7cdedc50d62827f2103e5065a8811400c23376aef28650f
7
- data.tar.gz: 3a4840105836a569f47af5cecb6bc57bd7aa79ee511b74d66e1cee2b96b4359c226e624849ab9f64be7d1d3cbd560d886bb0d33bd34fb51a6ebc9f6e53adbb71
6
+ metadata.gz: bb31b09e921a1222111ae2a5ce992ef12e76ac9c776039ebb7f408deaf6255a026f7f25ee820bddc055bbbb9ad84c3e24b493a9e489d6082ceba16fdd9037115
7
+ data.tar.gz: 0041f03e782e284867e46d744bf0d9360c6ff0f24bda8af74ae39dc396e0700bdd3b6fd85a22a618e9f9a3590f8632c7ba81c8f20d2848a365bea1105565acbb
data/.idea/saro-dat.iml CHANGED
@@ -14,9 +14,11 @@
14
14
  <orderEntry type="library" scope="PROVIDED" name="base64 (v0.3.0, rbenv: 4.0.5) [gem]" level="application" />
15
15
  <orderEntry type="library" scope="PROVIDED" name="benchmark (v0.5.0, rbenv: 4.0.5) [gem]" level="application" />
16
16
  <orderEntry type="library" scope="PROVIDED" name="bundler (v4.0.12, rbenv: 4.0.5) [gem]" level="application" />
17
+ <orderEntry type="library" scope="PROVIDED" name="drb (v2.2.3, rbenv: 4.0.5) [gem]" level="application" />
17
18
  <orderEntry type="library" scope="PROVIDED" name="logger (v1.7.0, rbenv: 4.0.5) [gem]" level="application" />
18
- <orderEntry type="library" scope="PROVIDED" name="minitest (v5.27.0, rbenv: 4.0.5) [gem]" level="application" />
19
+ <orderEntry type="library" scope="PROVIDED" name="minitest (v6.0.6, rbenv: 4.0.5) [gem]" level="application" />
19
20
  <orderEntry type="library" scope="PROVIDED" name="openssl (v4.0.2, rbenv: 4.0.5) [gem]" level="application" />
20
21
  <orderEntry type="library" scope="PROVIDED" name="parallel (v2.1.0, rbenv: 4.0.5) [gem]" level="application" />
22
+ <orderEntry type="library" scope="PROVIDED" name="prism (v1.9.0, rbenv: 4.0.5) [gem]" level="application" />
21
23
  </component>
22
24
  </module>
data/.idea/vcs.xml CHANGED
@@ -3,5 +3,6 @@
3
3
  <component name="VcsDirectoryMappings">
4
4
  <mapping directory="" vcs="Git" />
5
5
  <mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
6
+ <mapping directory="$PROJECT_DIR$/.." vcs="Git" />
6
7
  </component>
7
8
  </project>
data/PUBLISH.md CHANGED
@@ -9,7 +9,9 @@ bundle install
9
9
  ```
10
10
  gem build saro-dat.gemspec
11
11
  gem signin
12
- gem push saro-dat-4.3.4.gem
12
+ # glob, not a pinned filename: a hardcoded version silently pushes a stale
13
+ # package after a version bump
14
+ gem push saro-dat-*.gem
13
15
  ```
14
16
 
15
17
  ## install
@@ -36,3 +38,9 @@ ruby -v
36
38
  bundle add saro-dat
37
39
  bundle install
38
40
  ```
41
+
42
+ ## version up
43
+ ```
44
+ bundle outdated
45
+ bundle update --all
46
+ ```
data/README.md CHANGED
@@ -6,14 +6,14 @@
6
6
  - [Example](https://dat.saro.me/libs/gems-saro-dat)
7
7
 
8
8
  ### Support Platform
9
- - [Rust](https://github.com/saro-lab/dat/tree/master/clients/dat-rust)
10
- - [Java, Kotlin](https://github.com/saro-lab/dat/tree/master/clients/dat-maven)
11
- - [Javascript, Typescript](https://github.com/saro-lab/dat/tree/master/clients/dat-npm)
12
- - [C#](https://github.com/saro-lab/dat/tree/master/clients/dat-nuget)
13
- - [Python](https://github.com/saro-lab/dat/tree/master/clients/dat-pypi)
14
- - [Go](https://github.com/saro-lab/dat/tree/master/clients/dat-go)
15
- - [Ruby](https://github.com/saro-lab/dat/tree/master/clients/dat-ruby)
16
- - [C/C++ (Vcpkg)](https://github.com/saro-lab/dat/tree/master/clients/dat-vcpkg)
9
+ - [Rust](https://github.com/saro-lab/dat/tree/master/dat-rust)
10
+ - [Java, Kotlin](https://github.com/saro-lab/dat/tree/master/dat-maven)
11
+ - [Javascript, Typescript](https://github.com/saro-lab/dat/tree/master/dat-npm)
12
+ - [C#](https://github.com/saro-lab/dat/tree/master/dat-nuget)
13
+ - [Python](https://github.com/saro-lab/dat/tree/master/dat-pypi)
14
+ - [Go](https://github.com/saro-lab/dat/tree/master/dat-go)
15
+ - [Ruby](https://github.com/saro-lab/dat/tree/master/dat-ruby)
16
+ - [C/C++ (Vcpkg)](https://github.com/saro-lab/dat/tree/master/dat-vcpkg)
17
17
  - [Cert(Key) Server (Docker)](https://github.com/saro-lab/dat)
18
18
 
19
19
  ## Support algorithm
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'openssl'
4
4
  require 'securerandom'
5
+ require_relative 'error'
5
6
  require_relative 'util'
6
7
 
7
8
  module Saro
@@ -23,7 +24,7 @@ module Saro
23
24
  def self.get_crypto_config(algorithm)
24
25
  config = CRYPTO_CONFIG[algorithm]
25
26
  return config if config
26
- raise ArgumentError, "Unsupported DAT Crypto Algorithm: #{algorithm}"
27
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CONFIG_ALG_UNSUPPORTED, "unknown crypto algorithm: #{algorithm}")
27
28
  end
28
29
 
29
30
  class DatCrypto
@@ -31,6 +32,12 @@ module Saro
31
32
 
32
33
  def initialize(algorithm, key_bytes, config = nil)
33
34
  @config = config || Saro::Dat.get_crypto_config(algorithm)
35
+ if key_bytes.bytesize != @config[:length]
36
+ raise Saro::Dat::Error.new(
37
+ Saro::Dat::ErrorCode::KEY_INVALID,
38
+ "#{algorithm} key must be #{@config[:length]} bytes, got #{key_bytes.bytesize}"
39
+ )
40
+ end
34
41
  @algorithm = algorithm
35
42
  @key_bytes = key_bytes
36
43
  end
@@ -63,20 +70,27 @@ module Saro
63
70
  cipher.iv_len = 12
64
71
  cipher.iv = nonce
65
72
 
66
- ciphertext = cipher.update(data) + cipher.final
67
- tag = cipher.auth_tag
73
+ begin
74
+ ciphertext = cipher.update(data) + cipher.final
75
+ tag = cipher.auth_tag
76
+ rescue OpenSSL::Cipher::CipherError => e
77
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CRYPTO_BACKEND, "aes-gcm encrypt failed", cause: e)
78
+ end
68
79
 
69
80
  nonce + ciphertext + tag
70
81
  end
71
82
 
83
+ def decrypt_base64(base64_str)
84
+ decrypt(Saro::Dat::Util.decode_base64_url(base64_str))
85
+ end
86
+
72
87
  def decrypt(data)
73
- if data.is_a?(String) && data.encoding != Encoding::BINARY
74
- data = Saro::Dat::Util.decode_base64_url(data)
75
- end
76
88
  return "".b if data.nil? || data.empty?
77
89
 
78
- if data.length <= 12 + 16 # nonce(12) + tag(16)
79
- raise ArgumentError, "Invalid data length"
90
+ data = data.b if data.is_a?(String) && data.encoding != Encoding::BINARY
91
+
92
+ if data.bytesize <= 12 + 16
93
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CRYPTO_DATA_INVALID, "ciphertext is shorter than iv(12) + tag(16)")
80
94
  end
81
95
 
82
96
  nonce = data[0, 12]
@@ -90,7 +104,11 @@ module Saro
90
104
  cipher.iv = nonce
91
105
  cipher.auth_tag = tag
92
106
 
93
- res = cipher.update(ciphertext) + cipher.final
107
+ begin
108
+ res = cipher.update(ciphertext) + cipher.final
109
+ rescue OpenSSL::Cipher::CipherError => e
110
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CRYPTO_TAG_MISMATCH, "gcm authentication tag mismatch", cause: e)
111
+ end
94
112
  res.force_encoding('BINARY')
95
113
  res
96
114
  end
data/lib/saro/dat/dat.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'error'
3
4
  require_relative 'util'
4
5
 
5
6
  module Saro
@@ -7,6 +8,8 @@ module Saro
7
8
  class Dat
8
9
  attr_reader :dat, :expire, :cid, :plain, :secure, :signature, :format
9
10
 
11
+ attr_reader :error
12
+
10
13
  def initialize(dat_str)
11
14
  @dat = dat_str || ''
12
15
  @format = false
@@ -15,22 +18,70 @@ module Saro
15
18
  @plain = "".b
16
19
  @secure = "".b
17
20
  @signature = "".b
21
+ @error = nil
22
+
23
+ if @dat.empty?
24
+ @error = Saro::Dat::Error.new(Saro::Dat::ErrorCode::TOKEN_MALFORMED, "token is empty")
25
+ return
26
+ end
27
+
28
+ parts = @dat.split('.', -1)
29
+ if parts.length != 5
30
+ @error = Saro::Dat::Error.new(Saro::Dat::ErrorCode::TOKEN_MALFORMED, "expected exactly 5 dot-separated fields")
31
+ return
32
+ end
33
+
34
+ begin
35
+ @expire = Saro::Dat::Util.parse_u64(parts[0])
36
+ rescue Saro::Dat::Error => e
37
+ @error = Saro::Dat::Error.new(Saro::Dat::ErrorCode::TOKEN_MALFORMED, "expire field is not a plain decimal u64", cause: e)
38
+ return
39
+ end
40
+
41
+ begin
42
+ @cid = Saro::Dat::Util.parse_u64_hex(parts[1])
43
+ rescue Saro::Dat::Error => e
44
+ @error = Saro::Dat::Error.new(Saro::Dat::ErrorCode::TOKEN_MALFORMED, "cid field is not a plain hex u64", cause: e)
45
+ return
46
+ end
47
+
48
+ begin
49
+ @plain = Saro::Dat::Util.decode_base64_url(parts[2])
50
+ rescue Saro::Dat::Error => e
51
+ @error = Saro::Dat::Error.new(Saro::Dat::ErrorCode::TOKEN_MALFORMED, "plain field is not base64url", cause: e)
52
+ return
53
+ end
54
+
55
+ begin
56
+ @secure = Saro::Dat::Util.decode_base64_url(parts[3])
57
+ rescue Saro::Dat::Error => e
58
+ @error = Saro::Dat::Error.new(Saro::Dat::ErrorCode::TOKEN_MALFORMED, "secure field is not base64url", cause: e)
59
+ return
60
+ end
61
+
62
+ if parts[4].empty?
63
+ @error = Saro::Dat::Error.new(Saro::Dat::ErrorCode::SIG_MALFORMED, "signature field is empty")
64
+ return
65
+ end
18
66
 
19
- if !@dat.empty?
20
- parts = @dat.split('.')
21
- if parts.length == 5
22
- begin
23
- @expire = parts[0].to_i
24
- @cid = parts[1].to_i(16)
25
- @plain = Saro::Dat::Util.decode_base64_url(parts[2])
26
- @secure = Saro::Dat::Util.decode_base64_url(parts[3])
27
- @signature = Saro::Dat::Util.decode_base64_url(parts[4])
28
- @format = (!@signature.empty? && @expire >= 0)
29
- rescue StandardError
30
- @format = false
31
- end
32
- end
67
+ begin
68
+ @signature = Saro::Dat::Util.decode_base64_url(parts[4])
69
+ rescue Saro::Dat::Error => e
70
+ @error = Saro::Dat::Error.new(Saro::Dat::ErrorCode::SIG_MALFORMED, "signature field is not base64url", cause: e)
71
+ return
33
72
  end
73
+
74
+ if @signature.empty?
75
+ @error = Saro::Dat::Error.new(Saro::Dat::ErrorCode::SIG_MALFORMED, "signature field is empty")
76
+ return
77
+ end
78
+
79
+ @format = true
80
+ end
81
+
82
+ def raise_if_invalid!
83
+ raise @error if @error
84
+ nil
34
85
  end
35
86
 
36
87
  def self.from_value(value)
@@ -40,7 +91,7 @@ module Saro
40
91
 
41
92
  def expired
42
93
  return true unless @format
43
- Time.now.to_i > @expire
94
+ Time.now.to_i >= @expire
44
95
  end
45
96
 
46
97
  alias_method :expired?, :expired
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'error'
3
4
  require_relative 'crypto'
4
5
  require_relative 'signature'
5
6
  require_relative 'util'
@@ -10,10 +11,18 @@ module Saro
10
11
  attr_reader :cid, :signature_key, :crypto_key, :dat_issuance_start_seconds, :dat_issuance_end_seconds, :dat_ttl_seconds
11
12
 
12
13
  def initialize(cid, dat_issuance_start_seconds, dat_issuance_duration_seconds, dat_ttl_seconds, signature_key, crypto_key)
13
- @cid = cid
14
- @dat_issuance_start_seconds = dat_issuance_start_seconds
15
- @dat_issuance_end_seconds = dat_issuance_start_seconds + dat_issuance_duration_seconds
16
- @dat_ttl_seconds = dat_ttl_seconds
14
+ @cid = u64!("cid", cid)
15
+ @dat_issuance_start_seconds = u64!("dat_issuance_start_seconds", dat_issuance_start_seconds)
16
+ duration = u64!("dat_issuance_duration_seconds", dat_issuance_duration_seconds)
17
+ @dat_ttl_seconds = u64!("dat_ttl_seconds", dat_ttl_seconds)
18
+ @dat_issuance_end_seconds = u64!(
19
+ "dat_issuance_start_seconds + dat_issuance_duration_seconds",
20
+ @dat_issuance_start_seconds + duration
21
+ )
22
+ u64!(
23
+ "dat_issuance_start_seconds + dat_issuance_duration_seconds + dat_ttl_seconds",
24
+ @dat_issuance_end_seconds + @dat_ttl_seconds
25
+ )
17
26
  @signature_key = signature_key
18
27
  @crypto_key = crypto_key
19
28
  end
@@ -41,12 +50,14 @@ module Saro
41
50
 
42
51
  def self.imports(format_str)
43
52
  parts = format_str.split(".")
44
- raise ArgumentError, "Invalid Certificate format" if parts.length != 8
53
+ if parts.length != 8
54
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CERT_MALFORMED, "expected exactly 8 dot-separated fields")
55
+ end
45
56
 
46
- cid = parts[0].to_i(16)
47
- dat_issuance_start_seconds = parts[1].to_i
48
- dat_issuance_duration_seconds = parts[2].to_i
49
- dat_ttl_seconds = parts[3].to_i
57
+ cid = _field("cid") { Saro::Dat::Util.parse_u64_hex(parts[0]) }
58
+ dat_issuance_start_seconds = _field("issuance_start_seconds") { Saro::Dat::Util.parse_u64(parts[1]) }
59
+ dat_issuance_duration_seconds = _field("issuance_duration_seconds") { Saro::Dat::Util.parse_u64(parts[2]) }
60
+ dat_ttl_seconds = _field("dat_ttl_seconds") { Saro::Dat::Util.parse_u64(parts[3]) }
50
61
  signature_algorithm = parts[4]
51
62
  crypto_algorithm = parts[5]
52
63
  signature_key = Saro::Dat::DatSignature.imports(signature_algorithm, parts[6])
@@ -76,7 +87,23 @@ module Saro
76
87
  @signature_key.support_verify_only
77
88
  end
78
89
 
79
- # For Ruby conventions
90
+ U64_MAX = 0xFFFFFFFFFFFFFFFF
91
+ private_constant :U64_MAX
92
+
93
+ private def u64!(name, value)
94
+ unless value.is_a?(Integer) && !value.is_a?(TrueClass) && value >= 0 && value <= U64_MAX
95
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CERT_MALFORMED, "#{name} must fit in u64: #{value}")
96
+ end
97
+ value
98
+ end
99
+
100
+ def self._field(name)
101
+ yield
102
+ rescue Saro::Dat::Error => e
103
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CERT_MALFORMED, "#{name} field is not a plain number", cause: e)
104
+ end
105
+ private_class_method :_field
106
+
80
107
  alias_method :issuable?, :issuable
81
108
  alias_method :expired?, :expired
82
109
  alias_method :signable?, :signable
@@ -4,6 +4,7 @@ require 'net/http'
4
4
  require 'uri'
5
5
  require 'logger'
6
6
  require 'thread'
7
+ require_relative 'error'
7
8
  require_relative 'dat_manager'
8
9
  require_relative 'dat'
9
10
 
@@ -12,6 +13,8 @@ module Saro
12
13
  class DatCmsManager
13
14
  DAT_CMS_API_VERSION = "v1"
14
15
 
16
+ STOP_JOIN_TIMEOUT_SECONDS = 1.0
17
+
15
18
  def initialize(uri:, token:, interval_seconds: 60, verify_only: false, dat_manager: nil)
16
19
  @uri = uri
17
20
  @token = token
@@ -20,9 +23,12 @@ module Saro
20
23
  @manager = dat_manager || DatManager.new
21
24
  @version = 0
22
25
  @lock = Mutex.new
26
+ @lifecycle = Mutex.new
27
+ @stop_cond = ConditionVariable.new
23
28
  @stopped = false
24
29
  @logger = Logger.new($stdout)
25
30
  @logger.level = Logger::DEBUG
31
+ @last_error = Saro::Dat::Error.new(Saro::Dat::ErrorCode::CMS_NOT_SYNCED)
26
32
 
27
33
  sync
28
34
 
@@ -32,17 +38,47 @@ module Saro
32
38
  end
33
39
 
34
40
  def stop
35
- @lock.synchronize do
41
+ thread = nil
42
+ @lifecycle.synchronize do
43
+ return if @stopped
36
44
  @stopped = true
37
- @thread&.kill # 혹은 다른 방식으로 스레드 중지
45
+ @stop_cond.broadcast
46
+ thread = @thread
38
47
  end
48
+ thread&.join(STOP_JOIN_TIMEOUT_SECONDS)
49
+ nil
50
+ end
51
+
52
+ def stopped?
53
+ @lifecycle.synchronize { @stopped }
54
+ end
55
+
56
+ attr_reader :last_error
57
+
58
+ def version
59
+ @version
39
60
  end
40
61
 
41
62
  def sync
42
- # non-blocking lock
63
+ err = sync_or_raise
64
+ @last_error = nil
65
+ err
66
+ rescue Saro::Dat::Error => e
67
+ unless e.retry == :state
68
+ @last_error = e
69
+ @logger.error("[CRITICAL] DAT CMS SYNC #{@uri}: #{e.code} #{e.detail}")
70
+ end
71
+ nil
72
+ rescue StandardError => e
73
+ @last_error = Saro::Dat::Error.new(Saro::Dat::ErrorCode::CMS_UNKNOWN, "unclassified cms failure", cause: e)
74
+ @logger.error("[CRITICAL] DAT CMS SYNC #{@uri}: #{e.message}")
75
+ nil
76
+ end
77
+
78
+ private def sync_or_raise
43
79
  unless @lock.try_lock
44
- @logger.warn("Last request ignored (Duplicate request)")
45
- return
80
+ @logger.debug("cms sync skipped, previous sync still running: #{@uri}")
81
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CMS_SYNC_IN_PROGRESS)
46
82
  end
47
83
 
48
84
  begin
@@ -50,55 +86,70 @@ module Saro
50
86
  request = Net::HTTP::Get.new(url)
51
87
  request["Authorization"] = @token
52
88
 
53
- response = Net::HTTP.start(url.host, url.port, use_ssl: url.scheme == 'https', open_timeout: 10, read_timeout: 10) do |http|
54
- http.request(request)
55
- end
89
+ response =
90
+ begin
91
+ Net::HTTP.start(url.host, url.port, use_ssl: url.scheme == 'https', open_timeout: 10, read_timeout: 10) do |http|
92
+ http.request(request)
93
+ end
94
+ rescue StandardError => e
95
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CMS_UNREACHABLE, "cannot reach #{@uri}", cause: e)
96
+ end
56
97
 
57
- if response.code != "200"
58
- @logger.error("Response status error, status:#{response.code} in #{url}")
59
- return
60
- end
98
+ status = response.code.to_i
99
+ raise self.class.http_status_error(status) unless status.between?(200, 299)
61
100
 
62
101
  body = response.body
63
102
  if body.nil? || body.empty?
64
103
  @logger.debug("No new certificate: #{url}")
65
- return
104
+ return nil
66
105
  end
67
106
 
68
107
  lines = body.split("\n", 2)
69
108
  if lines.length < 2
70
109
  if body.start_with?("\n")
71
- @logger.error("Invalid response: #{url}")
72
- return
110
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CMS_MALFORMED, "response has no version line")
73
111
  end
74
112
  @logger.debug("No new certificate: #{url}")
75
- return
113
+ return nil
76
114
  end
77
115
 
78
116
  new_version_str = lines[0].strip
79
117
  new_certificates = lines[1].strip
80
118
 
81
- if new_version_str.empty?
82
- @logger.error("Invalid version in response: #{url}")
83
- return
119
+ unless new_version_str.match?(/\A[0-9]+\z/)
120
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CMS_MALFORMED, "version line is not a plain decimal integer")
84
121
  end
122
+ new_version = new_version_str.to_i
85
123
 
86
- begin
87
- new_version = Integer(new_version_str)
88
- renew_count = @manager.imports(new_certificates, clear: false)
89
- @version = new_version
90
- @logger.debug("Renewed #{renew_count} certificates for version #{new_version}: #{url}")
91
- rescue ArgumentError => e
92
- @logger.error("Failed to parse version or certificates: #{e.message}")
124
+ if new_version < @version
125
+ @logger.warn("#{Saro::Dat::ErrorCode::CMS_VERSION_RESET}: #{@version} -> #{new_version}")
93
126
  end
94
127
 
95
- rescue StandardError => e
96
- @logger.error("[Exception] DAT CMS Sync #{@uri}: #{e.message}")
128
+ renew_count =
129
+ begin
130
+ @manager.imports(new_certificates, clear: false)
131
+ rescue Saro::Dat::Error => e
132
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CMS_IMPORT_FAILED, "cannot apply received certificates", cause: e)
133
+ end
134
+
135
+ @version = new_version
136
+ @logger.debug("Renewed #{renew_count} certificates for version #{new_version}: #{url}")
137
+ nil
97
138
  ensure
98
139
  @lock.unlock
99
140
  end
100
141
  end
101
142
 
143
+ def self.http_status_error(status)
144
+ case status
145
+ when 401 then Saro::Dat::Error.new(Saro::Dat::ErrorCode::CMS_UNAUTHORIZED, "http 401")
146
+ when 403 then Saro::Dat::Error.new(Saro::Dat::ErrorCode::CMS_FORBIDDEN, "http 403")
147
+ when 404 then Saro::Dat::Error.new(Saro::Dat::ErrorCode::CMS_ENDPOINT_NOT_FOUND, "http 404")
148
+ when 500..599 then Saro::Dat::Error.new(Saro::Dat::ErrorCode::CMS_SERVER_ERROR, "http #{status}")
149
+ else Saro::Dat::Error.new(Saro::Dat::ErrorCode::CMS_HTTP_STATUS, "http #{status}")
150
+ end
151
+ end
152
+
102
153
  def get_manager
103
154
  @manager
104
155
  end
@@ -120,13 +171,20 @@ module Saro
120
171
  def schedule_sync
121
172
  @thread = Thread.new do
122
173
  loop do
123
- sleep(@interval_seconds)
124
- break if @stopped
174
+ break unless wait_interval
125
175
  run_sync_task
126
176
  end
127
177
  end
128
178
  end
129
179
 
180
+ def wait_interval
181
+ @lifecycle.synchronize do
182
+ return false if @stopped
183
+ @stop_cond.wait(@lifecycle, @interval_seconds)
184
+ !@stopped
185
+ end
186
+ end
187
+
130
188
  def run_sync_task
131
189
  sync
132
190
  rescue StandardError => e
@@ -168,13 +226,21 @@ module Saro
168
226
  end
169
227
 
170
228
  def build
171
- parsed = URI.parse(@uri)
172
-
229
+ parsed =
230
+ begin
231
+ URI.parse(@uri)
232
+ rescue URI::InvalidURIError => e
233
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CONFIG_URI_INVALID, "cannot be parsed as a uri", cause: e)
234
+ end
235
+
236
+ unless %w[http https].include?(parsed.scheme)
237
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CONFIG_URI_INVALID, "scheme must be http or https")
238
+ end
173
239
  if parsed.path && parsed.path != '' && parsed.path != '/'
174
- raise ArgumentError, "uri must be path-less: #{@uri}"
240
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CONFIG_URI_INVALID, "must be path-less: #{@uri}")
175
241
  end
176
242
  if parsed.query
177
- raise ArgumentError, "uri must be query-less: #{@uri}"
243
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CONFIG_URI_INVALID, "must be query-less: #{@uri}")
178
244
  end
179
245
 
180
246
  path = @verify_only ? "/v1/certs/verify-only" : "/v1/certs"
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'set'
4
+ require_relative 'error'
4
5
  require_relative 'dat_certificate'
5
6
  require_relative 'dat'
6
7
  require_relative 'signature'
@@ -9,9 +10,6 @@ require_relative 'util'
9
10
  module Saro
10
11
  module Dat
11
12
  class DatManager
12
- # Immutable snapshot of the manager state.
13
- # Readers access it lock-free via a single ivar read (atomic reference swap);
14
- # writers rebuild a new frozen snapshot under @write_lock.
15
13
  State = Struct.new(:issuer, :certificates, :by_cid)
16
14
 
17
15
  EMPTY_STATE = State.new(nil, [].freeze, {}.freeze).freeze
@@ -23,26 +21,33 @@ module Saro
23
21
  end
24
22
 
25
23
  def import_certificates(input_certs, clear: false)
24
+ return 0 if input_certs.nil? || input_certs.empty?
25
+
26
+ seen_cids = Set.new
27
+ input_certs.each do |cert|
28
+ if seen_cids.include?(cert.cid)
29
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CERT_DUPLICATE_CID, "duplicate cid #{cert.cid.to_s(16)}")
30
+ end
31
+ seen_cids.add(cert.cid)
32
+ end
33
+
26
34
  renew_count = 0
27
35
  @write_lock.synchronize do
28
36
  certificates = clear ? [] : @state.certificates.dup
29
37
 
30
- before_cids = Set.new(certificates.map(&:cid))
31
- seen_cids = Set.new
38
+ cids = Set.new(certificates.map(&:cid))
32
39
 
33
40
  input_certs.each do |cert|
34
- raise ArgumentError, "Duplicate CID: #{cert.cid}" if seen_cids.include?(cert.cid)
35
- seen_cids.add(cert.cid)
36
- next if cert.expired
37
- next if before_cids.include?(cert.cid)
41
+ next if cids.include?(cert.cid)
38
42
 
43
+ cids.add(cert.cid)
39
44
  certificates << cert
40
45
  renew_count += 1
41
46
  end
42
47
 
48
+ certificates.reject!(&:expired)
43
49
  certificates.sort_by!(&:dat_issuance_end_seconds)
44
50
 
45
- # Find latest issuable certificate as issuer
46
51
  issuer = certificates.reverse_each.find(&:issuable)
47
52
 
48
53
  by_cid = {}
@@ -68,24 +73,61 @@ module Saro
68
73
  end
69
74
 
70
75
  def issue(plain, secure)
71
- issuer = @state.issuer
72
- raise RuntimeError, "Invalid DAT: Signing Key Does Not Exist" unless issuer
76
+ state = @state
77
+ issuer = state.issuer
78
+ unless issuer
79
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::MANAGER_NO_CERTIFICATE) if state.certificates.empty?
80
+ raise Saro::Dat::Error.new(
81
+ Saro::Dat::ErrorCode::MANAGER_NO_ISSUABLE_CERTIFICATE,
82
+ cause: no_issuable_cause(state.certificates)
83
+ )
84
+ end
73
85
 
74
86
  self.class._issue(issuer, plain, secure)
75
87
  end
76
88
 
77
89
  def parse(dat_input)
78
90
  dat = Saro::Dat::Dat.from_value(dat_input)
79
- raise ArgumentError, "Invalid DAT: Format" unless dat.format
91
+ dat.raise_if_invalid!
80
92
 
81
- certificate = @state.by_cid[dat.cid]
93
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::TOKEN_EXPIRED) if dat.expired?
82
94
 
83
- raise ArgumentError, "Invalid DAT: CID(Certificate ID) Not Found" unless certificate
95
+ certificate = @state.by_cid[dat.cid]
96
+ unless certificate
97
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CERT_NOT_FOUND, "cid #{dat.cid.to_s(16)}")
98
+ end
84
99
 
85
100
  self.class._parse(certificate, dat)
86
101
  end
87
102
 
88
- private
103
+ private def no_issuable_cause(certificates)
104
+ now = Time.now.to_i
105
+ signable_seen = false
106
+ not_yet = false
107
+ ended = false
108
+
109
+ certificates.each do |cert|
110
+ next unless cert.signable
111
+ signable_seen = true
112
+ if now < cert.dat_issuance_start_seconds
113
+ not_yet = true
114
+ elsif now > cert.dat_issuance_end_seconds
115
+ ended = true
116
+ end
117
+ end
118
+
119
+ code =
120
+ if !signable_seen
121
+ Saro::Dat::ErrorCode::CERT_VERIFY_ONLY
122
+ elsif not_yet
123
+ Saro::Dat::ErrorCode::CERT_NOT_YET_ISSUABLE
124
+ elsif ended
125
+ Saro::Dat::ErrorCode::CERT_ISSUANCE_ENDED
126
+ else
127
+ Saro::Dat::ErrorCode::CERT_EXPIRED
128
+ end
129
+ Saro::Dat::Error.new(code)
130
+ end
89
131
 
90
132
  def self._issue(cert, plain, secure)
91
133
  now = Time.now.to_i
@@ -105,11 +147,11 @@ module Saro
105
147
 
106
148
  def self._parse(cert, dat_input)
107
149
  dat = Saro::Dat::Dat.from_value(dat_input)
108
- raise RuntimeError, "Invalid DAT: Format" unless dat.format
109
- raise RuntimeError, "Invalid DAT: Expired" if dat.expired?
150
+ dat.raise_if_invalid!
151
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::TOKEN_EXPIRED) if dat.expired?
110
152
 
111
153
  unless cert.signature_key.verify(dat.body_string, dat.signature)
112
- raise RuntimeError, "Invalid DAT: Signature"
154
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::SIG_MISMATCH)
113
155
  end
114
156
 
115
157
  decrypted_secure = cert.crypto_key.decrypt(dat.secure)
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Saro
4
+ module Dat
5
+ module ErrorCode
6
+ TOKEN_MALFORMED = "DAT_TOKEN_MALFORMED"
7
+ TOKEN_EXPIRED = "DAT_TOKEN_EXPIRED"
8
+ TOKEN_UNKNOWN = "DAT_TOKEN_UNKNOWN"
9
+
10
+ CERT_MALFORMED = "DAT_CERT_MALFORMED"
11
+ CERT_EXPIRED = "DAT_CERT_EXPIRED"
12
+ CERT_NOT_YET_ISSUABLE = "DAT_CERT_NOT_YET_ISSUABLE"
13
+ CERT_ISSUANCE_ENDED = "DAT_CERT_ISSUANCE_ENDED"
14
+ CERT_VERIFY_ONLY = "DAT_CERT_VERIFY_ONLY"
15
+ CERT_NOT_FOUND = "DAT_CERT_NOT_FOUND"
16
+ CERT_NOT_SYNCED = "DAT_CERT_NOT_SYNCED"
17
+ CERT_DUPLICATE_CID = "DAT_CERT_DUPLICATE_CID"
18
+ CERT_UNKNOWN = "DAT_CERT_UNKNOWN"
19
+
20
+ SIG_MISMATCH = "DAT_SIG_MISMATCH"
21
+ SIG_MALFORMED = "DAT_SIG_MALFORMED"
22
+ SIG_KEY_MISSING = "DAT_SIG_KEY_MISSING"
23
+ SIG_BACKEND = "DAT_SIG_BACKEND"
24
+ SIG_UNKNOWN = "DAT_SIG_UNKNOWN"
25
+
26
+ CRYPTO_TAG_MISMATCH = "DAT_CRYPTO_TAG_MISMATCH"
27
+ CRYPTO_DATA_INVALID = "DAT_CRYPTO_DATA_INVALID"
28
+ CRYPTO_BACKEND = "DAT_CRYPTO_BACKEND"
29
+ CRYPTO_UNKNOWN = "DAT_CRYPTO_UNKNOWN"
30
+
31
+ KEY_INVALID = "DAT_KEY_INVALID"
32
+ KEY_VERIFY_ONLY_UNSUPPORTED = "DAT_KEY_VERIFY_ONLY_UNSUPPORTED"
33
+ KEY_UNKNOWN = "DAT_KEY_UNKNOWN"
34
+
35
+ MANAGER_NO_CERTIFICATE = "DAT_MANAGER_NO_CERTIFICATE"
36
+ MANAGER_NO_ISSUABLE_CERTIFICATE = "DAT_MANAGER_NO_ISSUABLE_CERTIFICATE"
37
+ MANAGER_DISPOSED = "DAT_MANAGER_DISPOSED"
38
+ MANAGER_UNKNOWN = "DAT_MANAGER_UNKNOWN"
39
+
40
+ CMS_UNREACHABLE = "DAT_CMS_UNREACHABLE"
41
+ CMS_UNAUTHORIZED = "DAT_CMS_UNAUTHORIZED"
42
+ CMS_FORBIDDEN = "DAT_CMS_FORBIDDEN"
43
+ CMS_ENDPOINT_NOT_FOUND = "DAT_CMS_ENDPOINT_NOT_FOUND"
44
+ CMS_SERVER_ERROR = "DAT_CMS_SERVER_ERROR"
45
+ CMS_HTTP_STATUS = "DAT_CMS_HTTP_STATUS"
46
+ CMS_MALFORMED = "DAT_CMS_MALFORMED"
47
+ CMS_IMPORT_FAILED = "DAT_CMS_IMPORT_FAILED"
48
+ CMS_VERSION_RESET = "DAT_CMS_VERSION_RESET"
49
+ CMS_NOT_SYNCED = "DAT_CMS_NOT_SYNCED"
50
+ CMS_SYNC_IN_PROGRESS = "DAT_CMS_SYNC_IN_PROGRESS"
51
+ CMS_NOT_SUPPORTED = "DAT_CMS_NOT_SUPPORTED"
52
+ CMS_UNKNOWN = "DAT_CMS_UNKNOWN"
53
+
54
+ CONFIG_ALG_UNSUPPORTED = "DAT_CONFIG_ALG_UNSUPPORTED"
55
+ CONFIG_URI_INVALID = "DAT_CONFIG_URI_INVALID"
56
+ CONFIG_ARGUMENT_INVALID = "DAT_CONFIG_ARGUMENT_INVALID"
57
+ CONFIG_UNKNOWN = "DAT_CONFIG_UNKNOWN"
58
+
59
+ INTERNAL_UNAVAILABLE = "DAT_INTERNAL_UNAVAILABLE"
60
+ INTERNAL_UNKNOWN = "DAT_INTERNAL_UNKNOWN"
61
+
62
+ TRANSIENT = [
63
+ CERT_NOT_YET_ISSUABLE, CERT_NOT_SYNCED, MANAGER_NO_CERTIFICATE,
64
+ CMS_UNREACHABLE, CMS_SERVER_ERROR, CMS_NOT_SYNCED
65
+ ].freeze
66
+
67
+ STATE = [CMS_VERSION_RESET, CMS_SYNC_IN_PROGRESS].freeze
68
+
69
+ SECURITY = [SIG_MISMATCH, CRYPTO_TAG_MISMATCH].freeze
70
+ end
71
+
72
+ class Error < StandardError
73
+ attr_reader :code
74
+ attr_reader :detail
75
+
76
+ def initialize(code, detail = nil, cause: nil)
77
+ @code = code
78
+ @detail = detail
79
+ @explicit_cause = cause
80
+ super(detail ? "#{code}: #{detail}" : code)
81
+ end
82
+
83
+ def cause
84
+ @explicit_cause || super
85
+ end
86
+
87
+ def retry
88
+ if @code == ErrorCode::MANAGER_NO_ISSUABLE_CERTIFICATE
89
+ c = cause
90
+ return :transient if c.is_a?(Error) && c.code == ErrorCode::CERT_NOT_YET_ISSUABLE
91
+ return :permanent
92
+ end
93
+ return :transient if ErrorCode::TRANSIENT.include?(@code)
94
+ return :state if ErrorCode::STATE.include?(@code)
95
+ :permanent
96
+ end
97
+
98
+ def security_event?
99
+ ErrorCode::SECURITY.include?(@code)
100
+ end
101
+
102
+ def inspect
103
+ "#<Saro::Dat::Error #{@code}#{@detail ? " #{@detail.inspect}" : ''}>"
104
+ end
105
+
106
+ class << self
107
+ def code_of(e)
108
+ e.is_a?(Error) ? e.code : nil
109
+ end
110
+
111
+ def wrap(code, detail, e)
112
+ return e if e.is_a?(Error)
113
+ new(code, detail, cause: e)
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'openssl'
4
+ require_relative 'error'
4
5
  require_relative 'util'
5
6
 
6
7
  module Saro
@@ -30,7 +31,7 @@ module Saro
30
31
  def self.get_signature_config(algorithm)
31
32
  config = SIGNATURE_CONFIG[algorithm]
32
33
  return config if config
33
- raise ArgumentError, "Unsupported DAT Crypto Algorithm: #{algorithm}"
34
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CONFIG_ALG_UNSUPPORTED, "unknown signature algorithm: #{algorithm}")
34
35
  end
35
36
 
36
37
  class DatSignature
@@ -46,15 +47,28 @@ module Saro
46
47
  private_class_method def self.create_ec_key(curve_name, priv_bn = nil, pub_octet = nil)
47
48
  if priv_bn
48
49
  group = OpenSSL::PKey::EC::Group.new(curve_name)
50
+
51
+ if priv_bn <= 0 || priv_bn >= group.order
52
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::KEY_INVALID, "ecdsa private scalar out of range [1, n-1]")
53
+ end
54
+
49
55
  pub_octet ||= group.generator.mul(priv_bn).to_octet_string(:uncompressed)
50
-
56
+
51
57
  asn1 = OpenSSL::ASN1::Sequence.new([
52
58
  OpenSSL::ASN1::Integer.new(1),
53
59
  OpenSSL::ASN1::OctetString.new(priv_bn.to_s(2).rjust((group.degree + 7) / 8, "\x00".b)),
54
60
  OpenSSL::ASN1::ASN1Data.new([OpenSSL::ASN1::ObjectId.new(curve_name)], 0, :CONTEXT_SPECIFIC),
55
61
  OpenSSL::ASN1::ASN1Data.new([OpenSSL::ASN1::BitString.new(pub_octet)], 1, :CONTEXT_SPECIFIC)
56
62
  ])
57
- OpenSSL::PKey::EC.new(asn1.to_der)
63
+ key = OpenSSL::PKey::EC.new(asn1.to_der)
64
+
65
+ begin
66
+ key.check_key
67
+ rescue OpenSSL::PKey::PKeyError, OpenSSL::PKey::ECError => e
68
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::KEY_INVALID, "ecdsa key pair rejected: #{e.message}", cause: e)
69
+ end
70
+
71
+ key
58
72
  elsif pub_octet
59
73
  spki = OpenSSL::ASN1::Sequence.new([
60
74
  OpenSSL::ASN1::Sequence.new([
@@ -63,9 +77,15 @@ module Saro
63
77
  ]),
64
78
  OpenSSL::ASN1::BitString.new(pub_octet)
65
79
  ])
66
- OpenSSL::PKey::EC.new(spki.to_der)
80
+ key = OpenSSL::PKey::EC.new(spki.to_der)
81
+ begin
82
+ key.check_key
83
+ rescue OpenSSL::PKey::PKeyError, OpenSSL::PKey::ECError => e
84
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::KEY_INVALID, "ecdsa public key rejected: #{e.message}", cause: e)
85
+ end
86
+ key
67
87
  else
68
- raise ArgumentError, "Either private key or public key must be provided"
88
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CONFIG_ARGUMENT_INVALID, "either private key or public key must be provided")
69
89
  end
70
90
  end
71
91
 
@@ -76,6 +96,11 @@ module Saro
76
96
  new(algorithm, key, key, config)
77
97
  else
78
98
  key = OpenSSL::PKey::EC.generate(config[:curve])
99
+ begin
100
+ key.check_key
101
+ rescue OpenSSL::PKey::PKeyError, OpenSSL::PKey::ECError => e
102
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::INTERNAL_UNKNOWN, "generated ecdsa key pair is invalid: #{e.message}", cause: e)
103
+ end
79
104
  new(algorithm, key, key, config)
80
105
  end
81
106
  end
@@ -86,7 +111,7 @@ module Saro
86
111
 
87
112
  if config[:name] == "HMAC"
88
113
  if bytes_data.bytesize != config[:hmac_len]
89
- raise ArgumentError, "Invalid HMAC key length: expected #{config[:hmac_len]}, got #{bytes_data.bytesize}"
114
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::KEY_INVALID, "hmac key must be #{config[:hmac_len]} bytes, got #{bytes_data.bytesize}")
90
115
  end
91
116
  new(algorithm, bytes_data, bytes_data, config)
92
117
  else
@@ -106,7 +131,7 @@ module Saro
106
131
  elsif bytes_data.bytesize == public_len
107
132
  verifying_key = create_ec_key(config[:curve], nil, bytes_data)
108
133
  else
109
- raise ArgumentError, "Invalid ECDSA key length"
134
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::KEY_INVALID, "ecdsa key length matches neither private+public nor public")
110
135
  end
111
136
 
112
137
  new(algorithm, signing_key, verifying_key, config)
@@ -115,7 +140,7 @@ module Saro
115
140
 
116
141
  def exports(verify_only = false)
117
142
  if verify_only && !support_verify_only
118
- raise ArgumentError, "#{config[:name]} does not supported verifying only key"
143
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::KEY_VERIFY_ONLY_UNSUPPORTED, @algorithm.to_s)
119
144
  end
120
145
 
121
146
  if @config[:name] == "HMAC"
@@ -136,9 +161,9 @@ module Saro
136
161
  end
137
162
 
138
163
  def sign(body)
139
- raise ArgumentError, "Signature key is not supported - verifying only key" unless @signing_key
164
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::SIG_KEY_MISSING, "this key is verify-only") unless @signing_key
140
165
  body = normalize_body(body)
141
- raise ArgumentError, "Sign Error - body is empty" if body.nil? || body.empty?
166
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CONFIG_ARGUMENT_INVALID, "body to sign is empty") if body.nil? || body.empty?
142
167
 
143
168
  if @config[:name] == "HMAC"
144
169
  OpenSSL::HMAC.digest(@config[:hash], @signing_key, body)
@@ -149,29 +174,44 @@ module Saro
149
174
  end
150
175
 
151
176
  def verify(body, signature)
177
+ verify_bytes(body, signature)
178
+ end
179
+
180
+ def verify_base64(body, signature_base64)
181
+ sig_bytes = begin
182
+ Saro::Dat::Util.decode_base64_url(signature_base64)
183
+ rescue Saro::Dat::Error => e
184
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::SIG_MALFORMED, "signature is not base64url", cause: e)
185
+ end
186
+ verify_bytes(body, sig_bytes)
187
+ end
188
+
189
+ private def verify_bytes(body, sig_bytes)
152
190
  body = normalize_body(body)
153
191
  return false if body.nil? || body.empty?
154
192
 
155
- sig_bytes = if signature.is_a?(String) && signature.encoding != Encoding::BINARY
156
- Saro::Dat::Util.decode_base64_url(signature)
157
- else
158
- signature
159
- end
193
+ sig_bytes = sig_bytes.b if sig_bytes.is_a?(String) && sig_bytes.encoding != Encoding::BINARY
160
194
 
161
195
  if @config[:name] == "HMAC"
162
196
  begin
163
197
  actual_sig = OpenSSL::HMAC.digest(@config[:hash], @verifying_key, body)
164
- # Use fixed-time comparison if possible
165
- return actual_sig == sig_bytes
166
- rescue StandardError
167
- return false
198
+ rescue StandardError => e
199
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::SIG_BACKEND, "hmac computation failed", cause: e)
168
200
  end
201
+ return false unless sig_bytes.is_a?(String) && actual_sig.bytesize == sig_bytes.bytesize
202
+ OpenSSL.fixed_length_secure_compare(actual_sig, sig_bytes)
169
203
  else
204
+ der_sig = begin
205
+ raw_to_der_signature(sig_bytes)
206
+ rescue StandardError
207
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::SIG_MALFORMED, "ecdsa signature is not a valid r||s pair")
208
+ end
170
209
  begin
171
- der_sig = raw_to_der_signature(sig_bytes)
172
210
  @verifying_key.dsa_verify_asn1(OpenSSL::Digest.digest(@config[:hash], body), der_sig)
173
- rescue StandardError
211
+ rescue OpenSSL::PKey::PKeyError, OpenSSL::PKey::ECError
174
212
  false
213
+ rescue StandardError => e
214
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::SIG_BACKEND, "ecdsa verification failed to run", cause: e)
175
215
  end
176
216
  end
177
217
  end
@@ -190,7 +230,6 @@ module Saro
190
230
 
191
231
  private
192
232
 
193
- # Avoid a needless copy when the body is already UTF-8 encoded bytes.
194
233
  def normalize_body(body)
195
234
  return body unless body.is_a?(String)
196
235
  enc = body.encoding
@@ -203,7 +242,7 @@ module Saro
203
242
  r = asn1.value[0].value
204
243
  s = asn1.value[1].value
205
244
 
206
- size = @config[:private_len] # curve byte size: (group.degree + 7) / 8
245
+ size = @config[:private_len]
207
246
  r_bytes = r.to_s(2).rjust(size, "\x00".b)
208
247
  s_bytes = s.to_s(2).rjust(size, "\x00".b)
209
248
 
data/lib/saro/dat/util.rb CHANGED
@@ -1,12 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'base64'
4
+ require_relative 'error'
4
5
 
5
6
  module Saro
6
7
  module Dat
7
8
  module Util
8
9
  module_function
9
10
 
11
+ U64_MAX = 0xFFFFFFFFFFFFFFFF
12
+
13
+ def parse_u64(s)
14
+ unless s.is_a?(String) && s.match?(/\A[0-9]+\z/)
15
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CONFIG_ARGUMENT_INVALID, "not an unsigned decimal integer: #{s.inspect}")
16
+ end
17
+ v = s.to_i
18
+ if v > U64_MAX
19
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CONFIG_ARGUMENT_INVALID, "exceeds u64: #{s.inspect}")
20
+ end
21
+ v
22
+ end
23
+
24
+ def parse_u64_hex(s)
25
+ unless s.is_a?(String) && s.match?(/\A[0-9a-fA-F]+\z/)
26
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CONFIG_ARGUMENT_INVALID, "not an unsigned hex integer: #{s.inspect}")
27
+ end
28
+ v = s.to_i(16)
29
+ if v > U64_MAX
30
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CONFIG_ARGUMENT_INVALID, "exceeds u64: #{s.inspect}")
31
+ end
32
+ v
33
+ end
34
+
10
35
  def encode_base64_url(s)
11
36
  return "".b if s.nil?
12
37
  if s.is_a?(String)
@@ -27,12 +52,15 @@ module Saro
27
52
  return "".b if s.empty?
28
53
  end
29
54
 
30
- # More robust way for older Ruby
31
- s = s.to_s.tr('-_', '+/')
55
+ s = s.to_s
32
56
  rem = s.bytesize % 4
33
57
  s += ("=" * (4 - rem)) if rem > 0
34
58
 
35
- Base64.decode64(s).b
59
+ begin
60
+ Base64.urlsafe_decode64(s).b
61
+ rescue ArgumentError => e
62
+ raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CONFIG_ARGUMENT_INVALID, "not a valid base64url string", cause: e)
63
+ end
36
64
  end
37
65
 
38
66
  def decode_base64_url_str(s)
data/lib/saro-dat.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'saro/dat/error'
3
4
  require_relative 'saro/dat/util'
4
5
  require_relative 'saro/dat/crypto'
5
6
  require_relative 'saro/dat/signature'
@@ -7,9 +8,3 @@ require_relative 'saro/dat/dat_certificate'
7
8
  require_relative 'saro/dat/dat'
8
9
  require_relative 'saro/dat/dat_manager'
9
10
  require_relative 'saro/dat/dat_cms_manager'
10
-
11
- module Saro
12
- module Dat
13
- class Error < StandardError; end
14
- end
15
- end
data/saro-dat.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = "saro-dat"
5
- spec.version = "4.3.4"
5
+ spec.version = "4.6.1"
6
6
  spec.authors = ["marker"]
7
7
  spec.email = ["j@saro.me"]
8
8
 
@@ -17,8 +17,6 @@ Gem::Specification.new do |spec|
17
17
 
18
18
  spec.metadata["keywords"] = "dat, distributed, access, token, web, session, security, authentication"
19
19
 
20
- # Specify which files should be added to the gem when it is released.
21
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
20
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
21
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features|_pypi)/}) }
24
22
  end
@@ -30,7 +28,7 @@ Gem::Specification.new do |spec|
30
28
  spec.add_dependency "base64"
31
29
  spec.add_dependency "logger"
32
30
 
33
- spec.add_development_dependency "minitest", "~> 5.0"
31
+ spec.add_development_dependency "minitest", "~> 6.0"
34
32
  spec.add_development_dependency "benchmark"
35
33
  spec.add_development_dependency "parallel"
36
34
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: saro-dat
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.3.4
4
+ version: 4.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - marker
@@ -57,14 +57,14 @@ dependencies:
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: '5.0'
60
+ version: '6.0'
61
61
  type: :development
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: '5.0'
67
+ version: '6.0'
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: benchmark
70
70
  requirement: !ruby/object:Gem::Requirement
@@ -116,6 +116,7 @@ files:
116
116
  - lib/saro/dat/dat_certificate.rb
117
117
  - lib/saro/dat/dat_cms_manager.rb
118
118
  - lib/saro/dat/dat_manager.rb
119
+ - lib/saro/dat/error.rb
119
120
  - lib/saro/dat/signature.rb
120
121
  - lib/saro/dat/util.rb
121
122
  - saro-dat.gemspec