triauth 0.0.1 → 1.0.0.beta.4

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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +56 -0
  3. data/LICENSE +202 -28
  4. data/README.md +2036 -7
  5. data/lib/triauth/api/attestation.rb +419 -0
  6. data/lib/triauth/api/authentication.rb +651 -0
  7. data/lib/triauth/api/identity.rb +245 -0
  8. data/lib/triauth/api/signing.rb +740 -0
  9. data/lib/triauth/api/validation.rb +162 -0
  10. data/lib/triauth/api.rb +33 -0
  11. data/lib/triauth/authentication_endpoint.rb +196 -0
  12. data/lib/triauth/challenge.rb +115 -0
  13. data/lib/triauth/challenge_response_flow.rb +344 -0
  14. data/lib/triauth/config.rb +93 -0
  15. data/lib/triauth/error.rb +95 -0
  16. data/lib/triauth/helpers.rb +1222 -0
  17. data/lib/triauth/identity.rb +301 -0
  18. data/lib/triauth/identity_domain.rb +212 -0
  19. data/lib/triauth/identity_includes.rb +249 -0
  20. data/lib/triauth/identity_keys.rb +309 -0
  21. data/lib/triauth/logger.rb +156 -0
  22. data/lib/triauth/multi_signature.rb +177 -0
  23. data/lib/triauth/protocol.rb +74 -0
  24. data/lib/triauth/request_cipher.rb +121 -0
  25. data/lib/triauth/resolvers/base.rb +147 -0
  26. data/lib/triauth/resolvers/caching_resolver.rb +202 -0
  27. data/lib/triauth/resolvers/cloudflare.rb +40 -0
  28. data/lib/triauth/resolvers/dns_json.rb +391 -0
  29. data/lib/triauth/resolvers/dns_sb.rb +40 -0
  30. data/lib/triauth/resolvers/google.rb +39 -0
  31. data/lib/triauth/resolvers/multi_resolver.rb +202 -0
  32. data/lib/triauth/resolvers/system_dns.rb +259 -0
  33. data/lib/triauth/resolvers.rb +33 -0
  34. data/lib/triauth/response.rb +142 -0
  35. data/lib/triauth/response_cipher.rb +341 -0
  36. data/lib/triauth/signature.rb +405 -0
  37. data/lib/triauth/validator.rb +549 -0
  38. data/lib/triauth/verifiers/base.rb +147 -0
  39. data/lib/triauth/verifiers/ecdsa.rb +87 -0
  40. data/lib/triauth/verifiers/ed25519.rb +89 -0
  41. data/lib/triauth/verifiers/registry.rb +52 -0
  42. data/lib/triauth/verifiers/web_authn.rb +253 -0
  43. data/lib/triauth/verifiers.rb +37 -0
  44. data/lib/triauth/version.rb +23 -0
  45. data/lib/triauth.rb +104 -7
  46. metadata +55 -13
