app_identity 1.0.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/.rdoc_options +28 -0
- data/Changelog.md +5 -0
- data/Contributing.md +70 -0
- data/Licence.md +25 -0
- data/Manifest.txt +30 -0
- data/README.md +70 -0
- data/Rakefile +50 -0
- data/bin/app-identity-suite-ruby +12 -0
- data/lib/app_identity/app.rb +214 -0
- data/lib/app_identity/error.rb +42 -0
- data/lib/app_identity/faraday_middleware.rb +94 -0
- data/lib/app_identity/internal.rb +130 -0
- data/lib/app_identity/rack_middleware.rb +242 -0
- data/lib/app_identity/validation.rb +83 -0
- data/lib/app_identity/versions.rb +194 -0
- data/lib/app_identity.rb +233 -0
- data/licences/APACHE-2.0.txt +168 -0
- data/licences/DCO.txt +34 -0
- data/spec.md +409 -0
- data/support/app_identity/suite/generator.rb +242 -0
- data/support/app_identity/suite/optional.json +491 -0
- data/support/app_identity/suite/program.rb +204 -0
- data/support/app_identity/suite/required.json +514 -0
- data/support/app_identity/suite/runner.rb +132 -0
- data/support/app_identity/suite.rb +10 -0
- data/support/app_identity/support.rb +119 -0
- data/test/minitest_helper.rb +24 -0
- data/test/test_app_identity.rb +124 -0
- data/test/test_app_identity_app.rb +64 -0
- data/test/test_app_identity_rack_middleware.rb +90 -0
- metadata +306 -0
data/spec.md
ADDED
@@ -0,0 +1,409 @@
|
|
1
|
+
# App Identity Specification
|
2
|
+
|
3
|
+
App Identity provides a fast, lightweight, cryptographically secure app
|
4
|
+
authentication mechanism as an improvement over just using API keys or app IDs.
|
5
|
+
It does this by computing a proof with an application identifier, a nonce, an
|
6
|
+
application secret key, and a hashing algorithm. The secret key is embedded in
|
7
|
+
client applications and stored securely on the server, so it is never passed
|
8
|
+
over the wire.
|
9
|
+
|
10
|
+
## Terminology Notes
|
11
|
+
|
12
|
+
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD",
|
13
|
+
"SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be
|
14
|
+
interpreted as described in [RFC2199][rfc2119].
|
15
|
+
|
16
|
+
Only the text of this specification is _normative_. Code examples in this
|
17
|
+
document are _informative_ and may have bugs.
|
18
|
+
|
19
|
+
## Version and Versioning
|
20
|
+
|
21
|
+
This specification is versioned with a modified [Semantic Versioning][] scheme.
|
22
|
+
The major version of the specification will always be the highest algorithms
|
23
|
+
version defined. Minor versions may adjust the text of the specification.
|
24
|
+
|
25
|
+
As the current specification defines four algorithm versions, the current
|
26
|
+
specification is `4.0`.
|
27
|
+
|
28
|
+
## Application
|
29
|
+
|
30
|
+
For the purposes of this specification, an `application` requires the following
|
31
|
+
attributes:
|
32
|
+
|
33
|
+
- `id`: The unique identifier of the application. It is recommended that this
|
34
|
+
value is a `UUID`. When using an integer identifier, it is recommended that
|
35
|
+
this value be extended, such as that provided by Rails [global ID][global id].
|
36
|
+
Such representations are _also_ recommended if the ID is a compound value.
|
37
|
+
Non-string identifiers must be converted to string values.
|
38
|
+
|
39
|
+
- `secret`: The random value used as the secret key. This value _must_ be used
|
40
|
+
as presented (if presented as a base-64 value, the secret _is_ the base-64
|
41
|
+
value, not a decoded version of it).
|
42
|
+
|
43
|
+
- `version`: The minimum algorithm version supported by the application, an
|
44
|
+
integer value. The reference implementations of App Identity do not currently
|
45
|
+
restrict the `version` for compatibility purposes, but new applications
|
46
|
+
**should not** use version 1 applications.
|
47
|
+
|
48
|
+
- `config`: A configuration object. As of this writing, only one key for this
|
49
|
+
object is defined when `version` 2 or higher. The `config` only affects proof
|
50
|
+
verification.
|
51
|
+
|
52
|
+
- `fuzz`: The fuzziness of time stamp comparison, in seconds, for version `2`
|
53
|
+
or higher algorithms. If not present, defaults to `600` seconds, or ±600
|
54
|
+
seconds (±10 minutes). Depending on the nature of the app being verified and
|
55
|
+
the expected network conditions, a shorter time period than 600 seconds is
|
56
|
+
recommended.
|
57
|
+
|
58
|
+
### Suggested Extra Fields
|
59
|
+
|
60
|
+
The following fields are recommended for use on by proof verifiers.
|
61
|
+
|
62
|
+
- `code`: A unique text identifier for the application. Not used in the proof
|
63
|
+
algorithm, but used to manage the application on the servers.
|
64
|
+
|
65
|
+
- `name`: A displayable name for the application. Not used in the proof
|
66
|
+
algorithm, but used for human consumption.
|
67
|
+
|
68
|
+
- `type`: The type of the application. Because the server deals with many types
|
69
|
+
of applications, this value may be used to _limit_ access to APIs to only type
|
70
|
+
of applications.
|
71
|
+
|
72
|
+
## Algorithm Versions
|
73
|
+
|
74
|
+
The algorithm version controls both the shape of the nonce and the specific
|
75
|
+
algorithm chosen for hashing. Versions are _strictly upgradeable_. That is,
|
76
|
+
a version 1 app can verify version 1, 2, 3, or 4 proofs. However, a version
|
77
|
+
2 app will _never_ validate a version 1 proof.
|
78
|
+
|
79
|
+
<table>
|
80
|
+
<thead>
|
81
|
+
<tr>
|
82
|
+
<th rowspan=2>Version</th>
|
83
|
+
<th rowspan=2>Nonce</th>
|
84
|
+
<th rowspan=2>Digest Algorithm</th>
|
85
|
+
<th colspan=4>Can Verify</th>
|
86
|
+
</tr>
|
87
|
+
<tr><th>1</th><th>2</th><th>3</th><th>4</th></tr>
|
88
|
+
</thead>
|
89
|
+
<tbody>
|
90
|
+
<tr><th>1</th><td>random</td><td>SHA 256</td><td>✅</td><td>✅</td><td>✅</td><td>✅</td></tr>
|
91
|
+
<tr><th>2</th><td>timestamp ± fuzz</td><td>SHA 256</td><td>⛔️</td><td>✅</td><td>✅</td><td>✅</td></tr>
|
92
|
+
<tr><th>3</th><td>timestamp ± fuzz</td><td>SHA 384</td><td>⛔️</td><td>⛔️</td><td>✅</td><td>✅</td></tr>
|
93
|
+
<tr><th>4</th><td>timestamp ± fuzz</td><td>SHA 512</td><td>⛔️</td><td>⛔️</td><td>⛔️</td><td>✅</td></tr>
|
94
|
+
</tbody>
|
95
|
+
</table>
|
96
|
+
|
97
|
+
## Identity Proof
|
98
|
+
|
99
|
+
The client identity proof is a short signed value, composed from the _id_,
|
100
|
+
a _nonce_, and an intermediary _padlock_ generated using the _application
|
101
|
+
secret_. The application id and secret will be provided securely for
|
102
|
+
compile-time inclusion; all care should be taken to ensure that the secret is
|
103
|
+
not easily extractable from the application or shared in the clear.
|
104
|
+
|
105
|
+
The generation of a proof looks like this:
|
106
|
+
|
107
|
+
padlock = Padlock(Version, Identity, Nonce, Secret)
|
108
|
+
proof = Proof(Version, Identity, Nonce, padlock)
|
109
|
+
|
110
|
+
The verification of a proof looks like this:
|
111
|
+
|
112
|
+
(Version, Identity, Nonce, padlock) = Parse(proof)
|
113
|
+
app = FindApplication(Identity)
|
114
|
+
padlockʹ = Padlock(Version, app.Identity, Nonce, app.Secret)
|
115
|
+
proofʹ = Proof(Version, Identity, Nonce, padlockʹ)
|
116
|
+
|
117
|
+
ConstantTimeEqualityComparison(proof, proofʹ)
|
118
|
+
|
119
|
+
### Nonce
|
120
|
+
|
121
|
+
Depending on the version of the application algorithm, the _nonce_ may contain
|
122
|
+
any byte sequences _except_ ASCII colon (`:`), but it is recommended that the
|
123
|
+
value be UTF-8 safe.
|
124
|
+
|
125
|
+
#### Random Nonces
|
126
|
+
|
127
|
+
Version 1 nonces should be cryptographically secure and non-sequential, but
|
128
|
+
sufficiently fine-grained timestamps (those including microseconds, as
|
129
|
+
`yyyymmddHHMMSS.sss`) _may_ be used. Version 1 proofs verify that the nonce is
|
130
|
+
at least one byte long and do not contain a colon (`:`).
|
131
|
+
|
132
|
+
**Ruby**:
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
require 'securerandom'
|
136
|
+
nonce1 = SecureRandom.urlsafe_base64(32)
|
137
|
+
nonce2 = SecureRandom.hex(32)
|
138
|
+
```
|
139
|
+
|
140
|
+
**Elixir**:
|
141
|
+
|
142
|
+
```elixir
|
143
|
+
# Elixir
|
144
|
+
Base.url_encode64(:crypto.strong_rand_bytes(32), padding: true)
|
145
|
+
Base.encode16(:crypto.strong_rand_bytes(32))
|
146
|
+
```
|
147
|
+
|
148
|
+
**Typescript (Node)**:
|
149
|
+
|
150
|
+
```typescript
|
151
|
+
import { randomBytes } from 'crypto'
|
152
|
+
import base64url from 'base64-url' // https://www.npmjs.com/package/base64-url
|
153
|
+
|
154
|
+
base64url.encode(randomBytes(32).toString())
|
155
|
+
```
|
156
|
+
|
157
|
+
**Swift**:
|
158
|
+
|
159
|
+
```swift
|
160
|
+
func secure_random_base64_bytes(count: Int32 = 16) -> String? {
|
161
|
+
var data = Data(count: count)
|
162
|
+
let result = data.withUnsafeMutableBytes {
|
163
|
+
SecRandomCopyBytes(kSecRandomDefaults, data.count, $0)
|
164
|
+
}
|
165
|
+
if result == errSecSuccess {
|
166
|
+
return data.base64EncodedString()
|
167
|
+
} else {
|
168
|
+
return nil
|
169
|
+
}
|
170
|
+
}
|
171
|
+
|
172
|
+
secure_random_base64_bytes(32)
|
173
|
+
```
|
174
|
+
|
175
|
+
#### Timestamp Nonces
|
176
|
+
|
177
|
+
Version 2, 3, and 4 nonces **must** be a UTC timestamp formatted using ISO
|
178
|
+
8601 basic formatting. The timestamp _should_ be generated on a clock synced with
|
179
|
+
NTP and _should_ be verified using a clock synced with NTP.
|
180
|
+
|
181
|
+
For the purposes of this document, ISO 8601 basic formatting uses the
|
182
|
+
following [ABNF][] format, adapted from [RFC3339][]:
|
183
|
+
|
184
|
+
```abnf
|
185
|
+
date-fullyear = 4DIGIT
|
186
|
+
date-month = 2DIGIT ; 01-12
|
187
|
+
date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on
|
188
|
+
; month/year
|
189
|
+
time-hour = 2DIGIT ; 00-23
|
190
|
+
time-minute = 2DIGIT ; 00-59
|
191
|
+
time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second
|
192
|
+
; rules
|
193
|
+
time-secfrac = "." 1\*DIGIT
|
194
|
+
time-offset = "Z"
|
195
|
+
|
196
|
+
partial-time = time-hour time-minute time-second [time-secfrac]
|
197
|
+
full-date = date-fullyear date-month date-mday
|
198
|
+
full-time = partial-time time-offset
|
199
|
+
|
200
|
+
date-time = full-date "T" full-time
|
201
|
+
```
|
202
|
+
|
203
|
+
The timestamp must be an ASCII 7-bit or UTF-8 string using only ASCII
|
204
|
+
characters, and the special characters `T` and `Z` **must** be specified
|
205
|
+
uppercase.
|
206
|
+
|
207
|
+
This format differs from [RFC3339][] [§5.6][§5.6] timestamp format in the
|
208
|
+
following ways:
|
209
|
+
|
210
|
+
1. It **must** be UTC and the timezone character must be `Z`. No other timezone
|
211
|
+
specifier is permitted, and it must not be omitted.
|
212
|
+
2. It **must** only have the characters `[.0-9TZ]`. It **may** have the point
|
213
|
+
character (`.`) only preceding the _optional_ fractional seconds digits.
|
214
|
+
|
215
|
+
Therefore, a timestamp of `2020-02-25T23:20:03.321423-04:00` must be presented
|
216
|
+
as `20200225T192003.321423Z`.
|
217
|
+
|
218
|
+
C-style `strftime` formatting for this format would be `'%Y%m%dT%H%M%S.%6NZ'`,
|
219
|
+
and a PostgreSQL format for `TO_CHAR()` would `'YYYYMMDD"T"HH24MISS.FF6Z'`.
|
220
|
+
|
221
|
+
**Ruby**:
|
222
|
+
|
223
|
+
```ruby
|
224
|
+
require 'time'
|
225
|
+
Time.now.utc.strftime('%Y%m%dT%H%M%S.%6NZ')
|
226
|
+
```
|
227
|
+
|
228
|
+
**Elixir**:
|
229
|
+
|
230
|
+
```elixir
|
231
|
+
case DateTime.now("Etc/UTC") do
|
232
|
+
{:ok, stamp} ->
|
233
|
+
{:ok, DateTime.to_iso8601(stamp, :basic)}
|
234
|
+
|
235
|
+
{:error, reason} ->
|
236
|
+
{:error, String.replace(Kernel.to_string(reason), "_", " ")}
|
237
|
+
end
|
238
|
+
```
|
239
|
+
|
240
|
+
**Typescript (Node)**:
|
241
|
+
|
242
|
+
```typescript
|
243
|
+
new Date().toISOString().replace(/[-:]/g, '')
|
244
|
+
```
|
245
|
+
|
246
|
+
**Swift**:
|
247
|
+
|
248
|
+
```swift
|
249
|
+
func secure_random_base64_bytes(count: Int32 = 16) -> String? {
|
250
|
+
var data = Data(count: count)
|
251
|
+
let result = data.withUnsafeMutableBytes {
|
252
|
+
SecRandomCopyBytes(kSecRandomDefaults, data.count, $0)
|
253
|
+
}
|
254
|
+
if result == errSecSuccess {
|
255
|
+
return data.base64EncodedString()
|
256
|
+
} else {
|
257
|
+
return nil
|
258
|
+
}
|
259
|
+
}
|
260
|
+
|
261
|
+
secure_random_base64_bytes(32)
|
262
|
+
```
|
263
|
+
|
264
|
+
### Padlock Calculation
|
265
|
+
|
266
|
+
To compute the padlock value, concatenate the application id, the nonce, and the
|
267
|
+
application secret using colons, then calculate the digest of the value and
|
268
|
+
convert to a base 16 string representation. As noted previously, the digest
|
269
|
+
algorithm used varies based on the application version.
|
270
|
+
|
271
|
+
**Ruby**:
|
272
|
+
|
273
|
+
```ruby
|
274
|
+
require 'digest/sha2'
|
275
|
+
Digest::SHA256.hexdigest([version, id, nonce, secret].join(':')).upcase
|
276
|
+
```
|
277
|
+
|
278
|
+
**Elixir**:
|
279
|
+
|
280
|
+
```elixir
|
281
|
+
[id, nonce, secret]
|
282
|
+
|> Enum.join(":")
|
283
|
+
|> then(&:crypto.hash(:sha256, &1))
|
284
|
+
|> Base.encode16(case: :upper)
|
285
|
+
```
|
286
|
+
|
287
|
+
**Typescript (Node)**:
|
288
|
+
|
289
|
+
```typescript
|
290
|
+
import { createHash } from 'crypto'
|
291
|
+
const hash = createHash('sha384')
|
292
|
+
hash.update(raw, 'utf-8')
|
293
|
+
hash.digest('hex').toUpperCase()
|
294
|
+
```
|
295
|
+
|
296
|
+
**Swift**:
|
297
|
+
|
298
|
+
```swift
|
299
|
+
// Swift
|
300
|
+
extension Data {
|
301
|
+
func hexString() -> String {
|
302
|
+
return self.map {
|
303
|
+
Int($0).hexString()
|
304
|
+
}.joined()
|
305
|
+
}
|
306
|
+
|
307
|
+
func SHA256() -> Data {
|
308
|
+
var result = Data(count: Int(CC_SHA256_DIGEST_LENGTH))
|
309
|
+
_ = result.withUnsafeMutableBytes {
|
310
|
+
resultPtr in self.withUnsafeBytes {
|
311
|
+
CC_SHA256($0, CC_LONG(count), resultPtr)
|
312
|
+
}
|
313
|
+
}
|
314
|
+
return result
|
315
|
+
}
|
316
|
+
}
|
317
|
+
|
318
|
+
extension String {
|
319
|
+
func hexString() -> String {
|
320
|
+
return self.data(using: .utf8)!.hexString()
|
321
|
+
}
|
322
|
+
|
323
|
+
func SHA256() -> String {
|
324
|
+
return self.data(using: .utf8)!.SHA256().hexString()
|
325
|
+
}
|
326
|
+
}
|
327
|
+
|
328
|
+
let value = "\(id):\(nonce):\(secret)"
|
329
|
+
let padlock = value.SHA256()
|
330
|
+
```
|
331
|
+
|
332
|
+
**Java**:
|
333
|
+
|
334
|
+
```java
|
335
|
+
// Java (Android)
|
336
|
+
String value = id + ":" + nonce + ":" + secret;
|
337
|
+
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
338
|
+
byte[] hash = digest.digest(msg.getBytes(StandardCharsets.UTF_8));
|
339
|
+
StringBuffer padlock = new StringBuffer();
|
340
|
+
|
341
|
+
for (int i = 0; i < hash.length; i++) {
|
342
|
+
String hex = Integer.toHexString(0xFF & hash[i]);
|
343
|
+
if (hex.length() == 1) {
|
344
|
+
padlock.append('0');
|
345
|
+
}
|
346
|
+
|
347
|
+
padlock.append(hex);
|
348
|
+
}
|
349
|
+
```
|
350
|
+
|
351
|
+
Validation of the padlock will convert this digest to uppercase, so the values
|
352
|
+
`c0ffee` and `C0FFEE` are identical. It is recommended that padlocks be passed
|
353
|
+
as uppercase hex values.
|
354
|
+
|
355
|
+
### Padlock Presentation
|
356
|
+
|
357
|
+
The padlock cannot be presented by itself, because the digest used are one-way
|
358
|
+
cryptographic hashes. Therefore, the client must supply the id (used to find the
|
359
|
+
client definition on the server) and the nonce (because the nonce is generated
|
360
|
+
by the client application and not known by the server). This will typically be
|
361
|
+
provided as a concatenated, base-64-encoded string:
|
362
|
+
|
363
|
+
**Ruby**:
|
364
|
+
|
365
|
+
```ruby
|
366
|
+
require 'base64'
|
367
|
+
# Version 1
|
368
|
+
Base64.urlsafe_encode64([id, nonce, padlock].join(':'))
|
369
|
+
# Version 2+
|
370
|
+
Base64.urlsafe_encode64([version, id, nonce, padlock].join(':'))
|
371
|
+
```
|
372
|
+
|
373
|
+
**Elixir**:
|
374
|
+
|
375
|
+
```elixir
|
376
|
+
[version, id, nonce, padlock]
|
377
|
+
|> Enum.join(":")
|
378
|
+
|> Base.urlsafe_encode64(padding: false)
|
379
|
+
```
|
380
|
+
|
381
|
+
**Swift**:
|
382
|
+
|
383
|
+
```swift
|
384
|
+
let value = "\(version):\(id):\(nonce):\(padlock)"
|
385
|
+
let proof = Data(value.utf8).base64EncodedString()
|
386
|
+
```
|
387
|
+
|
388
|
+
**Typescript**:
|
389
|
+
|
390
|
+
```typescript
|
391
|
+
import base64url from 'base64-url' // https://www.npmjs.com/package/base64-url
|
392
|
+
const parts = version === 1 ? [id, nonce, padlock] : [version, id, nonce, padlock]
|
393
|
+
base64url.encode(parts.join(':'))
|
394
|
+
```
|
395
|
+
|
396
|
+
**Java**:
|
397
|
+
|
398
|
+
```java
|
399
|
+
String value = version + ":" + id + ":" + nonce + ":" + padlock.toString();
|
400
|
+
byte[] encodedHash = Base64.getEncoder().encode(encodeValue.getBytes());
|
401
|
+
String proof = new String(encodedHash, "UTF-8");
|
402
|
+
```
|
403
|
+
|
404
|
+
[global id]: https://github.com/rails/globalid
|
405
|
+
[rfc2119]: https://datatracker.ietf.org/doc/html/rfc2119
|
406
|
+
[rfc3339]: https://datatracker.ietf.org/doc/html/rfc3339
|
407
|
+
[§5.6]: https://tools.ietf.org/html/rfc3339#section-5.6
|
408
|
+
[semantic versioning]: http://semver.org/
|
409
|
+
[abnf]: https://www.rfc-editor.org/rfc/rfc2234.txt
|
@@ -0,0 +1,242 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "app_identity/support"
|
5
|
+
|
6
|
+
class AppIdentity::Suite::Generator # :nodoc:
|
7
|
+
include AppIdentity::Validation
|
8
|
+
include AppIdentity::Support
|
9
|
+
|
10
|
+
class << self
|
11
|
+
private :new
|
12
|
+
|
13
|
+
def run(name, options)
|
14
|
+
new(name, options).run
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(name, options)
|
19
|
+
@name = name.first || options[:default_suite]
|
20
|
+
@options = options
|
21
|
+
|
22
|
+
if !options[:stdout] && !@name.end_with?(".json")
|
23
|
+
@name =
|
24
|
+
if File.directory?(@name)
|
25
|
+
File.join(@name, options[:default_suite])
|
26
|
+
else
|
27
|
+
"#{@name}.json"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def run
|
33
|
+
suite = generate_suite
|
34
|
+
|
35
|
+
if !options[:stdout] && !options[:quiet]
|
36
|
+
puts "Generated #{suite[:tests].length} tests for #{suite[:name]} #{suite[:version]}."
|
37
|
+
end
|
38
|
+
|
39
|
+
if options[:stdout]
|
40
|
+
puts JSON.pretty_generate(suite)
|
41
|
+
else
|
42
|
+
File.write(name, JSON.generate(suite))
|
43
|
+
puts "Saved as #{name}" if !options[:quiet]
|
44
|
+
end
|
45
|
+
|
46
|
+
true
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def generate_suite
|
52
|
+
{
|
53
|
+
name: AppIdentity::NAME,
|
54
|
+
version: AppIdentity::VERSION,
|
55
|
+
spec_version: AppIdentity::SPEC_VERSION,
|
56
|
+
tests: [
|
57
|
+
*generate_tests("required", load_json(required_tests)),
|
58
|
+
*generate_tests("optional", load_json(optional_tests))
|
59
|
+
]
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
def load_json(name)
|
64
|
+
JSON.load_file!(name)
|
65
|
+
end
|
66
|
+
|
67
|
+
def required_tests
|
68
|
+
File.expand_path("../required.json", __FILE__)
|
69
|
+
end
|
70
|
+
|
71
|
+
def optional_tests
|
72
|
+
File.expand_path("../optional.json", __FILE__)
|
73
|
+
end
|
74
|
+
|
75
|
+
def generate_tests(type, input)
|
76
|
+
input.map.with_index { |entry, index| generate_test(type, entry, index) }
|
77
|
+
end
|
78
|
+
|
79
|
+
def generate_test(type, input, index)
|
80
|
+
normalized = normalize_test(type, input, index)
|
81
|
+
|
82
|
+
app =
|
83
|
+
if normalized[:app]
|
84
|
+
make_app(normalized.dig(:app, :version), normalized.dig(:app, :config, :fuzz))
|
85
|
+
else
|
86
|
+
make_app(1)
|
87
|
+
end
|
88
|
+
|
89
|
+
{
|
90
|
+
description: normalized.fetch(:description),
|
91
|
+
expect: normalized.fetch(:expect),
|
92
|
+
app: app.as_json,
|
93
|
+
proof: make_proof(type, normalized, index, app),
|
94
|
+
required: type == "required",
|
95
|
+
spec_version: normalized.fetch(:spec_version)
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
def normalize_test(type, input, index)
|
100
|
+
must_have!(type, input, index, "description")
|
101
|
+
must_have!(type, input, index, "expect")
|
102
|
+
must_have!(type, input, index, "proof")
|
103
|
+
must_have!(type, input, index, "spec_version")
|
104
|
+
|
105
|
+
if !%w[pass fail].include?(input["expect"])
|
106
|
+
fail!(type, input, index, "Invalid expect value '#{input["expect"]}'")
|
107
|
+
end
|
108
|
+
|
109
|
+
test = {
|
110
|
+
description: input["description"],
|
111
|
+
expect: input["expect"],
|
112
|
+
spec_version: input["spec_version"]
|
113
|
+
}
|
114
|
+
|
115
|
+
if input.key?("app")
|
116
|
+
tmp = input["app"]
|
117
|
+
|
118
|
+
must_have!(type, tmp, index, "version", input: input, name: "app.version")
|
119
|
+
|
120
|
+
app = {version: tmp["version"]}
|
121
|
+
|
122
|
+
if tmp.key?("config")
|
123
|
+
must_have!(type, tmp["config"], index, "fuzz", input: input, name: "app.config.fuzz")
|
124
|
+
|
125
|
+
app[:config] = {fuzz: tmp.dig("config", "fuzz")}
|
126
|
+
end
|
127
|
+
|
128
|
+
test[:app] = app
|
129
|
+
end
|
130
|
+
|
131
|
+
if input.key?("nonce")
|
132
|
+
nonce = {}
|
133
|
+
tmp = input["nonce"]
|
134
|
+
|
135
|
+
if tmp.key?("empty")
|
136
|
+
if tmp.key?("offset_minutes") || tmp.key?("value")
|
137
|
+
fail!(type, input, index, "nonce must only have one sub-key")
|
138
|
+
end
|
139
|
+
|
140
|
+
nonce[:empty] = !!tmp["empty"]
|
141
|
+
elsif tmp.key?("offset_minutes")
|
142
|
+
if tmp.key?("value")
|
143
|
+
fail!(type, input, index, "nonce must only have one sub-key")
|
144
|
+
end
|
145
|
+
|
146
|
+
nonce[:offset_minutes] = tmp["offset_minutes"]
|
147
|
+
elsif tmp.key?("value")
|
148
|
+
nonce[:value] = tmp["value"]
|
149
|
+
else
|
150
|
+
fail!(type, input, index, "nonce requires exactly one sub-key")
|
151
|
+
end
|
152
|
+
|
153
|
+
test[:nonce] = nonce
|
154
|
+
end
|
155
|
+
|
156
|
+
if input.key?("padlock")
|
157
|
+
tmp = input["padlock"]
|
158
|
+
must_have!(type, tmp, index, "nonce", input: input, name: "padlock.nonce")
|
159
|
+
test[:padlock] = {nonce: tmp["nonce"]}
|
160
|
+
end
|
161
|
+
|
162
|
+
tmp = input["proof"]
|
163
|
+
must_have!(type, tmp, index, "version", input: input, name: "proof.version")
|
164
|
+
|
165
|
+
proof = {version: tmp["version"]}
|
166
|
+
proof[:id] = tmp["id"] if tmp.key?("id")
|
167
|
+
proof[:secret] = tmp["secret"] if tmp.key?("secret")
|
168
|
+
|
169
|
+
test[:proof] = proof
|
170
|
+
|
171
|
+
test
|
172
|
+
end
|
173
|
+
|
174
|
+
def make_proof(type, input, index, app)
|
175
|
+
version = validate_version(input.fetch(:proof).fetch(:version))
|
176
|
+
|
177
|
+
nonce =
|
178
|
+
if input.dig(:nonce, :empty)
|
179
|
+
""
|
180
|
+
elsif (value = input.dig(:nonce, :offset_minutes))
|
181
|
+
timestamp_nonce(value)
|
182
|
+
elsif (value = input.dig(:nonce, :value))
|
183
|
+
value
|
184
|
+
else
|
185
|
+
app.generate_nonce(version)
|
186
|
+
end
|
187
|
+
|
188
|
+
if input[:padlock]
|
189
|
+
padlock = build_padlock(app, {
|
190
|
+
id: input.dig(:proof, :id),
|
191
|
+
nonce: input.dig(:padlock, :nonce),
|
192
|
+
secret: input.dig(:proof, :secret),
|
193
|
+
version: input.dig(:proof, :version)
|
194
|
+
})
|
195
|
+
|
196
|
+
return build_proof(app, padlock, {
|
197
|
+
id: input.dig(:proof, :id),
|
198
|
+
nonce: nonce,
|
199
|
+
secret: input.dig(:proof, :secret),
|
200
|
+
version: input.dig(:proof, :version)
|
201
|
+
})
|
202
|
+
end
|
203
|
+
|
204
|
+
if input.dig(:proof, :id) || input.dig(:proof, :secret) || input[:nonce]
|
205
|
+
padlock = build_padlock(app,
|
206
|
+
id: input.dig(:proof, :id),
|
207
|
+
nonce: nonce,
|
208
|
+
secret: input.dig(:proof, :secret),
|
209
|
+
version: input.dig(:proof, :version))
|
210
|
+
|
211
|
+
return build_proof(app, padlock,
|
212
|
+
id: input.dig(:proof, :id),
|
213
|
+
nonce: nonce,
|
214
|
+
secret: input.dig(:proof, :secret),
|
215
|
+
version: input.dig(:proof, :version))
|
216
|
+
end
|
217
|
+
|
218
|
+
AppIdentity::Internal.generate_proof!(app, nonce: nonce, version: version)
|
219
|
+
rescue => ex
|
220
|
+
fail!(type, input, index, ex.message)
|
221
|
+
end
|
222
|
+
|
223
|
+
def fail!(type, input, index, message)
|
224
|
+
extra = ""
|
225
|
+
|
226
|
+
if message.is_a?(Exception)
|
227
|
+
extra = "\n#{message.backtrace.join("\n")}"
|
228
|
+
message = message.message
|
229
|
+
end
|
230
|
+
|
231
|
+
raise "Error in #{type} item #{index + 1}: #{message}\n" +
|
232
|
+
JSON.pretty_generate(input) +
|
233
|
+
extra
|
234
|
+
end
|
235
|
+
|
236
|
+
def must_have!(type, input, index, key, options = {})
|
237
|
+
return if input.key?(key)
|
238
|
+
fail!(type, options[:input] || input, index, "missing #{options[:name] || key}")
|
239
|
+
end
|
240
|
+
|
241
|
+
attr_reader :name, :options
|
242
|
+
end
|