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.
@@ -0,0 +1,370 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EvidenceRecord
4
+ # Internal: a strict DER reader and the few encoders the library needs
5
+ # (docs/design.md 7.1).
6
+ #
7
+ # OpenSSL::ASN1 is not used to check DER: re-encoding what it decoded keeps
8
+ # indefinite lengths and constructed strings, does not reorder SETs, and
9
+ # drops the fractional seconds of GeneralizedTime, so "decode and re-encode
10
+ # gives the same bytes" accepts BER and rejects valid DER. This reader also
11
+ # keeps the original bytes of every element, which are what gets hashed.
12
+ module DER
13
+ BOOLEAN = 1
14
+ INTEGER = 2
15
+ BIT_STRING = 3
16
+ OCTET_STRING = 4
17
+ NULL = 5
18
+ OBJECT_IDENTIFIER = 6
19
+ ENUMERATED = 10
20
+ RELATIVE_OID = 13
21
+ SEQUENCE = 16
22
+ SET = 17
23
+ UTC_TIME = 23
24
+ GENERALIZED_TIME = 24
25
+
26
+ # Universal types that are always constructed: EXTERNAL, EMBEDDED PDV,
27
+ # SEQUENCE and SET. All others must use the primitive encoding in DER.
28
+ CONSTRUCTED_TYPES = [8, 11, SEQUENCE, SET].freeze
29
+ TAG_CLASSES = %i[universal application context private].freeze
30
+ MAX_DEPTH = 64
31
+ GENERALIZED_TIME_PATTERN = /\A(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(?:\.(\d*[1-9]))?Z\z/
32
+
33
+ # One element (TLV). raw is its whole encoding, content its contents
34
+ # octets; children is an Array of Nodes for a constructed element.
35
+ class Node
36
+ attr_reader :tag_class, :tag, :raw, :content, :children
37
+
38
+ def initialize(tag_class, constructed, tag, raw, content, children)
39
+ @tag_class = tag_class
40
+ @constructed = constructed
41
+ @tag = tag
42
+ @raw = raw.freeze
43
+ @content = content.freeze
44
+ @children = children&.freeze
45
+ freeze
46
+ end
47
+
48
+ def constructed?
49
+ @constructed
50
+ end
51
+
52
+ def universal?(tag)
53
+ @tag_class == :universal && @tag == tag
54
+ end
55
+
56
+ def context?(tag)
57
+ @tag_class == :context && @tag == tag
58
+ end
59
+ end
60
+
61
+ module_function
62
+
63
+ # Parses one DER element that must span all of bytes. what names the
64
+ # input in error messages.
65
+ def parse(bytes, what)
66
+ raise ArgumentError, "#{what} must be a String, got #{bytes.class}" unless bytes.is_a?(String)
67
+
68
+ bytes = bytes.b
69
+ node, pos = read(bytes, 0, bytes.bytesize, 0, what)
70
+ raise ParseError, "#{what}: #{bytes.bytesize - pos} bytes of trailing data" unless pos == bytes.bytesize
71
+
72
+ node
73
+ end
74
+
75
+ def read(bytes, pos, limit, depth, what)
76
+ raise ParseError, "#{what}: nested too deeply" if depth > MAX_DEPTH
77
+
78
+ start = pos
79
+ id = byte_at(bytes, pos, limit, what)
80
+ pos += 1
81
+ tag_class = TAG_CLASSES[id >> 6]
82
+ constructed = id.anybits?(0x20)
83
+ tag = id & 0x1f
84
+ tag, pos = read_high_tag(bytes, pos, limit, what) if tag == 0x1f
85
+ length, pos = read_length(bytes, pos, limit, what)
86
+ raise ParseError, "#{what}: truncated" if limit - pos < length
87
+
88
+ children = nil
89
+ if constructed
90
+ children = []
91
+ child_pos = pos
92
+ while child_pos < pos + length
93
+ child, child_pos = read(bytes, child_pos, pos + length, depth + 1, what)
94
+ children << child
95
+ end
96
+ end
97
+ node = Node.new(tag_class, constructed, tag, bytes.byteslice(start, pos + length - start),
98
+ bytes.byteslice(pos, length), children)
99
+ check_universal(node, what) if tag_class == :universal
100
+ [node, pos + length]
101
+ end
102
+
103
+ def byte_at(bytes, pos, limit, what)
104
+ raise ParseError, "#{what}: truncated" if pos >= limit
105
+
106
+ bytes.getbyte(pos)
107
+ end
108
+
109
+ def read_high_tag(bytes, pos, limit, what)
110
+ tag = 0
111
+ loop do
112
+ b = byte_at(bytes, pos, limit, what)
113
+ raise ParseError, "#{what}: non-minimal tag number" if tag.zero? && b == 0x80
114
+ raise ParseError, "#{what}: tag number too large" if tag > 0xffffff
115
+
116
+ tag = (tag << 7) | (b & 0x7f)
117
+ pos += 1
118
+ break if b < 0x80
119
+ end
120
+ raise ParseError, "#{what}: non-minimal tag number" if tag < 0x1f
121
+
122
+ [tag, pos]
123
+ end
124
+
125
+ def read_length(bytes, pos, limit, what)
126
+ first = byte_at(bytes, pos, limit, what)
127
+ pos += 1
128
+ return [first, pos] if first < 0x80
129
+ raise ParseError, "#{what}: indefinite length is not allowed in DER" if first == 0x80
130
+
131
+ count = first & 0x7f
132
+ raise ParseError, "#{what}: length too large" if count > 4
133
+ raise ParseError, "#{what}: truncated" if limit - pos < count
134
+
135
+ length_bytes = bytes.byteslice(pos, count)
136
+ length = length_bytes.unpack1("H*").to_i(16)
137
+ if length_bytes.getbyte(0).zero? || length < 0x80
138
+ raise ParseError, "#{what}: length is not in its shortest form"
139
+ end
140
+
141
+ [length, pos + count]
142
+ end
143
+
144
+ def check_universal(node, what)
145
+ tag = node.tag
146
+ raise ParseError, "#{what}: unexpected end-of-contents" if tag.zero?
147
+ if node.constructed? != CONSTRUCTED_TYPES.include?(tag)
148
+ raise ParseError, "#{what}: universal type #{tag} must use the " \
149
+ "#{node.constructed? ? 'primitive' : 'constructed'} encoding"
150
+ end
151
+
152
+ c = node.content
153
+ ok = case tag
154
+ when BOOLEAN then ["\x00".b, "\xff".b].include?(c)
155
+ when INTEGER, ENUMERATED then minimal_integer?(c)
156
+ when NULL then c.empty?
157
+ when BIT_STRING then valid_bit_string?(c)
158
+ when OBJECT_IDENTIFIER, RELATIVE_OID then valid_oid_content?(c)
159
+ when SET then sorted?(node.children)
160
+ when UTC_TIME then c.match?(/\A\d{12}Z\z/)
161
+ when GENERALIZED_TIME then c.match?(GENERALIZED_TIME_PATTERN)
162
+ else true
163
+ end
164
+ raise ParseError, "#{what}: invalid DER encoding of universal type #{tag}" unless ok
165
+ end
166
+
167
+ def minimal_integer?(c)
168
+ return false if c.empty?
169
+ return true if c.bytesize == 1
170
+
171
+ a = c.getbyte(0)
172
+ b = c.getbyte(1)
173
+ !((a.zero? && b < 0x80) || (a == 0xff && b >= 0x80))
174
+ end
175
+
176
+ def valid_bit_string?(c)
177
+ return false if c.empty?
178
+
179
+ unused = c.getbyte(0)
180
+ return unused.zero? if c.bytesize == 1
181
+
182
+ unused <= 7 && (c.getbyte(-1) & ((1 << unused) - 1)).zero?
183
+ end
184
+
185
+ def valid_oid_content?(c)
186
+ return false if c.empty? || c.getbyte(-1) >= 0x80
187
+
188
+ # no subidentifier may start with 0x80 (a non-minimal encoding)
189
+ c.each_byte.each_with_index.none? { |b, i| b == 0x80 && (i.zero? || c.getbyte(i - 1) < 0x80) }
190
+ end
191
+
192
+ # DER sorts the elements of a SET by their encodings (X.690 11.6).
193
+ def sorted?(nodes)
194
+ nodes.each_cons(2).all? { |a, b| (a.raw <=> b.raw) <= 0 }
195
+ end
196
+
197
+ # --- normalizing lengths ---------------------------------------------------
198
+
199
+ # Rewrites indefinite lengths and lengths not in their shortest form as
200
+ # definite, shortest lengths. Nothing else is changed: tags and the
201
+ # contents of primitive elements are copied as they are, so the result is
202
+ # DER only if the input was DER apart from its lengths (check it with
203
+ # parse). Used for time-stamp tokens from TSAs (docs/design.md 6).
204
+ def normalize_lengths(bytes, what)
205
+ raise ArgumentError, "#{what} must be a String, got #{bytes.class}" unless bytes.is_a?(String)
206
+
207
+ bytes = bytes.b
208
+ out, pos = normalize(bytes, 0, bytes.bytesize, 0, what)
209
+ raise ParseError, "#{what}: #{bytes.bytesize - pos} bytes of trailing data" unless pos == bytes.bytesize
210
+
211
+ out
212
+ end
213
+
214
+ def normalize(bytes, pos, limit, depth, what)
215
+ raise ParseError, "#{what}: nested too deeply" if depth > MAX_DEPTH
216
+
217
+ start = pos
218
+ id = byte_at(bytes, pos, limit, what)
219
+ pos += 1
220
+ _, pos = read_high_tag(bytes, pos, limit, what) if (id & 0x1f) == 0x1f
221
+ identifier = bytes.byteslice(start, pos - start)
222
+ constructed = id.anybits?(0x20)
223
+
224
+ first = byte_at(bytes, pos, limit, what)
225
+ if first == 0x80
226
+ raise ParseError, "#{what}: indefinite length on a primitive element" unless constructed
227
+
228
+ pos += 1
229
+ content = "".b
230
+ loop do
231
+ raise ParseError, "#{what}: missing end-of-contents" if limit - pos < 2
232
+ break if bytes.byteslice(pos, 2) == "\x00\x00".b
233
+
234
+ child, pos = normalize(bytes, pos, limit, depth + 1, what)
235
+ content << child
236
+ end
237
+ return [identifier + encode_length(content.bytesize) + content, pos + 2]
238
+ end
239
+
240
+ length, pos = read_any_length(bytes, pos, limit, what)
241
+ raise ParseError, "#{what}: truncated" if limit - pos < length
242
+
243
+ content = if constructed
244
+ children = "".b
245
+ child_pos = pos
246
+ while child_pos < pos + length
247
+ child, child_pos = normalize(bytes, child_pos, pos + length, depth + 1, what)
248
+ children << child
249
+ end
250
+ children
251
+ else
252
+ bytes.byteslice(pos, length)
253
+ end
254
+ [identifier + encode_length(content.bytesize) + content, pos + length]
255
+ end
256
+
257
+ # A definite length in any form (BER).
258
+ def read_any_length(bytes, pos, limit, what)
259
+ first = byte_at(bytes, pos, limit, what)
260
+ pos += 1
261
+ return [first, pos] if first < 0x80
262
+
263
+ count = first & 0x7f
264
+ raise ParseError, "#{what}: length too large" if count > 8 || first == 0xff
265
+ raise ParseError, "#{what}: truncated" if limit - pos < count
266
+
267
+ [bytes.byteslice(pos, count).unpack1("H*").to_i(16), pos + count]
268
+ end
269
+
270
+ # --- reading values ------------------------------------------------------
271
+
272
+ def expect(node, what, tag)
273
+ raise ParseError, "#{what}: expected universal type #{tag}" unless node&.universal?(tag)
274
+
275
+ node
276
+ end
277
+
278
+ # The children of a SEQUENCE (or of the constructed element given by
279
+ # context:), checking how many there are.
280
+ def children_of(node, what, size: nil, context: nil)
281
+ ok = node && node.constructed? &&
282
+ (context ? node.context?(context) : node.universal?(SEQUENCE))
283
+ raise ParseError, "#{what}: expected #{context ? "[#{context}]" : 'a SEQUENCE'}" unless ok
284
+ if size && !(size === node.children.size)
285
+ raise ParseError, "#{what}: unexpected number of elements (#{node.children.size})"
286
+ end
287
+
288
+ node.children
289
+ end
290
+
291
+ # The OID as a frozen dotted String.
292
+ def oid(node, what)
293
+ c = expect(node, what, OBJECT_IDENTIFIER).content
294
+ ids = []
295
+ value = 0
296
+ c.each_byte do |b|
297
+ value = (value << 7) | (b & 0x7f)
298
+ next if b >= 0x80
299
+
300
+ ids << value
301
+ value = 0
302
+ end
303
+ first = [ids[0] / 40, 2].min
304
+ [first, ids[0] - (first * 40), *ids[1..]].join(".").freeze
305
+ end
306
+
307
+ def integer(node, what)
308
+ c = expect(node, what, INTEGER).content
309
+ value = c.unpack1("H*").to_i(16)
310
+ c.getbyte(0) >= 0x80 ? value - (1 << (8 * c.bytesize)) : value
311
+ end
312
+
313
+ def octet_string(node, what)
314
+ expect(node, what, OCTET_STRING).content
315
+ end
316
+
317
+ # A frozen UTC Time, keeping fractions of a second. Frozen because Time
318
+ # has methods that change it (Time#localtime), and the Time is held by
319
+ # immutable objects (TimeStampToken#gen_time).
320
+ def generalized_time(node, what)
321
+ m = GENERALIZED_TIME_PATTERN.match(expect(node, what, GENERALIZED_TIME).content)
322
+ year, month, day, hour, minute, second = m.captures.first(6).map(&:to_i)
323
+ fraction = m[7] ? Rational(m[7].to_i, 10**m[7].size) : 0
324
+ time = Time.utc(year, month, day, hour, minute, second + fraction)
325
+ unless [time.year, time.month, time.day, time.hour, time.min, time.sec] ==
326
+ [year, month, day, hour, minute, second]
327
+ raise ParseError, "#{what}: invalid time"
328
+ end
329
+
330
+ time.freeze
331
+ rescue ArgumentError
332
+ raise ParseError, "#{what}: invalid time"
333
+ end
334
+
335
+ # --- encoding --------------------------------------------------------------
336
+
337
+ # A TLV with a one-byte identifier.
338
+ def encode(identifier, content)
339
+ [identifier].pack("C") + encode_length(content.bytesize) + content.b
340
+ end
341
+
342
+ def encode_length(length)
343
+ return [length].pack("C") if length < 0x80
344
+
345
+ bytes = [length.to_s(16).then { _1.size.odd? ? "0#{_1}" : _1 }].pack("H*")
346
+ [0x80 | bytes.bytesize].pack("C") + bytes
347
+ end
348
+
349
+ def sequence(parts)
350
+ encode(0x20 | SEQUENCE, parts.map(&:b).join)
351
+ end
352
+
353
+ # A constructed context-specific element [tag] (tag < 31).
354
+ def context(tag, content)
355
+ encode(0xa0 | tag, content)
356
+ end
357
+
358
+ def encode_octet_string(bytes)
359
+ encode(OCTET_STRING, bytes)
360
+ end
361
+
362
+ def encode_integer(value)
363
+ OpenSSL::ASN1::Integer.new(value).to_der
364
+ end
365
+
366
+ def encode_oid(dotted)
367
+ OpenSSL::ASN1::ObjectId.new(dotted).to_der
368
+ end
369
+ end
370
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EvidenceRecord
4
+ # An immutable hash value. The bytes are always held as a frozen BINARY
5
+ # String. The same design as CTMerkle::Digest of the ct_merkle gem.
6
+ #
7
+ # A Digest does not know its hash algorithm: in RFC 4998 the algorithm is
8
+ # given by the context (the ArchiveTimeStamp or ArchiveTimeStampChain).
9
+ class Digest
10
+ HEX_PATTERN = /\A(?:[0-9a-fA-F]{2})*\z/
11
+
12
+ class << self
13
+ def from_hex(str)
14
+ raise ArgumentError, "hex must be a String, got #{str.class}" unless str.is_a?(String)
15
+ raise ArgumentError, "invalid hex string: #{str.inspect}" unless HEX_PATTERN.match?(str)
16
+
17
+ new([str].pack("H*"))
18
+ end
19
+
20
+ def from_base64(str)
21
+ raise ArgumentError, "base64 must be a String, got #{str.class}" unless str.is_a?(String)
22
+
23
+ bytes = begin
24
+ str.unpack1("m0")
25
+ rescue ArgumentError
26
+ raise ArgumentError, "invalid base64 string: #{str.inspect}"
27
+ end
28
+ new(bytes)
29
+ end
30
+
31
+ # Returns the bytes of a Digest or a String as a frozen BINARY String.
32
+ # Used internally to accept both kinds of arguments.
33
+ def bytes_of(value, name = "hash")
34
+ case value
35
+ when Digest then value.to_binary
36
+ when String then value.b.freeze
37
+ else raise ArgumentError, "#{name} must be an EvidenceRecord::Digest or a binary String, got #{value.class}"
38
+ end
39
+ end
40
+
41
+ # Wraps a Digest or a binary String as a Digest.
42
+ def wrap(value, name = "hash")
43
+ value.is_a?(Digest) ? value : new(bytes_of(value, name))
44
+ end
45
+ end
46
+
47
+ def initialize(bytes)
48
+ raise ArgumentError, "bytes must be a String, got #{bytes.class}" unless bytes.is_a?(String)
49
+
50
+ @bytes = bytes.b.freeze
51
+ freeze
52
+ end
53
+
54
+ def to_binary
55
+ @bytes
56
+ end
57
+
58
+ def to_hex
59
+ @bytes.unpack1("H*")
60
+ end
61
+
62
+ def to_base64
63
+ [@bytes].pack("m0")
64
+ end
65
+
66
+ alias to_s to_hex
67
+
68
+ def inspect
69
+ "#<#{self.class.name} #{to_hex}>"
70
+ end
71
+
72
+ def bytesize
73
+ @bytes.bytesize
74
+ end
75
+
76
+ # Compares bytes with another Digest or a String (encoding is ignored).
77
+ def ==(other)
78
+ case other
79
+ when Digest then @bytes == other.to_binary
80
+ when String then @bytes == other.b
81
+ else false
82
+ end
83
+ end
84
+
85
+ # Only Digests are eql?, so that #hash stays consistent.
86
+ def eql?(other)
87
+ other.is_a?(Digest) && @bytes.eql?(other.to_binary)
88
+ end
89
+
90
+ def hash
91
+ [Digest, @bytes].hash
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+
5
+ module EvidenceRecord
6
+ # A supported hash algorithm. Only SHA-256, SHA-384 and SHA-512 are
7
+ # supported (docs/design.md 4.4).
8
+ #
9
+ # Instances are looked up with DigestAlgorithm.fetch (by Symbol) or
10
+ # DigestAlgorithm.from_oid; they are frozen singletons.
11
+ class DigestAlgorithm
12
+ attr_reader :name, :oid, :digest_size
13
+
14
+ def initialize(name, oid, openssl_name, digest_size)
15
+ @name = name
16
+ @oid = oid
17
+ @openssl_name = openssl_name
18
+ @digest_size = digest_size
19
+ freeze
20
+ end
21
+
22
+ SHA256 = new(:sha256, "2.16.840.1.101.3.4.2.1", "SHA256", 32)
23
+ SHA384 = new(:sha384, "2.16.840.1.101.3.4.2.2", "SHA384", 48)
24
+ SHA512 = new(:sha512, "2.16.840.1.101.3.4.2.3", "SHA512", 64)
25
+
26
+ ALL = [SHA256, SHA384, SHA512].freeze
27
+ BY_NAME = ALL.to_h { [_1.name, _1] }.freeze
28
+ BY_OID = ALL.to_h { [_1.oid, _1] }.freeze
29
+
30
+ class << self
31
+ # Accepts a Symbol (:sha256, :sha384, :sha512) or a DigestAlgorithm.
32
+ def fetch(value)
33
+ case value
34
+ when DigestAlgorithm then value
35
+ when Symbol
36
+ BY_NAME.fetch(value) do
37
+ raise UnsupportedAlgorithmError,
38
+ "unsupported digest algorithm #{value.inspect} (supported: #{BY_NAME.keys.map(&:inspect).join(', ')})"
39
+ end
40
+ else raise ArgumentError, "digest algorithm must be a Symbol, got #{value.class}"
41
+ end
42
+ end
43
+
44
+ def from_oid(oid)
45
+ BY_OID.fetch(oid) { raise UnsupportedAlgorithmError, "unsupported digest algorithm OID #{oid}" }
46
+ end
47
+ end
48
+
49
+ # The hash of the concatenation of parts, as a BINARY String.
50
+ def digest(*parts)
51
+ d = new_openssl_digest
52
+ parts.each { d.update(_1) }
53
+ d.digest
54
+ end
55
+
56
+ def new_openssl_digest
57
+ OpenSSL::Digest.new(@openssl_name)
58
+ end
59
+
60
+ def to_s
61
+ @name.to_s
62
+ end
63
+
64
+ def inspect
65
+ "#<#{self.class.name} #{@name}>"
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EvidenceRecord
4
+ # Base class of all errors raised by this library.
5
+ # Invalid arguments are reported with the standard ArgumentError instead.
6
+ class Error < StandardError; end
7
+
8
+ # The input is not valid DER, or does not match the RFC 4998 structures.
9
+ class ParseError < Error; end
10
+
11
+ # A hash algorithm that this library does not support (only SHA-256,
12
+ # SHA-384 and SHA-512 are supported).
13
+ class UnsupportedAlgorithmError < Error; end
14
+
15
+ # The time-stamp response from a TSA cannot be used: it was rejected, or its
16
+ # message imprint does not match the request.
17
+ class TimestampResponseError < Error; end
18
+
19
+ # Raised by verify! when an Evidence Record does not verify.
20
+ #
21
+ # chain_index and ats_index tell where the verification failed. They are nil
22
+ # when the failure is not tied to a specific ArchiveTimeStamp.
23
+ class VerificationError < Error
24
+ attr_reader :chain_index, :ats_index
25
+
26
+ def initialize(message = nil, chain_index: nil, ats_index: nil)
27
+ super(message)
28
+ @chain_index = chain_index
29
+ @ats_index = ats_index
30
+ end
31
+ end
32
+
33
+ # The Evidence Record is structurally invalid: an empty chain, mixed hash
34
+ # algorithms within a chain, and so on.
35
+ class MalformedRecordError < VerificationError; end
36
+
37
+ # The data is not covered by the Evidence Record, or a computed root does not
38
+ # match the message imprint of its time-stamp.
39
+ class HashMismatchError < VerificationError; end
40
+
41
+ # The time-stamp validator rejected a time-stamp token.
42
+ class TimestampInvalidError < VerificationError; end
43
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EvidenceRecord
4
+ # Builds Evidence Records for many data objects with one time-stamp
5
+ # (docs/design.md 3.3).
6
+ #
7
+ # gen = EvidenceRecord::Generator.new(digest_algorithm: :sha256)
8
+ # gen.add("doc-a") # => 0
9
+ # gen.add(EvidenceRecord::DataGroup.new([meta, body])) # => 1
10
+ # req = gen.timestamp_request(nonce: SecureRandom.random_number(2**64))
11
+ # res = post_to_tsa(req.to_der) # the TSA response, as a String
12
+ # records = gen.build(res) # => [Record, Record], in add order
13
+ #
14
+ # The data are hashed when they are added and are not kept. Data cannot be
15
+ # added once timestamp_request or build has been called, because the
16
+ # time-stamp covers the hash tree at that point.
17
+ #
18
+ # build checks that the response is granted and matches the request, but
19
+ # does not check its signature; verify the records for that.
20
+ #
21
+ # Not thread-safe.
22
+ class Generator
23
+ attr_reader :digest_algorithm
24
+
25
+ # digest_algorithm: :sha256, :sha384 or :sha512.
26
+ def initialize(digest_algorithm: :sha256)
27
+ @digest_algorithm = DigestAlgorithm.fetch(digest_algorithm)
28
+ @tree = HashTree.new(digest_algorithm: @digest_algorithm)
29
+ @groups = []
30
+ @closed = false
31
+ end
32
+
33
+ # Adds a data object (a String or a PrehashedData) or a DataGroup, and
34
+ # returns its index: the position of its record in the result of build.
35
+ def add(data)
36
+ raise Error, "data cannot be added after timestamp_request or build" if @closed
37
+
38
+ source = DataSource.wrap(data)
39
+ index = @tree.add(source.member_digests(@digest_algorithm))
40
+ @groups << source.group?
41
+ index
42
+ end
43
+
44
+ # The number of data objects and groups added.
45
+ def size
46
+ @tree.size
47
+ end
48
+
49
+ # The root of the hash tree: the hash value to be time-stamped.
50
+ def root
51
+ raise Error, "no data has been added" if @tree.empty?
52
+
53
+ @tree.root
54
+ end
55
+
56
+ # An OpenSSL::Timestamp::Request for the root, to be sent to a TSA. When
57
+ # nonce or policy_id is given, build checks that the response has the
58
+ # same values.
59
+ def timestamp_request(nonce: nil, cert_requested: true, policy_id: nil)
60
+ exchange.request(nonce: nonce, cert_requested: cert_requested, policy_id: policy_id)
61
+ end
62
+
63
+ # Builds the records from the TSA response (a String, or an
64
+ # OpenSSL::Timestamp::Response). Returns an Array of Records in the order
65
+ # the data were added. Raises TimestampResponseError when the response
66
+ # is not usable.
67
+ def build(response)
68
+ timestamp = exchange.token(response)
69
+ Array.new(size) { |i| Record.build([Chain.build([archive_timestamp(i, timestamp)])]) }
70
+ end
71
+
72
+ private
73
+
74
+ def exchange
75
+ @exchange ||= begin
76
+ imprint = root
77
+ @closed = true
78
+ TimestampExchange.new(@digest_algorithm, imprint)
79
+ end
80
+ end
81
+
82
+ # A single data object time-stamped alone needs no reduced hash tree: its
83
+ # hash is the message imprint (as Bouncy Castle does).
84
+ def archive_timestamp(index, timestamp)
85
+ return ArchiveTimeStamp.build(timestamp: timestamp) if size == 1 && !@groups[0]
86
+
87
+ ArchiveTimeStamp.build(timestamp: timestamp, reduced_hashtree: @tree.reduced_hashtree(index),
88
+ digest_algorithm: @digest_algorithm)
89
+ end
90
+ end
91
+ end