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,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EvidenceRecord
4
+ # A hash tree built from leaves, used to generate reduced hash trees
5
+ # (RFC 4998 §4.2). It only computes hashes; it knows nothing about ASN.1
6
+ # or time-stamps.
7
+ #
8
+ # Each leaf is the list of hashes of one data object (one hash) or of one
9
+ # data object group (the hashes of all its members). The tree is binary and
10
+ # built like Bouncy Castle's BinaryTreeRootCalculator (docs/design.md 4.2):
11
+ # the leaves are sorted by their hash, adjacent pairs are hashed together,
12
+ # and the last node of an odd-sized level is carried up unchanged.
13
+ class HashTree
14
+ attr_reader :digest_algorithm
15
+
16
+ def initialize(digest_algorithm: :sha256)
17
+ @digest_algorithm = DigestAlgorithm.fetch(digest_algorithm)
18
+ @leaves = []
19
+ @levels = nil
20
+ end
21
+
22
+ # Adds a leaf from the hashes of a data object ([hash]) or of the members
23
+ # of a data object group, and returns the index of the new leaf (in the
24
+ # order of addition).
25
+ def add(member_hashes)
26
+ unless member_hashes.is_a?(Array) && !member_hashes.empty?
27
+ raise ArgumentError, "member_hashes must be a non-empty Array"
28
+ end
29
+
30
+ values = member_hashes.map do |h|
31
+ bytes = Digest.bytes_of(h)
32
+ unless bytes.bytesize == @digest_algorithm.digest_size
33
+ raise ArgumentError,
34
+ "hash must be #{@digest_algorithm.digest_size} bytes for #{@digest_algorithm}, got #{bytes.bytesize}"
35
+ end
36
+ bytes
37
+ end.freeze
38
+ @leaves << values
39
+ @levels = nil
40
+ @leaves.size - 1
41
+ end
42
+
43
+ def size
44
+ @leaves.size
45
+ end
46
+
47
+ def empty?
48
+ @leaves.empty?
49
+ end
50
+
51
+ # The hash of the leaf at index (for a group, the hash of its members).
52
+ def leaf_hash(index)
53
+ check_index!(index)
54
+ Digest.new(node_hash(@leaves[index]))
55
+ end
56
+
57
+ def root
58
+ raise Error, "an empty hash tree has no root" if empty?
59
+
60
+ Digest.new(levels.last[0])
61
+ end
62
+
63
+ # The reduced hash tree for the leaf at index (in the order of addition).
64
+ def reduced_hashtree(index)
65
+ check_index!(index)
66
+ all_levels = levels # also computes @positions
67
+ partials = [@leaves[index]]
68
+ pos = @positions[index]
69
+ all_levels[0...-1].each do |level|
70
+ sibling = pos ^ 1
71
+ partials << [level[sibling]] if sibling < level.size
72
+ pos >>= 1
73
+ end
74
+ ReducedHashtree.new(partials)
75
+ end
76
+
77
+ def inspect
78
+ "#<#{self.class.name} #{@digest_algorithm} size=#{size}>"
79
+ end
80
+
81
+ private
82
+
83
+ def node_hash(values)
84
+ ReducedHashtree.node_hash(@digest_algorithm, values)
85
+ end
86
+
87
+ # levels[0] holds the leaf hashes sorted in binary ascending order;
88
+ # levels.last holds the root.
89
+ def levels
90
+ return @levels if @levels
91
+
92
+ sorted = @leaves.each_with_index.map { |values, i| [node_hash(values), i] }.sort
93
+ @positions = Array.new(@leaves.size)
94
+ sorted.each_with_index { |(_, i), pos| @positions[i] = pos }
95
+
96
+ level = sorted.map(&:first)
97
+ levels = [level]
98
+ while level.size > 1
99
+ level = level.each_slice(2).map { |pair| pair.size == 2 ? @digest_algorithm.digest(*pair.sort) : pair[0] }
100
+ levels << level
101
+ end
102
+ @levels = levels
103
+ end
104
+
105
+ def check_index!(index)
106
+ raise ArgumentError, "index must be an Integer, got #{index.class}" unless index.is_a?(Integer)
107
+ raise IndexError, "index #{index} out of range (size #{size})" unless index >= 0 && index < size
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EvidenceRecord
4
+ # A hash-tree renewal of a Record (RFC 4998 §5.2, docs/design.md 3.5): a
5
+ # new chain, whose first time-stamp covers the data and all the older
6
+ # chains, hashed with a new algorithm. Done before the hash algorithm in
7
+ # use weakens.
8
+ #
9
+ # renewal = record.hash_tree_renewal(data: "doc-a", digest_algorithm: :sha512)
10
+ # req = renewal.timestamp_request(nonce: SecureRandom.random_number(2**64))
11
+ # res = post_to_tsa(req.to_der) # the TSA response, as a String
12
+ # record2 = renewal.build(res) # => a new Record
13
+ #
14
+ # The new time-stamp covers H(H(d) + ha) for the data object d, where ha
15
+ # is the hash of the ArchiveTimeStampSequence so far (both with the new
16
+ # algorithm). For a data object group, it covers H(H(d_i) + ha) for each
17
+ # member, as one group. The concatenation is in this fixed order, as
18
+ # Bouncy Castle does (docs/design.md 3.5).
19
+ #
20
+ # The record is first checked against the data (Record#verify_hashes!), so
21
+ # that a record is never renewed with the wrong data. The signatures of the
22
+ # time-stamps are not checked; do that with Record#verify before renewing.
23
+ #
24
+ # See Renewal for #timestamp_request and #build.
25
+ class HashTreeRenewal
26
+ include Renewal
27
+
28
+ # Internal: use Record#hash_tree_renewal.
29
+ def initialize(record, data:, digest_algorithm:)
30
+ algorithm = DigestAlgorithm.fetch(digest_algorithm)
31
+ source = DataSource.wrap(data)
32
+ Verifier.new(record, source).verify_hashes!
33
+ ha = algorithm.digest(record.archive_timestamp_sequence_der)
34
+ prepare(record, algorithm, source.member_digests(algorithm).map { algorithm.digest(_1, ha) })
35
+ end
36
+
37
+ private
38
+
39
+ def renewed_chains(archive_timestamp)
40
+ @record.chains + [Chain.build([archive_timestamp])]
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EvidenceRecord
4
+ # An Evidence Record (RFC 4998 §3): the RFC's EvidenceRecord type.
5
+ #
6
+ # EvidenceRecord ::= SEQUENCE {
7
+ # version INTEGER { v1(1) },
8
+ # digestAlgorithms SEQUENCE OF AlgorithmIdentifier,
9
+ # cryptoInfos [0] CryptoInfos OPTIONAL,
10
+ # encryptionInfo [1] EncryptionInfo OPTIONAL,
11
+ # archiveTimeStampSequence ArchiveTimeStampSequence }
12
+ #
13
+ # Only DER is accepted, and the original DER is kept and returned by
14
+ # #to_der (docs/design.md 7.1).
15
+ class Record
16
+ VERSION = 1
17
+
18
+ attr_reader :version, :digest_algorithm_oids, :chains
19
+ # The cryptoInfos and encryptionInfo fields as they were read (DER,
20
+ # including their [0] / [1] tags), or nil. They are kept but not
21
+ # interpreted (docs/design.md 7.3).
22
+ attr_reader :crypto_infos, :encryption_info
23
+
24
+ class << self
25
+ # Parses a DER-encoded Evidence Record. Raises ParseError when der is not
26
+ # DER or does not match the structure of RFC 4998.
27
+ def from_der(der)
28
+ new(DER.parse(der, "EvidenceRecord"))
29
+ end
30
+
31
+ # Internal: encodes a new Evidence Record from Chains. digestAlgorithms
32
+ # lists the algorithms of the ArchiveTimeStamps in order of appearance.
33
+ def build(chains)
34
+ from_der(encode(digest_algorithm_ders([], chains), nil, nil, chains))
35
+ end
36
+
37
+ # Internal.
38
+ def encode(digest_algorithm_ders, crypto_infos, encryption_info, chains)
39
+ DER.sequence([DER.encode_integer(VERSION), DER.sequence(digest_algorithm_ders),
40
+ crypto_infos, encryption_info, DER.sequence(chains.map(&:to_der))].compact)
41
+ end
42
+
43
+ # Internal: existing (OID, DER) pairs plus AlgorithmIdentifiers for the
44
+ # algorithms of chains that are not among them.
45
+ def digest_algorithm_ders(existing, chains)
46
+ oids = existing.map(&:first)
47
+ added = chains.flat_map(&:archive_timestamps).map(&:digest_algorithm_oid).uniq - oids
48
+ existing.map(&:last) + added.map { AlgorithmIdentifier.encode(DigestAlgorithm.from_oid(_1)) }
49
+ end
50
+
51
+ private :new
52
+ end
53
+
54
+ def initialize(node)
55
+ what = "EvidenceRecord"
56
+ @der = node.raw
57
+ version, algorithms, *optional, sequence = DER.children_of(node, what, size: 3..5)
58
+ @version = DER.integer(version, "#{what} version")
59
+ raise ParseError, "#{what}: unsupported version #{@version}" unless @version == VERSION
60
+
61
+ @digest_algorithms = DER.children_of(algorithms, "#{what} digestAlgorithms").map do |alg|
62
+ [AlgorithmIdentifier.parse(alg, "#{what} digestAlgorithms"), alg.raw]
63
+ end.freeze
64
+ @digest_algorithm_oids = @digest_algorithms.map(&:first).freeze
65
+ parse_optional(optional, what)
66
+ @chains = DER.children_of(sequence, "#{what} archiveTimeStampSequence").each_with_index.map do |chain, i|
67
+ Chain.from_node(chain, "archiveTimeStampSequence[#{i}]")
68
+ end.freeze
69
+ freeze
70
+ end
71
+
72
+ # The digestAlgorithms field as DigestAlgorithms. Raises
73
+ # UnsupportedAlgorithmError if it lists other algorithms (see
74
+ # #digest_algorithm_oids).
75
+ def digest_algorithms
76
+ @digest_algorithm_oids.map { DigestAlgorithm.from_oid(_1) }
77
+ end
78
+
79
+ # The time the data is proven to have existed: the genTime of the first
80
+ # ArchiveTimeStamp. nil if the record has no ArchiveTimeStamp.
81
+ #
82
+ # This is only meaningful after the record has been verified.
83
+ def proven_time
84
+ @chains.first&.archive_timestamps&.first&.timestamp&.gen_time
85
+ end
86
+
87
+ # Whether other was built from the same hash tree: the first time-stamps
88
+ # are the same. Such records can share the TSA response of a time-stamp
89
+ # renewal.
90
+ def related_to?(other)
91
+ raise ArgumentError, "other must be an EvidenceRecord::Record, got #{other.class}" unless other.is_a?(Record)
92
+
93
+ mine = @chains.first&.archive_timestamps&.first
94
+ theirs = other.chains.first&.archive_timestamps&.first
95
+ !mine.nil? && !theirs.nil? && mine.timestamp == theirs.timestamp
96
+ end
97
+
98
+ # Verifies that this record proves the existence of data (docs/design.md
99
+ # 3.6, 5.1): the hash chain from data to every time-stamp, the order of
100
+ # the time-stamps, and the validity of each time-stamp, checked with
101
+ # timestamp_validator (see TimestampValidator).
102
+ #
103
+ # data is a String, a PrehashedData or a DataGroup. at is the time of
104
+ # verification; the last time-stamp must be valid at that time.
105
+ #
106
+ # Returns true or false. Invalid arguments raise ArgumentError, and
107
+ # unsupported hash algorithms raise UnsupportedAlgorithmError.
108
+ def verify(data:, timestamp_validator:, at: Time.now)
109
+ verify!(data: data, timestamp_validator: timestamp_validator, at: at)
110
+ rescue VerificationError
111
+ false
112
+ end
113
+
114
+ # Like #verify, but raises a VerificationError telling why and where the
115
+ # verification failed.
116
+ def verify!(data:, timestamp_validator:, at: Time.now)
117
+ unless timestamp_validator.respond_to?(:call)
118
+ raise ArgumentError, "timestamp_validator must respond to call(token, at:)"
119
+ end
120
+ raise ArgumentError, "at must be a Time, got #{at.class}" unless at.is_a?(Time)
121
+
122
+ Verifier.new(self, data).verify!(timestamp_validator: timestamp_validator, at: at)
123
+ end
124
+
125
+ # Verifies only the structure and the hash chain from data to every
126
+ # time-stamp. Neither the signatures nor the times of the time-stamps are
127
+ # checked, so this alone does not prove anything about time.
128
+ def verify_hashes(data:)
129
+ verify_hashes!(data: data)
130
+ rescue VerificationError
131
+ false
132
+ end
133
+
134
+ # Like #verify_hashes, but raises a VerificationError.
135
+ def verify_hashes!(data:)
136
+ Verifier.new(self, data).verify_hashes!
137
+ end
138
+
139
+ # A time-stamp renewal of this record (TimestampRenewal): a new time-stamp
140
+ # covering the time-stamps of the last chain. Raises MalformedRecordError
141
+ # when the record has no ArchiveTimeStamp, and UnsupportedAlgorithmError
142
+ # when the last chain uses an unsupported hash algorithm.
143
+ def timestamp_renewal
144
+ TimestampRenewal.new(self)
145
+ end
146
+
147
+ # A hash-tree renewal of this record (HashTreeRenewal): a new chain with
148
+ # the hash algorithm digest_algorithm (:sha256, :sha384 or :sha512),
149
+ # covering data and the older chains.
150
+ #
151
+ # data is a String, a PrehashedData or a DataGroup, as for #verify; a
152
+ # PrehashedData needs hashes for the algorithms of the record and for
153
+ # digest_algorithm. The record is first checked against data with
154
+ # #verify_hashes!, which raises a VerificationError when they do not
155
+ # match.
156
+ def hash_tree_renewal(data:, digest_algorithm:)
157
+ HashTreeRenewal.new(self, data: data, digest_algorithm: digest_algorithm)
158
+ end
159
+
160
+ def to_der
161
+ @der
162
+ end
163
+
164
+ def ==(other)
165
+ other.is_a?(Record) && to_der == other.to_der
166
+ end
167
+ alias eql? ==
168
+
169
+ def hash
170
+ [Record, @der].hash
171
+ end
172
+
173
+ def inspect
174
+ "#<#{self.class.name} digest_algorithm_oids=#{@digest_algorithm_oids} chains=#{@chains.inspect}>"
175
+ end
176
+
177
+ # Internal: the DER of an ArchiveTimeStampSequence holding the first count
178
+ # chains, whose hash (ha) is used by a hash-tree renewal (RFC 4998 §5.2).
179
+ def archive_timestamp_sequence_der(count = @chains.size)
180
+ DER.sequence(@chains.first(count).map(&:to_der))
181
+ end
182
+
183
+ # Internal: a new Record with chains replaced, keeping the other fields as
184
+ # they are and adding the algorithms of new chains to digestAlgorithms.
185
+ def with_chains(chains)
186
+ algorithms = self.class.digest_algorithm_ders(@digest_algorithms, chains)
187
+ self.class.from_der(self.class.encode(algorithms, @crypto_infos, @encryption_info, chains))
188
+ end
189
+
190
+ private
191
+
192
+ def parse_optional(nodes, what)
193
+ tags = nodes.map { |n| n.tag_class == :context && n.constructed? ? n.tag : nil }
194
+ raise ParseError, "#{what}: unexpected elements" unless [[], [0], [1], [0, 1]].include?(tags)
195
+
196
+ nodes.each do |node|
197
+ if node.tag.zero?
198
+ # CryptoInfos ::= SEQUENCE SIZE (1..MAX) OF Attribute
199
+ unless !node.children.empty? && node.children.all? { _1.universal?(DER::SEQUENCE) }
200
+ raise ParseError, "#{what}: invalid cryptoInfos"
201
+ end
202
+
203
+ @crypto_infos = node.raw
204
+ else
205
+ # EncryptionInfo ::= SEQUENCE { encryptionInfoType OBJECT IDENTIFIER,
206
+ # encryptionInfoValue ANY DEFINED BY encryptionInfoType }
207
+ type, = DER.children_of(node, "#{what} encryptionInfo", size: 2, context: 1)
208
+ DER.oid(type, "#{what} encryptionInfo")
209
+ @encryption_info = node.raw
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EvidenceRecord
4
+ # A reduced hash tree (RFC 4998 §4.1): the sequence of partial hash trees
5
+ # needed to recompute the root from one leaf. The first partial hash tree
6
+ # holds the leaf itself (for a data object group, the hashes of all its
7
+ # members) and possibly its siblings; each following one holds the siblings
8
+ # at the next level.
9
+ #
10
+ # The root is computed with the generic RFC algorithm, so trees of any shape
11
+ # (binary or n-ary) are supported (docs/design.md 4.1, 5.2).
12
+ class ReducedHashtree
13
+ attr_reader :partial_hashtrees
14
+
15
+ class << self
16
+ # The hash of one node from the values that are hashed together:
17
+ # a single value is used as it is, several values are sorted in binary
18
+ # ascending order, concatenated and hashed.
19
+ def node_hash(algorithm, values)
20
+ return values[0] if values.size == 1
21
+
22
+ algorithm.digest(*values.sort)
23
+ end
24
+ end
25
+
26
+ # partial_hashtrees: an Array of non-empty Arrays of Digests (or binary
27
+ # Strings).
28
+ def initialize(partial_hashtrees)
29
+ unless partial_hashtrees.is_a?(Array) && !partial_hashtrees.empty?
30
+ raise ArgumentError, "partial_hashtrees must be a non-empty Array"
31
+ end
32
+
33
+ @partial_hashtrees = partial_hashtrees.each_with_index.map do |values, i|
34
+ unless values.is_a?(Array) && !values.empty?
35
+ raise ArgumentError, "partial hash tree #{i} must be a non-empty Array"
36
+ end
37
+
38
+ values.map { Digest.wrap(_1, "partial hash tree #{i} value") }.freeze
39
+ end.freeze
40
+ freeze
41
+ end
42
+
43
+ # The first partial hash tree: the leaf, or the members of a data object
44
+ # group, possibly with siblings.
45
+ def leaf_values
46
+ @partial_hashtrees[0]
47
+ end
48
+
49
+ # The root hash recomputed with algorithm (a Symbol or DigestAlgorithm).
50
+ #
51
+ # The lengths of the values are not checked here; the verifier does that.
52
+ def root(algorithm)
53
+ algorithm = DigestAlgorithm.fetch(algorithm)
54
+ first, *rest = @partial_hashtrees.map { |values| values.map(&:to_binary) }
55
+ current = self.class.node_hash(algorithm, first)
56
+ rest.each { |values| current = algorithm.digest(*([current] + values).sort) }
57
+ Digest.new(current)
58
+ end
59
+
60
+ def ==(other)
61
+ other.is_a?(ReducedHashtree) && @partial_hashtrees == other.partial_hashtrees
62
+ end
63
+ alias eql? ==
64
+
65
+ def hash
66
+ [ReducedHashtree, @partial_hashtrees].hash
67
+ end
68
+
69
+ def inspect
70
+ "#<#{self.class.name} #{@partial_hashtrees.map { |v| v.map { _1.to_hex[0, 8] } }.inspect}>"
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EvidenceRecord
4
+ # Internal: what TimestampRenewal and HashTreeRenewal share. The including
5
+ # class sets @record, @digest_algorithm and @values (the hashes covered by
6
+ # the new time-stamp, as one data object group) with #prepare, and
7
+ # implements #renewed_chains(archive_timestamp).
8
+ module Renewal
9
+ attr_reader :record, :digest_algorithm, :root
10
+
11
+ # An OpenSSL::Timestamp::Request for the root, to be sent to a TSA. When
12
+ # nonce or policy_id is given, build checks that the response has the
13
+ # same values.
14
+ def timestamp_request(nonce: nil, cert_requested: true, policy_id: nil)
15
+ @exchange.request(nonce: nonce, cert_requested: cert_requested, policy_id: policy_id)
16
+ end
17
+
18
+ # Builds the renewed Record from the TSA response (a String, or an
19
+ # OpenSSL::Timestamp::Response). Raises TimestampResponseError when the
20
+ # response is not usable, including when its time-stamp is older than
21
+ # the last one of the record.
22
+ #
23
+ # The signature of the time-stamp is not checked; verify the new record
24
+ # for that.
25
+ def build(response)
26
+ timestamp = @exchange.token(response)
27
+ last = @record.chains.last.archive_timestamps.last.timestamp
28
+ if timestamp.gen_time < last.gen_time
29
+ raise TimestampResponseError, "the time-stamp is older than the last time-stamp of the record"
30
+ end
31
+
32
+ @record.with_chains(renewed_chains(archive_timestamp(timestamp)))
33
+ end
34
+
35
+ def inspect
36
+ "#<#{self.class.name} #{@digest_algorithm} root=#{@root.to_hex}>"
37
+ end
38
+
39
+ private
40
+
41
+ def prepare(record, digest_algorithm, values)
42
+ @record = record
43
+ @digest_algorithm = digest_algorithm
44
+ @tree = HashTree.new(digest_algorithm: digest_algorithm)
45
+ @tree.add(values)
46
+ @single = values.size == 1
47
+ @root = @tree.root
48
+ @exchange = TimestampExchange.new(digest_algorithm, @root)
49
+ end
50
+
51
+ # With a single hash to cover, the hash is the message imprint and no
52
+ # reduced hash tree is needed. Otherwise the reduced hash tree is the one
53
+ # partial hash tree holding the hashes. Both as Bouncy Castle does
54
+ # (ERSArchiveTimeStampGenerator).
55
+ def archive_timestamp(timestamp)
56
+ return ArchiveTimeStamp.build(timestamp: timestamp) if @single
57
+
58
+ ArchiveTimeStamp.build(timestamp: timestamp, reduced_hashtree: @tree.reduced_hashtree(0),
59
+ digest_algorithm: @digest_algorithm)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EvidenceRecord
4
+ # An RFC 3161 time-stamp token: the timeStamp field of an ArchiveTimeStamp.
5
+ #
6
+ # The fields of TSTInfo are read with this library's DER reader, not with
7
+ # OpenSSL::Timestamp, because Ruby's OpenSSL cannot parse tokens that carry
8
+ # OCSP responses in the CMS crls field (docs/design.md 7.2). The token's
9
+ # signature is not checked here; that is the job of a timestamp validator.
10
+ #
11
+ # The original DER is kept and returned by #to_der.
12
+ class TimeStampToken
13
+ ID_SIGNED_DATA = "1.2.840.113549.1.7.2"
14
+ ID_CT_TST_INFO = "1.2.840.113549.1.9.16.1.4"
15
+
16
+ attr_reader :gen_time, :message_imprint, :hash_algorithm_oid, :serial_number, :policy_id, :nonce
17
+
18
+ class << self
19
+ def from_der(der)
20
+ from_node(DER.parse(der, "time-stamp token"), "time-stamp token")
21
+ end
22
+
23
+ # Internal: from an already parsed element.
24
+ def from_node(node, what)
25
+ new(node, what)
26
+ end
27
+
28
+ private :new
29
+ end
30
+
31
+ def initialize(node, what)
32
+ @der = node.raw
33
+ content_type, wrapper = DER.children_of(node, "#{what} (ContentInfo)", size: 2)
34
+ unless DER.oid(content_type, what) == ID_SIGNED_DATA
35
+ raise ParseError, "#{what}: only RFC 3161 time-stamp tokens (CMS SignedData) are supported"
36
+ end
37
+
38
+ @signed_data = DER.children_of(wrapper, what, size: 1, context: 0)[0]
39
+ @certificate_ders = parse_signed_data(@signed_data, "#{what} (SignedData)")
40
+ freeze
41
+ end
42
+
43
+ # The hash algorithm of the message imprint, as a DigestAlgorithm.
44
+ # Raises UnsupportedAlgorithmError for other algorithms (see
45
+ # #hash_algorithm_oid).
46
+ def hash_algorithm
47
+ DigestAlgorithm.from_oid(@hash_algorithm_oid)
48
+ end
49
+
50
+ # The certificates included in the token (normally the TSA certificate and
51
+ # possibly its chain), as OpenSSL::X509::Certificate.
52
+ def certificates
53
+ @certificate_ders.map { OpenSSL::X509::Certificate.new(_1) }
54
+ end
55
+
56
+ def to_der
57
+ @der
58
+ end
59
+
60
+ # Internal: a copy of the token without the CMS crls field, which Ruby's
61
+ # OpenSSL cannot parse when it holds OCSP responses. The field is not
62
+ # signed, so removing it does not change the result of verifying the
63
+ # signature (docs/design.md 7.2). Never hash this copy.
64
+ def der_without_crls
65
+ kept = @signed_data.children.reject { _1.context?(1) }
66
+ return @der if kept.size == @signed_data.children.size
67
+
68
+ DER.sequence([DER.encode_oid(ID_SIGNED_DATA), DER.context(0, DER.sequence(kept.map(&:raw)))])
69
+ end
70
+
71
+ def ==(other)
72
+ other.is_a?(TimeStampToken) && to_der == other.to_der
73
+ end
74
+ alias eql? ==
75
+
76
+ def hash
77
+ [TimeStampToken, @der].hash
78
+ end
79
+
80
+ def inspect
81
+ "#<#{self.class.name} gen_time=#{@gen_time.getutc.strftime("%Y-%m-%dT%H:%M:%S.%LZ")} " \
82
+ "message_imprint=#{@message_imprint.to_hex} serial_number=#{@serial_number}>"
83
+ end
84
+
85
+ private
86
+
87
+ # SignedData ::= SEQUENCE { version, digestAlgorithms SET, encapContentInfo,
88
+ # certificates [0] IMPLICIT OPTIONAL, crls [1] IMPLICIT OPTIONAL,
89
+ # signerInfos SET }
90
+ def parse_signed_data(node, what)
91
+ children = DER.children_of(node, what, size: 4..6)
92
+ DER.integer(children[0], "#{what} version")
93
+ DER.expect(children[1], "#{what} digestAlgorithms", DER::SET)
94
+ DER.expect(children[-1], "#{what} signerInfos", DER::SET)
95
+ optional = children[3...-1]
96
+ tags = optional.map { |n| n.tag_class == :context && n.constructed? ? n.tag : nil }
97
+ unless [[], [0], [1], [0, 1]].include?(tags)
98
+ raise ParseError, "#{what}: unexpected elements before signerInfos"
99
+ end
100
+
101
+ parse_encap_content_info(children[2], "#{what} encapContentInfo")
102
+ certificates = optional.find { _1.tag.zero? }
103
+ (certificates&.children || []).select { _1.universal?(DER::SEQUENCE) }.map(&:raw)
104
+ end
105
+
106
+ def parse_encap_content_info(node, what)
107
+ content_type, wrapper = DER.children_of(node, what, size: 2)
108
+ unless DER.oid(content_type, what) == ID_CT_TST_INFO
109
+ raise ParseError, "#{what}: the content is not a TSTInfo"
110
+ end
111
+
112
+ econtent = DER.octet_string(DER.children_of(wrapper, what, size: 1, context: 0)[0], what)
113
+ parse_tst_info(DER.parse(econtent, "TSTInfo"), "TSTInfo")
114
+ end
115
+
116
+ # TSTInfo ::= SEQUENCE { version INTEGER { v1(1) }, policy, messageImprint,
117
+ # serialNumber, genTime, accuracy OPTIONAL, ordering BOOLEAN DEFAULT FALSE,
118
+ # nonce INTEGER OPTIONAL, tsa [0] OPTIONAL, extensions [1] OPTIONAL }
119
+ def parse_tst_info(node, what)
120
+ version, policy, imprint, serial, gen_time, *rest = DER.children_of(node, what, size: 5..10)
121
+ raise ParseError, "#{what}: unsupported version" unless DER.integer(version, "#{what} version") == 1
122
+
123
+ @policy_id = DER.oid(policy, "#{what} policy")
124
+ parse_message_imprint(imprint, "#{what} messageImprint")
125
+ @serial_number = DER.integer(serial, "#{what} serialNumber")
126
+ @gen_time = DER.generalized_time(gen_time, "#{what} genTime")
127
+ @nonce = rest.find { _1.universal?(DER::INTEGER) }&.then { DER.integer(_1, "#{what} nonce") }
128
+ end
129
+
130
+ def parse_message_imprint(node, what)
131
+ algorithm, hashed = DER.children_of(node, what, size: 2)
132
+ @hash_algorithm_oid = AlgorithmIdentifier.parse(algorithm, "#{what} hashAlgorithm")
133
+ @message_imprint = Digest.new(DER.octet_string(hashed, "#{what} hashedMessage"))
134
+ known = DigestAlgorithm::BY_OID[@hash_algorithm_oid]
135
+ return if known.nil? || known.digest_size == @message_imprint.bytesize
136
+
137
+ raise ParseError, "#{what}: the #{known} hash must be #{known.digest_size} bytes"
138
+ end
139
+ end
140
+ end