@@ -0,0 +1,245 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2026 The Triauth Authors (https://www.triauth.org/)
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+ # SPDX-License-Identifier: Apache-2.0
18
+
19
+
20
+ require_relative '../config'
21
+ require_relative '../helpers'
22
+ require_relative '../error'
23
+ require_relative '../identity'
24
+ require_relative '../validator'
25
+
26
+ module Triauth
27
+ module Api
28
+ # Methods related to identity that are a part of the official triauth-ruby API.
29
+ module Identity
30
+ # The whois method can be used to check if a given personal identifier exists, and obtain public details stored in its identity records.
31
+ # For domains operating in `mode=private`, the `lookupCode` is required to look up the identity records.
32
+ #
33
+ # Upon call, it returns a Hash with a `status` property of:
34
+ # `-1` if domain does not exist or is not configured for triauth,
35
+ # `0` if domain is configured but identifier does not exist or could not be located (e.g., a missing or wrong `lookupCode` under `mode=private`),
36
+ # and `1` if identifier exists.
37
+ # On error, it returns an `{error: {code, message}}` Hash instead (with no `status`).
38
+ #
39
+ # Synopsis:
40
+ #
41
+ # ```ruby
42
+ #
43
+ # whois_result = Triauth.whois({ identifier: identifier })
44
+ # whois_result = Triauth.whois({ identifier: identifier, lookupCode: lookup_code }) # for an identity under a mode=private domain
45
+ # # returns {error: {code: String, reason?: String, message: String}} upon error
46
+ # # returns {status: 1, identifier, authenticationEndpoint: {…}, identityDomain, secure, publicProfile: {…}, groups: […], devices: […], includes: […]} if identifier exists
47
+ # # returns {status: 0, identifier, authenticationEndpoint: {…}, identityDomain} if domain is configured for triauth but the identifier does not exist or, for mode=private domains, a missing or wrong lookupCode was supplied
48
+ # # returns {status: -1, identifier} if domain is not configured for triauth
49
+ # ```
50
+ #
51
+ # Example:
52
+ #
53
+ # ```ruby
54
+ # Triauth.whois({ identifier: 'john@triauthdemo.org' })
55
+ # #
56
+ # # {
57
+ # # status: 1,
58
+ # # identifier: "john@triauthdemo.org",
59
+ # # authenticationEndpoint: {
60
+ # # url: "https://auth.triauthdemo.org/",
61
+ # # options: {
62
+ # # mode: "public",
63
+ # # include: "any"
64
+ # # },
65
+ # # secure: true
66
+ # # },
67
+ # # identityDomain: "john._at.triauthdemo.org",
68
+ # # secure: true,
69
+ # # publicProfile: {
70
+ # # initials: "JD",
71
+ # # name: "John Doe"
72
+ # # },
73
+ # # groups: [],
74
+ # # devices: [
75
+ # # {
76
+ # # deviceName: "laptop",
77
+ # # deviceTag: "5I4NnX-4lW3gdTqGW3zKpLsLR9oRiWip8zp6mqQgkL4",
78
+ # # keys: [
79
+ # # {
80
+ # # value: "BCkB_Cr-7pvY1Y2buYRksJPb09Tqld7M3SDII36cp_C7k08NPTWvbj0p14oMBGDGbGZ1qLkfnJSARBbAj1sA0fs",
81
+ # # options: {
82
+ # # type: "es256",
83
+ # # use: "attest,auth,ping,sign,stamp"
84
+ # # }
85
+ # # },
86
+ # # {
87
+ # # value: "BLglw14iw47KUztRHdeKNfBBBMf5P4SsZg9O9za44mO2BceHZlcQwFmtO1crjVNnwwZszqtMhErs3divGeQrQI4",
88
+ # # options: {
89
+ # # type: "es256",
90
+ # # use: "attest,auth,ping,sign,stamp"
91
+ # # }
92
+ # # }
93
+ # # ]
94
+ # # },
95
+ # # {
96
+ # # deviceName: "desktop",
97
+ # # deviceTag: "3bi5oE_jXyjGY0WY5n6YqOuzRxfo40fuqDmLGaA5bKQ",
98
+ # # keys: [
99
+ # # {
100
+ # # value: "BHAILL142prn8rQsHm5ZlMPUFeMc6niVXXIM8biVY3HPjmaw4tfUD-YZ5unkRve1S9P70Mmgk7IBzgVgQPDRnk8",
101
+ # # options: {
102
+ # # type: "es256",
103
+ # # use: "attest,auth,ping,sign,stamp"
104
+ # # }
105
+ # # }
106
+ # # ]
107
+ # # }
108
+ # # ],
109
+ # # includes: []
110
+ # # }
111
+ # #
112
+ # ```
113
+ #
114
+ # The `includes` array lists the identity's published `include` statements, e.g.:
115
+ #
116
+ # ```ruby
117
+ # # includes: [
118
+ # # {
119
+ # # ref: "jane@triauthdemo.org",
120
+ # # options: {use: "sign", scope: "any"},
121
+ # # tag: "LJwdl7N9xsSO4LV6T3gmmxVk_eXLyS58jw7czzooByw"
122
+ # # }
123
+ # # ]
124
+ # ```
125
+ #
126
+ # @param options [Hash] whois lookup options. Defaults to nil (a missing argument is rejected with `invalid_arguments`).
127
+ # @option options [String] :identifier the identifier to look up
128
+ # @option options [String] :lookupCode the identity's lookup code (used when domain is in private mode)
129
+ # @param config [Hash] optional overrides for the global Triauth.config settings. Defaults to `{}`.
130
+ #
131
+ # @return [Hash] one of the following shapes:
132
+ # `{error: {code: String, reason?: String, message: String}}`,
133
+ # `{identifier: String, status: -1}`,
134
+ # `{identifier: String, status: 0, authenticationEndpoint: {url: String, options: Hash, secure: Boolean}, identityDomain: (String, nil)}`, or
135
+ # `{identifier: String, status: 1, authenticationEndpoint: {url: String, options: Hash, secure: Boolean}, identityDomain: String, secure: Boolean, publicProfile: Hash, groups: Array<String>, devices: Array<{deviceName: String, deviceTag: String, keys: Array<{value: String, options: Hash}>}>, includes: Array<{ref: String, options: Hash, tag: String}>}`
136
+ # - `{error: {code, message}}` — the arguments were rejected (`invalid_arguments` for a missing `identifier` or an unrecognized option key — a bare identifier string or a config Hash in the options position included, `invalid_config` for an unrecognized config key, `invalid_lookup_code` for a malformed `lookupCode`, `invalid_identifier` for an invalid identifier) or the DNS resolution failed (`dns_unavailable`).
137
+ # - `{identifier, status: -1}` — the identity domain does not exist or is not configured for triauth (no `authenticationEndpoint` could be resolved).
138
+ # - `{identifier, status: 0, authenticationEndpoint, identityDomain}` — the domain is triauth-configured but no identity records exist for this identifier. Under `mode=private` this is also the answer when the call carries no `lookupCode` (then `identityDomain` is `nil`, as no identity domain could be derived) or a wrong one (a malformed `lookupCode` is instead rejected with the error `invalid_lookup_code`).
139
+ # - `{identifier, status: 1, authenticationEndpoint, identityDomain, secure, publicProfile, groups, devices, includes}` — the identifier was fully resolved. The `secure` flag is resolution-level: true only when DNSSEC was reported for the domain's `triauth` configuration record and for every well-formed record of the identity answer (DNSSEC validates answers, not individual records, so one flag describes the identity answer; `authenticationEndpoint.secure` isolates the endpoint record's own status). The `groups` array lists the identity's fully-qualified group-membership claims (sorted, deduplicated; e.g., `['admins@triauthdemo.org']`). Each entry in `devices` carries:
140
+ # - `deviceName` `{String}` — a user given name of the device,
141
+ # - `deviceTag` `{String}` — a unique tag identifying the device by its associated public keys,
142
+ # - `keys` `{Array<{value, options}>}` — the device's individual published keys.
143
+ #
144
+ # Each entry in `includes` (ordered by `tag`) carries:
145
+ # - `ref` `{String}` — the delegate the grant covers, as published (lowercased): either an identifier (`jane@example.com`) or an identity domain (`jane._at.example.com`),
146
+ # - `options` `{Hash}` — the grant's options: `use` lists the flows the grant covers (all five when unpublished) and `scope` the service hosts it works at (`any` = every host, or a comma-separated host list),
147
+ # - `tag` `{String}` — the grant digest that appears as the first segment of a delegated result's composite `deviceTag`.
148
+ def self.whois(options = nil, config = {})
149
+ config = Helpers.merge_config(config)
150
+ config[:logger] = (config[:logger].respond_to?(:spawn) ? config[:logger].spawn(config[:cid]) : nil) || config[:logger]
151
+
152
+ Helpers.log(config, :info, 'Processing Triauth.whois')
153
+ ts = Helpers.now
154
+
155
+ begin
156
+ # Ensure only recognized options are present to help avoid misspellings
157
+ if !Helpers.has_only_known_properties(options, %i[identifier lookupCode])
158
+ raise Triauth::Error.new({ code: 'invalid_arguments', message: 'Unrecognized function argument' })
159
+ end
160
+
161
+ # Reject config carrying unknown keys
162
+ if !Helpers.has_only_known_properties(config, Triauth::KNOWN_CONFIG_KEYS)
163
+ raise Triauth::Error.new({ code: 'invalid_config', message: 'Unrecognized configuration key' })
164
+ end
165
+
166
+ identifier = options[:identifier]
167
+ lookup_code = options[:lookupCode]
168
+
169
+ if !Helpers.is_string(identifier)
170
+ raise Triauth::Error.new({ code: 'invalid_arguments', message: 'Invalid or missing function arguments' })
171
+ end
172
+
173
+ # A malformed lookup code is a caller error, never repaired, and rejected before any DNS lookup
174
+ # (`lookupCode !== undefined` is "member absent": an explicit `lookupCode: nil` is validated and rejected with `invalid_lookup_code`, the JS `null` outcome)
175
+ if options.key?(:lookupCode)
176
+ validation_result = Validator.validate_lookup_code(lookup_code)
177
+ if !validation_result[:valid]
178
+ raise Triauth::Error.new(validation_result[:errors][0])
179
+ end
180
+ end
181
+
182
+ retval = { status: nil, identifier: identifier }
183
+
184
+ # (`Triauth::Identity`, qualified: the bare `Identity` is this module. `{lookupCode}` is passed even when absent:
185
+ # `{ lookupCode: nil }` reaches `IdentityDomain#set_options`, whose `is_lookup_code(nil)` is false exactly as `isLookupCode(undefined)` is)
186
+ identity = Triauth::Identity.new(identifier, { lookupCode: lookup_code }, config)
187
+
188
+ if !identity.authentication_endpoint.resolve
189
+ # `Object.assign(retval, {...})` mutates and returns `retval`: so does `Hash#merge!` (`Helpers.object_assign` would return a new Hash)
190
+ return Helpers.clone(retval.merge!({ status: -1 }))
191
+ end
192
+
193
+ retval.merge!(
194
+ {
195
+ authenticationEndpoint: {
196
+ url: identity.authentication_endpoint.url_for(nil),
197
+ options: identity.authentication_endpoint.options,
198
+ secure: identity.authentication_endpoint.secure == true
199
+ }
200
+ }
201
+ )
202
+
203
+ if !identity.resolve
204
+ return Helpers.clone(retval.merge!({ status: 0, identityDomain: identity.identity_domain.domain_name }))
205
+ end
206
+
207
+ devices = []
208
+ # `for...in` over the key-group object: `Helpers.ordered_entries` yields the JS object key order (`key_groups` is keyed by device-name Symbols)
209
+ Helpers.ordered_entries(identity.keys.key_groups).each do |device_name, key_group|
210
+ if key_group[:valid]
211
+ # only expose white-listed properties
212
+ keys = key_group[:keys].map { |key| { value: key[:value], options: key[:options] } }
213
+
214
+ devices.push({ deviceName: device_name.to_s, deviceTag: key_group[:tag], keys: keys }) # the device name as a String, as `for...in` yields it
215
+ end
216
+ end
217
+
218
+ # only expose white-listed properties of the include statements (grants) admitted under the domain's include policy
219
+ includes = identity.includes.list.map { |incl| { ref: incl[:ref], options: incl[:options], tag: incl[:tag] } }
220
+
221
+ return Helpers.clone(
222
+ retval.merge!(
223
+ {
224
+ status: 1,
225
+
226
+ identityDomain: identity.identity_domain.domain_name,
227
+ secure: identity.secure,
228
+
229
+ publicProfile: identity.public_profile,
230
+ groups: identity.groups,
231
+
232
+ devices: devices,
233
+ includes: includes
234
+ }
235
+ )
236
+ )
237
+ rescue StandardError => err
238
+ return Triauth::Error.process(err, config)
239
+ ensure
240
+ Helpers.log(config, :info, "Completed in #{(Helpers.now - ts).round}ms")
241
+ end
242
+ end
243
+ end
244
+ end
245
+ end