human-attestation 0.3.7 → 0.4.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d32e8f6d80fbc741b843c0d367ceec4467cd192f26792afbdeb3a1d89bec88ee
4
- data.tar.gz: 5026790abb465412835388fa128314474e25fc0534eec42c7e3885bd88920b17
3
+ metadata.gz: d6db10c461f3561ad7514a96c9da975eae5cdcb71928b31cb8019091a0fc7b63
4
+ data.tar.gz: 7e4c6628871c67bff9adfa774680a3cd7838e3c3053603393787d572af216837
5
5
  SHA512:
6
- metadata.gz: 75d9aad061ab6cf02a7eeb5d0249641777fcb0ece9ec863dd11aef219993a1c2d71f61415045826d17d0ca99e91db1b4a08415229771488395056d189e76dd37
7
- data.tar.gz: e4190595e0c0907152ebbea6e5b12b8d0af4537fa883fc826456a7e272636411e92b698b716be4e2bff8bfc161dd34ae0c6e185b858c6c79482018fbffb05ccf
6
+ metadata.gz: c595e143adff3cc2fe84e8154e6eabc1deaa46e3e294b1381f8d0b62f2d98c58ea95955666482deca38a58d5433ce977c30e3a7b6c8d25b8a87693c9977f7bfe
7
+ data.tar.gz: 76d0ef14f114aa9a5e945ad62e76313169ded28aff69d105afbeb36ae34331fbd2baac83f58ee3fb7911c5ef13aeaee7769824a6d90c28f0a0b6c1c482ebf9d4
data/README.md CHANGED
@@ -23,20 +23,20 @@ gem install human-attestation
23
23
  ### Verifying a Claim (For Recipients)
24
24
 
25
25
  ```ruby
26
- require 'hap'
26
+ require 'human_attestation'
27
27
 
28
28
  # Verify a claim from a HAP ID
29
- claim = Hap.verify_hap_claim("hap_abc123xyz456", "ballista.jobs")
29
+ claim = HumanAttestation.verify_claim("hap_abc123xyz456", "ballista.jobs")
30
30
 
31
31
  if claim
32
32
  # Check if not expired
33
- if Hap.claim_expired?(claim)
33
+ if HumanAttestation.claim_expired?(claim)
34
34
  puts "Claim has expired"
35
35
  return
36
36
  end
37
37
 
38
38
  # Verify it's for your organization
39
- unless Hap.claim_for_recipient?(claim, "yourcompany.com")
39
+ unless HumanAttestation.claim_for_recipient?(claim, "yourcompany.com")
40
40
  puts "Claim is for a different recipient"
41
41
  return
42
42
  end
@@ -50,10 +50,10 @@ end
50
50
  ```ruby
51
51
  # Extract HAP ID from a verification URL
52
52
  url = "https://www.ballista.jobs/v/hap_abc123xyz456"
53
- hap_id = Hap.extract_hap_id_from_url(url)
53
+ hap_id = HumanAttestation.extract_id_from_url(url)
54
54
 
55
55
  if hap_id
56
- claim = Hap.verify_hap_claim(hap_id, "ballista.jobs")
56
+ claim = HumanAttestation.verify_claim(hap_id, "ballista.jobs")
57
57
  # ... handle claim
58
58
  end
59
59
  ```
@@ -62,11 +62,11 @@ end
62
62
 
63
63
  ```ruby
64
64
  # Fetch the claim
65
- response = Hap.fetch_claim("hap_abc123xyz456", "ballista.jobs")
65
+ response = HumanAttestation.fetch_claim("hap_abc123xyz456", "ballista.jobs")
66
66
 
67
67
  if response[:valid] && response[:jws]
68
68
  # Verify the cryptographic signature
69
- result = Hap.verify_signature(response[:jws], "ballista.jobs")
69
+ result = HumanAttestation.verify_signature(response[:jws], "ballista.jobs")
70
70
 
71
71
  if result[:valid]
72
72
  puts "Signature verified! Claim: #{result[:claim]}"
@@ -79,89 +79,58 @@ end
79
79
  ### Signing Claims (For Verification Authorities)
80
80
 
