did_resolver 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/CHANGELOG.md +38 -0
- data/LICENSE +190 -0
- data/README.md +222 -0
- data/lib/did_resolver/cache.rb +73 -0
- data/lib/did_resolver/did_document.rb +186 -0
- data/lib/did_resolver/methods/jwk.rb +175 -0
- data/lib/did_resolver/methods/key.rb +386 -0
- data/lib/did_resolver/methods/web.rb +154 -0
- data/lib/did_resolver/methods.rb +12 -0
- data/lib/did_resolver/resolver.rb +134 -0
- data/lib/did_resolver/version.rb +5 -0
- data/lib/did_resolver.rb +201 -0
- metadata +176 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DidResolver
|
|
4
|
+
# DID Document representation
|
|
5
|
+
# @see https://www.w3.org/TR/did-core/#did-document-properties
|
|
6
|
+
class DIDDocument
|
|
7
|
+
attr_reader :id, :also_known_as, :controller, :verification_method,
|
|
8
|
+
:authentication, :assertion_method, :key_agreement,
|
|
9
|
+
:capability_invocation, :capability_delegation, :service,
|
|
10
|
+
:context, :extra
|
|
11
|
+
|
|
12
|
+
def initialize(
|
|
13
|
+
id:,
|
|
14
|
+
context: nil,
|
|
15
|
+
also_known_as: nil,
|
|
16
|
+
controller: nil,
|
|
17
|
+
verification_method: nil,
|
|
18
|
+
authentication: nil,
|
|
19
|
+
assertion_method: nil,
|
|
20
|
+
key_agreement: nil,
|
|
21
|
+
capability_invocation: nil,
|
|
22
|
+
capability_delegation: nil,
|
|
23
|
+
service: nil,
|
|
24
|
+
**extra
|
|
25
|
+
)
|
|
26
|
+
@id = id
|
|
27
|
+
@context = context || ["https://www.w3.org/ns/did/v1"]
|
|
28
|
+
@also_known_as = also_known_as
|
|
29
|
+
@controller = controller
|
|
30
|
+
@verification_method = verification_method || []
|
|
31
|
+
@authentication = authentication || []
|
|
32
|
+
@assertion_method = assertion_method || []
|
|
33
|
+
@key_agreement = key_agreement || []
|
|
34
|
+
@capability_invocation = capability_invocation || []
|
|
35
|
+
@capability_delegation = capability_delegation || []
|
|
36
|
+
@service = service || []
|
|
37
|
+
@extra = extra
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Find a verification method by ID or reference
|
|
41
|
+
# @param id_or_ref [String] Full ID or fragment reference
|
|
42
|
+
# @return [VerificationMethod, nil]
|
|
43
|
+
def find_verification_method(id_or_ref)
|
|
44
|
+
# Normalize reference - could be full ID or just fragment
|
|
45
|
+
target_id = id_or_ref.start_with?("#") ? "#{@id}#{id_or_ref}" : id_or_ref
|
|
46
|
+
|
|
47
|
+
verification_method.find { |vm| vm[:id] == target_id || vm["id"] == target_id }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Get verification methods for a specific purpose
|
|
51
|
+
# @param purpose [Symbol] :authentication, :assertion_method, etc.
|
|
52
|
+
# @return [Array<VerificationMethod>]
|
|
53
|
+
def verification_methods_for(purpose)
|
|
54
|
+
refs = send(purpose)
|
|
55
|
+
return [] unless refs
|
|
56
|
+
|
|
57
|
+
refs.map do |ref|
|
|
58
|
+
if ref.is_a?(String)
|
|
59
|
+
find_verification_method(ref)
|
|
60
|
+
else
|
|
61
|
+
ref
|
|
62
|
+
end
|
|
63
|
+
end.compact
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Extract public key from a verification method
|
|
67
|
+
# @param method_id [String] Verification method ID
|
|
68
|
+
# @return [Hash, nil] Public key info with :type and :key
|
|
69
|
+
def public_key_for(method_id)
|
|
70
|
+
vm = find_verification_method(method_id)
|
|
71
|
+
return nil unless vm
|
|
72
|
+
|
|
73
|
+
extract_public_key(vm)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Get the first public key for a purpose (e.g., assertion)
|
|
77
|
+
# @param purpose [Symbol]
|
|
78
|
+
# @return [Hash, nil]
|
|
79
|
+
def first_public_key_for(purpose)
|
|
80
|
+
vms = verification_methods_for(purpose)
|
|
81
|
+
return nil if vms.empty?
|
|
82
|
+
|
|
83
|
+
extract_public_key(vms.first)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def to_h
|
|
87
|
+
result = {
|
|
88
|
+
"@context" => @context,
|
|
89
|
+
"id" => @id
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
result["alsoKnownAs"] = @also_known_as if @also_known_as&.any?
|
|
93
|
+
result["controller"] = @controller if @controller
|
|
94
|
+
result["verificationMethod"] = @verification_method if @verification_method&.any?
|
|
95
|
+
result["authentication"] = @authentication if @authentication&.any?
|
|
96
|
+
result["assertionMethod"] = @assertion_method if @assertion_method&.any?
|
|
97
|
+
result["keyAgreement"] = @key_agreement if @key_agreement&.any?
|
|
98
|
+
result["capabilityInvocation"] = @capability_invocation if @capability_invocation&.any?
|
|
99
|
+
result["capabilityDelegation"] = @capability_delegation if @capability_delegation&.any?
|
|
100
|
+
result["service"] = @service if @service&.any?
|
|
101
|
+
|
|
102
|
+
# Merge any extra properties
|
|
103
|
+
result.merge(@extra.transform_keys(&:to_s))
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def to_json(*)
|
|
107
|
+
to_h.to_json
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
class << self
|
|
111
|
+
# Parse a DID Document from a hash
|
|
112
|
+
# @param data [Hash] The raw DID Document data
|
|
113
|
+
# @return [DIDDocument]
|
|
114
|
+
def from_hash(data)
|
|
115
|
+
data = data.transform_keys { |k| underscore(k.to_s).to_sym }
|
|
116
|
+
|
|
117
|
+
new(
|
|
118
|
+
id: data[:id],
|
|
119
|
+
context: data[:@context] || data[:context],
|
|
120
|
+
also_known_as: data[:also_known_as],
|
|
121
|
+
controller: data[:controller],
|
|
122
|
+
verification_method: normalize_verification_methods(data[:verification_method]),
|
|
123
|
+
authentication: data[:authentication],
|
|
124
|
+
assertion_method: data[:assertion_method],
|
|
125
|
+
key_agreement: data[:key_agreement],
|
|
126
|
+
capability_invocation: data[:capability_invocation],
|
|
127
|
+
capability_delegation: data[:capability_delegation],
|
|
128
|
+
service: data[:service],
|
|
129
|
+
**data.reject { |k, _|
|
|
130
|
+
%i[id @context context also_known_as controller
|
|
131
|
+
verification_method authentication assertion_method
|
|
132
|
+
key_agreement capability_invocation capability_delegation service].include?(k)
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
private
|
|
138
|
+
|
|
139
|
+
# Convert camelCase to snake_case
|
|
140
|
+
def underscore(str)
|
|
141
|
+
str.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
142
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
143
|
+
.downcase
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def normalize_verification_methods(methods)
|
|
147
|
+
return [] unless methods
|
|
148
|
+
|
|
149
|
+
methods.map do |vm|
|
|
150
|
+
vm.is_a?(Hash) ? vm.transform_keys(&:to_s) : vm
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
private
|
|
156
|
+
|
|
157
|
+
def extract_public_key(vm)
|
|
158
|
+
vm = vm.transform_keys(&:to_s) if vm.is_a?(Hash)
|
|
159
|
+
|
|
160
|
+
type = vm["type"]
|
|
161
|
+
key_data = nil
|
|
162
|
+
|
|
163
|
+
# Handle different key formats
|
|
164
|
+
if vm["publicKeyJwk"]
|
|
165
|
+
key_data = { format: :jwk, value: vm["publicKeyJwk"] }
|
|
166
|
+
elsif vm["publicKeyMultibase"]
|
|
167
|
+
key_data = { format: :multibase, value: vm["publicKeyMultibase"] }
|
|
168
|
+
elsif vm["publicKeyBase58"]
|
|
169
|
+
key_data = { format: :base58, value: vm["publicKeyBase58"] }
|
|
170
|
+
elsif vm["publicKeyHex"]
|
|
171
|
+
key_data = { format: :hex, value: vm["publicKeyHex"] }
|
|
172
|
+
elsif vm["publicKeyPem"]
|
|
173
|
+
key_data = { format: :pem, value: vm["publicKeyPem"] }
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
return nil unless key_data
|
|
177
|
+
|
|
178
|
+
{
|
|
179
|
+
id: vm["id"],
|
|
180
|
+
type: type,
|
|
181
|
+
controller: vm["controller"],
|
|
182
|
+
**key_data
|
|
183
|
+
}
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module DidResolver
|
|
7
|
+
module Methods
|
|
8
|
+
# DID JWK Method Resolver
|
|
9
|
+
#
|
|
10
|
+
# Resolves did:jwk DIDs according to the DID JWK Method Specification
|
|
11
|
+
# @see https://github.com/quartzjer/did-jwk/blob/main/spec.md
|
|
12
|
+
#
|
|
13
|
+
# did:jwk encodes a JWK directly in the DID identifier using base64url encoding.
|
|
14
|
+
# The DID Document is deterministically generated from the JWK.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# resolver = DidResolver::Resolver.new(DidResolver::Methods::Jwk.resolver)
|
|
18
|
+
# result = resolver.resolve("did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6Ii4uLiIsInkiOiIuLi4ifQ")
|
|
19
|
+
#
|
|
20
|
+
class Jwk
|
|
21
|
+
# JWK key type to verification method type mapping
|
|
22
|
+
KEY_TYPE_TO_VM_TYPE = {
|
|
23
|
+
"EC" => "JsonWebKey2020",
|
|
24
|
+
"OKP" => "JsonWebKey2020",
|
|
25
|
+
"RSA" => "JsonWebKey2020"
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
# Curves that support key agreement (ECDH)
|
|
29
|
+
KEY_AGREEMENT_CURVES = %w[X25519 X448 P-256 P-384 P-521].freeze
|
|
30
|
+
|
|
31
|
+
# Curves that support signing
|
|
32
|
+
SIGNING_CURVES = %w[Ed25519 Ed448 P-256 P-384 P-521 secp256k1].freeze
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
# Get the resolver hash for registration
|
|
36
|
+
# @return [Hash] { "jwk" => resolve_proc }
|
|
37
|
+
def resolver
|
|
38
|
+
{ "jwk" => method(:resolve) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Resolve a did:jwk DID
|
|
42
|
+
# @param did [String] The full DID string
|
|
43
|
+
# @param parsed [ParsedDID] Parsed DID components
|
|
44
|
+
# @param _resolver [Resolver] Parent resolver
|
|
45
|
+
# @param _options [Hash] Resolution options
|
|
46
|
+
# @return [ResolutionResult]
|
|
47
|
+
def resolve(did, parsed, _resolver, _options = {})
|
|
48
|
+
# Decode the JWK from the method-specific identifier
|
|
49
|
+
jwk = decode_jwk(parsed.id)
|
|
50
|
+
return jwk if jwk.is_a?(ResolutionResult) # Error case
|
|
51
|
+
|
|
52
|
+
# Validate the JWK
|
|
53
|
+
validation = validate_jwk(jwk)
|
|
54
|
+
return validation if validation
|
|
55
|
+
|
|
56
|
+
# Build the DID Document
|
|
57
|
+
did_document = build_did_document(did, jwk)
|
|
58
|
+
|
|
59
|
+
ResolutionResult.success(did_document)
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
ResolutionResult.error("invalidDid", "Failed to resolve did:jwk: #{e.message}")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
# Decode the base64url-encoded JWK
|
|
67
|
+
# @param encoded [String] Base64url encoded JWK
|
|
68
|
+
# @return [Hash, ResolutionResult]
|
|
69
|
+
def decode_jwk(encoded)
|
|
70
|
+
# Add padding if needed
|
|
71
|
+
padding = (4 - encoded.length % 4) % 4
|
|
72
|
+
padded = encoded + ("=" * padding)
|
|
73
|
+
|
|
74
|
+
decoded = Base64.urlsafe_decode64(padded)
|
|
75
|
+
JSON.parse(decoded)
|
|
76
|
+
rescue ArgumentError => e
|
|
77
|
+
ResolutionResult.invalid_did("did:jwk:#{encoded}", "Invalid base64url encoding: #{e.message}")
|
|
78
|
+
rescue JSON::ParserError => e
|
|
79
|
+
ResolutionResult.invalid_did("did:jwk:#{encoded}", "Invalid JSON in JWK: #{e.message}")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Validate the JWK structure
|
|
83
|
+
# @param jwk [Hash] The JWK
|
|
84
|
+
# @return [ResolutionResult, nil] Error result or nil if valid
|
|
85
|
+
def validate_jwk(jwk)
|
|
86
|
+
unless jwk["kty"]
|
|
87
|
+
return ResolutionResult.error("invalidDid", "JWK missing required 'kty' parameter")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
unless %w[EC OKP RSA].include?(jwk["kty"])
|
|
91
|
+
return ResolutionResult.error(
|
|
92
|
+
"invalidDid",
|
|
93
|
+
"Unsupported JWK key type: #{jwk["kty"]}"
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# EC keys require curve and coordinates
|
|
98
|
+
if jwk["kty"] == "EC"
|
|
99
|
+
unless jwk["crv"] && jwk["x"]
|
|
100
|
+
return ResolutionResult.error("invalidDid", "EC JWK missing required 'crv' or 'x' parameter")
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# OKP keys require curve and x
|
|
105
|
+
if jwk["kty"] == "OKP"
|
|
106
|
+
unless jwk["crv"] && jwk["x"]
|
|
107
|
+
return ResolutionResult.error("invalidDid", "OKP JWK missing required 'crv' or 'x' parameter")
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# RSA keys require n and e
|
|
112
|
+
if jwk["kty"] == "RSA"
|
|
113
|
+
unless jwk["n"] && jwk["e"]
|
|
114
|
+
return ResolutionResult.error("invalidDid", "RSA JWK missing required 'n' or 'e' parameter")
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Build DID Document for the JWK
|
|
122
|
+
def build_did_document(did, jwk)
|
|
123
|
+
# Create a public-only JWK (strip private key parameters)
|
|
124
|
+
public_jwk = jwk.reject { |k, _| %w[d p q dp dq qi].include?(k) }
|
|
125
|
+
|
|
126
|
+
vm_id = "#{did}#0"
|
|
127
|
+
|
|
128
|
+
# Build verification method
|
|
129
|
+
verification_method = {
|
|
130
|
+
"id" => vm_id,
|
|
131
|
+
"type" => KEY_TYPE_TO_VM_TYPE[jwk["kty"]],
|
|
132
|
+
"controller" => did,
|
|
133
|
+
"publicKeyJwk" => public_jwk
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# Determine verification relationships based on key type and curve
|
|
137
|
+
authentication = []
|
|
138
|
+
assertion_method = []
|
|
139
|
+
key_agreement = []
|
|
140
|
+
capability_invocation = []
|
|
141
|
+
capability_delegation = []
|
|
142
|
+
|
|
143
|
+
curve = jwk["crv"]
|
|
144
|
+
|
|
145
|
+
# Key agreement capability
|
|
146
|
+
if KEY_AGREEMENT_CURVES.include?(curve)
|
|
147
|
+
key_agreement << vm_id
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Signing capability
|
|
151
|
+
if SIGNING_CURVES.include?(curve) || jwk["kty"] == "RSA"
|
|
152
|
+
authentication << vm_id
|
|
153
|
+
assertion_method << vm_id
|
|
154
|
+
capability_invocation << vm_id
|
|
155
|
+
capability_delegation << vm_id
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
DIDDocument.new(
|
|
159
|
+
id: did,
|
|
160
|
+
context: [
|
|
161
|
+
"https://www.w3.org/ns/did/v1",
|
|
162
|
+
"https://w3id.org/security/suites/jws-2020/v1"
|
|
163
|
+
],
|
|
164
|
+
verification_method: [verification_method],
|
|
165
|
+
authentication: authentication.any? ? authentication : nil,
|
|
166
|
+
assertion_method: assertion_method.any? ? assertion_method : nil,
|
|
167
|
+
key_agreement: key_agreement.any? ? key_agreement : nil,
|
|
168
|
+
capability_invocation: capability_invocation.any? ? capability_invocation : nil,
|
|
169
|
+
capability_delegation: capability_delegation.any? ? capability_delegation : nil
|
|
170
|
+
)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|