-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathopaque-api.yaml
More file actions
1072 lines (984 loc) · 43.6 KB
/
opaque-api.yaml
File metadata and controls
1072 lines (984 loc) · 43.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
openapi: "3.1.1"
info:
title: Hofmann OPAQUE API
description: |
OPAQUE aPAKE (Asymmetric Password-Authenticated Key Exchange) service
implementing RFC 9807 OPAQUE-3DH over NIST P-256 with SHA-256.
OPAQUE is a password authentication protocol that allows a client to
authenticate to a server **without the server ever seeing the password**,
not even during initial registration. The server cannot perform an offline
dictionary attack even if its database is fully compromised.
## Protocol Overview
### Registration (one-time, per user)
```
Client Server
| |
|─── POST /registration/start ───►| (KE1: blinded element + credential ID)
|◄─── RegistrationStartResponse ──| (evaluated element + server public key)
| |
| [client derives randomized_pwd, |
| creates envelope, keys] |
| |
|─── POST /registration/finish ──►| (client public key + sealed envelope)
|◄─── 204 No Content ─────────────|
```
### Authentication (each login)
```
Client Server
| |
|─── POST /auth/start ───────────►| (KE1: blinded element + ephemeral key)
|◄─── AuthStartResponse ──────────| (KE2: evaluated element + masked envelope +
| | ephemeral key + server MAC)
| |
| [client verifies server MAC, |
| unseals envelope, derives keys]|
| |
|─── POST /auth/finish ──────────►| (KE3: client MAC + session token)
|◄─── AuthFinishResponse ─────────| (shared session key + JWT)
```
## Security Properties
- **Zero password exposure**: The server never sees the plaintext password
- **Mutual authentication**: Both parties prove knowledge of the shared secret
- **Forward secrecy**: Ephemeral DH keys ensure past sessions remain safe if long-term keys are compromised
- **User enumeration resistance**: Unknown credential identifiers return a fake-but-valid-looking KE2 response (RFC 9807 §10.6)
- **Replay protection**: Session tokens are single-use; nonces bind each session
## Encoding
All binary fields (EC points, keys, nonces, MACs) are **Base64-encoded** strings
in JSON request and response bodies.
## Session Management
The server maintains in-memory pending sessions during the two-round authentication
handshake. Sessions expire after **120 seconds**. The server enforces a maximum of
**10,000 concurrent pending sessions** to prevent memory exhaustion.
version: "1.0.0"
contact:
url: https://github.com/wolpert/hofmann-elimination
license:
name: Apache 2.0
url: https://www.apache.org/licenses/LICENSE-2.0
servers:
- url: /
description: Base path (no prefix)
tags:
- name: Configuration
description: |
Server configuration endpoints for client self-configuration.
These endpoints are unauthenticated and safe to call before registration or login.
- name: Registration
description: |
Two-round registration flow that stores a password-derived record on the server
without transmitting the password. Requires no prior authentication.
Registration endpoints also support **account recovery re-registration** when an
`Authorization: Bearer <recoveryToken>` header is present. See the Recovery tag
for details on obtaining a recovery token.
- name: Authentication
description: |
Two-round OPAQUE-3DH login flow. On success, returns a shared session key and
a signed JWT for subsequent API calls. Requires a prior completed registration.
- name: Credential Management
description: |
Operations on existing registrations. Requires a valid JWT obtained from a
successful authentication.
- name: Recovery
description: |
Account recovery flow for users who have lost their password.
Because OPAQUE never exposes the password to the server, "password reset" is not
possible. Instead, the user proves their identity through an out-of-band mechanism
(email code, SMS OTP, TOTP, admin approval, etc.) and then re-registers with a
new password using the standard registration endpoints.
**Flow:**
1. `POST /opaque/recovery/start` — triggers an out-of-band challenge
2. `POST /opaque/recovery/verify` — verifies the challenge response, returns a
single-use recovery token
3. Use the recovery token as `Authorization: Bearer <token>` in
`POST /opaque/registration/start` and `POST /opaque/registration/finish`
to re-register with a new password
The recovery mechanism is **pluggable**: the server operator implements the
`RecoveryChallenger` interface to define how challenges are sent and verified.
If no `RecoveryChallenger` is configured, recovery endpoints return 404.
See [RECOVERY.md](../RECOVERY.md) for full implementation guide.
- name: Password Change
description: |
Change-password flow for authenticated users.
Because OPAQUE never exposes the password to the server, changing a password
requires the client to prove knowledge of the current password first (by
authenticating to obtain a JWT), then re-registering with the new password
using the JWT for authorization.
**Flow:**
1. Authenticate with the current password via `/opaque/auth/start` and
`/opaque/auth/finish` to obtain a JWT
2. `POST /opaque/password/start` — start re-registration (JWT required)
3. `POST /opaque/password/finish` — finish re-registration (JWT required)
After a successful password change, all existing sessions (JWTs) for the
credential are revoked. The user must re-authenticate with the new password.
paths:
/opaque/config:
get:
tags:
- Configuration
operationId: getOpaqueConfig
summary: Retrieve OPAQUE client configuration
description: |
Returns the OPAQUE cipher suite, context string, and KSF parameters the server is
using. Clients can call this endpoint at startup to self-configure instead of
hard-coding these values.
**Note:** `argon2MemoryKib: 0` indicates the server is using the identity KSF
(dev/test only — no password stretching).
responses:
"200":
description: OPAQUE client configuration
content:
application/json:
schema:
$ref: "#/components/schemas/OpaqueClientConfig"
examples:
production:
summary: Production config with Argon2id
value:
cipherSuite: "P256_SHA256"
context: "hofmann-opaque-v1"
argon2MemoryKib: 65536
argon2Iterations: 3
argon2Parallelism: 1
testing:
summary: Test/dev config with identity KSF
value:
cipherSuite: "P256_SHA256"
context: "hofmann-opaque-v1"
argon2MemoryKib: 0
argon2Iterations: 0
argon2Parallelism: 0
/opaque/registration/start:
post:
tags:
- Registration
operationId: registrationStart
summary: Start OPAQUE registration (phase 1 of 2)
description: |
Accepts the client's credential identifier and blinded OPRF element, and
returns the server's OPRF-evaluated element together with the server's
long-term public key.
The client should:
1. Generate a random scalar `r`
2. Compute `blindedElement = H(password) * r` using `hash_to_curve` (RFC 9380)
3. Send this endpoint the blinded element and the chosen credential identifier
The server:
- Evaluates the blinded element under its OPRF key
- Returns its long-term AKE public key so the client can bind it into the envelope
After receiving the response the client:
1. Unblinds the evaluated element: `oprfOutput = evaluatedElement * r⁻¹`
2. Derives `randomized_pwd` from `oprfOutput`
3. Derives an AKE key pair from `randomized_pwd`
4. Seals the server's public key in an envelope authenticated by `randomized_pwd`
5. Calls `/opaque/registration/finish`
**Account recovery:** This endpoint also supports re-registration during account
recovery. Include an `Authorization: Bearer <recoveryToken>` header (obtained from
`/opaque/recovery/verify`) to authorize re-registration for an existing credential.
**Error conditions:**
- `400 Bad Request` — Any field is missing, blank, or not valid base64.
- `401 Unauthorized` — Recovery token is invalid, expired, or does not match
the credential identifier (only when `Authorization` header is present).
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/RegistrationStartRequest"
examples:
typical:
summary: Typical registration start
value:
credentialIdentifier: "dXNlckBleGFtcGxlLmNvbQ=="
blindedElement: "Ao3YJ7mTTT4IpS5S3afavaxIT+4EpQIAAAAAAAAAAAAA"
responses:
"200":
description: Registration start succeeded; client should proceed to `/registration/finish`
content:
application/json:
schema:
$ref: "#/components/schemas/RegistrationStartResponse"
"400":
$ref: "#/components/responses/BadRequest"
/opaque/registration/finish:
post:
tags:
- Registration
operationId: registrationFinish
summary: Finish OPAQUE registration (phase 2 of 2)
description: |
Stores the client's registration record on the server. After this call
succeeds, the client can authenticate using `/opaque/auth/start` and
`/opaque/auth/finish`.
The client must supply the credential record produced by finalizing the
OPRF output from `/registration/start`:
- `clientPublicKey` — the AKE public key derived from `randomized_pwd`
- `maskingKey` — used during authentication to blind the stored envelope
- `envelopeNonce` — unique random nonce sealing the envelope
- `authTag` — authentication tag binding the server's public key to the envelope
**Account recovery:** When an `Authorization: Bearer <recoveryToken>` header is
present, this endpoint performs recovery re-registration: the old credential and
all its active JWT sessions are revoked before the new record is stored. The
recovery token is consumed (single-use) on success.
**Error conditions:**
- `400 Bad Request` — Any field is missing, blank, or not valid base64.
- `401 Unauthorized` — Recovery token is invalid, expired, or does not match
the credential identifier (only when `Authorization` header is present).
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/RegistrationFinishRequest"
responses:
"204":
description: Registration record stored successfully; no response body
"400":
$ref: "#/components/responses/BadRequest"
/opaque/registration:
delete:
tags:
- Credential Management
operationId: registrationDelete
summary: Delete a registration record
description: |
Permanently removes a credential registration from the server store.
After deletion the credential identifier can no longer authenticate.
**Authorization required:** A valid JWT bearer token obtained from a
prior successful authentication (`/opaque/auth/finish`). The token's
subject must match the `credentialIdentifier` being deleted, preventing
one user from deleting another's registration.
As a side effect, all active JWT sessions for the deleted credential
are revoked, even if they have not yet expired.
**Error conditions:**
- `400 Bad Request` — `credentialIdentifier` is missing, blank, or not valid base64.
- `401 Unauthorized` — `Authorization` header is absent, the token is invalid,
expired, or its subject does not match the credential being deleted.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/RegistrationDeleteRequest"
examples:
typical:
summary: Delete by credential identifier
value:
credentialIdentifier: "dXNlckBleGFtcGxlLmNvbQ=="
responses:
"204":
description: Registration deleted successfully; no response body
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
/opaque/recovery/start:
post:
tags:
- Recovery
operationId: recoveryStart
summary: Initiate account recovery (send out-of-band challenge)
description: |
Triggers an out-of-band identity challenge for the given credential identifier.
The challenge mechanism is determined by the server's `RecoveryChallenger`
implementation (e.g. email code, SMS OTP, TOTP prompt).
**User enumeration resistance:** This endpoint always returns `202 Accepted`,
even if the credential identifier is not registered. The `RecoveryChallenger`
implementation should likewise avoid revealing whether the account exists.
After the user receives the challenge, they should call `/opaque/recovery/verify`
with the challenge response.
**Error conditions:**
- `400 Bad Request` — `credentialIdentifier` is missing, blank, or not valid base64.
- `404 Not Found` — Account recovery is not configured on this server.
- `429 Too Many Requests` — Rate limit exceeded; retry after the `Retry-After` delay.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/RecoveryStartRequest"
examples:
typical:
summary: Start recovery for a user
value:
credentialIdentifier: "dXNlckBleGFtcGxlLmNvbQ=="
responses:
"202":
description: |
Challenge sent (or silently ignored if the credential is unknown).
The user should check their out-of-band channel (email, SMS, etc.)
and call `/opaque/recovery/verify` with the response.
"400":
$ref: "#/components/responses/BadRequest"
"404":
description: Account recovery is not configured on this server.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"429":
$ref: "#/components/responses/TooManyRequests"
/opaque/recovery/verify:
post:
tags:
- Recovery
operationId: recoveryVerify
summary: Verify recovery challenge and obtain recovery token
description: |
Verifies the user's response to the out-of-band challenge sent by
`/opaque/recovery/start`. On success, returns a single-use recovery token
that authorizes re-registration.
The recovery token should be used as `Authorization: Bearer <recoveryToken>`
in the subsequent `/opaque/registration/start` and `/opaque/registration/finish`
calls to complete the password reset.
**Recovery token properties:**
- **Single-use:** consumed when `/opaque/registration/finish` succeeds
- **Short TTL:** expires after 10 minutes (configurable)
- **Scoped:** bound to the specific credential identifier
**Error conditions:**
- `400 Bad Request` — A required field is missing, blank, or not valid base64.
- `401 Unauthorized` — The challenge response is incorrect or expired.
- `404 Not Found` — Account recovery is not configured on this server.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/RecoveryVerifyRequest"
examples:
emailCode:
summary: Verify a 6-digit email code
value:
credentialIdentifier: "dXNlckBleGFtcGxlLmNvbQ=="
challengeResponse: "482901"
totp:
summary: Verify a TOTP code
value:
credentialIdentifier: "dXNlckBleGFtcGxlLmNvbQ=="
challengeResponse: "731205"
responses:
"200":
description: |
Challenge verified. Use the `recoveryToken` as a Bearer token in the
registration endpoints to re-register with a new password.
content:
application/json:
schema:
$ref: "#/components/schemas/RecoveryVerifyResponse"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
description: Account recovery is not configured on this server.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/opaque/password/start:
post:
tags:
- Password Change
operationId: changePasswordStart
summary: Start password change (phase 1 of 2)
description: |
Identical to registration phase 1 (`/opaque/registration/start`) but requires
a valid JWT obtained from a prior successful authentication. The JWT subject
must match the `credentialIdentifier` in the request body, ensuring a user can
only change their own password.
The client should:
1. Authenticate with the current password to obtain a JWT
2. Generate a new OPRF blind using the **new** password
3. Call this endpoint with the blinded element and credential identifier
The server evaluates the blinded element and returns its public key, exactly
as in normal registration.
**Error conditions:**
- `400 Bad Request` — Any field is missing, blank, or not valid base64.
- `401 Unauthorized` — `Authorization` header is absent, the JWT is invalid,
expired, or its subject does not match the credential identifier.
- `429 Too Many Requests` — Rate limit exceeded; retry after the `Retry-After` delay.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/RegistrationStartRequest"
responses:
"200":
description: |
Password change phase 1 succeeded; client should proceed to
`/opaque/password/finish`.
content:
application/json:
schema:
$ref: "#/components/schemas/RegistrationStartResponse"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"429":
$ref: "#/components/responses/TooManyRequests"
/opaque/password/finish:
post:
tags:
- Password Change
operationId: changePasswordFinish
summary: Finish password change (phase 2 of 2)
description: |
Stores the new registration record derived from the new password, atomically
replacing the old credential record. On success, all existing JWT sessions for
the credential are revoked — the user must re-authenticate with the new password.
The request body is identical to `/opaque/registration/finish`: the client
supplies the new client public key, masking key, envelope nonce, and auth tag
computed from the new password's OPRF output.
**Error conditions:**
- `400 Bad Request` — Any field is missing, blank, or not valid base64.
- `401 Unauthorized` — `Authorization` header is absent, the JWT is invalid,
expired, or its subject does not match the credential identifier.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/RegistrationFinishRequest"
responses:
"204":
description: |
Password changed successfully. The old credential record has been replaced
and all existing sessions have been revoked. The client must re-authenticate
with the new password to obtain a fresh JWT.
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
/opaque/auth/start:
post:
tags:
- Authentication
operationId: authStart
summary: Start OPAQUE authentication (KE1 → KE2)
description: |
Accepts the client's KE1 message (blinded OPRF element + ephemeral AKE
public key + client nonce) and returns the server's KE2 message (evaluated
element + masked credential record + ephemeral AKE public key + server MAC).
**User enumeration resistance (RFC 9807 §10.6):** If the `credentialIdentifier`
is not registered, the server returns a fake-but-computationally-valid KE2
response rather than an error. The client will fail to verify the server MAC
and thereby learn that authentication failed — but the error reveals nothing
about whether the identifier exists. Callers must not distinguish "unknown user"
from "wrong password" based on this endpoint's response.
The client must:
1. Verify the `serverMac` in the response before computing KE3
2. Unmask the `maskedResponse` to recover the server's long-term public key
and envelope
3. Unseal the envelope using `randomized_pwd` to recover its AKE private key
4. Proceed to `/opaque/auth/finish` only if server MAC verification succeeds
**Error conditions:**
- `400 Bad Request` — Any field is missing, blank, or not valid base64.
- `503 Service Unavailable` — The server's pending-session store has reached
its 10,000-session capacity; retry after a short delay.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/AuthStartRequest"
responses:
"200":
description: |
KE2 message produced. The `sessionToken` in the response must be
echoed verbatim in the subsequent `/auth/finish` call.
content:
application/json:
schema:
$ref: "#/components/schemas/AuthStartResponse"
"400":
$ref: "#/components/responses/BadRequest"
"503":
$ref: "#/components/responses/ServiceUnavailable"
/opaque/auth/finish:
post:
tags:
- Authentication
operationId: authFinish
summary: Finish OPAQUE authentication (KE3 → session key + JWT)
description: |
Accepts the client's KE3 message (client MAC over the full AKE transcript)
and returns a shared session key and a signed JWT bearer token.
The `sessionToken` must match the one returned by the corresponding
`/auth/start` call. Sessions expire **120 seconds** after `/auth/start`;
requests with an expired or unknown token receive `401 Unauthorized`.
The `clientMac` is computed as:
```
clientMac = HMAC(Km3, SHA-256(preamble || serverMac))
```
where `preamble` is the concatenated KE1 and KE2 transcript bytes.
On success the server:
1. Verifies the client MAC proves knowledge of the correct password
2. Removes the pending session (tokens are single-use)
3. Issues a signed JWT with the credential identifier as the subject
**Error conditions:**
- `400 Bad Request` — `clientMac` is missing, blank, or not valid base64;
or `sessionToken` is missing or blank.
- `401 Unauthorized` — `sessionToken` is unknown, expired (>120 s), or
the `clientMac` does not verify (wrong password or tampered transcript).
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/AuthFinishRequest"
responses:
"200":
description: |
Authentication succeeded. The `token` is a signed JWT that the client
should present as `Authorization: Bearer <token>` for subsequent calls.
content:
application/json:
schema:
$ref: "#/components/schemas/AuthFinishResponse"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
JWT obtained from a successful `/opaque/auth/finish` response.
The token subject is the credential identifier used during registration.
schemas:
# ─── Configuration ─────────────────────────────────────────────────────────
OpaqueClientConfig:
type: object
required:
- cipherSuite
- context
- argon2MemoryKib
- argon2Iterations
- argon2Parallelism
properties:
cipherSuite:
type: string
description: |
The OPAQUE cipher suite the server is using. Clients must configure the same suite.
enum: [P256_SHA256, P384_SHA384, P521_SHA512, RISTRETTO255_SHA512]
example: P256_SHA256
context:
type: string
description: |
The OPAQUE protocol context string bound into every AKE transcript.
Must be supplied verbatim to the client's OPAQUE config.
example: "hofmann-opaque-v1"
argon2MemoryKib:
type: integer
description: |
Argon2id memory cost in kibibytes. A value of `0` indicates the server is
using the identity KSF (no password stretching — for dev/test only).
example: 65536
argon2Iterations:
type: integer
description: |
Argon2id iteration count. Ignored when `argon2MemoryKib` is `0`.
example: 3
argon2Parallelism:
type: integer
description: |
Argon2id parallelism degree. Ignored when `argon2MemoryKib` is `0`.
example: 1
# ─── Registration ──────────────────────────────────────────────────────────
RegistrationStartRequest:
type: object
required:
- credentialIdentifier
- blindedElement
properties:
credentialIdentifier:
type: string
format: byte
description: |
Base64-encoded credential identifier (username, email address, or any
opaque byte sequence that uniquely identifies the user on this server).
The server stores this as the key for the registration record.
example: "dXNlckBleGFtcGxlLmNvbQ=="
blindedElement:
type: string
format: byte
description: |
Base64-encoded blinded OPRF input: a compressed SEC1 P-256 point (33 bytes)
computed as `H(password) * r`, where `r` is a random scalar chosen by the
client and `H` is the `hash_to_curve` function (RFC 9380).
example: "Ao3YJ7mTTT4IpS5S3afavaxIT+4EpQIAAAAAAAAAAAAA"
RegistrationStartResponse:
type: object
required:
- evaluatedElement
- serverPublicKey
properties:
evaluatedElement:
type: string
format: byte
description: |
Base64-encoded OPRF-evaluated element: a compressed SEC1 P-256 point (33 bytes)
computed as `blindedElement * serverOprfKey`. The client unblinds this with
`evaluatedElement * r⁻¹` to obtain the OPRF output used to derive
`randomized_pwd`.
example: "A6GywnTj7YkHqO5KFuIjJrX9KlQgzGdCkQAAAAAAAAA"
serverPublicKey:
type: string
format: byte
description: |
Base64-encoded server long-term AKE public key: a compressed SEC1 P-256
point (33 bytes, first byte `0x02` or `0x03`). The client seals this into
the envelope during registration so that it can authenticate the server
during subsequent logins.
example: "Az1234567890abcdefghijklmnopqrstuvwxyzABCDE="
RegistrationFinishRequest:
type: object
required:
- credentialIdentifier
- clientPublicKey
- maskingKey
- envelopeNonce
- authTag
properties:
credentialIdentifier:
type: string
format: byte
description: |
Base64-encoded credential identifier. Must be identical to the value
sent in the corresponding `/registration/start` request.
example: "dXNlckBleGFtcGxlLmNvbQ=="
clientPublicKey:
type: string
format: byte
description: |
Base64-encoded client AKE public key: a compressed SEC1 P-256 point
(33 bytes) derived from `randomized_pwd`. The server stores this and
uses it in the 3DH computation during authentication.
example: "Aq+2abc123def456ghi789jkl012mno345pqr678st=="
maskingKey:
type: string
format: byte
description: |
Base64-encoded masking key (32 bytes) derived from `randomized_pwd`
via HKDF-Expand. During authentication the server uses this to XOR-encrypt
the envelope before transmission, preventing offline dictionary attacks even
if the credential store is compromised.
example: "c2VjcmV0TWFza2luZ0tleVZhbHVlSGVyZTEyMzQ1Ng=="
envelopeNonce:
type: string
format: byte
description: |
Base64-encoded random nonce (32 bytes) generated by the client and used
to seal the envelope. Must be unique per registration.
example: "cmFuZG9tTm9uY2UxMjM0NTY3ODkwMTIzNDU2Nzg5MA=="
authTag:
type: string
format: byte
description: |
Base64-encoded authentication tag (32 bytes) that authenticates the
envelope contents and binds the server's public key to the envelope.
Computed during client-side OPAQUE finalization.
example: "YXV0aFRhZ1ZhbHVlMTIzNDU2Nzg5MDEyMzQ1Njc4OTA="
RegistrationDeleteRequest:
type: object
required:
- credentialIdentifier
properties:
credentialIdentifier:
type: string
format: byte
description: |
Base64-encoded credential identifier of the registration to delete.
The JWT subject (from the `Authorization` header) must match this value.
example: "dXNlckBleGFtcGxlLmNvbQ=="
# ─── Authentication ────────────────────────────────────────────────────────
AuthStartRequest:
type: object
required:
- credentialIdentifier
- blindedElement
- clientNonce
- clientAkePublicKey
properties:
credentialIdentifier:
type: string
format: byte
description: |
Base64-encoded credential identifier. The server uses this to look up the
stored registration record. If not found, a fake KE2 is returned (see
user enumeration resistance note in the endpoint description).
example: "dXNlckBleGFtcGxlLmNvbQ=="
blindedElement:
type: string
format: byte
description: |
Base64-encoded blinded OPRF input element: a compressed SEC1 P-256 point
(33 bytes). Same construction as in registration: `H(password) * r`.
example: "Ao3YJ7mTTT4IpS5S3afavaxIT+4EpQIAAAAAAAAAAAAA"
clientNonce:
type: string
format: byte
description: |
Base64-encoded 32-byte random client nonce for this AKE session.
Included in the transcript preamble to prevent replay attacks and
bind this session uniquely.
example: "Y2xpZW50Tm9uY2UxMjM0NTY3ODkwMTIzNDU2Nzg5MA=="
clientAkePublicKey:
type: string
format: byte
description: |
Base64-encoded ephemeral client AKE public key: a compressed SEC1 P-256
point (33 bytes). This is the client's ephemeral Diffie-Hellman contribution
for the 3DH handshake. A fresh key pair must be generated for each login attempt.
example: "AiKE3phemeralClientPublicKey1234567890ABCD="
AuthStartResponse:
type: object
required:
- sessionToken
- evaluatedElement
- maskingNonce
- maskedResponse
- serverNonce
- serverAkePublicKey
- serverMac
properties:
sessionToken:
type: string
description: |
Server-generated opaque token identifying the pending AKE session.
Must be echoed verbatim in the `sessionToken` field of the subsequent
`/auth/finish` request. Has no cryptographic role; used only for server-side
session lookup. Sessions expire 120 seconds after `/auth/start`.
example: "7f3d9a1c-4b2e-4f8a-8c6d-1e2f3a4b5c6d"
evaluatedElement:
type: string
format: byte
description: |
Base64-encoded OPRF-evaluated element: a compressed SEC1 P-256 point (33 bytes).
The client unblinds this with `evaluatedElement * r⁻¹` to recover
`randomized_pwd` and then uses it to unseal the envelope.
example: "A6GywnTj7YkHqO5KFuIjJrX9KlQgzGdCkQAAAAAAAAA"
maskingNonce:
type: string
format: byte
description: |
Base64-encoded fresh random nonce (32 bytes) generated by the server.
Combined with the stored `maskingKey` (from registration) to derive
a one-time masking stream that XOR-encrypts the envelope.
example: "bWFza2luZ05vbmNlMTIzNDU2Nzg5MDEyMzQ1Njc4OTA="
maskedResponse:
type: string
format: byte
description: |
Base64-encoded masked (XOR-encrypted) credential record transmitted to the
client. Contains the server's long-term public key concatenated with the
stored envelope, both XORed with the masking stream derived from
`maskingNonce` and the registration `maskingKey`. The client unmasks
it to recover the envelope, then unseals the envelope using `randomized_pwd`.
example: "bWFza2VkUmVzcG9uc2VEYXRhMTIzNDU2Nzg5MDEyMzQ1Njc4OTA="
serverNonce:
type: string
format: byte
description: |
Base64-encoded 32-byte random server nonce for this AKE session.
Included in the transcript preamble alongside the client nonce to
uniquely bind this session.
example: "c2VydmVyTm9uY2UxMjM0NTY3ODkwMTIzNDU2Nzg5MA=="
serverAkePublicKey:
type: string
format: byte
description: |
Base64-encoded ephemeral server AKE public key: a compressed SEC1 P-256
point (33 bytes). The server's ephemeral Diffie-Hellman contribution for
the 3DH handshake. A fresh key pair is generated for each `/auth/start` call.
example: "A3NlcnZlckVwaGVtZXJhbFB1YmxpY0tleUFCQ0RFRg=="
serverMac:
type: string
format: byte
description: |
Base64-encoded server MAC (32 bytes) authenticating the server's commitment
to this AKE session. Computed as:
`HMAC-SHA-256(Km2, SHA-256(preamble))`
The client MUST verify this before computing and sending KE3. Failure to
verify means the password was wrong or the server is not authentic.
example: "c2VydmVyTWFjVmFsdWUxMjM0NTY3ODkwMTIzNDU2Nzg="
AuthFinishRequest:
type: object
required:
- sessionToken
- clientMac
properties:
sessionToken:
type: string
description: |
The opaque session token returned by the corresponding `/auth/start` call.
Used by the server to retrieve the pending AKE state. Must be provided
within 120 seconds of the `/auth/start` response.
example: "7f3d9a1c-4b2e-4f8a-8c6d-1e2f3a4b5c6d"
clientMac:
type: string
format: byte
description: |
Base64-encoded client MAC (32 bytes) authenticating the full AKE transcript.
Proves that the client knows the correct password. Computed as:
`HMAC-SHA-256(Km3, SHA-256(preamble || serverMac))`
where `preamble` is the concatenated KE1 and KE2 transcript bytes,
and `serverMac` is the value from the `/auth/start` response.
example: "Y2xpZW50TWFjVmFsdWUxMjM0NTY3ODkwMTIzNDU2Nzg="
AuthFinishResponse:
type: object
required:
- sessionKey
- token
properties:
sessionKey:
type: string
format: byte
description: |
Base64-encoded shared session key (32 bytes) derived from the 3DH handshake.
Both client and server independently compute this key; agreement confirms
mutual authentication. The client should verify its locally-computed session
key matches this value.
example: "c2Vzc2lvbktleVZhbHVlMTIzNDU2Nzg5MDEyMzQ1Njc="
token:
type: string
description: |
Signed JWT bearer token. Present this in the `Authorization: Bearer <token>`
header for API calls that require authentication (e.g., `/opaque/registration`
DELETE). The token subject is the credential identifier.
example: "eyJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJ1c2VyQGV4YW1wbGUuY29tIn0.signature"
# ─── Recovery ─────────────────────────────────────────────────────────────
RecoveryStartRequest:
type: object
required:
- credentialIdentifier
properties:
credentialIdentifier:
type: string
format: byte
description: |
Base64-encoded credential identifier of the account to recover.
example: "dXNlckBleGFtcGxlLmNvbQ=="
RecoveryVerifyRequest:
type: object
required:
- credentialIdentifier
- challengeResponse
properties:
credentialIdentifier:
type: string
format: byte
description: |
Base64-encoded credential identifier. Must match the value used in
the corresponding `/recovery/start` call.
example: "dXNlckBleGFtcGxlLmNvbQ=="
challengeResponse:
type: string
description: |
The user's response to the out-of-band challenge. The format depends
on the server's `RecoveryChallenger` implementation — typically a
6-digit code, TOTP value, or similar.
example: "482901"
RecoveryVerifyResponse:
type: object
required:
- recoveryToken
properties:
recoveryToken:
type: string
description: |
Single-use recovery token authorizing re-registration for the verified
credential. Use this as `Authorization: Bearer <recoveryToken>` in the
subsequent `/opaque/registration/start` and `/opaque/registration/finish`
calls. Expires after 10 minutes (configurable).
example: "a3f8c2d1-7b4e-4f9a-9c6e-2d1f3e4a5b6c"
# ─── Error responses ───────────────────────────────────────────────────────
ErrorResponse:
type: object
properties:
message:
type: string
description: Human-readable error description
example: "Invalid request data"
responses:
BadRequest:
description: |
Bad request. A required field is missing, blank, contains invalid base64,