81
81
  ```ruby
82
- require 'hap'
82
+ require 'human_attestation'
83
83
  require 'json'
84
84
 
85
85
  # Generate a key pair (do this once, store securely)
86
- private_key, public_key = Hap.generate_key_pair
86
+ private_key, public_key = HumanAttestation.generate_key_pair
87
87
 
88
88
  # Export public key for /.well-known/hap.json
89
- jwk = Hap.export_public_key_jwk(public_key, "my_key_001")
89
+ jwk = HumanAttestation.export_public_key_jwk(public_key, "my_key_001")
90
90
  well_known = { issuer: "my-va.com", keys: [jwk] }
91
91
  puts JSON.pretty_generate(well_known)
92
92
 
93
93
  # Create and sign a claim
94
- claim = Hap.create_human_effort_claim(
94
+ claim = HumanAttestation.create_claim(
95
95
  method: "physical_mail",
96
+ description: "Priority mail packet with handwritten cover letter",
96
97
  recipient_name: "Acme Corp",
97
98
  domain: "acme.com",
98
- tier: "standard",
99
99
  issuer: "my-va.com",
100
- expires_in_days: 730 # 2 years
100
+ expires_in_days: 730, # 2 years
101
+ cost: { amount: 1500, currency: "USD" },
102
+ time: 1800,
103
+ physical: true
101
104
  )
102
105
 
103
- jws = Hap.sign_claim(claim, private_key, kid: "my_key_001")
106
+ jws = HumanAttestation.sign_claim(claim, private_key, kid: "my_key_001")
104
107
  puts "Signed JWS: #{jws}"
105
108
  ```
106
109
 
107
- ### Creating Recipient Commitment Claims
108
-
109
- ```ruby
110
- claim = Hap.create_recipient_commitment_claim(
111
- recipient_name: "Acme Corp",
112
- recipient_domain: "acme.com",
113
- commitment: "review_verified",
114
- issuer: "my-va.com",
115
- expires_in_days: 365
116
- )
117
-
118
- jws = Hap.sign_claim(claim, private_key, kid: "my_key_001")
119
- ```
120
-
121
110
  ## API Reference
122
111
 
123
112
  ### Verification Functions
124
113
 
125
- | Method | Description |
126
- | --------------------------------------- | ----------------------------------------------- |
127
- | `Hap.verify_hap_claim(hap_id, issuer)` | Fetch and verify a claim, returns claim or nil |
128
- | `Hap.fetch_claim(hap_id, issuer)` | Fetch raw verification response from VA |
129
- | `Hap.verify_signature(jws, issuer)` | Verify JWS signature against VA's public keys |
130
- | `Hap.fetch_public_keys(issuer)` | Fetch VA's public keys from well-known endpoint |
131
- | `Hap.valid_hap_id?(id)` | Check if string matches HAP ID format |
132
- | `Hap.extract_hap_id_from_url(url)` | Extract HAP ID from verification URL |
133
- | `Hap.claim_expired?(claim)` | Check if claim has passed expiration |
134
- | `Hap.claim_for_recipient?(claim, domain)` | Check if claim targets specific recipient |
114
+ | Method | Description |
115
+ | ------------------------------------------------- | ----------------------------------------------- |
116
+ | `HumanAttestation.verify_claim(hap_id, issuer)` | Fetch and verify a claim, returns claim or nil |
117
+ | `HumanAttestation.fetch_claim(hap_id, issuer)` | Fetch raw verification response from VA |
118
+ | `HumanAttestation.verify_signature(jws, issuer)` | Verify JWS signature against VA's public keys |
119
+ | `HumanAttestation.fetch_public_keys(issuer)` | Fetch VA's public keys from well-known endpoint |
120
+ | `HumanAttestation.valid_id?(id)` | Check if string matches HAP ID format |
121
+ | `HumanAttestation.extract_id_from_url(url)` | Extract HAP ID from verification URL |
122
+ | `HumanAttestation.claim_expired?(claim)` | Check if claim has passed expiration |
123
+ | `HumanAttestation.claim_for_recipient?(claim, domain)` | Check if claim targets specific recipient |
135
124
 
136
125
  ### Signing Functions (For VAs)
137
126
 
