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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +56 -0
- data/LICENSE +202 -28
- data/README.md +2036 -7
- data/lib/triauth/api/attestation.rb +419 -0
- data/lib/triauth/api/authentication.rb +651 -0
- data/lib/triauth/api/identity.rb +245 -0
- data/lib/triauth/api/signing.rb +740 -0
- data/lib/triauth/api/validation.rb +162 -0
- data/lib/triauth/api.rb +33 -0
- data/lib/triauth/authentication_endpoint.rb +196 -0
- data/lib/triauth/challenge.rb +115 -0
- data/lib/triauth/challenge_response_flow.rb +344 -0
- data/lib/triauth/config.rb +93 -0
- data/lib/triauth/error.rb +95 -0
- data/lib/triauth/helpers.rb +1222 -0
- data/lib/triauth/identity.rb +301 -0
- data/lib/triauth/identity_domain.rb +212 -0
- data/lib/triauth/identity_includes.rb +249 -0
- data/lib/triauth/identity_keys.rb +309 -0
- data/lib/triauth/logger.rb +156 -0
- data/lib/triauth/multi_signature.rb +177 -0
- data/lib/triauth/protocol.rb +74 -0
- data/lib/triauth/request_cipher.rb +121 -0
- data/lib/triauth/resolvers/base.rb +147 -0
- data/lib/triauth/resolvers/caching_resolver.rb +202 -0
- data/lib/triauth/resolvers/cloudflare.rb +40 -0
- data/lib/triauth/resolvers/dns_json.rb +391 -0
- data/lib/triauth/resolvers/dns_sb.rb +40 -0
- data/lib/triauth/resolvers/google.rb +39 -0
- data/lib/triauth/resolvers/multi_resolver.rb +202 -0
- data/lib/triauth/resolvers/system_dns.rb +259 -0
- data/lib/triauth/resolvers.rb +33 -0
- data/lib/triauth/response.rb +142 -0
- data/lib/triauth/response_cipher.rb +341 -0
- data/lib/triauth/signature.rb +405 -0
- data/lib/triauth/validator.rb +549 -0
- data/lib/triauth/verifiers/base.rb +147 -0
- data/lib/triauth/verifiers/ecdsa.rb +87 -0
- data/lib/triauth/verifiers/ed25519.rb +89 -0
- data/lib/triauth/verifiers/registry.rb +52 -0
- data/lib/triauth/verifiers/web_authn.rb +253 -0
- data/lib/triauth/verifiers.rb +37 -0
- data/lib/triauth/version.rb +23 -0
- data/lib/triauth.rb +104 -7
- 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
|