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 +4 -4
- data/.idea/saro-dat.iml +3 -1
- data/.idea/vcs.xml +1 -0
- data/PUBLISH.md +9 -1
- data/README.md +8 -8
- data/lib/saro/dat/crypto.rb +27 -9
- data/lib/saro/dat/dat.rb +66 -15
- data/lib/saro/dat/dat_certificate.rb +37 -10
- data/lib/saro/dat/dat_cms_manager.rb +100 -34
- data/lib/saro/dat/dat_manager.rb +61 -19
- data/lib/saro/dat/error.rb +118 -0
- data/lib/saro/dat/signature.rb +62 -23
- data/lib/saro/dat/util.rb +31 -3
- data/lib/saro-dat.rb +1 -6
- data/saro-dat.gemspec +2 -4
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b6d5127dce2e1d7ca632f94e2fa8d6b6a1434a37ab285bbf43dfc589282fccbb
|
|
4
|
+
data.tar.gz: e26c938570bfe16eae48746935a541e6ed552653b302cf7d15c10b7304dbc0af
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 (
|
|
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
data/PUBLISH.md
CHANGED
|
@@ -9,7 +9,9 @@ bundle install
|
|
|
9
9
|
```
|
|
10
10
|
gem build saro-dat.gemspec
|
|
11
11
|
gem signin
|
|
12
|
-
|
|
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/
|
|
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,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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
47
|
-
dat_issuance_start_seconds = parts[1]
|
|
48
|
-
dat_issuance_duration_seconds = parts[2]
|
|
49
|
-
dat_ttl_seconds = parts[3]
|
|
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
|
-
|
|
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
|
-
|
|
41
|
+
thread = nil
|
|
42
|
+
@lifecycle.synchronize do
|
|
43
|
+
return if @stopped
|
|
36
44
|
@stopped = true
|
|
37
|
-
@
|
|
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
|
-
|
|
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.
|
|
45
|
-
|
|
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 =
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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"
|
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'
|
|
@@ -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
|
-
|
|
31
|
-
seen_cids = Set.new
|
|
38
|
+
cids = Set.new(certificates.map(&:cid))
|
|
32
39
|
|
|
33
40
|
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)
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
91
|
+
dat.raise_if_invalid!
|
|
80
92
|
|
|
81
|
-
|
|
93
|
+
raise Saro::Dat::Error.new(Saro::Dat::ErrorCode::TOKEN_EXPIRED) if dat.expired?
|
|
82
94
|
|
|
83
|
-
|
|
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
|
-
|
|
109
|
-
raise
|
|
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
|
|
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
|
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,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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
165
|
-
|
|
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
|
|
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]
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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", "~>
|
|
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.
|
|
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: '
|
|
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: '
|
|
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
|