138
- | Method | Description |
139
- | ------------------------------------------- | ---------------------------------------- |
140
- | `Hap.generate_key_pair` | Generate Ed25519 key pair |
141
- | `Hap.export_public_key_jwk(key, kid)` | Export public key as JWK |
142
- | `Hap.sign_claim(claim, private_key, kid:)` | Sign a claim, returns JWS |
143
- | `Hap.generate_hap_id` | Generate cryptographically secure HAP ID |
144
- | `Hap.create_human_effort_claim(...)` | Create human_effort claim with defaults |
145
- | `Hap.create_recipient_commitment_claim(...)` | Create recipient_commitment claim |
146
-
147
- ### Constants
148
-
149
- ```ruby
150
- # Claim types
151
- Hap::CLAIM_TYPE_HUMAN_EFFORT # "human_effort"
152
- Hap::CLAIM_TYPE_RECIPIENT_COMMITMENT # "recipient_commitment"
153
-
154
- # Verification methods
155
- Hap::METHOD_PHYSICAL_MAIL # "physical_mail"
156
- Hap::METHOD_VIDEO_INTERVIEW # "video_interview"
157
- Hap::METHOD_PAID_ASSESSMENT # "paid_assessment"
158
- Hap::METHOD_REFERRAL # "referral"
159
-
160
- # Commitment levels
161
- Hap::COMMITMENT_REVIEW_VERIFIED # "review_verified"
162
- Hap::COMMITMENT_PRIORITIZE_VERIFIED # "prioritize_verified"
163
- Hap::COMMITMENT_RESPOND_VERIFIED # "respond_verified"
164
- ```
127
+ | Method | Description |
128
+ | ----------------------------------------------------- | ---------------------------------------- |
129
+ | `HumanAttestation.generate_key_pair` | Generate Ed25519 key pair |
130
+ | `HumanAttestation.export_public_key_jwk(key, kid)` | Export public key as JWK |
131
+ | `HumanAttestation.sign_claim(claim, private_key, kid:)` | Sign a claim, returns JWS |
132
+ | `HumanAttestation.generate_id` | Generate cryptographically secure HAP ID |
133
+ | `HumanAttestation.create_claim(...)` | Create claim with defaults |
165
134
 
166
135
  ## Requirements
