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
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EvidenceRecord
|
|
4
|
+
# Internal: one exchange with a TSA for a hash value (docs/design.md 3.3,
|
|
5
|
+
# 6). Makes the request and takes the time-stamp token out of the response
|
|
6
|
+
# after checking it. Used by Generator and the renewals.
|
|
7
|
+
class TimestampExchange
|
|
8
|
+
GRANTED = 0
|
|
9
|
+
GRANTED_WITH_MODS = 1
|
|
10
|
+
|
|
11
|
+
def initialize(algorithm, imprint)
|
|
12
|
+
@algorithm = algorithm
|
|
13
|
+
@imprint = Digest.wrap(imprint)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# An OpenSSL::Timestamp::Request for the hash value. The nonce and policy
|
|
17
|
+
# given here are checked against the response in #token.
|
|
18
|
+
def request(nonce: nil, cert_requested: true, policy_id: nil)
|
|
19
|
+
raise ArgumentError, "nonce must be an Integer, got #{nonce.class}" unless nonce.nil? || nonce.is_a?(Integer)
|
|
20
|
+
unless policy_id.nil? || policy_id.is_a?(String)
|
|
21
|
+
raise ArgumentError, "policy_id must be a String (OID), got #{policy_id.class}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
@nonce = nonce
|
|
25
|
+
@policy_id = policy_id
|
|
26
|
+
request = OpenSSL::Timestamp::Request.new
|
|
27
|
+
request.algorithm = @algorithm.name.to_s.upcase
|
|
28
|
+
request.message_imprint = @imprint.to_binary
|
|
29
|
+
request.nonce = nonce if nonce
|
|
30
|
+
request.policy_id = policy_id if policy_id
|
|
31
|
+
request.cert_requested = cert_requested ? true : false
|
|
32
|
+
request
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The TimeStampToken in response: a String (the TimeStampResp as
|
|
36
|
+
# returned by the TSA) or an OpenSSL::Timestamp::Response. Lengths encoded
|
|
37
|
+
# as BER are rewritten as DER; anything else that is not DER is rejected
|
|
38
|
+
# (docs/design.md 6). Raises TimestampResponseError when the response
|
|
39
|
+
# cannot be used.
|
|
40
|
+
#
|
|
41
|
+
# The signature is not checked here; verify the resulting record.
|
|
42
|
+
def token(response)
|
|
43
|
+
token = extract(response_bytes(response))
|
|
44
|
+
check!(token)
|
|
45
|
+
token
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def response_bytes(response)
|
|
51
|
+
case response
|
|
52
|
+
when String then response
|
|
53
|
+
when OpenSSL::Timestamp::Response then response.to_der
|
|
54
|
+
else
|
|
55
|
+
raise ArgumentError,
|
|
56
|
+
"response must be a String or an OpenSSL::Timestamp::Response, got #{response.class}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# TimeStampResp ::= SEQUENCE { status PKIStatusInfo,
|
|
61
|
+
# timeStampToken TimeStampToken OPTIONAL }
|
|
62
|
+
# PKIStatusInfo ::= SEQUENCE { status PKIStatus,
|
|
63
|
+
# statusString PKIFreeText OPTIONAL, failInfo PKIFailureInfo OPTIONAL }
|
|
64
|
+
def extract(bytes)
|
|
65
|
+
node = DER.parse(DER.normalize_lengths(bytes, "time-stamp response"), "time-stamp response")
|
|
66
|
+
status_info, token = DER.children_of(node, "time-stamp response", size: 1..2)
|
|
67
|
+
status, *details = DER.children_of(status_info, "time-stamp response status", size: 1..3)
|
|
68
|
+
code = DER.integer(status, "time-stamp response status")
|
|
69
|
+
unless [GRANTED, GRANTED_WITH_MODS].include?(code)
|
|
70
|
+
raise TimestampResponseError, "the TSA rejected the request (status #{code}#{status_text(details)})"
|
|
71
|
+
end
|
|
72
|
+
raise TimestampResponseError, "the response has no time-stamp token" unless token
|
|
73
|
+
|
|
74
|
+
TimeStampToken.from_node(token, "time-stamp token")
|
|
75
|
+
rescue ParseError => e
|
|
76
|
+
raise TimestampResponseError, "invalid time-stamp response: #{e.message}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# PKIFreeText ::= SEQUENCE SIZE (1..MAX) OF UTF8String
|
|
80
|
+
def status_text(details)
|
|
81
|
+
text = details.find { _1.universal?(DER::SEQUENCE) }
|
|
82
|
+
return "" unless text
|
|
83
|
+
|
|
84
|
+
strings = text.children.select { _1.universal?(12) }.map { _1.content.dup.force_encoding(Encoding::UTF_8).scrub }
|
|
85
|
+
strings.empty? ? "" : ": #{strings.join('; ')}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def check!(token)
|
|
89
|
+
unless token.hash_algorithm_oid == @algorithm.oid
|
|
90
|
+
raise TimestampResponseError,
|
|
91
|
+
"the time-stamp uses hash algorithm #{token.hash_algorithm_oid}, but #{@algorithm} was requested"
|
|
92
|
+
end
|
|
93
|
+
unless token.message_imprint == @imprint
|
|
94
|
+
raise TimestampResponseError, "the message imprint of the time-stamp does not match the request"
|
|
95
|
+
end
|
|
96
|
+
if @nonce && token.nonce != @nonce
|
|
97
|
+
raise TimestampResponseError, "the nonce of the time-stamp does not match the request"
|
|
98
|
+
end
|
|
99
|
+
return unless @policy_id && token.policy_id != @policy_id
|
|
100
|
+
|
|
101
|
+
raise TimestampResponseError, "the policy of the time-stamp does not match the request"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EvidenceRecord
|
|
4
|
+
# A time-stamp renewal of a Record (RFC 4998 §5.2, docs/design.md 3.4):
|
|
5
|
+
# a new ArchiveTimeStamp, added to the last chain, that covers the
|
|
6
|
+
# time-stamps of that chain. Done before the time-stamps become invalid
|
|
7
|
+
# (for example, before the TSA certificate expires).
|
|
8
|
+
#
|
|
9
|
+
# renewal = record.timestamp_renewal
|
|
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 all the time-stamps of the last chain, as one
|
|
15
|
+
# data object group, as Bouncy Castle does. Its hash algorithm is the one
|
|
16
|
+
# of the last chain; when that algorithm weakens, use a hash-tree renewal
|
|
17
|
+
# (HashTreeRenewal) instead.
|
|
18
|
+
#
|
|
19
|
+
# The data are not needed and the record is not verified. Records built
|
|
20
|
+
# from the same hash tree and renewed in the same way have the same
|
|
21
|
+
# time-stamps, so one TSA response can renew all of them: build a renewal
|
|
22
|
+
# for each record and pass the same response to build.
|
|
23
|
+
#
|
|
24
|
+
# See Renewal for #timestamp_request and #build.
|
|
25
|
+
class TimestampRenewal
|
|
26
|
+
include Renewal
|
|
27
|
+
|
|
28
|
+
# Internal: use Record#timestamp_renewal.
|
|
29
|
+
def initialize(record)
|
|
30
|
+
chain = record.chains.last
|
|
31
|
+
if chain.nil? || chain.archive_timestamps.empty?
|
|
32
|
+
raise MalformedRecordError, "the record has no ArchiveTimeStamp to renew"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
oids = chain.archive_timestamps.map(&:digest_algorithm_oid).uniq
|
|
36
|
+
raise MalformedRecordError, "hash algorithms are mixed within the last chain" unless oids.size == 1
|
|
37
|
+
|
|
38
|
+
@chain = chain
|
|
39
|
+
algorithm = DigestAlgorithm.from_oid(oids[0])
|
|
40
|
+
prepare(record, algorithm, chain.archive_timestamps.map { algorithm.digest(_1.timestamp.to_der) })
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def renewed_chains(archive_timestamp)
|
|
46
|
+
@record.chains[0...-1] + [Chain.build(@chain.archive_timestamps + [archive_timestamp])]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EvidenceRecord
|
|
4
|
+
# Time-stamp validators: objects that check that a time-stamp token itself
|
|
5
|
+
# is valid (docs/design.md 5.3).
|
|
6
|
+
#
|
|
7
|
+
# A validator responds to call(token, at:), where token is a TimeStampToken
|
|
8
|
+
# and at is the Time at which the token must be valid. It returns a truthy
|
|
9
|
+
# value when the token is valid. When it is not, it raises
|
|
10
|
+
# EvidenceRecord::TimestampInvalidError or returns false or nil; both are
|
|
11
|
+
# treated as invalid, so a validator that forgets to return true fails
|
|
12
|
+
# closed. Other exceptions are not caught.
|
|
13
|
+
module TimestampValidator
|
|
14
|
+
# Checks the signature of the token and its certificate chain at the given
|
|
15
|
+
# time with OpenSSL, using a store of trusted certificates.
|
|
16
|
+
#
|
|
17
|
+
# store = OpenSSL::X509::Store.new
|
|
18
|
+
# store.add_cert(tsa_root)
|
|
19
|
+
# validator = EvidenceRecord::TimestampValidator::OpenSSL.new(store: store)
|
|
20
|
+
#
|
|
21
|
+
# What it checks: the signature, that the signing certificate is the one
|
|
22
|
+
# named in the token (ESS signing certificate), that it may be used for
|
|
23
|
+
# time-stamping, and that its chain to a trusted certificate is valid at
|
|
24
|
+
# the given time.
|
|
25
|
+
#
|
|
26
|
+
# What it does not check: revocation (CRL / OCSP) unless the store is set
|
|
27
|
+
# up for it, and whether the algorithms used are still considered secure.
|
|
28
|
+
# Give your own validator for those.
|
|
29
|
+
#
|
|
30
|
+
# The store's verification time is set on every call, so do not share the
|
|
31
|
+
# store with other code that relies on it.
|
|
32
|
+
class OpenSSL
|
|
33
|
+
def initialize(store:)
|
|
34
|
+
unless store.is_a?(::OpenSSL::X509::Store)
|
|
35
|
+
raise ArgumentError, "store must be an OpenSSL::X509::Store, got #{store.class}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
@store = store
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def call(token, at:)
|
|
42
|
+
raise ArgumentError, "token must be an EvidenceRecord::TimeStampToken" unless token.is_a?(TimeStampToken)
|
|
43
|
+
raise ArgumentError, "at must be a Time, got #{at.class}" unless at.is_a?(Time)
|
|
44
|
+
|
|
45
|
+
@store.time = at
|
|
46
|
+
response_for(token).verify(request_for(token), @store)
|
|
47
|
+
true
|
|
48
|
+
rescue ::OpenSSL::Timestamp::TimestampError => e
|
|
49
|
+
raise TimestampInvalidError, "invalid time-stamp: #{e.message}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# A request that the token must match. It is built from the token
|
|
55
|
+
# itself: the hash chain has already been checked against the imprint.
|
|
56
|
+
def request_for(token)
|
|
57
|
+
request = ::OpenSSL::Timestamp::Request.new
|
|
58
|
+
request.algorithm = token.hash_algorithm.name.to_s.upcase
|
|
59
|
+
request.message_imprint = token.message_imprint.to_binary
|
|
60
|
+
request.nonce = token.nonce if token.nonce
|
|
61
|
+
request
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# A granted TimeStampResp holding the token without its crls field
|
|
65
|
+
# (docs/design.md 7.2).
|
|
66
|
+
def response_for(token)
|
|
67
|
+
status = DER.sequence([DER.encode_integer(0)])
|
|
68
|
+
::OpenSSL::Timestamp::Response.new(DER.sequence([status, token.der_without_crls]))
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EvidenceRecord
|
|
4
|
+
# Internal: verifies a Record against data (docs/design.md 5.1). Used by
|
|
5
|
+
# Record#verify!, #verify_hashes! and the renewals.
|
|
6
|
+
class Verifier
|
|
7
|
+
def initialize(record, data)
|
|
8
|
+
@record = record
|
|
9
|
+
@data = DataSource.wrap(data)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Steps 1-3: the structure and the hash chain, without looking at the
|
|
13
|
+
# signatures or the times of the time-stamps.
|
|
14
|
+
def verify_hashes!
|
|
15
|
+
check_structure!
|
|
16
|
+
@record.chains.each_with_index { |chain, ci| verify_chain!(chain, ci) }
|
|
17
|
+
true
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# All steps.
|
|
21
|
+
def verify!(timestamp_validator:, at:)
|
|
22
|
+
verify_hashes!
|
|
23
|
+
check_times!(at)
|
|
24
|
+
validate_timestamps!(timestamp_validator, at)
|
|
25
|
+
true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# [ArchiveTimeStamp, chain index, index in chain] in chronological order.
|
|
31
|
+
def entries
|
|
32
|
+
@record.chains.each_with_index.flat_map do |chain, ci|
|
|
33
|
+
chain.archive_timestamps.each_with_index.map { |ats, ai| [ats, ci, ai] }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Step 1.
|
|
38
|
+
def check_structure!
|
|
39
|
+
raise MalformedRecordError, "the record has no ArchiveTimeStampChain" if @record.chains.empty?
|
|
40
|
+
|
|
41
|
+
@record.chains.each_with_index do |chain, ci|
|
|
42
|
+
if chain.archive_timestamps.empty?
|
|
43
|
+
raise MalformedRecordError.new("chain #{ci} has no ArchiveTimeStamp", chain_index: ci)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
oid = chain.archive_timestamps[0].digest_algorithm_oid
|
|
47
|
+
chain.archive_timestamps.each_with_index { |ats, ai| check_archive_timestamp!(ats, oid, ci, ai) }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def check_archive_timestamp!(ats, chain_oid, ci, ai)
|
|
52
|
+
where = { chain_index: ci, ats_index: ai }
|
|
53
|
+
unless ats.digest_algorithm_oid == chain_oid
|
|
54
|
+
raise MalformedRecordError.new("#{at(ci, ai)}: hash algorithms are mixed within a chain", **where)
|
|
55
|
+
end
|
|
56
|
+
declared = ats.declared_digest_algorithm_oid
|
|
57
|
+
if declared && declared != ats.timestamp.hash_algorithm_oid
|
|
58
|
+
raise MalformedRecordError.new(
|
|
59
|
+
"#{at(ci, ai)}: digestAlgorithm differs from the hash algorithm of the time-stamp", **where
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
algorithm = ats.digest_algorithm # raises UnsupportedAlgorithmError
|
|
64
|
+
values = ats.reduced_hashtree&.partial_hashtrees&.flatten || []
|
|
65
|
+
return if values.all? { _1.bytesize == algorithm.digest_size }
|
|
66
|
+
|
|
67
|
+
raise MalformedRecordError.new("#{at(ci, ai)}: a hash in reducedHashtree is not a #{algorithm} hash", **where)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Steps 2 and 3.
|
|
71
|
+
def verify_chain!(chain, ci)
|
|
72
|
+
algorithm = chain.archive_timestamps[0].digest_algorithm
|
|
73
|
+
chain.archive_timestamps.each_with_index do |ats, ai|
|
|
74
|
+
if ai.zero?
|
|
75
|
+
check_covered!(ats, algorithm, data_hash_candidates(algorithm, ci), "the data", ci, ai)
|
|
76
|
+
else
|
|
77
|
+
previous = chain.archive_timestamps[ai - 1].timestamp.to_der
|
|
78
|
+
check_covered!(ats, algorithm, [[algorithm.digest(previous)]], "the previous time-stamp", ci, ai)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# The hashes that the first partial hash tree of chain ci must contain,
|
|
84
|
+
# as alternatives. In the first chain they are the hashes of the data
|
|
85
|
+
# (each member of a data group). In later chains they are H(H(d) + ha),
|
|
86
|
+
# where ha is the hash of the older chains; both the fixed order and the
|
|
87
|
+
# sorted order of the concatenation are accepted (docs/design.md 3.5).
|
|
88
|
+
def data_hash_candidates(algorithm, ci)
|
|
89
|
+
hashes = @data.member_digests(algorithm)
|
|
90
|
+
return [hashes] if ci.zero?
|
|
91
|
+
|
|
92
|
+
ha = algorithm.digest(@record.archive_timestamp_sequence_der(ci))
|
|
93
|
+
[hashes.map { algorithm.digest(_1, ha) }, hashes.map { algorithm.digest(*[_1, ha].sort) }].uniq
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def check_covered!(ats, algorithm, candidates, subject, ci, ai)
|
|
97
|
+
imprint = ats.timestamp.message_imprint
|
|
98
|
+
contained = false
|
|
99
|
+
candidates.each do |hashes|
|
|
100
|
+
# Without a reduced hash tree, the hashes themselves make the root.
|
|
101
|
+
tree = ats.reduced_hashtree || ReducedHashtree.new([hashes])
|
|
102
|
+
next unless hashes.all? { |h| tree.leaf_values.include?(Digest.new(h)) }
|
|
103
|
+
return if tree.root(algorithm) == imprint
|
|
104
|
+
|
|
105
|
+
contained = true
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
where = { chain_index: ci, ats_index: ai }
|
|
109
|
+
if contained
|
|
110
|
+
raise HashMismatchError.new("#{at(ci, ai)}: the root of the hash tree does not match the time-stamp", **where)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
raise HashMismatchError.new("#{at(ci, ai)}: #{subject} is not covered by the reduced hash tree", **where)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Step 4.
|
|
117
|
+
def check_times!(time)
|
|
118
|
+
previous = nil
|
|
119
|
+
entries.each do |ats, ci, ai|
|
|
120
|
+
gen_time = ats.timestamp.gen_time
|
|
121
|
+
if previous && gen_time < previous
|
|
122
|
+
raise MalformedRecordError.new("#{at(ci, ai)}: the time-stamp is older than the one before",
|
|
123
|
+
chain_index: ci, ats_index: ai)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
previous = gen_time
|
|
127
|
+
end
|
|
128
|
+
return if previous <= time
|
|
129
|
+
|
|
130
|
+
_, ci, ai = entries.last
|
|
131
|
+
raise TimestampInvalidError.new("#{at(ci, ai)}: the time-stamp is later than the verification time",
|
|
132
|
+
chain_index: ci, ats_index: ai)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Step 5. Each time-stamp must have been valid when the next one, which
|
|
136
|
+
# covers it, was made; the last one must be valid at the verification
|
|
137
|
+
# time.
|
|
138
|
+
def validate_timestamps!(validator, time)
|
|
139
|
+
list = entries
|
|
140
|
+
list.each_with_index do |(ats, ci, ai), k|
|
|
141
|
+
reference = k + 1 < list.size ? list[k + 1][0].timestamp.gen_time : time
|
|
142
|
+
valid = begin
|
|
143
|
+
validator.call(ats.timestamp, at: reference)
|
|
144
|
+
rescue TimestampInvalidError => e
|
|
145
|
+
raise TimestampInvalidError.new("#{at(ci, ai)}: #{e.message}", chain_index: ci, ats_index: ai)
|
|
146
|
+
end
|
|
147
|
+
next if valid
|
|
148
|
+
|
|
149
|
+
raise TimestampInvalidError.new("#{at(ci, ai)}: rejected by the time-stamp validator",
|
|
150
|
+
chain_index: ci, ats_index: ai)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def at(ci, ai)
|
|
155
|
+
"chain #{ci}, ArchiveTimeStamp #{ai}"
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "evidence_record/version"
|
|
4
|
+
require_relative "evidence_record/errors"
|
|
5
|
+
require_relative "evidence_record/digest"
|
|
6
|
+
require_relative "evidence_record/digest_algorithm"
|
|
7
|
+
require_relative "evidence_record/data"
|
|
8
|
+
require_relative "evidence_record/reduced_hashtree"
|
|
9
|
+
require_relative "evidence_record/hash_tree"
|
|
10
|
+
require_relative "evidence_record/der"
|
|
11
|
+
require_relative "evidence_record/algorithm_identifier"
|
|
12
|
+
require_relative "evidence_record/time_stamp_token"
|
|
13
|
+
require_relative "evidence_record/archive_time_stamp"
|
|
14
|
+
require_relative "evidence_record/chain"
|
|
15
|
+
require_relative "evidence_record/record"
|
|
16
|
+
require_relative "evidence_record/verifier"
|
|
17
|
+
require_relative "evidence_record/timestamp_validator"
|
|
18
|
+
require_relative "evidence_record/timestamp_exchange"
|
|
19
|
+
require_relative "evidence_record/generator"
|
|
20
|
+
require_relative "evidence_record/renewal"
|
|
21
|
+
require_relative "evidence_record/timestamp_renewal"
|
|
22
|
+
require_relative "evidence_record/hash_tree_renewal"
|
|
23
|
+
|
|
24
|
+
# Evidence Record Syntax (ERS), RFC 4998.
|
|
25
|
+
module EvidenceRecord
|
|
26
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: evidence_record
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Ken Tanaka
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Generation, timestamp renewal, hash-tree renewal and verification of
|
|
13
|
+
Evidence Records as defined in RFC 4998, with no runtime dependencies beyond the
|
|
14
|
+
Ruby standard library.
|
|
15
|
+
email:
|
|
16
|
+
- tanaka-ken@netlab.jp
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- LICENSE.txt
|
|
22
|
+
- README.md
|
|
23
|
+
- lib/evidence_record.rb
|
|
24
|
+
- lib/evidence_record/algorithm_identifier.rb
|
|
25
|
+
- lib/evidence_record/archive_time_stamp.rb
|
|
26
|
+
- lib/evidence_record/chain.rb
|
|
27
|
+
- lib/evidence_record/data.rb
|
|
28
|
+
- lib/evidence_record/der.rb
|
|
29
|
+
- lib/evidence_record/digest.rb
|
|
30
|
+
- lib/evidence_record/digest_algorithm.rb
|
|
31
|
+
- lib/evidence_record/errors.rb
|
|
32
|
+
- lib/evidence_record/generator.rb
|
|
33
|
+
- lib/evidence_record/hash_tree.rb
|
|
34
|
+
- lib/evidence_record/hash_tree_renewal.rb
|
|
35
|
+
- lib/evidence_record/record.rb
|
|
36
|
+
- lib/evidence_record/reduced_hashtree.rb
|
|
37
|
+
- lib/evidence_record/renewal.rb
|
|
38
|
+
- lib/evidence_record/time_stamp_token.rb
|
|
39
|
+
- lib/evidence_record/timestamp_exchange.rb
|
|
40
|
+
- lib/evidence_record/timestamp_renewal.rb
|
|
41
|
+
- lib/evidence_record/timestamp_validator.rb
|
|
42
|
+
- lib/evidence_record/verifier.rb
|
|
43
|
+
- lib/evidence_record/version.rb
|
|
44
|
+
homepage: https://github.com/xketanaka/evidence_record
|
|
45
|
+
licenses:
|
|
46
|
+
- MIT
|
|
47
|
+
metadata:
|
|
48
|
+
source_code_uri: https://github.com/xketanaka/evidence_record
|
|
49
|
+
bug_tracker_uri: https://github.com/xketanaka/evidence_record/issues
|
|
50
|
+
documentation_uri: https://github.com/xketanaka/evidence_record/blob/main/README.md
|
|
51
|
+
rubygems_mfa_required: 'true'
|
|
52
|
+
rdoc_options: []
|
|
53
|
+
require_paths:
|
|
54
|
+
- lib
|
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '3.4'
|
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
61
|
+
requirements:
|
|
62
|
+
- - ">="
|
|
63
|
+
- !ruby/object:Gem::Version
|
|
64
|
+
version: '0'
|
|
65
|
+
requirements: []
|
|
66
|
+
rubygems_version: 3.6.9
|
|
67
|
+
specification_version: 4
|
|
68
|
+
summary: Evidence Record Syntax (ERS) compliant with RFC 4998
|
|
69
|
+
test_files: []
|