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.
@@ -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