167
136
 
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ed25519"
4
+ require "base64"
5
+ require "cgi"
6
+ require "time"
7
+
8
+ module HumanAttestation
9
+ # HAP Compact Format utilities for space-efficient serialization
10
+ #
11
+ # Format: HAP{version}.{id}.{method}.{to_name}.{to_domain}.{at}.{exp}.{iss}.{signature}
12
+ #
13
+ # Note: Effort dimensions (cost, time, physical, energy) are NOT included in compact format.
14
+ # Compact is for QR codes - minimal representation. Full claims in JWS include all dimensions.
15
+ #
16
+ # @example
17
+ # HAP1.hap_abc123xyz456.ba_priority_mail.Acme%20Corp.acme%2Ecom.1706169600.1769241600.ballista%2Ejobs.MEUCIQDx...
18
+ module Compact
19
+ module_function
20
+
21
+ # Encodes a field for compact format (URL-encode + encode dots)
22
+ #
23
+ # @param value [String] The value to encode
24
+ # @return [String] Encoded value
25
+ def encode_compact_field(value)
26
+ CGI.escape(value.to_s).gsub(".", "%2E")
27
+ end
28
+
29
+ # Decodes a compact format field
30
+ #
31
+ # @param value [String] The encoded value
32
+ # @return [String] Decoded value
33
+ def decode_compact_field(value)
34
+ CGI.unescape(value)
35
+ end
36
+
37
+ # Base64url encode without padding
38
+ #
39
+ # @param data [String] Binary data to encode
40
+ # @return [String] Base64url encoded string
41
+ def base64url_encode(data)
42
+ Base64.urlsafe_encode64(data, padding: false)
43
+ end
44
+
45
+ # Base64url decode
46
+ #
47
+ # @param data [String] Base64url encoded string
48
+ # @return [String] Decoded binary data
49
+ def base64url_decode(data)
50
+ Base64.urlsafe_decode64(data)
51
+ end
52
+
53
+ # Convert ISO 8601 timestamp to Unix epoch seconds
54
+ #
55
+ # @param iso [String] ISO 8601 timestamp
56
+ # @return [Integer] Unix timestamp
57
+ def iso_to_unix(iso)
58
+ Time.parse(iso).to_i
59
+ end
60
+
61
+ # Convert Unix epoch seconds to ISO 8601 timestamp
62
+ #
63
+ # @param unix [Integer] Unix timestamp
64
+ # @return [String] ISO 8601 timestamp
65
+ def unix_to_iso(unix)
66
+ Time.at(unix).utc.iso8601
67
+ end
68
+
69
+ # Encodes a HAP claim and signature into compact format (9 fields)
70
+ #
71
+ # @param claim [Hash] The claim to encode
72
+ # @param signature [String] The Ed25519 signature bytes (64 bytes)
73
+ # @return [String] Compact format string
74
+ def encode_compact(claim, signature)
75
+ to = claim[:to] || {}
76
+ name = to[:name] || ""
77
+ domain = to[:domain] || ""
78
+
79
+ at_unix = iso_to_unix(claim[:at])
80
+ exp_unix = claim[:exp] ? iso_to_unix(claim[:exp]) : 0
81
+
82
+ [
83
+ "HAP#{COMPACT_VERSION}",
84
+ claim[:id],
85
+ claim[:method] || "",
86
+ encode_compact_field(name),
87
+ encode_compact_field(domain),
88
+ at_unix.to_s,
89
+ exp_unix.to_s,
90
+ encode_compact_field(claim[:iss]),
91
+ base64url_encode(signature)
92
+ ].join(".")
93
+ end
94
+
95
+ # Decodes a compact format string into claim and signature
96
+ #
97
+ # @param compact [String] The compact format string
98
+ # @return [Hash] { claim: Hash, signature: String }
99
+ # @raise [ArgumentError] If format is invalid
100
+ def decode_compact(compact)
101
+ raise ArgumentError, "Invalid HAP Compact format" unless valid_compact?(compact)
102
+
103
+ parts = compact.split(".")
104
+ raise ArgumentError, "Invalid HAP Compact format: expected 9 fields" unless parts.length == 9
105
+
106
+ version, hap_id, method, encoded_name, encoded_domain, at_unix_str, exp_unix_str, encoded_iss, sig_b64 = parts
107
+
108
+ raise ArgumentError, "Unsupported compact version: #{version}" unless version == "HAP#{COMPACT_VERSION}"
109
+
110
+ name = decode_compact_field(encoded_name)
111
+ domain = decode_compact_field(encoded_domain)
112
+ iss = decode_compact_field(encoded_iss)
113
+ at_unix = at_unix_str.to_i
114
+ exp_unix = exp_unix_str.to_i
115
+ signature = base64url_decode(sig_b64)
116
+
117
+ at = unix_to_iso(at_unix)
118
+ exp = exp_unix != 0 ? unix_to_iso(exp_unix) : nil
119
+
120
+ claim = {
121
+ v: VERSION_PROTOCOL,
122
+ id: hap_id,
123
+ method: method,
124
+ description: "", # Not included in compact format
125
+ to: { name: name },
126
+ at: at,
127
+ iss: iss
128
+ }
129
+ claim[:to][:domain] = domain unless domain.empty?
130
+ claim[:exp] = exp if exp
131
+
132
+ { claim: claim, signature: signature }
133
+ end
134
+
135
+ # Validates if a string is a valid HAP Compact format
136
+ #
137
+ # @param compact [String] The string to validate
138
+ # @return [Boolean] true if valid compact format
139
+ def valid_compact?(compact)
140
+ COMPACT_REGEX.match?(compact.to_s)
141
+ end
142
+
143
+ # Builds the compact payload (everything before the signature)
144
+ # This is what gets signed.
145
+ #
146
+ # @param claim [Hash] The claim
147
+ # @return [String] Compact payload string (8 fields)
148
+ def build_compact_payload(claim)
149
+ to = claim[:to] || {}
150
+ name = to[:name] || ""
151
+ domain = to[:domain] || ""
152
+
153
+ at_unix = iso_to_unix(claim[:at])
154
+ exp_unix = claim[:exp] ? iso_to_unix(claim[:exp]) : 0
155
+
156
+ [
157
+ "HAP#{COMPACT_VERSION}",
158
+ claim[:id],
159
+ claim[:method] || "",
160
+ encode_compact_field(name),
161
+ encode_compact_field(domain),
162
+ at_unix.to_s,
163
+ exp_unix.to_s,
164
+ encode_compact_field(claim[:iss])
165
+ ].join(".")
166
+ end
167
+
168
+ # Signs a claim and returns it in compact format
169
+ #
170
+ # @param claim [Hash] The claim to sign
171
+ # @param private_key [Ed25519::SigningKey] The Ed25519 private key
172
+ # @return [String] Signed compact format string
173
+ def sign_compact(claim, private_key)
174
+ payload = build_compact_payload(claim)
175
+ signature = private_key.sign(payload)
176
+ "#{payload}.#{base64url_encode(signature)}"
177
+ end
178
+
179
+ # Verifies a compact format string using provided public keys
180
+ #
181
+ # @param compact [String] The compact format string
182
+ # @param public_keys [Array<Hash>] Array of JWK public keys to try
183
+ # @return [Hash] { valid: Boolean, claim: Hash or nil, error: String or nil }
184
+ def verify_compact(compact, public_keys)
185
+ return { valid: false, error: "Invalid compact format" } unless valid_compact?(compact)
186
+
187
+ # Split to get payload and signature
188
+ last_dot = compact.rindex(".")
189
+ payload = compact[0...last_dot]
190
+ sig_b64 = compact[(last_dot + 1)..]
191
+ signature = base64url_decode(sig_b64)
192
+
193
+ # Try each public key
194
+ public_keys.each do |jwk|
195
+ begin
196
+ x_bytes = base64url_decode(jwk[:x] || jwk["x"])
197
+ verify_key = Ed25519::VerifyKey.new(x_bytes)
198
+
199
+ # Verify signature
200
+ verify_key.verify(signature, payload)
201
+
202
+ # Signature is valid
203
+ decoded = decode_compact(compact)
204
+ return { valid: true, claim: decoded[:claim] }
205
+ rescue Ed25519::VerifyError, StandardError
206
+ # Try next key
207
+ next
208
+ end
209
+ end
210
+
211
+ { valid: false, error: "Signature verification failed" }
212
+ end
213
+
214
+ # Generates a verification URL with embedded compact claim
215
+ #
216
+ # @param base_url [String] Base verification URL (e.g., "https://ballista.jobs/v")
217
+ # @param compact [String] The compact format string
218
+ # @return [String] URL with compact claim in query parameter
219
+ def generate_verification_url(base_url, compact)
220
+ "#{base_url}?c=#{CGI.escape(compact)}"
221
+ end
222
+
223
+ # Extracts compact claim from a verification URL
224
+ #
225
+ # @param url [String] The verification URL
226
+ # @return [String, nil] Compact string or nil if not found
227
+ def extract_compact_from_url(url)
228
+ uri = URI.parse(url)
229
+ params = URI.decode_www_form(uri.query || "").to_h
230
+ compact = params["c"]
231
+ return compact if compact && valid_compact?(compact)
232
+
233
+ nil
234
+ rescue StandardError
235
+ nil
236
+ end
237
+ end
238
+ end
@@ -5,21 +5,47 @@ require "json"
5
5
  require "base64"
