PPAP integration

This commit is contained in:
2026-07-16 19:36:30 +03:00
parent ba64a0c95a
commit 4cd9f928ff
36 changed files with 6586 additions and 656 deletions
+691
View File
@@ -0,0 +1,691 @@
# PPAP ML-KEM-768 v1
## Status and scope
This document specifies the byte-for-byte interoperable form of Esiur's fixed
PPAP suite named `ppap-mlkem768-v1`, including its required encrypted
post-authentication registration rotation. It describes the protocol currently
implemented by `PpapWire`, `PpapCryptography`, `PpapAuthenticationHandler`, and
`EpConnection`.
The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHOULD**, and **SHOULD NOT**
are normative requirements. There is no algorithm negotiation inside PPAP v1.
## 1. Notation and canonical encodings
- `A || B` is octet concatenation.
- `U8(n)` is one unsigned octet.
- `U32(n)` is an unsigned 32-bit integer in big-endian order. The current wire
reader accepts only `0..2^31-1`; values with the high bit set are invalid.
- `U64(n)` is an unsigned 64-bit integer in big-endian order. The implementation
uses a non-negative signed 64-bit value, so the accepted range is
`0..2^63-1`.
- `F(x)` is `U32(len(x)) || x`.
- `FRAMES(x1, ..., xn)` is `F(x1) || ... || F(xn)`. A null or absent input is
encoded as a zero-length field, `00 00 00 00`.
- `H(x)` is SHA3-256 over `x`.
- `MAC(k, x)` is HMAC-SHA3-256 with key `k` over `x`.
- `HKDF(ikm, salt, info, L)` is RFC 5869 HKDF using SHA3-256 and producing `L`
octets.
- `UTF8(s)` is UTF-8 without a BOM. Encoders and decoders MUST reject invalid
Unicode/UTF-8. Identities and domains MUST be normalized to Unicode NFC before
UTF-8 encoding and before ordinal comparison.
An identity MUST be non-null, non-empty, and not entirely whitespace. A domain
may be empty; a null domain is the empty domain. An encoded identity or domain
MUST NOT exceed 512 octets. Implementations MUST use the canonical encodings in
this section even when their host language has a different default byte order,
text encoding, or string comparison.
## 2. Fixed suite
| Parameter | Required value |
|---|---:|
| Protocol name | `ppap-mlkem768-v1` |
| PPAP wire version | `1` |
| KEM | ML-KEM-768 |
| ML-KEM public key | 1184 octets |
| ML-KEM private-key encoding | 2400 octets |
| ML-KEM ciphertext | 1088 octets |
| ML-KEM shared secret | 32 octets |
| Deterministic ML-KEM seed | 64 octets |
| Hash/MAC/Finished | 32 octets |
| Identity mask and registration nonce | 32 octets each |
| Session key | 32 octets |
| Descriptor AEAD | AES-256-GCM, 128-bit tag |
| Descriptor plaintext/key/nonce/tag | 128/32/12/16 octets |
| Protected descriptor | 144 octets |
| Maximum password | 4096 octets, non-empty |
| Maximum complete PPAP envelope | 8192 octets |
ML-KEM key parsing MUST validate the encoded key as well as its exact length.
ML-KEM encapsulation randomness and every randomly generated ephemeral/static
key seed, identity mask, and registration nonce MUST come from a
cryptographically secure random source.
### 2.1 Domain-separation labels
Every label below is the exact sequence of displayed ASCII octets, without a
terminating NUL:
| Purpose | ASCII label |
|---|---|
| Password seed | `esiur/ppap-mlkem768-v1/password-seed` |
| Masked identity | `esiur/ppap-mlkem768-v1/masked-identity` |
| Descriptor salt | `esiur/ppap-mlkem768-v1/descriptor-salt` |
| Descriptor key | `esiur/ppap-mlkem768-v1/descriptor-key` |
| Descriptor nonce | `esiur/ppap-mlkem768-v1/descriptor-nonce` |
| Descriptor AAD | `esiur/ppap-mlkem768-v1/descriptor-aad` |
| Initiator descriptor role | `esiur/ppap-mlkem768-v1/initiator-registration` |
| Responder descriptor role | `esiur/ppap-mlkem768-v1/responder-registration` |
| Transcript | `esiur/ppap-mlkem768-v1/transcript` |
| Key-schedule input | `esiur/ppap-mlkem768-v1/key-schedule` |
| Session-key info | `esiur/ppap-mlkem768-v1/session-key` |
| Initiator Finished-key info | `esiur/ppap-mlkem768-v1/initiator-finished-key` |
| Responder Finished-key info | `esiur/ppap-mlkem768-v1/responder-finished-key` |
| Initiator Finished | `esiur/ppap-mlkem768-v1/initiator-finished` |
| Responder Finished | `esiur/ppap-mlkem768-v1/responder-finished` |
| Rotation proof | `esiur/ppap-mlkem768-v1/rotation-proof` |
## 3. Codes and PPAP envelope
All numeric codes in this section are hexadecimal. Authentication modes use
these exact octets:
| Code | Name | Authenticated role(s) |
|---:|---|---|
| `01` | `InitializerIdentity` | Initiator |
| `02` | `ResponderIdentity` | Responder |
| `03` | `DualIdentity` | Initiator and responder |
Mode `00` (`None`) is not supported by PPAP. The code spelling
`InitializerIdentity` is retained for compatibility; it means the connection
initiator.
Identity roles are initiator `01` and responder `02`. Identity kinds are
password-derived `01` and static ML-KEM `02`.
Every PPAP message is encoded as:
```text
50 50 41 50 || 01 || U8(messageType) || U32(payloadLength) || payload
```
`50 50 41 50` is ASCII `PPAP`. The total envelope MUST be between 10 and 8192
octets, so the payload is at most 8182 octets. The declared payload length MUST
equal the exact remaining length, and trailing data is forbidden.
| Type | Code | Type | Code |
|---|---:|---|---:|
| ClientHello | `01` | RotationStart | `10` |
| ServerHello | `02` | RotationOffer | `11` |
| InitiatorProof | `03` | RotationChallenge | `12` |
| ResponderProof | `04` | RotationProof | `13` |
| InitiatorFinished | `05` | RotationCommit | `14` |
| | | RotationCommitAck | `15` |
| | | RotationDone | `16` |
All variable fields below use `F(value)`, including absent fields. A decoder
MUST enforce each field's exact required size and MUST reject trailing payload
octets.
## 4. Registration material
### 4.1 Password-derived ML-KEM key
The default KDF profile is Argon2id version `0x13` (1.3), memory 32768 KiB,
three iterations, and parallelism one. An accepted profile MUST satisfy all of:
- version is exactly `0x13`;
- memory is 8192 through 262144 KiB, inclusive;
- iterations are 1 through 10, inclusive;
- parallelism is 1 through 16, inclusive; and
- memory in KiB is at least `parallelism * 8`.
Given a 32-octet registration nonce, password octets are converted to a
deterministic ML-KEM-768 private key as follows:
```text
argonInput = FRAMES(
ASCII("esiur/ppap-mlkem768-v1/password-seed"),
UTF8(domain),
UTF8(identity),
passwordBytes,
registrationNonce)
seed = Argon2id(
password = argonInput,
salt = registrationNonce,
version/memory/iterations/parallelism = registration profile,
outputLength = 64)
d = seed[0..32)
z = seed[32..64)
(publicKey, privateKey) = ML-KEM-768.KeyGen_internal(d, z)
```
`KeyGen_internal` is the deterministic internal key-generation operation from
FIPS 203; the public and private values use the standard ML-KEM-768 encapsulation
and decapsulation-key encodings. This is the cross-language meaning of the .NET
implementation's `MLKemPrivateKeyParameters.FromSeed(seed)`. The text-password
API normalizes the password to NFC and then UTF-8 encodes it. The byte-password
API uses the supplied octets unchanged. Provisioning and authentication
implementations MUST make the same choice.
A static identity instead uses a provisioned ML-KEM-768 key pair and has neither
a registration nonce nor a KDF profile. The `CreateStatic` API generates a new
pair from a cryptographically random seed; the `FromStaticKey` API may import an
existing validated 2400-octet private-key encoding.
### 4.2 Canonical registration descriptor
```text
U8(kind)
|| U64(version)
|| F(nonce)
|| U8(hasProfile)
|| profile-if-present
profile-if-present =
U32(argonVersion)
|| U32(memoryKiB)
|| U32(iterations)
|| U32(parallelism)
```
The version MUST be at least one. A password-derived descriptor has a 32-octet
nonce, `hasProfile = 01`, and the 16-octet profile, for a canonical total of 62
octets. A static descriptor has an empty nonce and `hasProfile = 00`, for a
canonical total of 14 octets. Other marker values, kinds, combinations, and
trailing data MUST be rejected. The generic descriptor decoder bounds input to
10 through 128 octets.
### 4.3 Protected handshake descriptor
Descriptors in `InitiatorProof` and `ResponderProof` are protected independently
of the later EP transport encryption. The plaintext is:
```text
F(canonicalDescriptor) || zeroPadding
```
It is exactly 128 octets. The descriptor length MUST be 1 through 124, and every
padding octet MUST be zero. Define the role-specific values:
| Described registration | `role` | `roleLabel` | Containing type | `binding` |
|---|---:|---|---:|---|
| Initiator | `01` | initiator descriptor role label | `04` ResponderProof | `01 01 04` |
| Responder | `02` | responder descriptor role label | `03` InitiatorProof | `01 02 03` |
Here the first binding octet is the PPAP wire version. Let `D = UTF8(domain)`:
```text
salt = H(FRAMES(descriptor-salt-label, roleLabel, D, binding))
key = HKDF(ephemeralSecret, salt,
FRAMES(descriptor-key-label, roleLabel, D, binding), 32)
nonce = HKDF(ephemeralSecret, salt,
FRAMES(descriptor-nonce-label, roleLabel, D, binding), 12)
aad = FRAMES(descriptor-aad-label, roleLabel, D, binding)
```
The protected descriptor is AES-256-GCM encryption of the 128-octet plaintext,
with `key`, `nonce`, and `aad`, serialized as 128 octets of ciphertext followed
by the 16-octet tag. The derived nonce is not sent. An implementation MUST
encrypt at most one descriptor for each role under one ephemeral secret. In
particular, the responder MUST reuse the same protected initiator descriptor
when constructing `ResponderProofCore` and the transmitted `ResponderProof`.
## 5. Authentication messages
Let `ephemeralPublicKey` be the initiator's fresh ML-KEM-768 public key; `mI`
and `mR` are fresh 32-octet masks selected by initiator and responder.
```text
ClientHello (01) payload =
U8(mode)
|| F(UTF8(domain))
|| F(ephemeralPublicKey[1184])
|| F(mI[32])
ServerHello (02) payload =
F(ephemeralCiphertext[1088])
|| F(mR[32])
|| F(maskedResponderIdentity[32] or empty)
InitiatorProof (03) payload =
F(maskedInitiatorIdentity[32] or empty)
|| F(responderIdentityCiphertext[1088] or empty)
|| F(protectedResponderDescriptor[144] or empty)
ResponderProof (04) payload =
F(initiatorIdentityCiphertext[1088] or empty)
|| F(protectedInitiatorDescriptor[144] or empty)
|| F(responderFinished[32])
InitiatorFinished (05) payload =
F(initiatorFinished[32])
```
Field presence is exact:
| Field | `InitializerIdentity` | `ResponderIdentity` | `DualIdentity` |
|---|:---:|:---:|:---:|
| `maskedResponderIdentity` | empty | 32 | 32 |
| `maskedInitiatorIdentity` | 32 | empty | 32 |
| responder identity ciphertext + descriptor | empty | present | present |
| initiator identity ciphertext + descriptor | present | empty | present |
An absent field is encoded as `F(empty)`, not omitted.
### 5.1 KEM and masked identities
The responder encapsulates to `ephemeralPublicKey`; both endpoints call the
resulting 32-octet value `ephemeralSecret`. For each authenticated identity, the
endpoint holding that identity's verifier registration encapsulates to its
registered public key. The identity subject decapsulates using its static or
password-derived private key. The resulting values are
`initiatorIdentitySecret` and `responderIdentitySecret`; the secret for an
unauthenticated role is empty in the key schedule.
An identity is masked as:
```text
maskedIdentity = MAC(
ephemeralSecret,
FRAMES(masked-identity-label, UTF8(domain), UTF8(identity), mask))
```
The responder identity uses `mI`; the initiator identity uses `mR`. A verifier
store resolves the result only within the normalized domain. It MUST compare
candidates in constant time, reject no match or more than one match, and MUST
NOT persist the per-handshake `ephemeralSecret` or masked value as a stable
lookup token.
This masking provides identity confidentiality against a passive observer of
the clear authentication exchange. It does not provide anonymity from an active
peer: a peer participating in the ephemeral KEM knows `ephemeralSecret` and can
test identities it is otherwise able to enumerate.
### 5.2 Authentication context, transcript, and keys
The authentication context is:
```text
FRAMES(
ASCII("ppap-mlkem768-v1"),
U8(mode),
UTF8(domain),
UTF8(initiatorIdentity) or empty,
UTF8(responderIdentity) or empty,
U8(initiatorKind or 0) || U8(responderKind or 0))
```
`ResponderProofCore` is a complete PPAP envelope of type `04` whose payload is
only:
```text
F(initiatorIdentityCiphertext or empty)
|| F(protectedInitiatorDescriptor or empty)
```
It omits the Finished field entirely; it does not append `F(empty)`. Its PPAP
payload length therefore differs from the transmitted `ResponderProof`.
```text
transcriptHash = H(FRAMES(
transcript-label,
authenticationContext,
full ClientHello envelope,
full ServerHello envelope,
full InitiatorProof envelope,
ResponderProofCore envelope))
ikm = FRAMES(
key-schedule-label,
ephemeralSecret,
initiatorIdentitySecret or empty,
responderIdentitySecret or empty)
sessionKey = HKDF(
ikm, transcriptHash,
FRAMES(session-key-label, authenticationContext), 32)
initiatorFinishedKey = HKDF(
ikm, transcriptHash,
FRAMES(initiator-finished-key-label, authenticationContext), 32)
responderFinishedKey = HKDF(
ikm, transcriptHash,
FRAMES(responder-finished-key-label, authenticationContext), 32)
initiatorFinished = MAC(
initiatorFinishedKey,
FRAMES(initiator-finished-label, transcriptHash))
responderFinished = MAC(
responderFinishedKey,
FRAMES(responder-finished-label, transcriptHash))
```
Finished values MUST be compared in constant time. A successfully returned
ML-KEM decapsulation alone MUST NOT be treated as authentication; the Finished
exchange confirms possession and the complete transcript.
### 5.3 Authentication state sequence
All three supported modes use the same ordered state sequence; only the field
presence from the preceding table differs.
| Step | Sender | PPAP message | EP carriage |
|---:|---|---|---|
| 1 | Initiator | ClientHello | `Initialize`, `SessionHeaders.AuthenticationData` |
| 2 | Responder | ServerHello | `ProceedToHandshake`, `SessionHeaders.AuthenticationData` |
| 3 | Initiator | InitiatorProof | `Handshake` action |
| 4 | Responder | ResponderProof | `Handshake` action |
| 5 | Initiator | InitiatorFinished | `FinalHandshake` action |
| 6 | Responder | no PPAP message | encrypted `Established` event |
The initiator validates the responder Finished before sending step 5. The
responder validates the initiator Finished before step 6. The session key,
local identity, and remote identity become handshake results only on successful
validation. PPAP still is not application-ready at this point: the protected
post-authentication phase in section 7 is mandatory.
## 6. Rotation messages and proof
The following are ordinary PPAP envelopes using the type codes in section 3.
All role fields MUST be initiator `01` or responder `02`.
```text
RotationStart payload =
U8(role)
RotationOffer payload =
U8(role)
|| F(UTF8(identity))
|| U64(expectedVersion)
|| F(newPublicKey[1184])
|| F(newPasswordDescriptor[62])
RotationChallenge payload =
U8(role)
|| F(ciphertext[1088])
RotationProof payload =
U8(role)
|| F(proof[32])
RotationCommit payload =
U8(role)
|| U64(committedVersion)
RotationCommitAck payload =
U8(role)
|| U64(committedVersion)
RotationDone payload = empty
```
For an offer, `expectedVersion` MUST be in `1..2^63-2`. The embedded descriptor
MUST be password-derived and its version MUST equal `expectedVersion + 1`.
Commit and acknowledgement versions MUST be at least two. The descriptor in an
offer is not protected by the descriptor AEAD from section 4.3; the complete
rotation exchange is instead REQUIRED to be inside the negotiated encrypted EP
transport.
The subject derives a new password key using a fresh 32-octet nonce, the same
identity/domain, and the unchanged KDF profile. The verifier encapsulates to the
offered public key, producing `challengeSecret` and the challenge ciphertext.
Using the exact, complete PPAP envelope octets as received or sent:
```text
rotationContext = H(FRAMES(
rotation-proof-label,
sessionKey,
full RotationOffer envelope,
full RotationChallenge envelope))
proof = MAC(challengeSecret, rotationContext)
```
The verifier MUST compare the proof in constant time. ML-KEM decapsulation does
not by itself validate a challenge.
### 6.1 Atomic registration commit
After proof verification, the verifier MUST perform one linearizable atomic
compare-and-swap keyed by the normalized domain and identity. A separate read
followed by an unconditional write is not conforming. The operation MUST return
failure without mutation unless all of these are true:
- the current record exists and its version equals `expectedVersion`;
- the replacement identity is exactly the same normalized identity under
ordinal comparison;
- the replacement version is exactly `expectedVersion + 1`;
- both records are password-derived;
- the Argon2 version, memory, iterations, and parallelism are unchanged;
- the new nonce is not reused; and
- the new encapsulation public key is not reused.
Material equality checks SHOULD be constant-time. At minimum, equality with the
current nonce and key MUST be rejected; a durable store that retains historical
material MUST also reject known historical reuse. A database implementation
MUST use a conditional update or transaction that provides the same CAS
semantics.
The verifier MUST NOT send `RotationCommit` unless the CAS succeeds. The subject
MUST retain its pending registration until it receives a commit for the exact
role and pending version, and only then adopt it. A CAS failure, version mismatch,
or concurrent update terminates the protocol rather than being retried in the
same session.
Consequently, when multiple successfully authenticated sessions concurrently
offer replacements for the same registration version, exactly one CAS can win.
The other sessions fail rotation and MUST begin a fresh authentication attempt
against the newly committed version before retrying.
The two role updates in `DualIdentity` mode are sequential CAS operations, not
one transaction spanning both records. A committed initiator-role update is not
rolled back if the later responder-role rotation fails; a retry starts from the
versions then present in the store.
## 7. Required post-authentication state sequence
A role requires a real rotation exactly when it was authenticated and its
registration kind is password-derived. Static identities are never rotated.
The initiator role is processed before the responder role. `I` and `R` below are
the connection initiator and responder; a parenthesized role is the registration
being rotated.
```text
No password-derived role:
I -> R RotationDone
Initiator role only:
I -> R RotationOffer(I)
R -> I RotationChallenge(I)
I -> R RotationProof(I)
R -> I RotationCommit(I) # after responder-side CAS
I -> R RotationDone
Responder role only:
I -> R RotationStart(R)
R -> I RotationOffer(R)
I -> R RotationChallenge(R)
R -> I RotationProof(R)
I -> R RotationCommit(R) # after initiator-side CAS
R -> I RotationCommitAck(R)
I -> R RotationDone
Both roles:
I -> R RotationOffer(I)
R -> I RotationChallenge(I)
I -> R RotationProof(I)
R -> I RotationCommit(I) # after responder-side CAS
I -> R RotationStart(R)
R -> I RotationOffer(R)
I -> R RotationChallenge(R)
R -> I RotationProof(R)
I -> R RotationCommit(R) # after initiator-side CAS
R -> I RotationCommitAck(R)
I -> R RotationDone
```
After accepting `RotationDone`, the responder queues the encrypted EP
`KeyRotationEstablished` event first, then sets `Session.Authenticated` and
publishes local readiness. Even when no password-derived role exists, the
encrypted `RotationDone`/`KeyRotationEstablished` exchange is mandatory. The
initiator MUST NOT publish readiness before receiving that event.
Messages for another role, duplicates, reordered messages, unexpected
acknowledgements, or messages received after completion MUST fail the exchange.
## 8. EP transport binding and encryption requirement
PPAP is registered only under the exact protocol string
`ppap-mlkem768-v1`; implementations MUST NOT silently accept aliases.
The EP method codes used by this suite are:
| Logical EP method | Code |
|---|---:|
| `Handshake` action | `80` |
| `FinalHandshake` action | `81` |
| `KeyRotation` action | `82` |
| `ProceedToHandshake` acknowledgement | `44` |
| `Established` event | `C0` |
| `ErrorTerminate` event | `C1` |
| `ErrorMustEncrypt` event | `C2` |
| `KeyRotationEstablished` event | `C9` |
The `Initialize` method octet, when its required headers TDU is present, is:
```text
20 | (authenticationMode << 2) | encryptionMode
```
The encryption-mode codes are `00` none, `01` session key, and `02` session key
plus addresses. PPAP MUST use `01` or `02`; `00` MUST be rejected because PPAP
always requires the protected post-authentication phase.
`Initialize` and `ProceedToHandshake` carry the PPAP envelope as the byte-array
value of indexed `SessionHeaders.AuthenticationData` (index 15), using the
standard EP indexed-structure codec. `SessionHeaders.AuthenticationProtocol`
(index 14) is the exact PPAP protocol name. The domain header (index 1) carries
the connection's configured domain; the PPAP handler normalizes that value to
NFC for its context. ClientHello carries the normalized domain, and the
responder compares the two normalized values.
For subsequent messages, a non-null PPAP envelope is encoded as an EP RawData
TDU. Since a PPAP envelope is 10 through 8192 octets, its exact direct carriage
is one of:
```text
U8(logicalMethod | 20) || 48 || U8(L) || ppapEnvelope # 10 <= L <= 255
U8(logicalMethod | 20) || 50 || U16(L) || ppapEnvelope # 256 <= L <= 8192
```
Here `20`, `48`, and `50` are hexadecimal octets and `U16` is unsigned
big-endian. Thus data-bearing Handshake, FinalHandshake, and KeyRotation method
octets are respectively `A0`, `A1`, and `A2`. A method without data is its lone
logical method octet; PPAP's final `KeyRotationEstablished` is therefore `C9`
before record protection.
The authentication messages through `InitiatorFinished` are sent before full
bidirectional transport protection. After verifying `InitiatorFinished`, the
responder enables encryption and sends `Established` as its first protected EP
message. The initiator must already accept protected inbound data; on receiving
that protected event it enables protected outbound data and begins rotation.
Every `KeyRotation` action, `RotationDone`, and `KeyRotationEstablished` event
MUST be encrypted. EP encrypted records are:
```text
U32(len(protectedPayload)) || protectedPayload
```
where `protectedPayload` is produced by the separately negotiated EP encryption
provider over the complete method/TDU plaintext. Its provider-specific format is
outside this PPAP suite. Receipt of rotation data while encryption is inactive
MUST terminate authentication. `Session.Authenticated` and application readiness
MUST remain false until rotation succeeds and readiness is published.
## 9. Interoperability and security invariants
A conforming implementation MUST fail closed on any of the following:
- wrong magic, wire version, expected message type, role, state, mode, or domain;
- a total, payload, field, key, ciphertext, mask, tag, proof, or Finished length
outside the exact bounds above;
- a declared length mismatch, integer outside the accepted range, truncated
input, or trailing input;
- an optional field whose presence does not exactly match the authentication
mode;
- invalid UTF-8, identity kind, KDF profile, descriptor marker, descriptor
combination, AEAD tag, or nonzero descriptor padding;
- an unresolved, ambiguous, or unexpected masked identity;
- a registration kind/profile/version/identity mismatch;
- a Finished, rotation proof, commit, acknowledgement, or CAS failure; or
- a rotation message outside active encrypted transport.
Proofs, Finished values, masked-identity candidates, and secret-derived material
comparisons MUST use constant-time equality. Implementations SHOULD erase
passwords, private keys, KEM secrets, Finished keys, derived AEAD material, and
pending rotation secrets as soon as their state no longer needs them. A handler
instance is single-connection state and MUST NOT be reused.
The clear authentication exchange carries only per-handshake masked identities;
it does not carry clear identity strings. A `RotationOffer` does contain its
normalized identity and descriptor, which is why rotation is forbidden outside
the encrypted EP transport. The deterministic descriptor nonce is safe only
under the one-descriptor-per-role-per-ephemeral-secret rule.
ML-KEM's implicit rejection behavior MUST remain indistinguishable at the
decapsulation API boundary. Only the transcript-bound Finished exchange or the
session-bound rotation proof establishes success. Expensive Argon2 work SHOULD
also be concurrency-limited so unauthenticated peers cannot exhaust local
memory.
### 9.1 Current local abuse controls (not wire protocol)
These controls do not change any PPAP octet and are not cross-language
interoperability requirements, but deployments need equivalent resource policy:
- The shared .NET password-derivation limiter allows
`max(1, min(processorCount, 4))` concurrent Argon2 operations. When more than
one processor is available, one slot is reserved for post-authentication
rotation. A saturated limiter rejects a derivation instead of queueing it.
- The default Warehouse connection-admission policy permits 120 connection
attempts per IP address in a one-minute window. Both values are
configurable, and setting the attempt limit to zero disables it.
- `InMemoryPpapRegistrationStore.ResolveMasked` deliberately scans the entire
store, evaluating candidates in the normalized domain, and is therefore
`O(N)` in total registrations per masked lookup. It is intended for tests and
small deployments. A large store needs a bounded or optimized lookup strategy
that still avoids persistent/stable masked tokens, uses constant-time
candidate comparison, and rejects ambiguity.
## 10. Implementation correspondence
The constants and encodings in this document are derived from these source
files:
- `Libraries/Esiur/Security/Authority/Providers/Ppap/PpapCryptography.cs`
- `Libraries/Esiur/Security/Authority/Providers/Ppap/PpapWire.cs`
- `Libraries/Esiur/Security/Authority/Providers/Ppap/PpapRotationWire.cs`
- `Libraries/Esiur/Security/Authority/Providers/Ppap/PpapModels.cs`
- `Libraries/Esiur/Security/Authority/Providers/Ppap/PpapAuthenticationHandler.cs`
- `Libraries/Esiur/Security/Authority/Providers/Ppap/PpapAuthenticationHandler.Rotation.cs`
- `Libraries/Esiur/Security/Authority/Providers/Ppap/PpapAuthenticationProvider.cs`
- `Libraries/Esiur/Security/Authority/Providers/Ppap/PpapPasswordDerivationLimiter.cs`
- `Libraries/Esiur/Security/Authority/AuthenticationMode.cs`
- `Libraries/Esiur/Security/Authority/Session.cs`
- `Libraries/Esiur/Security/Cryptography/EncryptionMode.cs`
- `Libraries/Esiur/Protocol/EpConnection.cs`
- `Libraries/Esiur/Protocol/EpConnectionProtocol.cs`
- `Libraries/Esiur/Protocol/EpServer.cs`
- `Libraries/Esiur/Net/Packets/EpAuthPacket.cs`
- `Libraries/Esiur/Net/Packets/EpAuthPacketMethod.cs`
- `Libraries/Esiur/Data/Tdu.cs`
- `Libraries/Esiur/Data/TduIdentifier.cs`
- `Libraries/Esiur/Resource/WarehouseConfiguration.cs`