saro-dat 4.3.4 → 4.6.0
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 +4 -4
- data/.idea/vcs.xml +1 -0
- data/PUBLISH.md +3 -1
- data/README.md +8 -8
- data/lib/saro/dat/crypto.rb +41 -9
- data/lib/saro/dat/dat.rb +80 -15
- data/lib/saro/dat/dat_certificate.rb +40 -9
- data/lib/saro/dat/dat_cms_manager.rb +131 -33
- data/lib/saro/dat/dat_manager.rb +83 -15
- data/lib/saro/dat/error.rb +159 -0
- data/lib/saro/dat/signature.rb +80 -21
- data/lib/saro/dat/util.rb +41 -3
- data/lib/saro-dat.rb +2 -6
- data/saro-dat.gemspec +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9e9d9a8073169dbbd0fb625d174acca960f30687ccf78b0d0d9424ca2d4d8ba2
|
|
4
|
+
data.tar.gz: 1e727fef007baebc0960c14cbe57362db21d0311fc6486b0d6f3164d5da42dee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: afea00f884102f1a5e84bf8992a024cb08810d04637625f0d09553b9e48ae0e1d01e9946734177db764a76846c1ff083d02c592c492d331d39bf1ffd8d05945b
|
|
7
|
+
data.tar.gz: 2b8184f5c8279fc6717d15ccd4a82f4580d6e90dfb660eb6c4a151fa03ae5727cfa7cd08e694a192c8ddd96ecf9ba2a0b64d77dd6fcf21121d577445231bcb6c
|
data/.idea/vcs.xml
CHANGED
data/PUBLISH.md
CHANGED
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/
|
|
10
|
-
- [Java, Kotlin](https://github.com/saro-lab/dat/tree/master/
|
|
11
|
-
- [Javascript, Typescript](https://github.com/saro-lab/dat/tree/master/
|
|
12
|
-
- [C#](https://github.com/saro-lab/dat/tree/master/
|
|
13
|
-
- [Python](https://github.com/saro-lab/dat/tree/master/
|
|
14
|
-
- [Go](https://github.com/saro-lab/dat/tree/master/
|
|
15
|
-
- [Ruby](https://github.com/saro-lab/dat/tree/master/
|
|
16
|
-
- [C/C++ (Vcpkg)](https://github.com/saro-lab/dat/tree/master/
|
|
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
|
data/lib/saro/dat/crypto.rb
CHANGED
|
@@ -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
|
|
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,14 @@ module Saro
|
|
|
31
32
|
|
|
32
33
|
def initialize(algorithm, key_bytes, config = nil)
|
|
33
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
|
+
if key_bytes.bytesize != @config[:length]
|
|
38
|
+
raise Saro::Dat::Error.new(
|
|
39
|
+
Saro::Dat::ErrorCode::KEY_INVALID,
|
|
40
|
+
"#{algorithm} key must be #{@config[:length]} bytes, got #{key_bytes.bytesize}"
|
|
41
|
+
)
|
|
42
|
+
end
|
|
34
43
|
@algorithm = algorithm
|
|
35
44
|
@key_bytes = key_bytes
|
|
36
45
|
end
|
|
@@ -63,20 +72,35 @@ module Saro
|
|
|
63
72
|
cipher.iv_len = 12
|
|
64
73
|
cipher.iv = nonce
|
|
65
74
|
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
begin
|
|
76
|
+
ciphertext = cipher.update(data) + cipher.final
|
|
77
|
+
tag = cipher.auth_tag
|
|
78
|
+
rescue OpenSSL::Cipher::CipherError => e
|
|
79
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CRYPTO_BACKEND, "aes-gcm encrypt failed", cause: e)
|
|
80
|
+
end
|
|
68
81
|
|
|
69
82
|
nonce + ciphertext + tag
|
|
70
83
|
end
|
|
71
84
|
|
|
85
|
+
# Decrypts base64url-encoded ciphertext.
|
|
86
|
+
def decrypt_base64(base64_str)
|
|
87
|
+
decrypt(Saro::Dat::Util.decode_base64_url(base64_str))
|
|
88
|
+
end
|
|
89
|
+
|
|
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.
|
|
72
94
|
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
95
|
return "".b if data.nil? || data.empty?
|
|
77
96
|
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
data = data.b if data.is_a?(String) && data.encoding != Encoding::BINARY
|
|
101
|
+
|
|
102
|
+
if data.bytesize <= 12 + 16 # nonce(12) + tag(16)
|
|
103
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CRYPTO_DATA_INVALID, "ciphertext is shorter than iv(12) + tag(16)")
|
|
80
104
|
end
|
|
81
105
|
|
|
82
106
|
nonce = data[0, 12]
|
|
@@ -90,7 +114,15 @@ module Saro
|
|
|
90
114
|
cipher.iv = nonce
|
|
91
115
|
cipher.auth_tag = tag
|
|
92
116
|
|
|
93
|
-
|
|
117
|
+
# cipher.final 에서 나는 실패는 GCM 인증 태그 불일치다 — 변조된 secure 이거나
|
|
118
|
+
# 잘못된 인증서 키다. 예전에는 OpenSSL::Cipher::CipherError 가 그대로 공개
|
|
119
|
+
# API 밖으로 새어 나갔다. 서명 검증을 건너뛰는 경로에서는 이것이 유일한
|
|
120
|
+
# 무결성 검사이므로 백엔드 오류(CRYPTO_BACKEND)와 반드시 구분한다.
|
|
121
|
+
begin
|
|
122
|
+
res = cipher.update(ciphertext) + cipher.final
|
|
123
|
+
rescue OpenSSL::Cipher::CipherError => e
|
|
124
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CRYPTO_TAG_MISMATCH, "gcm authentication tag mismatch", cause: e)
|
|
125
|
+
end
|
|
94
126
|
res.force_encoding('BINARY')
|
|
95
127
|
res
|
|
96
128
|
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,13 @@ module Saro
|
|
|
7
8
|
class Dat
|
|
8
9
|
attr_reader :dat, :expire, :cid, :plain, :secure, :signature, :format
|
|
9
10
|
|
|
11
|
+
# 파싱이 실패한 이유. 성공이면 nil 이다.
|
|
12
|
+
#
|
|
13
|
+
# 예전에는 빈 `rescue StandardError` 가 모든 실패를 삼키고 @format=false
|
|
14
|
+
# 하나만 남겼다. 어느 필드가 왜 틀렸는지가 전부 사라져 호출부는
|
|
15
|
+
# "Invalid DAT: Format" 밖에 볼 수 없었다.
|
|
16
|
+
attr_reader :error
|
|
17
|
+
|
|
10
18
|
def initialize(dat_str)
|
|
11
19
|
@dat = dat_str || ''
|
|
12
20
|
@format = false
|
|
@@ -15,22 +23,79 @@ module Saro
|
|
|
15
23
|
@plain = "".b
|
|
16
24
|
@secure = "".b
|
|
17
25
|
@signature = "".b
|
|
26
|
+
@error = nil
|
|
27
|
+
|
|
28
|
+
if @dat.empty?
|
|
29
|
+
@error = Saro::Dat::Error.new(Saro::Dat::ErrorCode::TOKEN_MALFORMED, "token is empty")
|
|
30
|
+
return
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# 1) 먼저 구조를 확정한다. 파트가 5개가 아니면 그건 만료된 토큰이 아니라
|
|
34
|
+
# 애초에 토큰이 아니다.
|
|
35
|
+
# split 의 limit 을 -1 로 준다: 기본값은 뒤쪽 빈 필드를 버려서
|
|
36
|
+
# "a.b.c.d.e." (6필드) 가 5파트로 보였고, 빈 서명("a.b.c.d.")은
|
|
37
|
+
# 4파트로 보여 서명 오류를 구조 오류로 오인하게 만들었다.
|
|
38
|
+
parts = @dat.split('.', -1)
|
|
39
|
+
if parts.length != 5
|
|
40
|
+
@error = Saro::Dat::Error.new(Saro::Dat::ErrorCode::TOKEN_MALFORMED, "expected exactly 5 dot-separated fields")
|
|
41
|
+
return
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# 2) 구조가 맞은 뒤에야 값을 본다. 필드마다 어디서 틀렸는지 코드를 붙인다.
|
|
45
|
+
begin
|
|
46
|
+
@expire = Saro::Dat::Util.parse_u64(parts[0])
|
|
47
|
+
rescue Saro::Dat::Error => e
|
|
48
|
+
@error = Saro::Dat::Error.new(Saro::Dat::ErrorCode::TOKEN_MALFORMED, "expire field is not a plain decimal u64", cause: e)
|
|
49
|
+
return
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
begin
|
|
53
|
+
@cid = Saro::Dat::Util.parse_u64_hex(parts[1])
|
|
54
|
+
rescue Saro::Dat::Error => e
|
|
55
|
+
@error = Saro::Dat::Error.new(Saro::Dat::ErrorCode::TOKEN_MALFORMED, "cid field is not a plain hex u64", cause: e)
|
|
56
|
+
return
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
begin
|
|
60
|
+
@plain = Saro::Dat::Util.decode_base64_url(parts[2])
|
|
61
|
+
rescue Saro::Dat::Error => e
|
|
62
|
+
@error = Saro::Dat::Error.new(Saro::Dat::ErrorCode::TOKEN_MALFORMED, "plain field is not base64url", cause: e)
|
|
63
|
+
return
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
begin
|
|
67
|
+
@secure = Saro::Dat::Util.decode_base64_url(parts[3])
|
|
68
|
+
rescue Saro::Dat::Error => e
|
|
69
|
+
@error = Saro::Dat::Error.new(Saro::Dat::ErrorCode::TOKEN_MALFORMED, "secure field is not base64url", cause: e)
|
|
70
|
+
return
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# 빈 서명은 구조 오류가 아니라 서명 오류다 (error.pre2.md: DAT_SIG_MALFORMED
|
|
74
|
+
# 가 "빈 서명"을 포함한다). 위조(SIG_MISMATCH)와도 구분된다.
|
|
75
|
+
if parts[4].empty?
|
|
76
|
+
@error = Saro::Dat::Error.new(Saro::Dat::ErrorCode::SIG_MALFORMED, "signature field is empty")
|
|
77
|
+
return
|
|
78
|
+
end
|
|
18
79
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
80
|
+
begin
|
|
81
|
+
@signature = Saro::Dat::Util.decode_base64_url(parts[4])
|
|
82
|
+
rescue Saro::Dat::Error => e
|
|
83
|
+
@error = Saro::Dat::Error.new(Saro::Dat::ErrorCode::SIG_MALFORMED, "signature field is not base64url", cause: e)
|
|
84
|
+
return
|
|
33
85
|
end
|
|
86
|
+
|
|
87
|
+
if @signature.empty?
|
|
88
|
+
@error = Saro::Dat::Error.new(Saro::Dat::ErrorCode::SIG_MALFORMED, "signature field is empty")
|
|
89
|
+
return
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
@format = true
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# 파싱에 실패했으면 그 코드로 던진다.
|
|
96
|
+
def raise_if_invalid!
|
|
97
|
+
raise @error if @error
|
|
98
|
+
nil
|
|
34
99
|
end
|
|
35
100
|
|
|
36
101
|
def self.from_value(value)
|
|
@@ -40,7 +105,7 @@ module Saro
|
|
|
40
105
|
|
|
41
106
|
def expired
|
|
42
107
|
return true unless @format
|
|
43
|
-
Time.now.to_i
|
|
108
|
+
Time.now.to_i >= @expire
|
|
44
109
|
end
|
|
45
110
|
|
|
46
111
|
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,20 @@ 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
|
-
|
|
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
|
+
# Ruby 정수는 자동으로 bignum 이 되므로 그냥 더하면 u64 를 넘겨도 조용히
|
|
19
|
+
# 통과한다. 기준 구현(rust)의 checked_add 와 같은 경계를 여기서 강제한다.
|
|
20
|
+
@dat_issuance_end_seconds = u64!(
|
|
21
|
+
"dat_issuance_start_seconds + dat_issuance_duration_seconds",
|
|
22
|
+
@dat_issuance_start_seconds + duration
|
|
23
|
+
)
|
|
24
|
+
u64!(
|
|
25
|
+
"dat_issuance_start_seconds + dat_issuance_duration_seconds + dat_ttl_seconds",
|
|
26
|
+
@dat_issuance_end_seconds + @dat_ttl_seconds
|
|
27
|
+
)
|
|
17
28
|
@signature_key = signature_key
|
|
18
29
|
@crypto_key = crypto_key
|
|
19
30
|
end
|
|
@@ -41,12 +52,15 @@ module Saro
|
|
|
41
52
|
|
|
42
53
|
def self.imports(format_str)
|
|
43
54
|
parts = format_str.split(".")
|
|
44
|
-
|
|
55
|
+
if parts.length != 8
|
|
56
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CERT_MALFORMED, "expected exactly 8 dot-separated fields")
|
|
57
|
+
end
|
|
45
58
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
59
|
+
# 필드 파싱 실패는 인증서가 깨진 것이지 호출자의 인자 문제가 아니다.
|
|
60
|
+
cid = _field("cid") { Saro::Dat::Util.parse_u64_hex(parts[0]) }
|
|
61
|
+
dat_issuance_start_seconds = _field("issuance_start_seconds") { Saro::Dat::Util.parse_u64(parts[1]) }
|
|
62
|
+
dat_issuance_duration_seconds = _field("issuance_duration_seconds") { Saro::Dat::Util.parse_u64(parts[2]) }
|
|
63
|
+
dat_ttl_seconds = _field("dat_ttl_seconds") { Saro::Dat::Util.parse_u64(parts[3]) }
|
|
50
64
|
signature_algorithm = parts[4]
|
|
51
65
|
crypto_algorithm = parts[5]
|
|
52
66
|
signature_key = Saro::Dat::DatSignature.imports(signature_algorithm, parts[6])
|
|
@@ -76,6 +90,23 @@ module Saro
|
|
|
76
90
|
@signature_key.support_verify_only
|
|
77
91
|
end
|
|
78
92
|
|
|
93
|
+
U64_MAX = 0xFFFFFFFFFFFFFFFF
|
|
94
|
+
private_constant :U64_MAX
|
|
95
|
+
|
|
96
|
+
private def u64!(name, value)
|
|
97
|
+
unless value.is_a?(Integer) && !value.is_a?(TrueClass) && value >= 0 && value <= U64_MAX
|
|
98
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CERT_MALFORMED, "#{name} must fit in u64: #{value}")
|
|
99
|
+
end
|
|
100
|
+
value
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def self._field(name)
|
|
104
|
+
yield
|
|
105
|
+
rescue Saro::Dat::Error => e
|
|
106
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CERT_MALFORMED, "#{name} field is not a plain number", cause: e)
|
|
107
|
+
end
|
|
108
|
+
private_class_method :_field
|
|
109
|
+
|
|
79
110
|
# For Ruby conventions
|
|
80
111
|
alias_method :issuable?, :issuable
|
|
81
112
|
alias_method :expired?, :expired
|
|
@@ -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,11 @@ module Saro
|
|
|
12
13
|
class DatCmsManager
|
|
13
14
|
DAT_CMS_API_VERSION = "v1"
|
|
14
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
|
+
STOP_JOIN_TIMEOUT_SECONDS = 1.0
|
|
20
|
+
|
|
15
21
|
def initialize(uri:, token:, interval_seconds: 60, verify_only: false, dat_manager: nil)
|
|
16
22
|
@uri = uri
|
|
17
23
|
@token = token
|
|
@@ -19,10 +25,19 @@ module Saro
|
|
|
19
25
|
@verify_only = verify_only
|
|
20
26
|
@manager = dat_manager || DatManager.new
|
|
21
27
|
@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.
|
|
22
32
|
@lock = Mutex.new
|
|
33
|
+
@lifecycle = Mutex.new
|
|
34
|
+
@stop_cond = ConditionVariable.new
|
|
23
35
|
@stopped = false
|
|
24
36
|
@logger = Logger.new($stdout)
|
|
25
37
|
@logger.level = Logger::DEBUG
|
|
38
|
+
# 최초 sync 실패는 여전히 생성을 막지 않는다(list.md F-3). 다만 이제 로그로만
|
|
39
|
+
# 사라지지 않고 #last_error 로 조회할 수 있다.
|
|
40
|
+
@last_error = Saro::Dat::Error.new(Saro::Dat::ErrorCode::CMS_NOT_SYNCED)
|
|
26
41
|
|
|
27
42
|
sync
|
|
28
43
|
|
|
@@ -32,17 +47,55 @@ module Saro
|
|
|
32
47
|
end
|
|
33
48
|
|
|
34
49
|
def stop
|
|
35
|
-
|
|
50
|
+
thread = nil
|
|
51
|
+
@lifecycle.synchronize do
|
|
52
|
+
return if @stopped
|
|
36
53
|
@stopped = true
|
|
37
|
-
|
|
54
|
+
# Wakes the scheduler out of its sleep immediately instead of killing
|
|
55
|
+
# it in the middle of a request.
|
|
56
|
+
@stop_cond.broadcast
|
|
57
|
+
thread = @thread
|
|
38
58
|
end
|
|
59
|
+
thread&.join(STOP_JOIN_TIMEOUT_SECONDS)
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def stopped?
|
|
64
|
+
@lifecycle.synchronize { @stopped }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# 마지막 동기화 실패. 한 번도 성공하지 못했으면 DAT_CMS_NOT_SYNCED, 정상이면 nil.
|
|
68
|
+
# 재시도 여부는 err.retry 로 판정한다.
|
|
69
|
+
attr_reader :last_error
|
|
70
|
+
|
|
71
|
+
def version
|
|
72
|
+
@version
|
|
39
73
|
end
|
|
40
74
|
|
|
41
75
|
def sync
|
|
76
|
+
err = sync_or_raise
|
|
77
|
+
@last_error = nil
|
|
78
|
+
err
|
|
79
|
+
rescue Saro::Dat::Error => e
|
|
80
|
+
# 상태 신호는 실패로 기록하지 않는다 — 이전 동기화가 도는 중일 뿐이다.
|
|
81
|
+
unless e.retry == :state
|
|
82
|
+
@last_error = e
|
|
83
|
+
@logger.error("[CRITICAL] DAT CMS SYNC #{@uri}: #{e.code} #{e.detail}")
|
|
84
|
+
end
|
|
85
|
+
nil
|
|
86
|
+
rescue StandardError => e
|
|
87
|
+
@last_error = Saro::Dat::Error.new(Saro::Dat::ErrorCode::CMS_UNKNOWN, "unclassified cms failure", cause: e)
|
|
88
|
+
@logger.error("[CRITICAL] DAT CMS SYNC #{@uri}: #{e.message}")
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# 실패를 코드로 던진다. #sync 는 이것을 잡아 #last_error 에 담기만 한다 —
|
|
93
|
+
# 기존 호출부가 갑자기 예외를 받지 않도록.
|
|
94
|
+
private def sync_or_raise
|
|
42
95
|
# non-blocking lock
|
|
43
96
|
unless @lock.try_lock
|
|
44
|
-
@logger.
|
|
45
|
-
|
|
97
|
+
@logger.debug("cms sync skipped, previous sync still running: #{@uri}")
|
|
98
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CMS_SYNC_IN_PROGRESS)
|
|
46
99
|
end
|
|
47
100
|
|
|
48
101
|
begin
|
|
@@ -50,55 +103,82 @@ module Saro
|
|
|
50
103
|
request = Net::HTTP::Get.new(url)
|
|
51
104
|
request["Authorization"] = @token
|
|
52
105
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
106
|
+
# 연결 거부·DNS 실패·TLS 실패·타임아웃이 전부 여기로 온다. 전부 일시적이다.
|
|
107
|
+
response =
|
|
108
|
+
begin
|
|
109
|
+
Net::HTTP.start(url.host, url.port, use_ssl: url.scheme == 'https', open_timeout: 10, read_timeout: 10) do |http|
|
|
110
|
+
http.request(request)
|
|
111
|
+
end
|
|
112
|
+
rescue StandardError => e
|
|
113
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CMS_UNREACHABLE, "cannot reach #{@uri}", cause: e)
|
|
114
|
+
end
|
|
56
115
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
116
|
+
# HTTP 상태를 갈라 낸다. 예전에는 전부 하나의 로그라 401(영구)에도
|
|
117
|
+
# 60초마다 영원히 재시도했다.
|
|
118
|
+
status = response.code.to_i
|
|
119
|
+
raise self.class.http_status_error(status) unless status.between?(200, 299)
|
|
61
120
|
|
|
62
121
|
body = response.body
|
|
63
122
|
if body.nil? || body.empty?
|
|
64
123
|
@logger.debug("No new certificate: #{url}")
|
|
65
|
-
return
|
|
124
|
+
return nil
|
|
66
125
|
end
|
|
67
126
|
|
|
68
127
|
lines = body.split("\n", 2)
|
|
69
128
|
if lines.length < 2
|
|
70
129
|
if body.start_with?("\n")
|
|
71
|
-
|
|
72
|
-
return
|
|
130
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CMS_MALFORMED, "response has no version line")
|
|
73
131
|
end
|
|
74
132
|
@logger.debug("No new certificate: #{url}")
|
|
75
|
-
return
|
|
133
|
+
return nil
|
|
76
134
|
end
|
|
77
135
|
|
|
78
136
|
new_version_str = lines[0].strip
|
|
79
137
|
new_certificates = lines[1].strip
|
|
80
138
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return
|
|
139
|
+
unless new_version_str.match?(/\A[0-9]+\z/)
|
|
140
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CMS_MALFORMED, "version line is not a plain decimal integer")
|
|
84
141
|
end
|
|
142
|
+
new_version = new_version_str.to_i
|
|
85
143
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
@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}")
|
|
144
|
+
# 서버가 우리보다 과거 버전을 돌려주면 전체 재동기화 지시다. 오류가 아니라
|
|
145
|
+
# 상태 신호이며, 아래 imports 가 clear: true 라 그 자체로 처리된다.
|
|
146
|
+
if new_version < @version
|
|
147
|
+
@logger.warn("#{Saro::Dat::ErrorCode::CMS_VERSION_RESET}: #{@version} -> #{new_version}")
|
|
93
148
|
end
|
|
94
149
|
|
|
95
|
-
|
|
96
|
-
|
|
150
|
+
# 인증서 적용 실패의 원인(CERT_*/KEY_*)을 버리지 않고 체이닝한다.
|
|
151
|
+
renew_count =
|
|
152
|
+
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)
|
|
158
|
+
rescue Saro::Dat::Error => e
|
|
159
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CMS_IMPORT_FAILED, "cannot apply received certificates", cause: e)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Only a successful import advances the version, matching rust:
|
|
163
|
+
# a rejected payload is re-requested rather than skipped.
|
|
164
|
+
@version = new_version
|
|
165
|
+
@logger.debug("Renewed #{renew_count} certificates for version #{new_version}: #{url}")
|
|
166
|
+
nil
|
|
97
167
|
ensure
|
|
98
168
|
@lock.unlock
|
|
99
169
|
end
|
|
100
170
|
end
|
|
101
171
|
|
|
172
|
+
def self.http_status_error(status)
|
|
173
|
+
case status
|
|
174
|
+
when 401 then Saro::Dat::Error.new(Saro::Dat::ErrorCode::CMS_UNAUTHORIZED, "http 401")
|
|
175
|
+
when 403 then Saro::Dat::Error.new(Saro::Dat::ErrorCode::CMS_FORBIDDEN, "http 403")
|
|
176
|
+
when 404 then Saro::Dat::Error.new(Saro::Dat::ErrorCode::CMS_ENDPOINT_NOT_FOUND, "http 404")
|
|
177
|
+
when 500..599 then Saro::Dat::Error.new(Saro::Dat::ErrorCode::CMS_SERVER_ERROR, "http #{status}")
|
|
178
|
+
else Saro::Dat::Error.new(Saro::Dat::ErrorCode::CMS_HTTP_STATUS, "http #{status}")
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
102
182
|
def get_manager
|
|
103
183
|
@manager
|
|
104
184
|
end
|
|
@@ -120,16 +200,26 @@ module Saro
|
|
|
120
200
|
def schedule_sync
|
|
121
201
|
@thread = Thread.new do
|
|
122
202
|
loop do
|
|
123
|
-
|
|
124
|
-
break if @stopped
|
|
203
|
+
break unless wait_interval
|
|
125
204
|
run_sync_task
|
|
126
205
|
end
|
|
127
206
|
end
|
|
128
207
|
end
|
|
129
208
|
|
|
209
|
+
# Interruptible sleep: returns false as soon as `stop` signals, so the
|
|
210
|
+
# scheduler exits promptly without Thread#kill.
|
|
211
|
+
def wait_interval
|
|
212
|
+
@lifecycle.synchronize do
|
|
213
|
+
return false if @stopped
|
|
214
|
+
@stop_cond.wait(@lifecycle, @interval_seconds)
|
|
215
|
+
!@stopped
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
130
219
|
def run_sync_task
|
|
131
220
|
sync
|
|
132
221
|
rescue StandardError => e
|
|
222
|
+
# sync 는 이미 스스로 삼키므로 여기 오는 것은 예상 밖의 실패다.
|
|
133
223
|
@logger.error("Error in sync task: #{e.message}")
|
|
134
224
|
end
|
|
135
225
|
end
|
|
@@ -168,13 +258,21 @@ module Saro
|
|
|
168
258
|
end
|
|
169
259
|
|
|
170
260
|
def build
|
|
171
|
-
parsed =
|
|
172
|
-
|
|
261
|
+
parsed =
|
|
262
|
+
begin
|
|
263
|
+
URI.parse(@uri)
|
|
264
|
+
rescue URI::InvalidURIError => e
|
|
265
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CONFIG_URI_INVALID, "cannot be parsed as a uri", cause: e)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
unless %w[http https].include?(parsed.scheme)
|
|
269
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CONFIG_URI_INVALID, "scheme must be http or https")
|
|
270
|
+
end
|
|
173
271
|
if parsed.path && parsed.path != '' && parsed.path != '/'
|
|
174
|
-
raise
|
|
272
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CONFIG_URI_INVALID, "must be path-less: #{@uri}")
|
|
175
273
|
end
|
|
176
274
|
if parsed.query
|
|
177
|
-
raise
|
|
275
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CONFIG_URI_INVALID, "must be query-less: #{@uri}")
|
|
178
276
|
end
|
|
179
277
|
|
|
180
278
|
path = @verify_only ? "/v1/certs/verify-only" : "/v1/certs"
|
data/lib/saro/dat/dat_manager.rb
CHANGED
|
@@ -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'
|
|
@@ -23,23 +24,38 @@ module Saro
|
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
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
|
+
return 0 if input_certs.nil? || input_certs.empty?
|
|
31
|
+
|
|
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
|
+
seen_cids = Set.new
|
|
35
|
+
input_certs.each do |cert|
|
|
36
|
+
if seen_cids.include?(cert.cid)
|
|
37
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CERT_DUPLICATE_CID, "duplicate cid #{cert.cid.to_s(16)}")
|
|
38
|
+
end
|
|
39
|
+
seen_cids.add(cert.cid)
|
|
40
|
+
end
|
|
41
|
+
|
|
26
42
|
renew_count = 0
|
|
27
43
|
@write_lock.synchronize do
|
|
28
44
|
certificates = clear ? [] : @state.certificates.dup
|
|
29
45
|
|
|
30
|
-
|
|
31
|
-
seen_cids = Set.new
|
|
46
|
+
cids = Set.new(certificates.map(&:cid))
|
|
32
47
|
|
|
33
48
|
input_certs.each do |cert|
|
|
34
|
-
|
|
35
|
-
seen_cids.add(cert.cid)
|
|
36
|
-
next if cert.expired
|
|
37
|
-
next if before_cids.include?(cert.cid)
|
|
49
|
+
next if cids.include?(cert.cid)
|
|
38
50
|
|
|
51
|
+
cids.add(cert.cid)
|
|
39
52
|
certificates << cert
|
|
40
53
|
renew_count += 1
|
|
41
54
|
end
|
|
42
55
|
|
|
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
|
+
certificates.reject!(&:expired)
|
|
43
59
|
certificates.sort_by!(&:dat_issuance_end_seconds)
|
|
44
60
|
|
|
45
61
|
# Find latest issuable certificate as issuer
|
|
@@ -68,24 +84,73 @@ module Saro
|
|
|
68
84
|
end
|
|
69
85
|
|
|
70
86
|
def issue(plain, secure)
|
|
71
|
-
|
|
72
|
-
|
|
87
|
+
state = @state
|
|
88
|
+
issuer = state.issuer
|
|
89
|
+
unless issuer
|
|
90
|
+
# 예전에는 이 다섯 가지가 "Invalid DAT: Signing Key Does Not Exist"
|
|
91
|
+
# 문자열 하나였다. 대응이 전부 다르다 — 발급창 전이면 기다리면 되고,
|
|
92
|
+
# verify-only 뿐이면 배포 설정 실수이며, 0건이면 CMS 접속 문제다.
|
|
93
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::MANAGER_NO_CERTIFICATE) if state.certificates.empty?
|
|
94
|
+
raise Saro::Dat::Error.new(
|
|
95
|
+
Saro::Dat::ErrorCode::MANAGER_NO_ISSUABLE_CERTIFICATE,
|
|
96
|
+
cause: no_issuable_cause(state.certificates)
|
|
97
|
+
)
|
|
98
|
+
end
|
|
73
99
|
|
|
74
100
|
self.class._issue(issuer, plain, secure)
|
|
75
101
|
end
|
|
76
102
|
|
|
77
103
|
def parse(dat_input)
|
|
78
104
|
dat = Saro::Dat::Dat.from_value(dat_input)
|
|
79
|
-
|
|
105
|
+
# 파싱 실패의 코드를 그대로 올린다.
|
|
106
|
+
dat.raise_if_invalid!
|
|
80
107
|
|
|
81
|
-
|
|
108
|
+
# 만료를 cid 조회보다 먼저 본다. 기준 구현(rust)이 토큰을 읽는 시점에
|
|
109
|
+
# 만료를 판정하므로, 모르는 cid 의 만료 토큰도 만료로 보고된다.
|
|
110
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::TOKEN_EXPIRED) if dat.expired?
|
|
82
111
|
|
|
83
|
-
|
|
112
|
+
certificate = @state.by_cid[dat.cid]
|
|
113
|
+
unless certificate
|
|
114
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CERT_NOT_FOUND, "cid #{dat.cid.to_s(16)}")
|
|
115
|
+
end
|
|
84
116
|
|
|
85
117
|
self.class._parse(certificate, dat)
|
|
86
118
|
end
|
|
87
119
|
|
|
88
|
-
|
|
120
|
+
# 발급 가능한 인증서가 없을 때 왜 없는지 가려낸다.
|
|
121
|
+
private def no_issuable_cause(certificates)
|
|
122
|
+
now = Time.now.to_i
|
|
123
|
+
signable_seen = false
|
|
124
|
+
not_yet = false
|
|
125
|
+
ended = false
|
|
126
|
+
|
|
127
|
+
certificates.each do |cert|
|
|
128
|
+
next unless cert.signable
|
|
129
|
+
signable_seen = true
|
|
130
|
+
if now < cert.dat_issuance_start_seconds
|
|
131
|
+
not_yet = true
|
|
132
|
+
elsif now > cert.dat_issuance_end_seconds
|
|
133
|
+
ended = true
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
code =
|
|
138
|
+
if !signable_seen
|
|
139
|
+
Saro::Dat::ErrorCode::CERT_VERIFY_ONLY
|
|
140
|
+
elsif not_yet
|
|
141
|
+
# 기다리면 풀리는 유일한 사유다. 하나라도 있으면 이것을 앞세운다.
|
|
142
|
+
Saro::Dat::ErrorCode::CERT_NOT_YET_ISSUABLE
|
|
143
|
+
elsif ended
|
|
144
|
+
Saro::Dat::ErrorCode::CERT_ISSUANCE_ENDED
|
|
145
|
+
else
|
|
146
|
+
Saro::Dat::ErrorCode::CERT_EXPIRED
|
|
147
|
+
end
|
|
148
|
+
Saro::Dat::Error.new(code)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# NOTE: 아래 `private` 는 `def self.` 메서드에 적용되지 않는다. _issue/_parse 는
|
|
152
|
+
# 예전부터 실제로는 public 이었고 테스트·벤치가 그렇게 쓰고 있다. 오해를 없애려
|
|
153
|
+
# 위치를 옮기고, 정말 감춰야 하는 것만 private_class_method 로 막는다.
|
|
89
154
|
|
|
90
155
|
def self._issue(cert, plain, secure)
|
|
91
156
|
now = Time.now.to_i
|
|
@@ -105,11 +170,14 @@ module Saro
|
|
|
105
170
|
|
|
106
171
|
def self._parse(cert, dat_input)
|
|
107
172
|
dat = Saro::Dat::Dat.from_value(dat_input)
|
|
108
|
-
|
|
109
|
-
|
|
173
|
+
# 같은 조건이 여기서는 RuntimeError, DatManager#parse 에서는 ArgumentError
|
|
174
|
+
# 였다. 이제 양쪽 모두 같은 코드를 던진다.
|
|
175
|
+
dat.raise_if_invalid!
|
|
176
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::TOKEN_EXPIRED) if dat.expired?
|
|
110
177
|
|
|
178
|
+
# verify 는 불일치일 때만 false 를 준다. 연산 실패는 SIG_BACKEND 로 올라온다.
|
|
111
179
|
unless cert.signature_key.verify(dat.body_string, dat.signature)
|
|
112
|
-
raise
|
|
180
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::SIG_MISMATCH)
|
|
113
181
|
end
|
|
114
182
|
|
|
115
183
|
decrypted_secure = cert.crypto_key.decrypt(dat.secure)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Saro
|
|
4
|
+
module Dat
|
|
5
|
+
# DAT 통합 오류 코드 (error.pre2.md).
|
|
6
|
+
#
|
|
7
|
+
# 코드 문자열은 모든 공식 클라이언트와 CMS 서버가 공유하는 공개 계약이다. 메시지는 자유롭게 바꿔도
|
|
8
|
+
# 되지만 코드는 바꾸지 않는다.
|
|
9
|
+
#
|
|
10
|
+
# - 분류는 원인이다. "어느 함수에서 났는가"가 아니라 "무엇이 잘못됐는가"다.
|
|
11
|
+
# - *_UNKNOWN 은 각 영역의 폴백 전용이다. "알 수 없는 X" 라는 뜻으로 쓰지 않는다.
|
|
12
|
+
# - 하위 원인은 버리지 않고 #cause 로 보존한다.
|
|
13
|
+
module ErrorCode
|
|
14
|
+
# TOKEN : DAT 토큰 문자열
|
|
15
|
+
TOKEN_MALFORMED = "DAT_TOKEN_MALFORMED"
|
|
16
|
+
TOKEN_EXPIRED = "DAT_TOKEN_EXPIRED"
|
|
17
|
+
TOKEN_UNKNOWN = "DAT_TOKEN_UNKNOWN"
|
|
18
|
+
|
|
19
|
+
# CERT : 인증서
|
|
20
|
+
CERT_MALFORMED = "DAT_CERT_MALFORMED"
|
|
21
|
+
CERT_EXPIRED = "DAT_CERT_EXPIRED"
|
|
22
|
+
CERT_NOT_YET_ISSUABLE = "DAT_CERT_NOT_YET_ISSUABLE"
|
|
23
|
+
CERT_ISSUANCE_ENDED = "DAT_CERT_ISSUANCE_ENDED"
|
|
24
|
+
CERT_VERIFY_ONLY = "DAT_CERT_VERIFY_ONLY"
|
|
25
|
+
CERT_NOT_FOUND = "DAT_CERT_NOT_FOUND"
|
|
26
|
+
CERT_NOT_SYNCED = "DAT_CERT_NOT_SYNCED"
|
|
27
|
+
CERT_DUPLICATE_CID = "DAT_CERT_DUPLICATE_CID"
|
|
28
|
+
CERT_UNKNOWN = "DAT_CERT_UNKNOWN"
|
|
29
|
+
|
|
30
|
+
# SIG : 서명
|
|
31
|
+
SIG_MISMATCH = "DAT_SIG_MISMATCH"
|
|
32
|
+
SIG_MALFORMED = "DAT_SIG_MALFORMED"
|
|
33
|
+
SIG_KEY_MISSING = "DAT_SIG_KEY_MISSING"
|
|
34
|
+
SIG_BACKEND = "DAT_SIG_BACKEND"
|
|
35
|
+
SIG_UNKNOWN = "DAT_SIG_UNKNOWN"
|
|
36
|
+
|
|
37
|
+
# CRYPTO : secure 페이로드
|
|
38
|
+
CRYPTO_TAG_MISMATCH = "DAT_CRYPTO_TAG_MISMATCH"
|
|
39
|
+
CRYPTO_DATA_INVALID = "DAT_CRYPTO_DATA_INVALID"
|
|
40
|
+
CRYPTO_BACKEND = "DAT_CRYPTO_BACKEND"
|
|
41
|
+
CRYPTO_UNKNOWN = "DAT_CRYPTO_UNKNOWN"
|
|
42
|
+
|
|
43
|
+
# KEY : 키 재료
|
|
44
|
+
KEY_INVALID = "DAT_KEY_INVALID"
|
|
45
|
+
KEY_VERIFY_ONLY_UNSUPPORTED = "DAT_KEY_VERIFY_ONLY_UNSUPPORTED"
|
|
46
|
+
KEY_UNKNOWN = "DAT_KEY_UNKNOWN"
|
|
47
|
+
|
|
48
|
+
# MANAGER : 매니저 보유 상태
|
|
49
|
+
MANAGER_NO_CERTIFICATE = "DAT_MANAGER_NO_CERTIFICATE"
|
|
50
|
+
MANAGER_NO_ISSUABLE_CERTIFICATE = "DAT_MANAGER_NO_ISSUABLE_CERTIFICATE"
|
|
51
|
+
MANAGER_DISPOSED = "DAT_MANAGER_DISPOSED"
|
|
52
|
+
MANAGER_UNKNOWN = "DAT_MANAGER_UNKNOWN"
|
|
53
|
+
|
|
54
|
+
# CMS : 서버 응답·전송
|
|
55
|
+
CMS_UNREACHABLE = "DAT_CMS_UNREACHABLE"
|
|
56
|
+
CMS_UNAUTHORIZED = "DAT_CMS_UNAUTHORIZED"
|
|
57
|
+
CMS_FORBIDDEN = "DAT_CMS_FORBIDDEN"
|
|
58
|
+
CMS_ENDPOINT_NOT_FOUND = "DAT_CMS_ENDPOINT_NOT_FOUND"
|
|
59
|
+
CMS_SERVER_ERROR = "DAT_CMS_SERVER_ERROR"
|
|
60
|
+
CMS_HTTP_STATUS = "DAT_CMS_HTTP_STATUS"
|
|
61
|
+
CMS_MALFORMED = "DAT_CMS_MALFORMED"
|
|
62
|
+
CMS_IMPORT_FAILED = "DAT_CMS_IMPORT_FAILED"
|
|
63
|
+
CMS_VERSION_RESET = "DAT_CMS_VERSION_RESET"
|
|
64
|
+
CMS_NOT_SYNCED = "DAT_CMS_NOT_SYNCED"
|
|
65
|
+
CMS_SYNC_IN_PROGRESS = "DAT_CMS_SYNC_IN_PROGRESS"
|
|
66
|
+
CMS_NOT_SUPPORTED = "DAT_CMS_NOT_SUPPORTED"
|
|
67
|
+
CMS_UNKNOWN = "DAT_CMS_UNKNOWN"
|
|
68
|
+
|
|
69
|
+
# CONFIG : 호출자가 넘긴 값
|
|
70
|
+
CONFIG_ALG_UNSUPPORTED = "DAT_CONFIG_ALG_UNSUPPORTED"
|
|
71
|
+
CONFIG_URI_INVALID = "DAT_CONFIG_URI_INVALID"
|
|
72
|
+
CONFIG_ARGUMENT_INVALID = "DAT_CONFIG_ARGUMENT_INVALID"
|
|
73
|
+
CONFIG_UNKNOWN = "DAT_CONFIG_UNKNOWN"
|
|
74
|
+
|
|
75
|
+
# INTERNAL : 실행 환경
|
|
76
|
+
INTERNAL_UNAVAILABLE = "DAT_INTERNAL_UNAVAILABLE"
|
|
77
|
+
INTERNAL_UNKNOWN = "DAT_INTERNAL_UNKNOWN"
|
|
78
|
+
|
|
79
|
+
# 재시도 분류. 애매하면 :permanent 다 — 영구 오류에 대한 무한 재시도가
|
|
80
|
+
# 이 체계 이전의 실제 결함이었다.
|
|
81
|
+
TRANSIENT = [
|
|
82
|
+
CERT_NOT_YET_ISSUABLE, CERT_NOT_SYNCED, MANAGER_NO_CERTIFICATE,
|
|
83
|
+
CMS_UNREACHABLE, CMS_SERVER_ERROR, CMS_NOT_SYNCED
|
|
84
|
+
].freeze
|
|
85
|
+
|
|
86
|
+
STATE = [CMS_VERSION_RESET, CMS_SYNC_IN_PROGRESS].freeze
|
|
87
|
+
|
|
88
|
+
# 위조·변조 시도의 직접 증거.
|
|
89
|
+
SECURITY = [SIG_MISMATCH, CRYPTO_TAG_MISMATCH].freeze
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# DAT 의 단일 오류 타입.
|
|
93
|
+
#
|
|
94
|
+
# 예전에는 정의만 되어 있고 아무도 쓰지 않았다. 대신 ArgumentError 와
|
|
95
|
+
# RuntimeError 가 같은 조건에 뒤섞여 던져져서(예: "Invalid DAT: Format" 이
|
|
96
|
+
# DatManager#parse 에서는 ArgumentError, DatManager._parse 에서는
|
|
97
|
+
# RuntimeError) 호출부가 어느 쪽을 잡아야 할지 알 수 없었다.
|
|
98
|
+
class Error < StandardError
|
|
99
|
+
# 공개 계약인 오류 코드. 모든 공식 클라이언트에서 동일하다.
|
|
100
|
+
attr_reader :code
|
|
101
|
+
# 사람이 읽는 설명. 자유롭게 바꿔도 된다.
|
|
102
|
+
attr_reader :detail
|
|
103
|
+
|
|
104
|
+
def initialize(code, detail = nil, cause: nil)
|
|
105
|
+
@code = code
|
|
106
|
+
@detail = detail
|
|
107
|
+
@explicit_cause = cause
|
|
108
|
+
super(detail ? "#{code}: #{detail}" : code)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Ruby 는 rescue 안에서 raise 하면 #cause 를 자동으로 채운다. 그 동작은
|
|
112
|
+
# 그대로 두고, 명시적으로 넘긴 하위 원인이 있으면 그쪽을 우선한다.
|
|
113
|
+
# DAT_MANAGER_NO_ISSUABLE_CERTIFICATE 의 사유 코드가 이 경로로 실린다.
|
|
114
|
+
def cause
|
|
115
|
+
@explicit_cause || super
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# :transient - 같은 입력으로 재시도하면 해소될 수 있다. 백오프 후 재시도.
|
|
119
|
+
# :permanent - 설정·입력·배포를 고쳐야 한다. 재시도하지 않는다.
|
|
120
|
+
# :state - 오류가 아닌 상태 신호. 흐름 제어에만 쓴다.
|
|
121
|
+
#
|
|
122
|
+
# 중간값을 두지 않는다 — 호출부가 분기할 수 없기 때문이다.
|
|
123
|
+
def retry
|
|
124
|
+
if @code == ErrorCode::MANAGER_NO_ISSUABLE_CERTIFICATE
|
|
125
|
+
# 발급창 시작 전이면 기다리면 풀린다. 나머지 사유는 안 풀린다.
|
|
126
|
+
c = cause
|
|
127
|
+
return :transient if c.is_a?(Error) && c.code == ErrorCode::CERT_NOT_YET_ISSUABLE
|
|
128
|
+
return :permanent
|
|
129
|
+
end
|
|
130
|
+
return :transient if ErrorCode::TRANSIENT.include?(@code)
|
|
131
|
+
return :state if ErrorCode::STATE.include?(@code)
|
|
132
|
+
:permanent
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# 위조·변조 시도의 직접 증거. 다른 실패와 같은 경로로 로깅하지 않는다.
|
|
136
|
+
def security_event?
|
|
137
|
+
ErrorCode::SECURITY.include?(@code)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def inspect
|
|
141
|
+
"#<Saro::Dat::Error #{@code}#{@detail ? " #{@detail.inspect}" : ''}>"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
class << self
|
|
145
|
+
# 어떤 예외에서든 DAT 오류 코드를 꺼낸다. DAT 오류가 아니면 nil 이다.
|
|
146
|
+
def code_of(e)
|
|
147
|
+
e.is_a?(Error) ? e.code : nil
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# DAT 오류가 아닌 것을 감싼다. 이미 Error 면 그대로 둔다.
|
|
151
|
+
# 원본은 cause 로 반드시 보존한다.
|
|
152
|
+
def wrap(code, detail, e)
|
|
153
|
+
return e if e.is_a?(Error)
|
|
154
|
+
new(code, detail, cause: e)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
data/lib/saro/dat/signature.rb
CHANGED
|
@@ -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
|
|
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,34 @@ 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
|
+
# d must be in [1, n-1]; OpenSSL would otherwise happily build a key
|
|
52
|
+
# whose signatures can never verify.
|
|
53
|
+
if priv_bn <= 0 || priv_bn >= group.order
|
|
54
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::KEY_INVALID, "ecdsa private scalar out of range [1, n-1]")
|
|
55
|
+
end
|
|
56
|
+
|
|
49
57
|
pub_octet ||= group.generator.mul(priv_bn).to_octet_string(:uncompressed)
|
|
50
|
-
|
|
58
|
+
|
|
51
59
|
asn1 = OpenSSL::ASN1::Sequence.new([
|
|
52
60
|
OpenSSL::ASN1::Integer.new(1),
|
|
53
61
|
OpenSSL::ASN1::OctetString.new(priv_bn.to_s(2).rjust((group.degree + 7) / 8, "\x00".b)),
|
|
54
62
|
OpenSSL::ASN1::ASN1Data.new([OpenSSL::ASN1::ObjectId.new(curve_name)], 0, :CONTEXT_SPECIFIC),
|
|
55
63
|
OpenSSL::ASN1::ASN1Data.new([OpenSSL::ASN1::BitString.new(pub_octet)], 1, :CONTEXT_SPECIFIC)
|
|
56
64
|
])
|
|
57
|
-
OpenSSL::PKey::EC.new(asn1.to_der)
|
|
65
|
+
key = OpenSSL::PKey::EC.new(asn1.to_der)
|
|
66
|
+
|
|
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
|
+
begin
|
|
72
|
+
key.check_key
|
|
73
|
+
rescue OpenSSL::PKey::PKeyError, OpenSSL::PKey::ECError => e
|
|
74
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::KEY_INVALID, "ecdsa key pair rejected: #{e.message}", cause: e)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
key
|
|
58
78
|
elsif pub_octet
|
|
59
79
|
spki = OpenSSL::ASN1::Sequence.new([
|
|
60
80
|
OpenSSL::ASN1::Sequence.new([
|
|
@@ -63,9 +83,15 @@ module Saro
|
|
|
63
83
|
]),
|
|
64
84
|
OpenSSL::ASN1::BitString.new(pub_octet)
|
|
65
85
|
])
|
|
66
|
-
OpenSSL::PKey::EC.new(spki.to_der)
|
|
86
|
+
key = OpenSSL::PKey::EC.new(spki.to_der)
|
|
87
|
+
begin
|
|
88
|
+
key.check_key
|
|
89
|
+
rescue OpenSSL::PKey::PKeyError, OpenSSL::PKey::ECError => e
|
|
90
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::KEY_INVALID, "ecdsa public key rejected: #{e.message}", cause: e)
|
|
91
|
+
end
|
|
92
|
+
key
|
|
67
93
|
else
|
|
68
|
-
raise
|
|
94
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CONFIG_ARGUMENT_INVALID, "either private key or public key must be provided")
|
|
69
95
|
end
|
|
70
96
|
end
|
|
71
97
|
|
|
@@ -76,6 +102,11 @@ module Saro
|
|
|
76
102
|
new(algorithm, key, key, config)
|
|
77
103
|
else
|
|
78
104
|
key = OpenSSL::PKey::EC.generate(config[:curve])
|
|
105
|
+
begin
|
|
106
|
+
key.check_key
|
|
107
|
+
rescue OpenSSL::PKey::PKeyError, OpenSSL::PKey::ECError => e
|
|
108
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::INTERNAL_UNKNOWN, "generated ecdsa key pair is invalid: #{e.message}", cause: e)
|
|
109
|
+
end
|
|
79
110
|
new(algorithm, key, key, config)
|
|
80
111
|
end
|
|
81
112
|
end
|
|
@@ -86,7 +117,7 @@ module Saro
|
|
|
86
117
|
|
|
87
118
|
if config[:name] == "HMAC"
|
|
88
119
|
if bytes_data.bytesize != config[:hmac_len]
|
|
89
|
-
raise
|
|
120
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::KEY_INVALID, "hmac key must be #{config[:hmac_len]} bytes, got #{bytes_data.bytesize}")
|
|
90
121
|
end
|
|
91
122
|
new(algorithm, bytes_data, bytes_data, config)
|
|
92
123
|
else
|
|
@@ -106,7 +137,7 @@ module Saro
|
|
|
106
137
|
elsif bytes_data.bytesize == public_len
|
|
107
138
|
verifying_key = create_ec_key(config[:curve], nil, bytes_data)
|
|
108
139
|
else
|
|
109
|
-
raise
|
|
140
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::KEY_INVALID, "ecdsa key length matches neither private+public nor public")
|
|
110
141
|
end
|
|
111
142
|
|
|
112
143
|
new(algorithm, signing_key, verifying_key, config)
|
|
@@ -115,7 +146,7 @@ module Saro
|
|
|
115
146
|
|
|
116
147
|
def exports(verify_only = false)
|
|
117
148
|
if verify_only && !support_verify_only
|
|
118
|
-
raise
|
|
149
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::KEY_VERIFY_ONLY_UNSUPPORTED, @algorithm.to_s)
|
|
119
150
|
end
|
|
120
151
|
|
|
121
152
|
if @config[:name] == "HMAC"
|
|
@@ -136,9 +167,9 @@ module Saro
|
|
|
136
167
|
end
|
|
137
168
|
|
|
138
169
|
def sign(body)
|
|
139
|
-
raise
|
|
170
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::SIG_KEY_MISSING, "this key is verify-only") unless @signing_key
|
|
140
171
|
body = normalize_body(body)
|
|
141
|
-
raise
|
|
172
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CONFIG_ARGUMENT_INVALID, "body to sign is empty") if body.nil? || body.empty?
|
|
142
173
|
|
|
143
174
|
if @config[:name] == "HMAC"
|
|
144
175
|
OpenSSL::HMAC.digest(@config[:hash], @signing_key, body)
|
|
@@ -148,30 +179,58 @@ module Saro
|
|
|
148
179
|
end
|
|
149
180
|
end
|
|
150
181
|
|
|
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.
|
|
151
186
|
def verify(body, signature)
|
|
187
|
+
verify_bytes(body, signature)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Verifies a base64url-encoded signature.
|
|
191
|
+
def verify_base64(body, signature_base64)
|
|
192
|
+
sig_bytes = begin
|
|
193
|
+
Saro::Dat::Util.decode_base64_url(signature_base64)
|
|
194
|
+
rescue Saro::Dat::Error => e
|
|
195
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::SIG_MALFORMED, "signature is not base64url", cause: e)
|
|
196
|
+
end
|
|
197
|
+
verify_bytes(body, sig_bytes)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
private def verify_bytes(body, sig_bytes)
|
|
152
201
|
body = normalize_body(body)
|
|
153
202
|
return false if body.nil? || body.empty?
|
|
154
203
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
signature
|
|
159
|
-
end
|
|
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
|
+
sig_bytes = sig_bytes.b if sig_bytes.is_a?(String) && sig_bytes.encoding != Encoding::BINARY
|
|
160
207
|
|
|
208
|
+
# false 는 "서명이 안 맞는다"만 뜻한다. 예전에는 `rescue StandardError => false`
|
|
209
|
+
# 가 잘못된 키 타입·손상된 핸들·라이브러리 버그까지 전부 삼켜서 프로그래밍
|
|
210
|
+
# 오류가 위조 시도로 보고됐다. 그래서 연산 실패는 SIG_BACKEND 로 갈라 낸다.
|
|
161
211
|
if @config[:name] == "HMAC"
|
|
162
212
|
begin
|
|
163
213
|
actual_sig = OpenSSL::HMAC.digest(@config[:hash], @verifying_key, body)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
rescue StandardError
|
|
167
|
-
return false
|
|
214
|
+
rescue StandardError => e
|
|
215
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::SIG_BACKEND, "hmac computation failed", cause: e)
|
|
168
216
|
end
|
|
217
|
+
# 길이가 다르면 볼 것도 없이 불일치다.
|
|
218
|
+
return false unless sig_bytes.is_a?(String) && actual_sig.bytesize == sig_bytes.bytesize
|
|
219
|
+
OpenSSL.fixed_length_secure_compare(actual_sig, sig_bytes)
|
|
169
220
|
else
|
|
221
|
+
der_sig = begin
|
|
222
|
+
raw_to_der_signature(sig_bytes)
|
|
223
|
+
rescue StandardError
|
|
224
|
+
# r||s 길이가 곡선과 안 맞는다 = 서명 자체의 형식 오류. 위조가 아니다.
|
|
225
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::SIG_MALFORMED, "ecdsa signature is not a valid r||s pair")
|
|
226
|
+
end
|
|
170
227
|
begin
|
|
171
|
-
der_sig = raw_to_der_signature(sig_bytes)
|
|
172
228
|
@verifying_key.dsa_verify_asn1(OpenSSL::Digest.digest(@config[:hash], body), der_sig)
|
|
173
|
-
rescue
|
|
229
|
+
rescue OpenSSL::PKey::PKeyError, OpenSSL::PKey::ECError
|
|
230
|
+
# OpenSSL 이 서명을 해독조차 못 한 경우다. 불일치로 본다.
|
|
174
231
|
false
|
|
232
|
+
rescue StandardError => e
|
|
233
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::SIG_BACKEND, "ecdsa verification failed to run", cause: e)
|
|
175
234
|
end
|
|
176
235
|
end
|
|
177
236
|
end
|
data/lib/saro/dat/util.rb
CHANGED
|
@@ -1,12 +1,44 @@
|
|
|
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
|
+
# 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
|
+
def parse_u64(s)
|
|
20
|
+
unless s.is_a?(String) && s.match?(/\A[0-9]+\z/)
|
|
21
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CONFIG_ARGUMENT_INVALID, "not an unsigned decimal integer: #{s.inspect}")
|
|
22
|
+
end
|
|
23
|
+
v = s.to_i
|
|
24
|
+
if v > U64_MAX
|
|
25
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CONFIG_ARGUMENT_INVALID, "exceeds u64: #{s.inspect}")
|
|
26
|
+
end
|
|
27
|
+
v
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Strict unsigned 64-bit hex parse, matching rust's `u64::from_str_radix(s, 16)`.
|
|
31
|
+
def parse_u64_hex(s)
|
|
32
|
+
unless s.is_a?(String) && s.match?(/\A[0-9a-fA-F]+\z/)
|
|
33
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CONFIG_ARGUMENT_INVALID, "not an unsigned hex integer: #{s.inspect}")
|
|
34
|
+
end
|
|
35
|
+
v = s.to_i(16)
|
|
36
|
+
if v > U64_MAX
|
|
37
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CONFIG_ARGUMENT_INVALID, "exceeds u64: #{s.inspect}")
|
|
38
|
+
end
|
|
39
|
+
v
|
|
40
|
+
end
|
|
41
|
+
|
|
10
42
|
def encode_base64_url(s)
|
|
11
43
|
return "".b if s.nil?
|
|
12
44
|
if s.is_a?(String)
|
|
@@ -27,12 +59,18 @@ module Saro
|
|
|
27
59
|
return "".b if s.empty?
|
|
28
60
|
end
|
|
29
61
|
|
|
30
|
-
#
|
|
31
|
-
|
|
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
|
+
s = s.to_s
|
|
32
66
|
rem = s.bytesize % 4
|
|
33
67
|
s += ("=" * (4 - rem)) if rem > 0
|
|
34
68
|
|
|
35
|
-
|
|
69
|
+
begin
|
|
70
|
+
Base64.urlsafe_decode64(s).b
|
|
71
|
+
rescue ArgumentError => e
|
|
72
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::CONFIG_ARGUMENT_INVALID, "not a valid base64url string", cause: e)
|
|
73
|
+
end
|
|
36
74
|
end
|
|
37
75
|
|
|
38
76
|
def decode_base64_url_str(s)
|
data/lib/saro-dat.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Error 는 다른 모든 파일이 참조하므로 가장 먼저 로드한다.
|
|
4
|
+
require_relative 'saro/dat/error'
|
|
3
5
|
require_relative 'saro/dat/util'
|
|
4
6
|
require_relative 'saro/dat/crypto'
|
|
5
7
|
require_relative 'saro/dat/signature'
|
|
@@ -7,9 +9,3 @@ require_relative 'saro/dat/dat_certificate'
|
|
|
7
9
|
require_relative 'saro/dat/dat'
|
|
8
10
|
require_relative 'saro/dat/dat_manager'
|
|
9
11
|
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
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.
|
|
4
|
+
version: 4.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- marker
|
|
@@ -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
|