6
6
  require "securerandom"
7
7
  require "time"
8
+ require "digest"
8
9
 
9
- module Hap
10
+ module HumanAttestation
10
11
  # Signing functions for HAP claims (for Verification Authorities)
11
12
  module Sign
12
13
  # Characters used for HAP ID generation
13
- HAP_ID_CHARS = ("A".."Z").to_a + ("a".."z").to_a + ("0".."9").to_a
14
+ ID_CHARS = ("A".."Z").to_a + ("a".."z").to_a + ("0".."9").to_a
14
15
 
15
16
  # Generates a cryptographically secure random HAP ID
16
17
  #
17
18
  # @return [String] A HAP ID in the format hap_[a-zA-Z0-9]{12}
18
- def generate_hap_id
19
- suffix = Array.new(12) { HAP_ID_CHARS[SecureRandom.random_number(HAP_ID_CHARS.length)] }.join
19
+ def generate_id
20
+ suffix = Array.new(12) { ID_CHARS[SecureRandom.random_number(ID_CHARS.length)] }.join
20
21
  "hap_#{suffix}"
21
22
  end
22
23
 
24
+ # Generates a test HAP ID (for previews and development)
25
+ #
26
+ # @return [String] A test HAP ID in the format hap_test_[a-zA-Z0-9]{8}
27
+ def generate_test_id
28
+ suffix = Array.new(8) { ID_CHARS[SecureRandom.random_number(ID_CHARS.length)] }.join
29
+ "hap_test_#{suffix}"
30
+ end
31
+
32
+ # Checks if a HAP ID is a test ID
33
+ #
34
+ # @param id [String] The HAP ID to check
35
+ # @return [Boolean] true if the ID is a test ID
36
+ def test_id?(id)
37
+ TEST_ID_REGEX.match?(id)
38
+ end
39
+
40
+ # Computes SHA-256 hash of content with prefix
41
+ #
42
+ # @param content [String] The content to hash
43
+ # @return [String] Hash string in format "sha256:xxxxx"
44
+ def hash_content(content)
45
+ hash = Digest::SHA256.hexdigest(content)
46
+ "sha256:#{hash}"
47
+ end
48
+
23
49
  # Generates a new Ed25519 key pair for signing HAP claims
24
50
  #
25
51
  # @return [Array<Ed25519::SigningKey, Ed25519::VerifyKey>] Private and public keys
@@ -71,23 +97,28 @@ module Hap
71
97
  "#{signing_input}.#{signature_b64}"
72
98
  end
73
99
 
