evidence_record 0.1.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +160 -0
- data/lib/evidence_record/algorithm_identifier.rb +37 -0
- data/lib/evidence_record/archive_time_stamp.rb +131 -0
- data/lib/evidence_record/chain.rb +54 -0
- data/lib/evidence_record/data.rb +143 -0
- data/lib/evidence_record/der.rb +370 -0
- data/lib/evidence_record/digest.rb +94 -0
- data/lib/evidence_record/digest_algorithm.rb +68 -0
- data/lib/evidence_record/errors.rb +43 -0
- data/lib/evidence_record/generator.rb +91 -0
- data/lib/evidence_record/hash_tree.rb +110 -0
- data/lib/evidence_record/hash_tree_renewal.rb +43 -0
- data/lib/evidence_record/record.rb +214 -0
- data/lib/evidence_record/reduced_hashtree.rb +73 -0
- data/lib/evidence_record/renewal.rb +62 -0
- data/lib/evidence_record/time_stamp_token.rb +140 -0
- data/lib/evidence_record/timestamp_exchange.rb +104 -0
- data/lib/evidence_record/timestamp_renewal.rb +49 -0
- data/lib/evidence_record/timestamp_validator.rb +72 -0
- data/lib/evidence_record/verifier.rb +158 -0
- data/lib/evidence_record/version.rb +5 -0
- data/lib/evidence_record.rb +26 -0
- metadata +69 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: bbd1dbdd156796ea4d19b859ab779f227e646dd67bbdc3395a82e322bef66831
|
|
4
|
+
data.tar.gz: ea49206348d9bf5e361375613b0bc7b0e6a6cbb3790efc0333abc88541e331c4
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 2b93957420913d30bfbef1c4387207dc1796100c3ceabde8b794e666b775e7f84b85c1c56685dbd3ab69b4fab25ea9dc5848f39052952612ae977194c015a4f9
|
|
7
|
+
data.tar.gz: 80225299d4546a4fcd95ea35baa382ed8c5b8d0ed834ff9d2e8b5a3c2d5a95b5b08c19806de6b95f040a26a5be208bb6ece4d62ac367f23015907ffeb3ce5bd2
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ken Tanaka
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# evidence_record
|
|
2
|
+
|
|
3
|
+
[RFC 4998](https://www.rfc-editor.org/rfc/rfc4998)(Evidence Record Syntax, ERS)に準拠した、Evidence Record の Ruby 実装。
|
|
4
|
+
|
|
5
|
+
Evidence Record とは、データがある時点に存在しそれ以降改ざんされてないことを長期間にわたって証明するデータ構造。多数のデータをハッシュツリーにまとめてタイムスタンプ(RFC 3161)で保護する。タイムスタンプやハッシュアルゴリズムがが弱くなる前に新しいタイムスタンプやハッシュアルゴリズムに更新することができる。
|
|
6
|
+
|
|
7
|
+
## 機能
|
|
8
|
+
|
|
9
|
+
- 文字列または計算済みのハッシュ値を対象に Evidence Record を生成
|
|
10
|
+
- 多数のデータの Evidence Record を、1 つのタイムスタンプでまとめて生成
|
|
11
|
+
- タイムスタンプ更新(TSA の証明書の期限が切れる前にタイムスタンプを掛け直す)、ハッシュツリー更新(新しいハッシュアルゴリズムに移行する)
|
|
12
|
+
- 外部から与えられた Evidence Record の検証。任意のハッシュツリーを RFC 4998 の手順で検証する
|
|
13
|
+
- タイムスタンプの検証は差し替え可能。OpenSSL で署名と証明書チェーンを検証する検証器を同梱
|
|
14
|
+
- TSA との通信は行わない
|
|
15
|
+
- [BSI のテストデータ](https://github.com/bcgit/bc-test-data)で動作を確認済み。[Bouncy Castle](https://www.bouncycastle.org/) と双方向に相互運用できることを確認済み
|
|
16
|
+
- 依存ライブラリなし
|
|
17
|
+
|
|
18
|
+
## インストール
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
gem install evidence_record
|
|
22
|
+
|
|
23
|
+
# or
|
|
24
|
+
|
|
25
|
+
bundle add evidence_record
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 使い方
|
|
29
|
+
|
|
30
|
+
### 生成
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
gen = EvidenceRecord::Generator.new(digest_algorithm: :sha256)
|
|
34
|
+
gen.add("doc-a")
|
|
35
|
+
gen.add(EvidenceRecord::DataGroup.new([meta_xml, body_pdf])) # グループは 1 つの Evidence Record になる
|
|
36
|
+
|
|
37
|
+
# タイムスタンプ要求(OpenSSL::Timestamp::Request)を作り、TSA に送る
|
|
38
|
+
req = gen.timestamp_request(nonce: SecureRandom.random_number(2**64))
|
|
39
|
+
res = post_to_tsa(req.to_der) # res: TSA の応答(TimeStampResp のバイト列)
|
|
40
|
+
|
|
41
|
+
# add した順に Evidence Record が返る
|
|
42
|
+
records = gen.build(res) # => [Record, Record]
|
|
43
|
+
File.binwrite("doc-a.ers", records[0].to_der)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
タイムスタンプ取得(post_to_tsa)の実装例
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
def post_to_tsa(der)
|
|
50
|
+
uri = URI("https://tsa.example.com/")
|
|
51
|
+
res = Net::HTTP.post(uri, der, "Content-Type" => "application/timestamp-query")
|
|
52
|
+
raise "TSA error: #{res.code}" unless res.is_a?(Net::HTTPSuccess)
|
|
53
|
+
|
|
54
|
+
res.body
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 検証
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
record = EvidenceRecord::Record.from_der(File.binread("doc-a.ers"))
|
|
62
|
+
|
|
63
|
+
# TSA の証明書チェーンを検証する
|
|
64
|
+
store = OpenSSL::X509::Store.new
|
|
65
|
+
store.add_cert(tsa_root_certificate)
|
|
66
|
+
validator = EvidenceRecord::TimestampValidator::OpenSSL.new(store: store)
|
|
67
|
+
|
|
68
|
+
record.verify(data: "doc-a", timestamp_validator: validator) # => true / false
|
|
69
|
+
record.verify!(data: "doc-a", timestamp_validator: validator) # 失敗すると例外
|
|
70
|
+
record.proven_time # データの存在が証明される時刻(タイムスタンプの genTime)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
検証では、データからすべてのタイムスタンプまでのハッシュの連鎖、各タイムスタンプの正当性を確認する。
|
|
74
|
+
|
|
75
|
+
### タイムスタンプ更新
|
|
76
|
+
|
|
77
|
+
TSA の証明書の期限が切れる前にタイムスタンプを掛け直す
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
renewal = record.timestamp_renewal
|
|
81
|
+
req = renewal.timestamp_request(nonce: SecureRandom.random_number(2**64))
|
|
82
|
+
record2 = renewal.build(post_to_tsa(req.to_der)) # => 新しい Record
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### ハッシュツリー更新
|
|
86
|
+
|
|
87
|
+
使っているハッシュアルゴリズムが弱くなった際に新しいアルゴリズムに移行する。データが必要。
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
renewal = record.hash_tree_renewal(data: "doc-a", digest_algorithm: :sha512)
|
|
91
|
+
req = renewal.timestamp_request(nonce: SecureRandom.random_number(2**64))
|
|
92
|
+
record3 = renewal.build(post_to_tsa(req.to_der)) # => 新しい Chain が追加された Record
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 計算済みのハッシュ値
|
|
96
|
+
|
|
97
|
+
データ本体の代わりにあらかじめ手計算したハッシュ値を指定可能。
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
pre = EvidenceRecord::PrehashedData.new(
|
|
101
|
+
sha256: OpenSSL::Digest.new("SHA256").file("large.bin").digest,
|
|
102
|
+
sha512: OpenSSL::Digest.new("SHA512").file("large.bin").digest,
|
|
103
|
+
)
|
|
104
|
+
gen.add(pre)
|
|
105
|
+
record.verify(data: pre, timestamp_validator: validator)
|
|
106
|
+
record.hash_tree_renewal(data: pre, digest_algorithm: :sha512)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Evidence Record の内容確認
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
record.chains.each do |chain|
|
|
113
|
+
chain.archive_timestamps.each do |ats|
|
|
114
|
+
ts = ats.timestamp
|
|
115
|
+
puts "#{ats.digest_algorithm} #{ts.gen_time} serial=#{ts.serial_number}"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# 最後のタイムスタンプの TSA 証明書の期限を調べる
|
|
120
|
+
last = record.chains.last.archive_timestamps.last.timestamp
|
|
121
|
+
last.certificates.map(&:not_after)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 参照
|
|
125
|
+
|
|
126
|
+
- 各クラス・メソッドの仕様は [docs/reference.md](docs/reference.md) を参照
|
|
127
|
+
|
|
128
|
+
## 開発
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
bundle install
|
|
132
|
+
bundle exec rake test
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
BSI のテストデータを使うテストは、データをダウンロードしてから実行する。
|
|
136
|
+
テストデータは [bcgit/bc-test-data](https://github.com/bcgit/bc-test-data) の `tsp/20170316_Testdaten` (BSI TR-ESOR / ERS テストツール)から固定したコミットのものを取得し、SHA-256 で内容を確認する。
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
bundle exec rake test:fetch_vectors # test/fixtures/bsi に取得する
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Bouncy Castle との相互運用テストには JDK が必要。Bouncy Castle は Maven Central から固定したバージョンのものを取得し、SHA-256 で内容を確認する。
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
bundle exec rake test:fetch_bouncy_castle # interop/lib に取得する
|
|
146
|
+
bundle exec rake test:interop
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Docker を使う場合:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
docker compose run --rm test # Ruby 4.0
|
|
153
|
+
docker compose run --rm test-3.4 # Ruby 3.4
|
|
154
|
+
docker compose run --rm test bundle exec rake test:fetch_vectors
|
|
155
|
+
docker compose run --rm interop # Bouncy Castle との相互運用テスト(JDK 入りのイメージ)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## ライセンス
|
|
159
|
+
|
|
160
|
+
[MIT License](LICENSE.txt)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EvidenceRecord
|
|
4
|
+
# Internal: reading and writing the AlgorithmIdentifiers of hash
|
|
5
|
+
# algorithms.
|
|
6
|
+
#
|
|
7
|
+
# Any OID is read, so that Evidence Records using other algorithms can still
|
|
8
|
+
# be parsed; DigestAlgorithm.from_oid raises UnsupportedAlgorithmError when
|
|
9
|
+
# such an algorithm is used. Parameters of SHA-2 must be absent or NULL
|
|
10
|
+
# (RFC 5754 §2); absent is generated.
|
|
11
|
+
module AlgorithmIdentifier
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# node: a SEQUENCE, or with context: n, a constructed element with the
|
|
15
|
+
# implicit tag [n]. Returns the OID as a dotted String.
|
|
16
|
+
def parse(node, what, context: nil)
|
|
17
|
+
children = DER.children_of(node, "#{what} (AlgorithmIdentifier)", size: 1..2, context: context)
|
|
18
|
+
oid = DER.oid(children[0], what)
|
|
19
|
+
params = children[1]
|
|
20
|
+
if params && DigestAlgorithm::BY_OID.key?(oid) && !params.universal?(DER::NULL)
|
|
21
|
+
raise ParseError, "#{what}: parameters of #{DigestAlgorithm::BY_OID[oid]} must be absent or NULL"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
oid
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The contents octets of an AlgorithmIdentifier (to be wrapped in a
|
|
28
|
+
# SEQUENCE or an implicit tag).
|
|
29
|
+
def encode_content(algorithm)
|
|
30
|
+
DER.encode_oid(algorithm.oid)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def encode(algorithm)
|
|
34
|
+
DER.sequence([encode_content(algorithm)])
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EvidenceRecord
|
|
4
|
+
# An ArchiveTimeStamp (RFC 4998 §4.1): a time-stamp token and the reduced
|
|
5
|
+
# hash tree leading from a data object (or an earlier time-stamp) to the
|
|
6
|
+
# time-stamped root.
|
|
7
|
+
#
|
|
8
|
+
# ArchiveTimeStamp ::= SEQUENCE {
|
|
9
|
+
# digestAlgorithm [0] AlgorithmIdentifier OPTIONAL,
|
|
10
|
+
# attributes [1] Attributes OPTIONAL,
|
|
11
|
+
# reducedHashtree [2] SEQUENCE OF PartialHashtree OPTIONAL,
|
|
12
|
+
# timeStamp ContentInfo }
|
|
13
|
+
#
|
|
14
|
+
# The original DER is kept and returned by #to_der.
|
|
15
|
+
class ArchiveTimeStamp
|
|
16
|
+
# The time-stamp token (TimeStampToken).
|
|
17
|
+
attr_reader :timestamp
|
|
18
|
+
# The ReducedHashtree, or nil when the field is absent (the time-stamp
|
|
19
|
+
# covers the hash of a single data object directly).
|
|
20
|
+
attr_reader :reduced_hashtree
|
|
21
|
+
# The attributes field as it was read (DER, including its [1] tag), or
|
|
22
|
+
# nil. It is kept but not interpreted (docs/design.md 7.3).
|
|
23
|
+
attr_reader :attributes
|
|
24
|
+
# Internal: the OID of the digestAlgorithm field, or nil when absent.
|
|
25
|
+
attr_reader :declared_digest_algorithm_oid
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
def from_der(der)
|
|
29
|
+
from_node(DER.parse(der, "ArchiveTimeStamp"), "ArchiveTimeStamp")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Internal: from an already parsed element.
|
|
33
|
+
def from_node(node, what)
|
|
34
|
+
new(node, what)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Internal: encodes a new ArchiveTimeStamp. Used by the generator and
|
|
38
|
+
# the renewals.
|
|
39
|
+
def build(timestamp:, reduced_hashtree: nil, digest_algorithm: nil)
|
|
40
|
+
parts = []
|
|
41
|
+
parts << DER.context(0, AlgorithmIdentifier.encode_content(digest_algorithm)) if digest_algorithm
|
|
42
|
+
if reduced_hashtree
|
|
43
|
+
partials = reduced_hashtree.partial_hashtrees.map do |values|
|
|
44
|
+
DER.sequence(values.map { DER.encode_octet_string(_1.to_binary) })
|
|
45
|
+
end
|
|
46
|
+
parts << DER.context(2, partials.join)
|
|
47
|
+
end
|
|
48
|
+
parts << timestamp.to_der
|
|
49
|
+
from_der(DER.sequence(parts))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private :new
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def initialize(node, what)
|
|
56
|
+
@der = node.raw
|
|
57
|
+
*fields, timestamp = DER.children_of(node, what, size: 1..4)
|
|
58
|
+
@timestamp = TimeStampToken.from_node(timestamp, "#{what} timeStamp")
|
|
59
|
+
|
|
60
|
+
previous_tag = -1
|
|
61
|
+
fields.each do |field|
|
|
62
|
+
tag = field.tag_class == :context && field.constructed? ? field.tag : nil
|
|
63
|
+
unless tag && tag <= 2 && tag > previous_tag
|
|
64
|
+
raise ParseError, "#{what}: unexpected element before timeStamp"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
previous_tag = tag
|
|
68
|
+
case tag
|
|
69
|
+
when 0 then @declared_digest_algorithm_oid = AlgorithmIdentifier.parse(field, "#{what} digestAlgorithm", context: 0)
|
|
70
|
+
when 1 then @attributes = parse_attributes(field, "#{what} attributes")
|
|
71
|
+
when 2 then @reduced_hashtree = parse_reduced_hashtree(field, "#{what} reducedHashtree")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
freeze
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The OID of the hash algorithm of the reduced hash tree: the
|
|
78
|
+
# digestAlgorithm field, or the hash algorithm of the time-stamp when the
|
|
79
|
+
# field is absent (RFC 4998 §4.1).
|
|
80
|
+
def digest_algorithm_oid
|
|
81
|
+
@declared_digest_algorithm_oid || @timestamp.hash_algorithm_oid
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# The hash algorithm of the reduced hash tree, as a DigestAlgorithm.
|
|
85
|
+
# Raises UnsupportedAlgorithmError for other algorithms.
|
|
86
|
+
def digest_algorithm
|
|
87
|
+
DigestAlgorithm.from_oid(digest_algorithm_oid)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def to_der
|
|
91
|
+
@der
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def ==(other)
|
|
95
|
+
other.is_a?(ArchiveTimeStamp) && to_der == other.to_der
|
|
96
|
+
end
|
|
97
|
+
alias eql? ==
|
|
98
|
+
|
|
99
|
+
def hash
|
|
100
|
+
[ArchiveTimeStamp, @der].hash
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def inspect
|
|
104
|
+
shape = @reduced_hashtree ? @reduced_hashtree.partial_hashtrees.map(&:size).join("-") : "none"
|
|
105
|
+
"#<#{self.class.name} digest_algorithm_oid=#{digest_algorithm_oid} " \
|
|
106
|
+
"reduced_hashtree=#{shape} timestamp=#{@timestamp.inspect}>"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
# Attributes ::= SET SIZE (1..MAX) OF Attribute
|
|
112
|
+
def parse_attributes(node, what)
|
|
113
|
+
if node.children.empty? || !node.children.all? { _1.universal?(DER::SEQUENCE) } ||
|
|
114
|
+
!DER.sorted?(node.children)
|
|
115
|
+
raise ParseError, "#{what}: invalid Attributes"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
node.raw
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# SEQUENCE OF PartialHashtree; PartialHashtree ::= SEQUENCE OF OCTET STRING
|
|
122
|
+
def parse_reduced_hashtree(node, what)
|
|
123
|
+
partials = node.children.each_with_index.map do |partial, i|
|
|
124
|
+
DER.children_of(partial, "#{what}[#{i}]").map { DER.octet_string(_1, "#{what}[#{i}]") }
|
|
125
|
+
end
|
|
126
|
+
ReducedHashtree.new(partials)
|
|
127
|
+
rescue ArgumentError => e
|
|
128
|
+
raise ParseError, "#{what}: #{e.message}"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EvidenceRecord
|
|
4
|
+
# An ArchiveTimeStampChain (RFC 4998 §5): ArchiveTimeStamps that use the
|
|
5
|
+
# same hash algorithm, each covering the time-stamp of the one before.
|
|
6
|
+
#
|
|
7
|
+
# The original DER is kept and returned by #to_der.
|
|
8
|
+
class Chain
|
|
9
|
+
attr_reader :archive_timestamps
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def from_der(der)
|
|
13
|
+
from_node(DER.parse(der, "ArchiveTimeStampChain"), "ArchiveTimeStampChain")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Internal: from an already parsed element.
|
|
17
|
+
def from_node(node, what)
|
|
18
|
+
new(node, what)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Internal: encodes a new chain from ArchiveTimeStamps.
|
|
22
|
+
def build(archive_timestamps)
|
|
23
|
+
from_der(DER.sequence(archive_timestamps.map(&:to_der)))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private :new
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(node, what)
|
|
30
|
+
@der = node.raw
|
|
31
|
+
@archive_timestamps = DER.children_of(node, what).each_with_index.map do |ats, i|
|
|
32
|
+
ArchiveTimeStamp.from_node(ats, "#{what}[#{i}]")
|
|
33
|
+
end.freeze
|
|
34
|
+
freeze
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def to_der
|
|
38
|
+
@der
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def ==(other)
|
|
42
|
+
other.is_a?(Chain) && to_der == other.to_der
|
|
43
|
+
end
|
|
44
|
+
alias eql? ==
|
|
45
|
+
|
|
46
|
+
def hash
|
|
47
|
+
[Chain, @der].hash
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def inspect
|
|
51
|
+
"#<#{self.class.name} #{@archive_timestamps.inspect}>"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EvidenceRecord
|
|
4
|
+
# Hash values of a data object computed elsewhere, given per algorithm
|
|
5
|
+
# (docs/design.md 3.7.1). Use it for data that is not at hand or too large
|
|
6
|
+
# to read into a String.
|
|
7
|
+
#
|
|
8
|
+
# EvidenceRecord::PrehashedData.new(sha256: d256, sha512: d512)
|
|
9
|
+
#
|
|
10
|
+
# The library cannot check that the hashes were computed from the same data.
|
|
11
|
+
class PrehashedData
|
|
12
|
+
def initialize(**hashes)
|
|
13
|
+
raise ArgumentError, "at least one hash must be given" if hashes.empty?
|
|
14
|
+
|
|
15
|
+
@hashes = hashes.to_h do |name, value|
|
|
16
|
+
algorithm = DigestAlgorithm.fetch(name)
|
|
17
|
+
bytes = Digest.bytes_of(value, "#{name} hash")
|
|
18
|
+
unless bytes.bytesize == algorithm.digest_size
|
|
19
|
+
raise ArgumentError,
|
|
20
|
+
"#{name} hash must be #{algorithm.digest_size} bytes, got #{bytes.bytesize}"
|
|
21
|
+
end
|
|
22
|
+
[algorithm, bytes]
|
|
23
|
+
end.freeze
|
|
24
|
+
freeze
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def algorithms
|
|
28
|
+
@hashes.keys
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The hash for algorithm (a Symbol or DigestAlgorithm) as a Digest, or nil.
|
|
32
|
+
def [](algorithm)
|
|
33
|
+
bytes = @hashes[DigestAlgorithm.fetch(algorithm)]
|
|
34
|
+
bytes && Digest.new(bytes)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def inspect
|
|
38
|
+
"#<#{self.class.name} #{@hashes.map { |a, b| "#{a}=#{b.unpack1('H*')}" }.join(' ')}>"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# A data object group: several data objects covered by one Evidence Record
|
|
43
|
+
# (RFC 4998 §4.2). Members are Strings or PrehashedData; groups cannot be
|
|
44
|
+
# nested.
|
|
45
|
+
class DataGroup
|
|
46
|
+
attr_reader :members
|
|
47
|
+
|
|
48
|
+
def initialize(members)
|
|
49
|
+
raise ArgumentError, "members must be an Array, got #{members.class}" unless members.is_a?(Array)
|
|
50
|
+
raise ArgumentError, "a data group needs at least one member" if members.empty?
|
|
51
|
+
|
|
52
|
+
members.each_with_index do |m, i|
|
|
53
|
+
raise ArgumentError, "data groups cannot be nested (member #{i})" if m.is_a?(DataGroup)
|
|
54
|
+
|
|
55
|
+
DataSource.check!(m, "member #{i}")
|
|
56
|
+
end
|
|
57
|
+
@members = members.dup.freeze
|
|
58
|
+
freeze
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def size
|
|
62
|
+
@members.size
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Internal: turns the values accepted as data (String, PrehashedData,
|
|
67
|
+
# DataGroup) into objects that compute hash values, caching them per
|
|
68
|
+
# algorithm.
|
|
69
|
+
module DataSource
|
|
70
|
+
module_function
|
|
71
|
+
|
|
72
|
+
def check!(value, name = "data")
|
|
73
|
+
return if value.is_a?(String) || value.is_a?(PrehashedData)
|
|
74
|
+
|
|
75
|
+
raise ArgumentError,
|
|
76
|
+
"#{name} must be a String, an EvidenceRecord::PrehashedData " \
|
|
77
|
+
"or an EvidenceRecord::DataGroup, got #{value.class}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def wrap(value)
|
|
81
|
+
case value
|
|
82
|
+
when Single, Group then value
|
|
83
|
+
when DataGroup then Group.new(value.members.map { Single.new(_1) })
|
|
84
|
+
else
|
|
85
|
+
check!(value)
|
|
86
|
+
Single.new(value)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# One data object.
|
|
91
|
+
class Single
|
|
92
|
+
def initialize(value)
|
|
93
|
+
@value = value
|
|
94
|
+
@cache = {}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def group?
|
|
98
|
+
false
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# The hash of the data object, as a BINARY String.
|
|
102
|
+
def digest(algorithm)
|
|
103
|
+
algorithm = DigestAlgorithm.fetch(algorithm)
|
|
104
|
+
@cache[algorithm] ||= compute(algorithm)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# [hash]; the list that becomes the first partial hash tree.
|
|
108
|
+
def member_digests(algorithm)
|
|
109
|
+
[digest(algorithm)]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def compute(algorithm)
|
|
115
|
+
case @value
|
|
116
|
+
when PrehashedData
|
|
117
|
+
d = @value[algorithm] or
|
|
118
|
+
raise ArgumentError, "PrehashedData has no #{algorithm} hash, which is required here"
|
|
119
|
+
d.to_binary
|
|
120
|
+
else
|
|
121
|
+
algorithm.digest(@value).freeze
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# A data object group.
|
|
127
|
+
class Group
|
|
128
|
+
attr_reader :members
|
|
129
|
+
|
|
130
|
+
def initialize(members)
|
|
131
|
+
@members = members
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def group?
|
|
135
|
+
true
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def member_digests(algorithm)
|
|
139
|
+
@members.map { _1.digest(algorithm) }
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|