saro-dat 4.6.0 → 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: 9e9d9a8073169dbbd0fb625d174acca960f30687ccf78b0d0d9424ca2d4d8ba2
4
- data.tar.gz: 1e727fef007baebc0960c14cbe57362db21d0311fc6486b0d6f3164d5da42dee
3
+ metadata.gz: b6d5127dce2e1d7ca632f94e2fa8d6b6a1434a37ab285bbf43dfc589282fccbb
4
+ data.tar.gz: e26c938570bfe16eae48746935a541e6ed552653b302cf7d15c10b7304dbc0af
5
5
  SHA512:
6
- metadata.gz: afea00f884102f1a5e84bf8992a024cb08810d04637625f0d09553b9e48ae0e1d01e9946734177db764a76846c1ff083d02c592c492d331d39bf1ffd8d05945b
7
- data.tar.gz: 2b8184f5c8279fc6717d15ccd4a82f4580d6e90dfb660eb6c4a151fa03ae5727cfa7cd08e694a192c8ddd96ecf9ba2a0b64d77dd6fcf21121d577445231bcb6c
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/PUBLISH.md CHANGED
@@ -38,3 +38,9 @@ ruby -v
38
38
  bundle add saro-dat
39
39
  bundle install
40
40
  ```
41
+
42
+ ## version up
43
+ ```
44
+ bundle outdated
45
+ bundle update --all
46
+ ```
@@ -32,8 +32,6 @@ module Saro
32
32
 
33
33
  def initialize(algorithm, key_bytes, config = nil)
34
34
  @config = config || Saro::Dat.get_crypto_config(algorithm)
35
- # OpenSSL accepts any valid AES length, so a 16-byte key would silently
36
- # become AES-128 under an IV-AES256-GCM label without this check.
37
35
  if key_bytes.bytesize != @config[:length]
38
36
  raise Saro::Dat::Error.new(
39
37
  Saro::Dat::ErrorCode::KEY_INVALID,
@@ -82,24 +80,16 @@ module Saro
82
80
  nonce + ciphertext + tag
83
81
  end
84
82
 
85
- # Decrypts base64url-encoded ciphertext.
86
83
  def decrypt_base64(base64_str)
87
84
  decrypt(Saro::Dat::Util.decode_base64_url(base64_str))
88
85
  end
89
86
 
90
- # Decrypts raw (already decoded) ciphertext: nonce(12) + ciphertext + tag(16).
91
- # Use #decrypt_base64 for encoded input. The branch used to be picked from
92
- # the string's encoding tag, so raw ciphertext that merely carried a UTF-8
93
- # tag was silently mangled by the base64 decoder.
94
87
  def decrypt(data)
95
88
  return "".b if data.nil? || data.empty?
96
89
 
97
- # Slice by bytes, never by characters: String#[] is character-indexed on
98
- # a non-binary string, so raw ciphertext carrying a UTF-8 tag would be
99
- # split at the wrong offsets.
100
90
  data = data.b if data.is_a?(String) && data.encoding != Encoding::BINARY
101
91
 
102
- if data.bytesize <= 12 + 16 # nonce(12) + tag(16)
92
+ if data.bytesize <= 12 + 16
103
93
  raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CRYPTO_DATA_INVALID, "ciphertext is shorter than iv(12) + tag(16)")
104
94
  end
105
95
 
@@ -114,10 +104,6 @@ module Saro
114
104
  cipher.iv = nonce
115
105
  cipher.auth_tag = tag
116
106
 
117
- # cipher.final 에서 나는 실패는 GCM 인증 태그 불일치다 — 변조된 secure 이거나
118
- # 잘못된 인증서 키다. 예전에는 OpenSSL::Cipher::CipherError 가 그대로 공개
119
- # API 밖으로 새어 나갔다. 서명 검증을 건너뛰는 경로에서는 이것이 유일한
120
- # 무결성 검사이므로 백엔드 오류(CRYPTO_BACKEND)와 반드시 구분한다.
121
107
  begin
122
108
  res = cipher.update(ciphertext) + cipher.final
123
109
  rescue OpenSSL::Cipher::CipherError => e
data/lib/saro/dat/dat.rb CHANGED
@@ -8,11 +8,6 @@ module Saro
8
8
  class Dat
9
9
  attr_reader :dat, :expire, :cid, :plain, :secure, :signature, :format
10
10
 
11
- # 파싱이 실패한 이유. 성공이면 nil 이다.
12
- #
13
- # 예전에는 빈 `rescue StandardError` 가 모든 실패를 삼키고 @format=false
14
- # 하나만 남겼다. 어느 필드가 왜 틀렸는지가 전부 사라져 호출부는
15
- # "Invalid DAT: Format" 밖에 볼 수 없었다.
16
11
  attr_reader :error
17
12
 
18
13
  def initialize(dat_str)
@@ -30,18 +25,12 @@ module Saro
30
25
  return
31
26
  end
32
27
 
33
- # 1) 먼저 구조를 확정한다. 파트가 5개가 아니면 그건 만료된 토큰이 아니라
34
- # 애초에 토큰이 아니다.
35
- # split 의 limit 을 -1 로 준다: 기본값은 뒤쪽 빈 필드를 버려서
36
- # "a.b.c.d.e." (6필드) 가 5파트로 보였고, 빈 서명("a.b.c.d.")은
37
- # 4파트로 보여 서명 오류를 구조 오류로 오인하게 만들었다.
38
28
  parts = @dat.split('.', -1)
39
29
  if parts.length != 5
40
30
  @error = Saro::Dat::Error.new(Saro::Dat::ErrorCode::TOKEN_MALFORMED, "expected exactly 5 dot-separated fields")
41
31
  return
42
32
  end
43
33
 
44
- # 2) 구조가 맞은 뒤에야 값을 본다. 필드마다 어디서 틀렸는지 코드를 붙인다.
45
34
  begin
46
35
  @expire = Saro::Dat::Util.parse_u64(parts[0])
47
36
  rescue Saro::Dat::Error => e
@@ -70,8 +59,6 @@ module Saro
70
59
  return
71
60
  end
72
61
 
73
- # 빈 서명은 구조 오류가 아니라 서명 오류다 (error.pre2.md: DAT_SIG_MALFORMED
74
- # 가 "빈 서명"을 포함한다). 위조(SIG_MISMATCH)와도 구분된다.
75
62
  if parts[4].empty?
76
63
  @error = Saro::Dat::Error.new(Saro::Dat::ErrorCode::SIG_MALFORMED, "signature field is empty")
77
64
  return
@@ -92,7 +79,6 @@ module Saro
92
79
  @format = true
93
80
  end
94
81
 
95
- # 파싱에 실패했으면 그 코드로 던진다.
96
82
  def raise_if_invalid!
97
83
  raise @error if @error
98
84
  nil
@@ -15,8 +15,6 @@ module Saro
15
15
  @dat_issuance_start_seconds = u64!("dat_issuance_start_seconds", dat_issuance_start_seconds)
16
16
  duration = u64!("dat_issuance_duration_seconds", dat_issuance_duration_seconds)
17
17
  @dat_ttl_seconds = u64!("dat_ttl_seconds", dat_ttl_seconds)
18
- # Ruby 정수는 자동으로 bignum 이 되므로 그냥 더하면 u64 를 넘겨도 조용히
19
- # 통과한다. 기준 구현(rust)의 checked_add 와 같은 경계를 여기서 강제한다.
20
18
  @dat_issuance_end_seconds = u64!(
21
19
  "dat_issuance_start_seconds + dat_issuance_duration_seconds",
22
20
  @dat_issuance_start_seconds + duration
@@ -56,7 +54,6 @@ module Saro
56
54
  raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CERT_MALFORMED, "expected exactly 8 dot-separated fields")
57
55
  end
58
56
 
59
- # 필드 파싱 실패는 인증서가 깨진 것이지 호출자의 인자 문제가 아니다.
60
57
  cid = _field("cid") { Saro::Dat::Util.parse_u64_hex(parts[0]) }
61
58
  dat_issuance_start_seconds = _field("issuance_start_seconds") { Saro::Dat::Util.parse_u64(parts[1]) }
62
59
  dat_issuance_duration_seconds = _field("issuance_duration_seconds") { Saro::Dat::Util.parse_u64(parts[2]) }
@@ -107,7 +104,6 @@ module Saro
107
104
  end
108
105
  private_class_method :_field
109
106
 
110
- # For Ruby conventions
111
107
  alias_method :issuable?, :issuable
112
108
  alias_method :expired?, :expired
113
109
  alias_method :signable?, :signable
@@ -13,9 +13,6 @@ module Saro
13
13
  class DatCmsManager
14
14
  DAT_CMS_API_VERSION = "v1"
15
15
 
16
- # `stop` waits at most this long for an in-flight HTTP sync to finish.
17
- # The thread is never killed: past the grace period it is left to complete
18
- # its request and exit on its own at the top of the loop.
19
16
  STOP_JOIN_TIMEOUT_SECONDS = 1.0
20
17
 
21
18
  def initialize(uri:, token:, interval_seconds: 60, verify_only: false, dat_manager: nil)
@@ -25,18 +22,12 @@ module Saro
25
22
  @verify_only = verify_only
26
23
  @manager = dat_manager || DatManager.new
27
24
  @version = 0
28
- # Two separate locks: @lock guards an in-flight sync (taken non-blocking,
29
- # held across the HTTP request), @lifecycle guards the stop flag and the
30
- # sleep condition. Sharing one lock made `stop` block for the whole
31
- # request timeout.
32
25
  @lock = Mutex.new
33
26
  @lifecycle = Mutex.new
34
27
  @stop_cond = ConditionVariable.new
35
28
  @stopped = false
36
29
  @logger = Logger.new($stdout)
37
30
  @logger.level = Logger::DEBUG
38
- # 최초 sync 실패는 여전히 생성을 막지 않는다(list.md F-3). 다만 이제 로그로만
39
- # 사라지지 않고 #last_error 로 조회할 수 있다.
40
31
  @last_error = Saro::Dat::Error.new(Saro::Dat::ErrorCode::CMS_NOT_SYNCED)
41
32
 
42
33
  sync
@@ -51,8 +42,6 @@ module Saro
51
42
  @lifecycle.synchronize do
52
43
  return if @stopped
53
44
  @stopped = true
54
- # Wakes the scheduler out of its sleep immediately instead of killing
55
- # it in the middle of a request.
56
45
  @stop_cond.broadcast
57
46
  thread = @thread
58
47
  end
@@ -64,8 +53,6 @@ module Saro
64
53
  @lifecycle.synchronize { @stopped }
65
54
  end
66
55
 
67
- # 마지막 동기화 실패. 한 번도 성공하지 못했으면 DAT_CMS_NOT_SYNCED, 정상이면 nil.
68
- # 재시도 여부는 err.retry 로 판정한다.
69
56
  attr_reader :last_error
70
57
 
71
58
  def version
@@ -77,7 +64,6 @@ module Saro
77
64
  @last_error = nil
78
65
  err
79
66
  rescue Saro::Dat::Error => e
80
- # 상태 신호는 실패로 기록하지 않는다 — 이전 동기화가 도는 중일 뿐이다.
81
67
  unless e.retry == :state
82
68
  @last_error = e
83
69
  @logger.error("[CRITICAL] DAT CMS SYNC #{@uri}: #{e.code} #{e.detail}")
@@ -89,10 +75,7 @@ module Saro
89
75
  nil
90
76
  end
91
77
 
92
- # 실패를 코드로 던진다. #sync 는 이것을 잡아 #last_error 에 담기만 한다 —
93
- # 기존 호출부가 갑자기 예외를 받지 않도록.
94
78
  private def sync_or_raise
95
- # non-blocking lock
96
79
  unless @lock.try_lock
97
80
  @logger.debug("cms sync skipped, previous sync still running: #{@uri}")
98
81
  raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CMS_SYNC_IN_PROGRESS)
@@ -103,7 +86,6 @@ module Saro
103
86
  request = Net::HTTP::Get.new(url)
104
87
  request["Authorization"] = @token
105
88
 
106
- # 연결 거부·DNS 실패·TLS 실패·타임아웃이 전부 여기로 온다. 전부 일시적이다.
107
89
  response =
108
90
  begin
109
91
  Net::HTTP.start(url.host, url.port, use_ssl: url.scheme == 'https', open_timeout: 10, read_timeout: 10) do |http|
@@ -113,8 +95,6 @@ module Saro
113
95
  raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CMS_UNREACHABLE, "cannot reach #{@uri}", cause: e)
114
96
  end
115
97
 
116
- # HTTP 상태를 갈라 낸다. 예전에는 전부 하나의 로그라 401(영구)에도
117
- # 60초마다 영원히 재시도했다.
118
98
  status = response.code.to_i
119
99
  raise self.class.http_status_error(status) unless status.between?(200, 299)
120
100
 
@@ -141,26 +121,17 @@ module Saro
141
121
  end
142
122
  new_version = new_version_str.to_i
143
123
 
144
- # 서버가 우리보다 과거 버전을 돌려주면 전체 재동기화 지시다. 오류가 아니라
145
- # 상태 신호이며, 아래 imports 가 clear: true 라 그 자체로 처리된다.
146
124
  if new_version < @version
147
125
  @logger.warn("#{Saro::Dat::ErrorCode::CMS_VERSION_RESET}: #{@version} -> #{new_version}")
148
126
  end
149
127
 
150
- # 인증서 적용 실패의 원인(CERT_*/KEY_*)을 버리지 않고 체이닝한다.
151
128
  renew_count =
152
129
  begin
153
- # clear: true, like rust's `manager.import(&certs, true)`. The CMS
154
- # response is the authoritative full set for that version, so
155
- # revoked certificates disappear instead of lingering until they
156
- # expire on their own.
157
- @manager.imports(new_certificates, clear: true)
130
+ @manager.imports(new_certificates, clear: false)
158
131
  rescue Saro::Dat::Error => e
159
132
  raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CMS_IMPORT_FAILED, "cannot apply received certificates", cause: e)
160
133
  end
161
134
 
162
- # Only a successful import advances the version, matching rust:
163
- # a rejected payload is re-requested rather than skipped.
164
135
  @version = new_version
165
136
  @logger.debug("Renewed #{renew_count} certificates for version #{new_version}: #{url}")
166
137
  nil
@@ -206,8 +177,6 @@ module Saro
206
177
  end
207
178
  end
208
179
 
209
- # Interruptible sleep: returns false as soon as `stop` signals, so the
210
- # scheduler exits promptly without Thread#kill.
211
180
  def wait_interval
212
181
  @lifecycle.synchronize do
213
182
  return false if @stopped
@@ -219,7 +188,6 @@ module Saro
219
188
  def run_sync_task
220
189
  sync
221
190
  rescue StandardError => e
222
- # sync 는 이미 스스로 삼키므로 여기 오는 것은 예상 밖의 실패다.
223
191
  @logger.error("Error in sync task: #{e.message}")
224
192
  end
225
193
  end
@@ -10,9 +10,6 @@ require_relative 'util'
10
10
  module Saro
11
11
  module Dat
12
12
  class DatManager
13
- # Immutable snapshot of the manager state.
14
- # Readers access it lock-free via a single ivar read (atomic reference swap);
15
- # writers rebuild a new frozen snapshot under @write_lock.
16
13
  State = Struct.new(:issuer, :certificates, :by_cid)
17
14
 
18
15
  EMPTY_STATE = State.new(nil, [].freeze, {}.freeze).freeze
@@ -24,13 +21,8 @@ module Saro
24
21
  end
25
22
 
26
23
  def import_certificates(input_certs, clear: false)
27
- # rust returns early on an empty input without touching the state, so an
28
- # empty CMS response can never wipe the certificates held by a manager
29
- # that is importing with clear: true.
30
24
  return 0 if input_certs.nil? || input_certs.empty?
31
25
 
32
- # Duplicate detection runs before any mutation (rust checks the whole
33
- # input up front), so a bad payload cannot leave a half-applied state.
34
26
  seen_cids = Set.new
35
27
  input_certs.each do |cert|
36
28
  if seen_cids.include?(cert.cid)
@@ -53,12 +45,9 @@ module Saro
53
45
  renew_count += 1
54
46
  end
55
47
 
56
- # Expired certificates are dropped from the *merged* list, not just
57
- # from the incoming one, so a renewal sweeps out what has aged out.
58
48
  certificates.reject!(&:expired)
59
49
  certificates.sort_by!(&:dat_issuance_end_seconds)
60
50
 
61
- # Find latest issuable certificate as issuer
62
51
  issuer = certificates.reverse_each.find(&:issuable)
63
52
 
64
53
  by_cid = {}
@@ -87,9 +76,6 @@ module Saro
87
76
  state = @state
88
77
  issuer = state.issuer
89
78
  unless issuer
90
- # 예전에는 이 다섯 가지가 "Invalid DAT: Signing Key Does Not Exist"
91
- # 문자열 하나였다. 대응이 전부 다르다 — 발급창 전이면 기다리면 되고,
92
- # verify-only 뿐이면 배포 설정 실수이며, 0건이면 CMS 접속 문제다.
93
79
  raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::MANAGER_NO_CERTIFICATE) if state.certificates.empty?
94
80
  raise Saro::Dat::Error.new(
95
81
  Saro::Dat::ErrorCode::MANAGER_NO_ISSUABLE_CERTIFICATE,
@@ -102,11 +88,8 @@ module Saro
102
88
 
103
89
  def parse(dat_input)
104
90
  dat = Saro::Dat::Dat.from_value(dat_input)
105
- # 파싱 실패의 코드를 그대로 올린다.
106
91
  dat.raise_if_invalid!
107
92
 
108
- # 만료를 cid 조회보다 먼저 본다. 기준 구현(rust)이 토큰을 읽는 시점에
109
- # 만료를 판정하므로, 모르는 cid 의 만료 토큰도 만료로 보고된다.
110
93
  raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::TOKEN_EXPIRED) if dat.expired?
111
94
 
112
95
  certificate = @state.by_cid[dat.cid]
@@ -117,7 +100,6 @@ module Saro
117
100
  self.class._parse(certificate, dat)
118
101
  end
119
102
 
120
- # 발급 가능한 인증서가 없을 때 왜 없는지 가려낸다.
121
103
  private def no_issuable_cause(certificates)
122
104
  now = Time.now.to_i
123
105
  signable_seen = false
@@ -138,7 +120,6 @@ module Saro
138
120
  if !signable_seen
139
121
  Saro::Dat::ErrorCode::CERT_VERIFY_ONLY
140
122
  elsif not_yet
141
- # 기다리면 풀리는 유일한 사유다. 하나라도 있으면 이것을 앞세운다.
142
123
  Saro::Dat::ErrorCode::CERT_NOT_YET_ISSUABLE
143
124
  elsif ended
144
125
  Saro::Dat::ErrorCode::CERT_ISSUANCE_ENDED
@@ -148,10 +129,6 @@ module Saro
148
129
  Saro::Dat::Error.new(code)
149
130
  end
150
131
 
151
- # NOTE: 아래 `private` 는 `def self.` 메서드에 적용되지 않는다. _issue/_parse 는
152
- # 예전부터 실제로는 public 이었고 테스트·벤치가 그렇게 쓰고 있다. 오해를 없애려
153
- # 위치를 옮기고, 정말 감춰야 하는 것만 private_class_method 로 막는다.
154
-
155
132
  def self._issue(cert, plain, secure)
156
133
  now = Time.now.to_i
157
134
  expire = now + cert.dat_ttl_seconds
@@ -170,12 +147,9 @@ module Saro
170
147
 
171
148
  def self._parse(cert, dat_input)
172
149
  dat = Saro::Dat::Dat.from_value(dat_input)
173
- # 같은 조건이 여기서는 RuntimeError, DatManager#parse 에서는 ArgumentError
174
- # 였다. 이제 양쪽 모두 같은 코드를 던진다.
175
150
  dat.raise_if_invalid!
176
151
  raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::TOKEN_EXPIRED) if dat.expired?
177
152
 
178
- # verify 는 불일치일 때만 false 를 준다. 연산 실패는 SIG_BACKEND 로 올라온다.
179
153
  unless cert.signature_key.verify(dat.body_string, dat.signature)
180
154
  raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::SIG_MISMATCH)
181
155
  end
@@ -2,21 +2,11 @@
2
2
 
3
3
  module Saro
4
4
  module Dat
5
- # DAT 통합 오류 코드 (error.pre2.md).
6
- #
7
- # 코드 문자열은 모든 공식 클라이언트와 CMS 서버가 공유하는 공개 계약이다. 메시지는 자유롭게 바꿔도
8
- # 되지만 코드는 바꾸지 않는다.
9
- #
10
- # - 분류는 원인이다. "어느 함수에서 났는가"가 아니라 "무엇이 잘못됐는가"다.
11
- # - *_UNKNOWN 은 각 영역의 폴백 전용이다. "알 수 없는 X" 라는 뜻으로 쓰지 않는다.
12
- # - 하위 원인은 버리지 않고 #cause 로 보존한다.
13
5
  module ErrorCode
14
- # TOKEN : DAT 토큰 문자열
15
6
  TOKEN_MALFORMED = "DAT_TOKEN_MALFORMED"
16
7
  TOKEN_EXPIRED = "DAT_TOKEN_EXPIRED"
17
8
  TOKEN_UNKNOWN = "DAT_TOKEN_UNKNOWN"
18
9
 
19
- # CERT : 인증서
20
10
  CERT_MALFORMED = "DAT_CERT_MALFORMED"
21
11
  CERT_EXPIRED = "DAT_CERT_EXPIRED"
22
12
  CERT_NOT_YET_ISSUABLE = "DAT_CERT_NOT_YET_ISSUABLE"
@@ -27,31 +17,26 @@ module Saro
27
17
  CERT_DUPLICATE_CID = "DAT_CERT_DUPLICATE_CID"
28
18
  CERT_UNKNOWN = "DAT_CERT_UNKNOWN"
29
19
 
30
- # SIG : 서명
31
20
  SIG_MISMATCH = "DAT_SIG_MISMATCH"
32
21
  SIG_MALFORMED = "DAT_SIG_MALFORMED"
33
22
  SIG_KEY_MISSING = "DAT_SIG_KEY_MISSING"
34
23
  SIG_BACKEND = "DAT_SIG_BACKEND"
35
24
  SIG_UNKNOWN = "DAT_SIG_UNKNOWN"
36
25
 
37
- # CRYPTO : secure 페이로드
38
26
  CRYPTO_TAG_MISMATCH = "DAT_CRYPTO_TAG_MISMATCH"
39
27
  CRYPTO_DATA_INVALID = "DAT_CRYPTO_DATA_INVALID"
40
28
  CRYPTO_BACKEND = "DAT_CRYPTO_BACKEND"
41
29
  CRYPTO_UNKNOWN = "DAT_CRYPTO_UNKNOWN"
42
30
 
43
- # KEY : 키 재료
44
31
  KEY_INVALID = "DAT_KEY_INVALID"
45
32
  KEY_VERIFY_ONLY_UNSUPPORTED = "DAT_KEY_VERIFY_ONLY_UNSUPPORTED"
46
33
  KEY_UNKNOWN = "DAT_KEY_UNKNOWN"
47
34
 
48
- # MANAGER : 매니저 보유 상태
49
35
  MANAGER_NO_CERTIFICATE = "DAT_MANAGER_NO_CERTIFICATE"
50
36
  MANAGER_NO_ISSUABLE_CERTIFICATE = "DAT_MANAGER_NO_ISSUABLE_CERTIFICATE"
51
37
  MANAGER_DISPOSED = "DAT_MANAGER_DISPOSED"
52
38
  MANAGER_UNKNOWN = "DAT_MANAGER_UNKNOWN"
53
39
 
54
- # CMS : 서버 응답·전송
55
40
  CMS_UNREACHABLE = "DAT_CMS_UNREACHABLE"
56
41
  CMS_UNAUTHORIZED = "DAT_CMS_UNAUTHORIZED"
57
42
  CMS_FORBIDDEN = "DAT_CMS_FORBIDDEN"
@@ -66,18 +51,14 @@ module Saro
66
51
  CMS_NOT_SUPPORTED = "DAT_CMS_NOT_SUPPORTED"
67
52
  CMS_UNKNOWN = "DAT_CMS_UNKNOWN"
68
53
 
69
- # CONFIG : 호출자가 넘긴 값
70
54
  CONFIG_ALG_UNSUPPORTED = "DAT_CONFIG_ALG_UNSUPPORTED"
71
55
  CONFIG_URI_INVALID = "DAT_CONFIG_URI_INVALID"
72
56
  CONFIG_ARGUMENT_INVALID = "DAT_CONFIG_ARGUMENT_INVALID"
73
57
  CONFIG_UNKNOWN = "DAT_CONFIG_UNKNOWN"
74
58
 
75
- # INTERNAL : 실행 환경
76
59
  INTERNAL_UNAVAILABLE = "DAT_INTERNAL_UNAVAILABLE"
77
60
  INTERNAL_UNKNOWN = "DAT_INTERNAL_UNKNOWN"
78
61
 
79
- # 재시도 분류. 애매하면 :permanent 다 — 영구 오류에 대한 무한 재시도가
80
- # 이 체계 이전의 실제 결함이었다.
81
62
  TRANSIENT = [
82
63
  CERT_NOT_YET_ISSUABLE, CERT_NOT_SYNCED, MANAGER_NO_CERTIFICATE,
83
64
  CMS_UNREACHABLE, CMS_SERVER_ERROR, CMS_NOT_SYNCED
@@ -85,20 +66,11 @@ module Saro
85
66
 
86
67
  STATE = [CMS_VERSION_RESET, CMS_SYNC_IN_PROGRESS].freeze
87
68
 
88
- # 위조·변조 시도의 직접 증거.
89
69
  SECURITY = [SIG_MISMATCH, CRYPTO_TAG_MISMATCH].freeze
90
70
  end
91
71
 
92
- # DAT 의 단일 오류 타입.
93
- #
94
- # 예전에는 정의만 되어 있고 아무도 쓰지 않았다. 대신 ArgumentError 와
95
- # RuntimeError 가 같은 조건에 뒤섞여 던져져서(예: "Invalid DAT: Format" 이
96
- # DatManager#parse 에서는 ArgumentError, DatManager._parse 에서는
97
- # RuntimeError) 호출부가 어느 쪽을 잡아야 할지 알 수 없었다.
98
72
  class Error < StandardError
99
- # 공개 계약인 오류 코드. 모든 공식 클라이언트에서 동일하다.
100
73
  attr_reader :code
101
- # 사람이 읽는 설명. 자유롭게 바꿔도 된다.
102
74
  attr_reader :detail
103
75
 
104
76
  def initialize(code, detail = nil, cause: nil)
@@ -108,21 +80,12 @@ module Saro
108
80
  super(detail ? "#{code}: #{detail}" : code)
109
81
  end
110
82
 
111
- # Ruby 는 rescue 안에서 raise 하면 #cause 를 자동으로 채운다. 그 동작은
112
- # 그대로 두고, 명시적으로 넘긴 하위 원인이 있으면 그쪽을 우선한다.
113
- # DAT_MANAGER_NO_ISSUABLE_CERTIFICATE 의 사유 코드가 이 경로로 실린다.
114
83
  def cause
115
84
  @explicit_cause || super
116
85
  end
117
86
 
118
- # :transient - 같은 입력으로 재시도하면 해소될 수 있다. 백오프 후 재시도.
119
- # :permanent - 설정·입력·배포를 고쳐야 한다. 재시도하지 않는다.
120
- # :state - 오류가 아닌 상태 신호. 흐름 제어에만 쓴다.
121
- #
122
- # 중간값을 두지 않는다 — 호출부가 분기할 수 없기 때문이다.
123
87
  def retry
124
88
  if @code == ErrorCode::MANAGER_NO_ISSUABLE_CERTIFICATE
125
- # 발급창 시작 전이면 기다리면 풀린다. 나머지 사유는 안 풀린다.
126
89
  c = cause
127
90
  return :transient if c.is_a?(Error) && c.code == ErrorCode::CERT_NOT_YET_ISSUABLE
128
91
  return :permanent
@@ -132,7 +95,6 @@ module Saro
132
95
  :permanent
133
96
  end
134
97
 
135
- # 위조·변조 시도의 직접 증거. 다른 실패와 같은 경로로 로깅하지 않는다.
136
98
  def security_event?
137
99
  ErrorCode::SECURITY.include?(@code)
138
100
  end
@@ -142,13 +104,10 @@ module Saro
142
104
  end
143
105
 
144
106
  class << self
145
- # 어떤 예외에서든 DAT 오류 코드를 꺼낸다. DAT 오류가 아니면 nil 이다.
146
107
  def code_of(e)
147
108
  e.is_a?(Error) ? e.code : nil
148
109
  end
149
110
 
150
- # DAT 오류가 아닌 것을 감싼다. 이미 Error 면 그대로 둔다.
151
- # 원본은 cause 로 반드시 보존한다.
152
111
  def wrap(code, detail, e)
153
112
  return e if e.is_a?(Error)
154
113
  new(code, detail, cause: e)
@@ -48,8 +48,6 @@ module Saro
48
48
  if priv_bn
49
49
  group = OpenSSL::PKey::EC::Group.new(curve_name)
50
50
 
51
- # d must be in [1, n-1]; OpenSSL would otherwise happily build a key
52
- # whose signatures can never verify.
53
51
  if priv_bn <= 0 || priv_bn >= group.order
54
52
  raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::KEY_INVALID, "ecdsa private scalar out of range [1, n-1]")
55
53
  end
@@ -64,10 +62,6 @@ module Saro
64
62
  ])
65
63
  key = OpenSSL::PKey::EC.new(asn1.to_der)
66
64
 
67
- # Cross-check that the imported public point really belongs to the
68
- # private scalar. Without this a mismatched pair imports cleanly and
69
- # produces signatures that can never verify (rust's
70
- # EcdsaKeyPair::from_private_key_and_public_key rejects it at import).
71
65
  begin
72
66
  key.check_key
73
67
  rescue OpenSSL::PKey::PKeyError, OpenSSL::PKey::ECError => e
@@ -179,15 +173,10 @@ module Saro
179
173
  end
180
174
  end
181
175
 
182
- # Verifies a raw (already decoded) signature.
183
- # Use #verify_base64 for a base64url-encoded signature: the branch used to
184
- # be picked from the string's encoding tag, so a raw signature that merely
185
- # carried a UTF-8 tag was silently run through the base64 decoder.
186
176
  def verify(body, signature)
187
177
  verify_bytes(body, signature)
188
178
  end
189
179
 
190
- # Verifies a base64url-encoded signature.
191
180
  def verify_base64(body, signature_base64)
192
181
  sig_bytes = begin
193
182
  Saro::Dat::Util.decode_base64_url(signature_base64)
@@ -201,33 +190,25 @@ module Saro
201
190
  body = normalize_body(body)
202
191
  return false if body.nil? || body.empty?
203
192
 
204
- # Same reason as DatCrypto#decrypt: the raw signature is split in half by
205
- # byte offset, which String#[] only honours on a binary string.
206
193
  sig_bytes = sig_bytes.b if sig_bytes.is_a?(String) && sig_bytes.encoding != Encoding::BINARY
207
194
 
208
- # false 는 "서명이 안 맞는다"만 뜻한다. 예전에는 `rescue StandardError => false`
209
- # 가 잘못된 키 타입·손상된 핸들·라이브러리 버그까지 전부 삼켜서 프로그래밍
210
- # 오류가 위조 시도로 보고됐다. 그래서 연산 실패는 SIG_BACKEND 로 갈라 낸다.
211
195
  if @config[:name] == "HMAC"
212
196
  begin
213
197
  actual_sig = OpenSSL::HMAC.digest(@config[:hash], @verifying_key, body)
214
198
  rescue StandardError => e
215
199
  raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::SIG_BACKEND, "hmac computation failed", cause: e)
216
200
  end
217
- # 길이가 다르면 볼 것도 없이 불일치다.
218
201
  return false unless sig_bytes.is_a?(String) && actual_sig.bytesize == sig_bytes.bytesize
219
202
  OpenSSL.fixed_length_secure_compare(actual_sig, sig_bytes)
220
203
  else
221
204
  der_sig = begin
222
205
  raw_to_der_signature(sig_bytes)
223
206
  rescue StandardError
224
- # r||s 길이가 곡선과 안 맞는다 = 서명 자체의 형식 오류. 위조가 아니다.
225
207
  raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::SIG_MALFORMED, "ecdsa signature is not a valid r||s pair")
226
208
  end
227
209
  begin
228
210
  @verifying_key.dsa_verify_asn1(OpenSSL::Digest.digest(@config[:hash], body), der_sig)
229
211
  rescue OpenSSL::PKey::PKeyError, OpenSSL::PKey::ECError
230
- # OpenSSL 이 서명을 해독조차 못 한 경우다. 불일치로 본다.
231
212
  false
232
213
  rescue StandardError => e
233
214
  raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::SIG_BACKEND, "ecdsa verification failed to run", cause: e)
@@ -249,7 +230,6 @@ module Saro
249
230
 
250
231
  private
251
232
 
252
- # Avoid a needless copy when the body is already UTF-8 encoded bytes.
253
233
  def normalize_body(body)
254
234
  return body unless body.is_a?(String)
255
235
  enc = body.encoding
@@ -262,7 +242,7 @@ module Saro
262
242
  r = asn1.value[0].value
263
243
  s = asn1.value[1].value
264
244
 
265
- size = @config[:private_len] # curve byte size: (group.degree + 7) / 8
245
+ size = @config[:private_len]
266
246
  r_bytes = r.to_s(2).rjust(size, "\x00".b)
267
247
  s_bytes = s.to_s(2).rjust(size, "\x00".b)
268
248
 
data/lib/saro/dat/util.rb CHANGED
@@ -10,12 +10,6 @@ module Saro
10
10
 
11
11
  U64_MAX = 0xFFFFFFFFFFFFFFFF
12
12
 
13
- # Strict unsigned 64-bit decimal parse, matching rust's `parse::<u64>()`.
14
- # Ruby's String#to_i never raises and Integer() accepts `0x`, `_` and
15
- # surrounding whitespace, so the character set is checked explicitly.
16
- # 무엇을 파싱하다 실패했는지에 따라 코드가 갈린다(토큰이면 TOKEN_MALFORMED,
17
- # 인증서면 CERT_MALFORMED). 여기서는 중립적인 인자 오류로 두고, 각 호출부에서
18
- # 정확한 코드로 감싼다.
19
13
  def parse_u64(s)
20
14
  unless s.is_a?(String) && s.match?(/\A[0-9]+\z/)
21
15
  raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CONFIG_ARGUMENT_INVALID, "not an unsigned decimal integer: #{s.inspect}")
@@ -27,7 +21,6 @@ module Saro
27
21
  v
28
22
  end
29
23
 
30
- # Strict unsigned 64-bit hex parse, matching rust's `u64::from_str_radix(s, 16)`.
31
24
  def parse_u64_hex(s)
32
25
  unless s.is_a?(String) && s.match?(/\A[0-9a-fA-F]+\z/)
33
26
  raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CONFIG_ARGUMENT_INVALID, "not an unsigned hex integer: #{s.inspect}")
@@ -59,9 +52,6 @@ module Saro
59
52
  return "".b if s.empty?
60
53
  end
61
54
 
62
- # Base64.decode64 is RFC 2045 and silently drops invalid characters, so
63
- # "!!!!invalid@@@@" would decode to arbitrary bytes instead of raising.
64
- # urlsafe_decode64 is strict, matching rust's decoder.
65
55
  s = s.to_s
66
56
  rem = s.bytesize % 4
67
57
  s += ("=" * (4 - rem)) if rem > 0
data/lib/saro-dat.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Error 는 다른 모든 파일이 참조하므로 가장 먼저 로드한다.
4
3
  require_relative 'saro/dat/error'
5
4
  require_relative 'saro/dat/util'
6
5
  require_relative 'saro/dat/crypto'
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.6.0"
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.6.0
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