74
- # Creates a complete human effort claim with all required fields
100
+ # Creates a complete HAP claim with all required fields
75
101
  #
76
- # @param method [String] Verification method (e.g., "physical_mail")
102
+ # @param method [String] VA-specific verification method identifier
103
+ # @param description [String] Human-readable description of the effort
77
104
  # @param recipient_name [String] Recipient name
78
105
  # @param issuer [String] VA's domain
79
106
  # @param domain [String, nil] Recipient domain (optional)
80
107
  # @param tier [String, nil] Service tier (optional)
81
108
  # @param expires_in_days [Integer, nil] Days until expiration (optional)
82
- # @return [Hash] A complete human effort claim
83
- def create_human_effort_claim(method:, recipient_name:, issuer:, domain: nil, tier: nil, expires_in_days: nil)
109
+ # @param cost [Hash, nil] Monetary cost { amount: Integer, currency: String } (optional)
110
+ # @param time [Integer, nil] Time in seconds (optional)
111
+ # @param physical [Boolean, nil] Whether physical atoms involved (optional)
112
+ # @param energy [Integer, nil] Energy in kilocalories (optional)
113
+ # @return [Hash] A complete HAP claim
114
+ def create_claim(method:, description:, recipient_name:, issuer:, domain: nil, tier: nil, expires_in_days: nil, cost: nil, time: nil, physical: nil, energy: nil)
84
115
  now = Time.now.utc
85
116
 
86
117
  claim = {
87
118
  v: VERSION_PROTOCOL,
88
- id: generate_hap_id,
89
- type: CLAIM_TYPE_HUMAN_EFFORT,
119
+ id: generate_id,
90
120
  method: method,
121
+ description: description,
91
122
  to: { name: recipient_name },
92
123
  at: now.iso8601,
93
124
  iss: issuer
@@ -101,36 +132,11 @@ module Hap
101
132
  claim[:exp] = exp.iso8601
102
133
  end
103
134
 
104
- claim
105
- end
106
-
107
- # Creates a complete recipient commitment claim with all required fields
108
- #
109
- # @param recipient_name [String] Recipient's name
110
- # @param commitment [String] Commitment level (e.g., "review_verified")
111
- # @param issuer [String] VA's domain
112
- # @param recipient_domain [String, nil] Recipient's domain (optional)
113
- # @param expires_in_days [Integer, nil] Days until expiration (optional)
114
- # @return [Hash] A complete recipient commitment claim
115
- def create_recipient_commitment_claim(recipient_name:, commitment:, issuer:, recipient_domain: nil, expires_in_days: nil)
116
- now = Time.now.utc
117
-
118
- claim = {
119
- v: VERSION_PROTOCOL,
120
- id: generate_hap_id,
121
- type: CLAIM_TYPE_RECIPIENT_COMMITMENT,
122
- recipient: { name: recipient_name },
123
- commitment: commitment,
124
- at: now.iso8601,
125
- iss: issuer
126
- }
127
-
128
- claim[:recipient][:domain] = recipient_domain if recipient_domain
129
-
130
- if expires_in_days
131
- exp = now + (expires_in_days * 24 * 60 * 60)
132
- claim[:exp] = exp.iso8601
133
- end
135
+ # Add effort dimensions if provided
136
+ claim[:cost] = cost if cost
137
+ claim[:time] = time if time
138
+ claim[:physical] = physical unless physical.nil?
139
+ claim[:energy] = energy if energy
134
140
 
135
141
  claim
136
142
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HumanAttestation
4
+ # HAP Compact format version
5
+ COMPACT_VERSION = "1"
6
+
7
+ # Test HAP ID format regex
8
+ TEST_ID_REGEX = /\Ahap_test_[a-zA-Z0-9]{8}\z/
9
+
10
+ # HAP Compact format regex (9 fields, no type)
11
+ COMPACT_REGEX = /\AHAP1\.hap_[a-zA-Z0-9_]+\.[^.]+\.[^.]+\.[^.]*\.\d+\.\d+\.[^.]+\.[A-Za-z0-9_-]+\z/
12
+
13
+ # Revocation reasons
14
+ REVOCATION_FRAUD = "fraud"
15
+ REVOCATION_ERROR = "error"
16
+ REVOCATION_LEGAL = "legal"
17
+ REVOCATION_USER_REQUEST = "user_request"
18
+ end
@@ -8,7 +8,7 @@ require "base64"
8
8
  require "uri"
9
9
  require "time"
10
10
 
11
- module Hap
11
+ module HumanAttestation
12
12
  # Verification functions for HAP claims
13
13
  module Verify
14
14
  # Default timeout for HTTP requests (seconds)
@@ -18,8 +18,8 @@ module Hap
18
18
  #
19
19
  # @param id [String] The HAP ID to validate
20
20
  # @return [Boolean] true if the ID matches the format hap_[a-zA-Z0-9]{12}
21
- def valid_hap_id?(id)
22
- HAP_ID_REGEX.match?(id.to_s)
21
+ def valid_id?(id)
22
+ ID_REGEX.match?(id.to_s)
23
23
  end
24
24
 
25
25
  # Fetches the public keys from a VA's well-known endpoint
@@ -47,7 +47,7 @@ module Hap
47
47
  # @param timeout [Integer] Request timeout in seconds
48
48
  # @return [Hash] The verification response from the VA
49
49
  def fetch_claim(hap_id, issuer_domain, timeout: DEFAULT_TIMEOUT)
50
- return { valid: false, error: "invalid_format" } unless valid_hap_id?(hap_id)
50
+ return { valid: false, error: "invalid_format" } unless valid_id?(hap_id)
51
51
 
52
52
  url = "https://#{issuer_domain}/api/v1/verify/#{hap_id}"
53
53
 
@@ -115,7 +115,7 @@ module Hap
115
115
  # @param verify_sig [Boolean] Whether to verify the cryptographic signature
116
116
  # @param timeout [Integer] Request timeout in seconds
117
117
  # @return [Hash, nil] The claim if valid, nil if not found or invalid
118
- def verify_hap_claim(hap_id, issuer_domain, verify_sig: true, timeout: DEFAULT_TIMEOUT)
118
+ def verify_claim(hap_id, issuer_domain, verify_sig: true, timeout: DEFAULT_TIMEOUT)
119
119
  response = fetch_claim(hap_id, issuer_domain, timeout: timeout)
120
120
 
121
121
  return nil unless response[:valid]
@@ -132,12 +132,12 @@ module Hap
132
132
  #
133
133
  # @param url [String] The verification URL
134
134
  # @return [String, nil] The HAP ID or nil if not found
135
- def extract_hap_id_from_url(url)
135
+ def extract_id_from_url(url)
136
136
  uri = URI.parse(url)
137
137
  parts = uri.path.split("/")
138
138
  last_part = parts.last
139
139
 
140
- valid_hap_id?(last_part) ? last_part : nil
140
+ valid_id?(last_part) ? last_part : nil
141
141
  rescue URI::InvalidURIError
142
142
  nil
143
143
  end
@@ -161,14 +161,7 @@ module Hap
161
161
  # @param recipient_domain [String] The expected recipient domain
162
162
  # @return [Boolean] true if the claim's target domain matches
163
163
  def claim_for_recipient?(claim, recipient_domain)
164
- case claim[:type]
165
- when CLAIM_TYPE_HUMAN_EFFORT
166
- claim.dig(:to, :domain) == recipient_domain
167
- when CLAIM_TYPE_RECIPIENT_COMMITMENT
168
- claim.dig(:recipient, :domain) == recipient_domain
169
- else
170
- false
171
- end
164
+ claim.dig(:to, :domain) == recipient_domain
172
165
  end
173
166
  end
174
167
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HumanAttestation
4
+ VERSION = "0.4.1"
5
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "human_attestation/version"
4
+ require_relative "human_attestation/types"
5
+ require_relative "human_attestation/verify"
6
+ require_relative "human_attestation/sign"
7
+ require_relative "human_attestation/compact"
8
+
9
+ # HAP (Human Attestation Protocol) SDK for Ruby
10
+ #
11
+ # HAP is an open standard for verified human effort. It enables Verification
12
+ # Authorities (VAs) to cryptographically attest that a sender took deliberate,
13
+ # costly action when communicating with a recipient.
14
+ #
15
+ # @example Verifying a claim (for recipients)
16
+ # claim = HumanAttestation.verify_claim("hap_abc123xyz456", "ballista.jobs")
17
+ # if claim && !HumanAttestation.claim_expired?(claim)
18
+ # puts "Verified application to #{claim[:to][:name]}"
19
+ # end
20
+ #
21
+ # @example Signing a claim (for VAs)
22
+ # private_key, public_key = HumanAttestation.generate_key_pair
23
+ # claim = HumanAttestation.create_claim(
24
+ # method: "ba_priority_mail",
25
+ # description: "Priority mail packet with handwritten cover letter",
26
+ # recipient_name: "Acme Corp",
27
+ # domain: "acme.com",
28
+ # issuer: "my-va.com",
29
+ # cost: { amount: 1500, currency: "USD" },
30
+ # time: 1800,
31
+ # physical: true
32
+ # )
33
+ # jws = HumanAttestation.sign_claim(claim, private_key, kid: "key_001")
34
+ #
35
+ module HumanAttestation
36
+ class Error < StandardError; end
37
+ class VerificationError < Error; end
38
+ class SigningError < Error; end
39
+
40
+ # Protocol version
41
+ VERSION_PROTOCOL = "0.1"
42
+
43
+ # HAP ID format regex
44
+ ID_REGEX = /\Ahap_[a-zA-Z0-9]{12}\z/
45
+
46
+ # Extend self to allow calling methods directly on the module
47
+ extend Verify
48
+ extend Sign
49
+ extend Compact
50
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: human-attestation
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.7
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - BlueScroll Inc.
@@ -90,11 +90,12 @@ extra_rdoc_files: []
90
90
  files:
91
91
  - LICENSE
92
92
  - README.md
93
- - lib/hap.rb
94
- - lib/hap/sign.rb
95
- - lib/hap/types.rb
96
- - lib/hap/verify.rb
97
- - lib/hap/version.rb
93
+ - lib/human_attestation.rb
94
+ - lib/human_attestation/compact.rb
95
+ - lib/human_attestation/sign.rb
96
+ - lib/human_attestation/types.rb
97
+ - lib/human_attestation/verify.rb
98
+ - lib/human_attestation/version.rb
98
99
  homepage: https://github.com/Blue-Scroll/hap
99
100
  licenses:
100
101
  - Apache-2.0
data/lib/hap/types.rb DELETED
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Hap
4
- # Claim types
5
- CLAIM_TYPE_HUMAN_EFFORT = "human_effort"
6
- CLAIM_TYPE_RECIPIENT_COMMITMENT = "recipient_commitment"
7
-
8
- # Core verification methods
9
- METHOD_PHYSICAL_MAIL = "physical_mail"
10
- METHOD_VIDEO_INTERVIEW = "video_interview"
11
- METHOD_PAID_ASSESSMENT = "paid_assessment"
12
- METHOD_REFERRAL = "referral"
13
-
14
- # Commitment levels
15
- COMMITMENT_REVIEW_VERIFIED = "review_verified"
16
- COMMITMENT_PRIORITIZE_VERIFIED = "prioritize_verified"
17
- COMMITMENT_RESPOND_VERIFIED = "respond_verified"
18
-
19
- # Revocation reasons
20
- REVOCATION_FRAUD = "fraud"
21
- REVOCATION_ERROR = "error"
22
- REVOCATION_LEGAL = "legal"
23
- REVOCATION_USER_REQUEST = "user_request"
24
- end
data/lib/hap/version.rb DELETED
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Hap
4
- VERSION = "0.3.7"
5
- end
data/lib/hap.rb DELETED
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "hap/version"
4
- require_relative "hap/types"
5
- require_relative "hap/verify"
6
- require_relative "hap/sign"
7
-
8
- # HAP (Human Attestation Protocol) SDK for Ruby
9
- #
10
- # HAP is an open standard for verified human effort. It enables Verification
11
- # Authorities (VAs) to cryptographically attest that a sender took deliberate,
12
- # costly action when communicating with a recipient.
13
- #
14
- # @example Verifying a claim (for recipients)
15
- # claim = Hap.verify_hap_claim("hap_abc123xyz456", "ballista.jobs")
16
- # if claim && !Hap.claim_expired?(claim)
17
- # puts "Verified application to #{claim[:to][:name]}"
18
- # end
19
- #
20
- # @example Signing a claim (for VAs)
21
- # private_key, public_key = Hap.generate_key_pair
22
- # claim = Hap.create_human_effort_claim(
23
- # method: "physical_mail",
24
- # recipient_name: "Acme Corp",
25
- # domain: "acme.com",
26
- # issuer: "my-va.com"
27
- # )
28
- # jws = Hap.sign_claim(claim, private_key, kid: "key_001")
29
- #
30
- module Hap
31
- class Error < StandardError; end
32
- class VerificationError < Error; end
33
- class SigningError < Error; end
34
-
35
- # Protocol version
36
- VERSION_PROTOCOL = "0.1"
37
-
38
- # HAP ID format regex
39
- HAP_ID_REGEX = /\Ahap_[a-zA-Z0-9]{12}\z/
40
-
41
- # Extend self to allow calling methods directly on the module
42
- extend Verify
43
- extend Sign
44
- end