pulseproof-sentinel

PulseProof Sentinel Protocol Specification

Protocol Name: PulseProof Sentinel Protocol Short Name: PPS Version: 1.0.0-draft Status: Experimental / Draft Specification Intended Status: Informational / Experimental Last Updated: 2026-07-23


Abstract

This document specifies the PulseProof Sentinel Protocol (PPS), an experimental, technology-agnostic protocol for time-bound asymmetric authentication proofs. PPS is not a new cryptographic primitive; it profiles established mechanisms — Ed25519 signatures, SHA-256 hashing, HKDF key derivation, and deterministic CBOR encoding — into a compact, interoperable verification model suited to offline, embedded, and non-browser environments where a full WebAuthn ceremony is unavailable.

A PPS authentication event produces a Pulse: a signed statement bound to a relying-party identifier, time epoch, monotonic counter, expiration, and optional nonce, context, policy, and transaction data. Because the verifier stores only public keys, PPS removes the shared-secret exposure problem inherent in TOTP.

PulseProof Sentinel extends the basic Pulse model with several optional but interoperable security extensions:

  1. Verifiable Asymmetric Forward-Secure Ratcheting
  2. Silent Duress and Honey-Pulse signaling
  3. Offline threshold co-signing
  4. Environmental and proximity context binding
  5. Dynamic policy negotiation
  6. Human-verifiable Trust Codes with transaction AmountMark embedding

This specification defines the wire format, cryptographic profiles, verification rules, security model, and conformance requirements for PPS version 1.


1. Requirements Language

The keywords MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, NOT RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.


2. Terminology

2.1. Relying Party

The Relying Party, abbreviated RP, is the service verifying authentication.

Examples:

example.com
login.example.com:8443
android:com.example.app
ios:com.example.app
urn:pps:service:internal:billing

2.2. Authenticator

The Authenticator is the user-controlled device or software module that holds private key material and generates Pulses.

Examples:

2.3. Pulse

A Pulse is a signed authentication statement generated by an Authenticator.

A Pulse asserts possession of a private key bound to a specific RP, time window, counter, and optional session/context/policy/transaction data.

2.4. Pulse Token

A Pulse Token is the serialized form of a Pulse envelope.

The canonical binary encoding is deterministic CBOR. The textual encoding is base64url without padding.

2.5. Rotation Certificate

A Rotation Certificate is a signed statement authorizing transition from one public verification key to the next public verification key.

Rotation certificates enable asynchronous or offline forward-secure key ratcheting.

2.6. Duress Key

A Duress Key is a separate private key that can be used when the user is coerced.

A Pulse signed by a Duress Key is called a Honey-Pulse.

2.7. Honey-Pulse

A Honey-Pulse is cryptographically valid but indicates that the user may be under duress.

A verifier MUST NOT reveal duress detection to the client in the outward authentication response.

2.8. Threshold Group

A Threshold Group is a set of registered signer keys where a minimum number of signatures is required to authorize a Pulse.

2.9. Trust Code

A Trust Code is a short human-verifiable numeric value derived from the cryptographic proof.

The Trust Code is not the primary authentication secret.

2.10. AmountMark

An AmountMark is a two-digit transaction checksum embedded in a Trust Code.


3. Design Goals

PulseProof Sentinel is designed to provide:

  1. No shared secret stored on the verifier.
  2. Strong cryptographic authentication using digital signatures.
  3. Replay resistance using nonce, epoch, expiration, and monotonic counters.
  4. Service binding using an RP identifier hash.
  5. Session binding using a server-issued nonce.
  6. Context binding using optional environmental or transaction data.
  7. Policy binding using a negotiated security policy hash.
  8. Forward-secure key evolution using verifiable asymmetric ratcheting.
  9. Silent duress signaling using indistinguishable Honey-Pulses.
  10. Offline threshold approval using multiple authenticator devices.
  11. Human-verifiable Trust Codes with optional transaction checksums.
  12. Implementation feasibility across common programming languages.

4. Non-Goals

PulseProof Sentinel does not define:

PPS may be combined with such systems but does not standardize them.


5. Notation and Encoding

5.1. Byte Order

All multi-byte unsigned integers explicitly serialized by this specification use big-endian byte order when raw integer bytes are required.

CBOR integer encoding follows RFC 8949 [RFC8949].

5.2. Base64url

Textual representations of binary values use base64url encoding without padding as defined in Section 5 of [RFC4648].

base64url(binary) = base64url-no-pad(binary)

5.3. CBOR

All signed structures defined by this specification are encoded using deterministic CBOR.

Implementations MUST follow the deterministic encoding rules described in Section 4.2 of [RFC8949].

Additional requirements:

  1. Map keys MUST be sorted in ascending numeric order.
  2. Indefinite-length items MUST NOT be used.
  3. Duplicate map keys MUST be rejected.
  4. Non-minimal integer encodings MUST be rejected.
  5. Floating-point values MUST NOT appear in signed structures.
  6. Undefined values MUST NOT appear in signed structures.
  7. Optional absent fields MUST be omitted.
  8. Tags MUST NOT be used unless explicitly defined by a future version.

5.4. Hashing

The mandatory hash algorithm is SHA-256 [FIPS180-4].

All hash outputs used in PPS fields are raw byte strings.

5.5. Key-derivation

The mandatory key-derivation function is HKDF-SHA256 [RFC5869].

5.6. Randomness

All random values MUST be generated using a cryptographically secure pseudorandom number generator suitable for key generation and nonce generation [RFC4086].

Nonces SHOULD be at least 128 bits and MAY be 256 bits.


6. Cryptographic Suites

PPS uses algorithm identifiers to allow cryptographic agility.

6.1. Mandatory Suite

The mandatory-to-implement suite for PPS version 1 is:

PPS-ED25519-CBOR30

This suite specifies:

Property Value
Signature algorithm Ed25519 [RFC8032]
Public key length 32 octets
Signature length 64 octets
Hash algorithm SHA-256
KDF HKDF-SHA256
Default epoch length 30 seconds
Canonical encoding deterministic CBOR
Text encoding base64url without padding

6.2. Optional Suites

The following suites are reserved for future or experimental use:

PPS-MLDSA44-CBOR30
PPS-MLDSA65-CBOR30
PPS-HYB-ED25519-MLDSA44-CBOR30

These suites reference ML-DSA [FIPS204].

Support for optional suites is OPTIONAL.

A verifier MAY reject unknown or disallowed algorithms.


7. Time Model

7.1. Unix Time

PPS uses Unix time, defined as the number of seconds since 1970-01-01T00:00:00Z, ignoring leap seconds.

7.2. Epoch

The default epoch length is 30 seconds.

epoch = floor(unix_time / 30)

The epoch length is fixed by the algorithm identifier.

For PPS-ED25519-CBOR30, the epoch length is 30 seconds.

7.3. Clock Skew

Verifiers SHOULD accept Pulses whose epoch is within one epoch of the verifier’s current epoch:

current_epoch - 1 <= pulse_epoch <= current_epoch + 1

Verifiers MAY permit a larger skew through local policy, but larger skew increases replay risk.

7.4. Expiration

The exp field is an epoch number, not a Unix timestamp.

A Pulse MUST be rejected if:

current_epoch > exp

The RECOMMENDED default is:

exp = epoch + 2

8. Identifiers

8.1. Relying Party Identifier

The RP identifier, rp_id, is a canonical ASCII string.

For web origins:

rp_id = lowercase(host) [ ":" port ]

The RP identifier MUST NOT include:

Examples:

example.com
login.example.com
example.com:8443

For application origins:

android:com.example.app
ios:com.example.app

For internal services:

urn:pps:service:internal:billing

The RP hash stored in signed structures is:

rp_hash = SHA256(utf8(rp_id))

8.2. Key Identifier

A key identifier, kid, is a byte string of length 16 to 64 octets.

It SHOULD be randomly generated and stable for a registered device.

It SHOULD NOT contain personally identifiable information.

8.3. Group Identifier

A group identifier, gid, is a byte string of length 16 to 64 octets.

It identifies a threshold group.

8.4. User Handle

A user handle is an opaque byte string of length 16 to 64 octets.

It SHOULD NOT contain personally identifiable information unless required by the deployment.


9. Data Structures

All structures in this section are CBOR maps with integer keys.

9.1. Message Types

1 = registration
2 = pulse
3 = rotation

9.2. Registration Payload

registration-payload = {
  1: version,
  2: type,
  3: alg,
  4: kid,
  5: rp_hash,
  6: pk,
  7: user_handle,
  8: nonce,
  9: created_at,
  ? 10: device_name,
  ? 11: attestation,
  ? 12: duress_pk
}

Fields:

Key Name Type Required Description
1 version uint YES MUST be 1
2 type uint YES MUST be 1
3 alg tstr YES algorithm identifier
4 kid bstr YES device key identifier
5 rp_hash bstr YES SHA256(rp_id), 32 octets
6 pk bstr YES normal public key
7 user_handle bstr YES opaque user identifier
8 nonce bstr YES registration challenge
9 created_at uint YES Unix timestamp
10 device_name tstr NO human-readable device name
11 attestation bstr NO platform attestation blob
12 duress_pk bstr NO duress public key

If duress_pk is present, the registration envelope MUST include a duress signature.

9.3. Registration Envelope

registration-envelope = {
  1: message,
  2: signature,
  ? 3: duress_signature
}

Fields:

Key Name Type Required Description
1 message bstr YES CBOR-encoded registration-payload
2 signature bstr YES signature by pk
3 duress_signature bstr CONDITIONAL signature by duress_pk if duress_pk present

The signature is computed over the raw bytes of message.

signature = Ed25519_sign(sk, message)

If duress_pk is present:

duress_signature = Ed25519_sign(duress_sk, message)

9.4. Rotation Payload

rotation-payload = {
  1: version,
  2: type,
  3: from_pk,
  4: to_pk,
  5: seq,
  6: epoch,
  7: exp,
  ? 8: rp_hash,
  ? 9: nonce
}

Fields:

Key Name Type Required Description
1 version uint YES MUST be 1
2 type uint YES MUST be 3
3 from_pk bstr YES current public key
4 to_pk bstr YES next public key
5 seq uint YES rotation sequence number
6 epoch uint YES creation epoch
7 exp uint YES expiration epoch
8 rp_hash bstr OPTIONAL SHA256(rp_id)
9 nonce bstr OPTIONAL server nonce

9.5. Rotation Certificate

rotation-certificate = {
  1: message,
  2: signature
}

Fields:

Key Name Type Required Description
1 message bstr YES CBOR-encoded rotation-payload
2 signature bstr YES signature by from_pk

The signature is computed over the raw bytes of message.

signature = Ed25519_sign(from_sk, message)

9.6. Pulse Payload

pulse-payload = {
  1: version,
  2: type,
  3: alg,
  5: rp_hash,
  6: epoch,
  7: ctr,
  8: exp,
  ? 11: nonce,
  ? 12: ctx,
  ? 13: pol,
  ? 14: amt,
  (single-mode-fields // threshold-mode-fields)
}

Common fields:

Key Name Type Required Description
1 version uint YES MUST be 1
2 type uint YES MUST be 2
3 alg tstr YES algorithm identifier
5 rp_hash bstr YES SHA256(rp_id), 32 octets
6 epoch uint YES creation epoch
7 ctr uint YES operation counter
8 exp uint YES expiration epoch
11 nonce bstr CONDITIONAL server nonce
12 ctx bstr OPTIONAL SHA256(context-object)
13 pol bstr OPTIONAL SHA256(policy-object)
14 amt uint OPTIONAL AmountMark, 0..99

9.6.1. Single-Device Ratchet Mode Fields

single-mode-fields = (
  4: kid,
  9: pk,
  10: next_pk,
  15: rot_seq
)

Fields:

Key Name Type Required Description
4 kid bstr YES device key identifier
9 pk bstr YES current signing public key
10 next_pk bstr YES next public key after rotation
15 rot_seq uint YES rotation sequence number

In single-device ratchet mode:

9.6.2. Threshold Mode Fields

threshold-mode-fields = (
  20: gid,
  21: thr
)

Fields:

Key Name Type Required Description
20 gid bstr YES threshold group identifier
21 thr uint YES required signature threshold

In threshold mode:

9.7. Pulse Envelope

pulse-envelope = {
  1: message,
  2: signatures,
  ? 3: certificates
}

Fields:

Key Name Type Required Description
1 message bstr YES CBOR-encoded pulse-payload
2 signatures map YES map of signer kid to signature
3 certificates array OPTIONAL rotation certificates

The signature map is:

signatures = {
  * bstr => bstr
}

The map key is the signer kid.

The map value is the signature over message.

In single-device ratchet mode, the signature map MUST contain exactly one entry whose key equals the Pulse payload kid.

In threshold mode, the signature map MUST contain at least thr valid signatures from registered group members.

9.8. Context Object

The context object is OPTIONAL.

context-object = {
  ? 1: session_id,
  ? 2: tx_hash,
  ? 3: ble_hash,
  ? 4: acoustic_hash,
  ? 5: ip_hash,
  ? 6: ua_hash,
  ? 7: device_integrity_hash,
  ? 8: custom_hash
}

All values are byte strings.

The context hash included in a Pulse is:

ctx = SHA256(cbor(context-object))

If no context is used, the ctx field MUST be omitted.

9.9. Policy Object

The policy object is OPTIONAL.

policy-object = {
  ? 1: min_trust_digits,
  ? 2: require_biometric,
  ? 3: require_secure_enclave,
  ? 4: threshold,
  ? 5: max_amount_minor,
  ? 6: require_proximity,
  ? 7: max_clock_skew,
  ? 8: allow_offline,
  ? 9: require_amount_mark,
  ? 10: require_duress_support
}

Types:

Key Name Type
1 min_trust_digits uint
2 require_biometric bool
3 require_secure_enclave bool
4 threshold uint
5 max_amount_minor uint
6 require_proximity bool
7 max_clock_skew uint
8 allow_offline bool
9 require_amount_mark bool
10 require_duress_support bool

The policy hash included in a Pulse is:

pol = SHA256(cbor(policy-object))

If no policy is used, the pol field MUST be omitted.


10. Pulse Token Encoding

The binary Pulse Token is:

pulse_token_binary = cbor(pulse-envelope)

The textual Pulse Token is:

pulse_token = base64url(pulse_token_binary)

The RECOMMENDED media type is:

application/pulseproof+cbor

When transmitted in JSON, binary fields SHOULD be represented as base64url strings.


11. Registration

11.1. Registration Flow

Registration binds a device key to a user account at an RP.

The RP SHOULD issue a fresh registration nonce.

The Authenticator constructs a registration payload and signs it.

If a duress key is registered, the Authenticator MUST also sign the same registration message with the duress private key.

11.2. Registration Verification

The verifier MUST perform the following checks:

  1. Decode the registration envelope.
  2. Decode the registration payload.
  3. Verify version == 1.
  4. Verify type == 1.
  5. Verify alg is allowed.
  6. Verify rp_hash == SHA256(expected_rp_id).
  7. Verify the registration nonce is valid and unused.
  8. Verify the signature using pk.
  9. If duress_pk is present, verify duress_signature using duress_pk.
  10. Store the normal public key and initial key state.
  11. If present, store the duress public key and initial duress key state.

11.3. Initial Server Key State

For each registered device, the verifier SHOULD store:

kid
current_pk
key_seq = 0
last_ctr = 0
revoked = false

If duress is registered:

duress_pk
duress_key_seq = 0
duress_last_ctr = 0

12. Verifiable Asymmetric Forward-Secure Ratchet

PPS defines a practical asymmetric forward-secure ratchet using signed key rotation.

12.1. Ratchet Principle

The Authenticator maintains a current signing key pair:

K_t = (sk_t, pk_t)

When generating a Pulse, the Authenticator generates a next key pair:

K_{t+1} = (sk_{t+1}, pk_{t+1})

The Pulse payload includes:

pk = pk_t
next_pk = pk_{t+1}
rot_seq = t + 1

The Pulse is signed with sk_t.

Because the signed payload includes next_pk, the signature authorizes rotation from pk_t to pk_{t+1}.

After creating the Pulse, the Authenticator SHOULD delete sk_t and store sk_{t+1}.

The verifier, after successful verification, updates:

current_pk = next_pk
key_seq = rot_seq

12.2. Rotation Sequence

The rotation sequence is a monotonic unsigned 64-bit integer.

After registration:

key_seq = 0

Each rotation increments the sequence by one:

new_key_seq = old_key_seq + 1

A verifier MUST reject a Pulse whose rot_seq is not exactly one greater than the working key sequence after processing any rotation certificates.

12.3. Rotation Certificates

Rotation certificates allow an Authenticator to rotate locally before the verifier has observed the previous Pulse.

A rotation certificate is signed by the old key and authorizes a new key.

rotation-payload = {
  1: 1,
  2: 3,
  3: from_pk,
  4: to_pk,
  5: seq,
  6: epoch,
  7: exp,
  ? 8: rp_hash,
  ? 9: nonce
}

The certificate signature is made by from_sk.

A verifier processing certificates MUST:

  1. Set working_pk to the stored current_pk.
  2. Set working_seq to the stored key_seq.
  3. For each certificate:
    • decode rotation-payload
    • verify version == 1
    • verify type == 3
    • verify from_pk == working_pk
    • verify seq == working_seq + 1
    • verify expiration according to policy
    • verify signature using working_pk
    • set working_pk = to_pk
    • set working_seq = seq
  4. After all certificates, require pulse.pk == working_pk.
  5. Verify the Pulse signature.
  6. Verify pulse.rot_seq == working_seq + 1.
  7. Update stored key state to pulse.next_pk and pulse.rot_seq.

12.4. Secure Deletion

An Authenticator claiming forward-secure operation MUST make a best-effort attempt to securely delete old private key material after rotation.

Secure hardware-backed storage is RECOMMENDED.

Verifiers cannot fully enforce secure deletion.


13. Pulse Generation

13.1. Inputs

An Authenticator generating a Pulse requires:

rp_id
current private key
current public key
kid
nonce, optional but RECOMMENDED
context object, optional
policy object, optional
amount_minor, optional
duress flag, optional

13.2. Single-Device Pulse Generation

The Authenticator MUST:

  1. Compute the current epoch.
  2. Increment the operation counter.
  3. Generate a next Ed25519 key pair.
  4. Construct the Pulse payload.
  5. Encode the payload using deterministic CBOR.
  6. Sign the encoded payload with the current private key.
  7. Construct the envelope.
  8. Encode the envelope using deterministic CBOR.
  9. Encode the envelope as base64url if textual transport is required.
  10. Delete the current private key after successful Pulse creation, subject to local recovery policy.

13.3. Duress Pulse Generation

If the user activates duress mode, the Authenticator MUST sign using the duress private key instead of the normal private key.

The Pulse payload MUST NOT contain an explicit duress flag.

Duress detection is performed by the verifier based on the public key used.

The duress key MAY have its own forward-secure ratchet state.

13.4. Threshold Pulse Generation

In threshold mode:

  1. The initiating device constructs the Pulse payload.
  2. The payload includes gid and thr.
  3. The payload omits single-device ratchet fields.
  4. Each required signer signs the same message.
  5. Signatures are collected into the signature map.
  6. The final envelope is transmitted to the verifier.

Threshold mode in version 1 uses static registered signer keys unless a future extension defines per-signer ratcheting.


14. Pulse Verification

A verifier MUST perform validation in a strict order.

14.1. Token Parsing

The verifier MUST:

  1. Decode the Pulse Token from base64url if textual.
  2. Decode the CBOR envelope.
  3. Extract message, signatures, and optional certificates.
  4. Decode message as a Pulse payload.

If parsing fails, the verifier MUST reject the token.

14.2. Common Payload Checks

The verifier MUST check:

  1. version == 1
  2. type == 2
  3. alg is allowed
  4. rp_hash == SHA256(expected_rp_id)
  5. epoch is within allowed skew
  6. current epoch is not greater than exp
  7. if a session nonce is required, nonce matches and is unused
  8. if context is required, ctx matches expected context hash
  9. if policy is required, pol matches expected policy hash
  10. if AmountMark is required, amt matches expected amount modulo 100

14.3. Single-Device Verification

For single-device ratchet mode:

  1. Load device state by kid.
  2. Reject if device is revoked.
  3. Process optional rotation certificates.
  4. Require pk to equal the working public key.
  5. Verify the signature in the signature map using pk.
  6. Require the signature map to contain exactly one signature for kid.
  7. Reject if ctr <= last_ctr.
  8. Reject if rot_seq != working_key_seq + 1.
  9. If Trust Code was entered, verify it after signature verification.
  10. Update state:
    • current_pk = next_pk
    • key_seq = rot_seq
    • last_ctr = ctr

State updates MUST be atomic.

14.4. Duress Verification

A verifier MAY maintain a separate duress key state for a device.

If the working public key matches the stored duress public key, the verifier MUST classify the Pulse as duress.

The verifier MUST NOT return an explicit duress indicator to the client.

The verifier MAY:

The verifier SHOULD apply the same response latency and response shape for duress and normal success.

14.5. Threshold Verification

For threshold mode:

  1. Load threshold group by gid.
  2. Reject if group is revoked.
  3. Verify thr equals the stored group threshold.
  4. Verify the signature map contains at least thr distinct signatures.
  5. Verify each signature corresponds to a registered group signer.
  6. Verify each signature over the same message.
  7. Reject if ctr <= group_last_ctr.
  8. Update group_last_ctr = ctr.

Threshold mode does not require pk, next_pk, or rot_seq.


15. Trust Code

The Trust Code is a short numeric value derived from the cryptographic proof.

It is intended for human comparison and optional typed fallback.

The Trust Code is not a replacement for the full Pulse Token.

15.1. Trust Input

For single-device mode:

trust_ikm = signature

where signature is the single signature over the Pulse message.

For threshold mode:

trust_ikm = SHA256(cbor(signatures))

where signatures is the complete signature map after collecting the required signatures.

15.2. Trust Salt

trust_salt = SHA256(
  utf8(rp_id)
  || 0x00
  || nonce_or_empty
  || 0x00
  || uint64_be(epoch)
)

If no nonce is present, nonce_or_empty is the empty byte string.

15.3. Trust OKM

trust_okm = HKDF-SHA256(
  ikm = trust_ikm,
  salt = trust_salt,
  info = utf8("PPS/trust/v1"),
  length = 32
)

15.4. Numeric Trust Code Length

The default Trust Code length is 8 digits.

If an AmountMark is present, the total length MUST be at least 8 digits.

The base signature-derived portion MUST be at least 6 digits.

total_digits = base_digits + amount_digits
amount_digits = 2 if AmountMark is present, otherwise 0
base_digits >= 6

15.5. Unbiased Decimal Derivation

Implementations MUST avoid modulo bias when deriving decimal digits.

The following algorithm is normative for the mandatory suite:

DecimalFromOKM(okm, digits):

  modulus = 10 ^ digits
  limit = 2^64 - (2^64 mod modulus)

  for i from 0 to 255:
    block = SHA256(
      okm
      || 0x00
      || uint8(i)
      || utf8("PPS/decimal")
    )

    x = uint64_be(block[0:8])

    if x < limit:
      value = x mod modulus
      return zero_padded_decimal(value, digits)

  fail

Failure is cryptographically negligible.

15.6. AmountMark Embedding

If an amount is known, the AmountMark is:

amount_mark = amount_minor mod 100

represented as two decimal digits.

The final Trust Code is:

trust_code = base_code || amount_mark

Example:

base_code = 493718
amount_minor = 2500067
amount_mark = 67
trust_code = 49371867

15.7. Trust Code Verification

A verifier MUST derive the expected Trust Code only after successful signature verification.

Trust Code comparison MUST be constant-time.


16. Trust Phrase

Trust Phrase support is OPTIONAL.

If implemented, the RECOMMENDED construction is:

phrase_index_0 = uint16_be(trust_okm[8:10])  mod 2048
phrase_index_1 = uint16_be(trust_okm[10:12]) mod 2048
phrase_index_2 = uint16_be(trust_okm[12:14]) mod 2048

Each index selects a word from the BIP39 English wordlist.

The Trust Phrase is:

word_0 + " " + word_1 + " " + word_2

Example:

river bronze lantern

The Trust Phrase is intended for visual comparison and SHOULD NOT be used as the sole authentication factor.


17. Context Binding

Context binding allows a Pulse to be bound to environmental or session data.

17.1. Context Hash

The context object is encoded using deterministic CBOR and hashed:

ctx = SHA256(cbor(context-object))
Field Meaning
session_id server session identifier
tx_hash transaction hash
ble_hash BLE proximity beacon hash
acoustic_hash acoustic nonce hash
ip_hash privacy-preserving IP hash
ua_hash user agent hash
device_integrity_hash device integrity measurement
custom_hash application-specific hash

17.3. Proximity Example

An RP web client may broadcast a temporary BLE beacon:

ble_beacon = random_nonce || timestamp

The Authenticator receives the beacon and includes:

ble_hash = SHA256(ble_beacon)

The verifier compares the expected hash.

This does not prove physical presence with mathematical certainty, but increases phishing and relay cost.


18. Policy Binding

Policy binding ensures that the Pulse was generated under the security policy expected by the RP.

18.1. Policy Hash

pol = SHA256(cbor(policy-object))

18.2. Policy Verification

If the RP issued a policy, the verifier MUST compare the Pulse pol field with the expected policy hash.

If the values differ, the Pulse MUST be rejected.

18.3. Downgrade Protection

Verifiers SHOULD NOT accept Pulses with weaker policy hashes than required for the requested operation.

For high-value transactions, the policy SHOULD include:

require_amount_mark = true
require_biometric = true
require_secure_enclave = true
threshold >= 2, if applicable
max_amount_minor = expected maximum

19. Transaction Binding

PPS does not mandate a transaction format.

However, when transaction binding is used, the RP SHOULD define a canonical transaction object and compute:

tx_hash = SHA256(cbor(transaction-object))

The tx_hash is included in the context object.

If AmountMark is used, the transaction amount modulo 100 is embedded in the Trust Code.

The verifier MUST verify both:

  1. ctx matches expected context including tx_hash
  2. amt matches expected amount modulo 100, if present

20. Nonce and Replay Protection

20.1. Online Mode

In online mode, the verifier SHOULD issue a fresh nonce for each authentication attempt.

The nonce MUST be:

The RECOMMENDED nonce lifetime is 60 seconds.

20.2. Offline Mode

In offline mode, the nonce MAY be omitted.

Offline Pulses MUST be protected by:

Verifiers SHOULD treat offline Pulses as lower assurance unless additional context is present.

20.3. Counter

The operation counter ctr is a monotonic unsigned 64-bit integer.

A verifier MUST reject any Pulse where:

ctr <= stored_last_ctr

The counter MUST be updated only after successful cryptographic verification.


21. Error Handling

Implementations SHOULD use stable internal error codes.

Error Meaning
invalid_token parsing failure
bad_version unsupported version
bad_type incorrect message type
bad_alg algorithm not allowed
bad_rp RP hash mismatch
bad_epoch epoch outside allowed window
expired expiration passed
bad_nonce nonce missing, invalid, or reused
bad_context context hash mismatch
bad_policy policy hash mismatch
bad_amount AmountMark mismatch
unknown_key public key not recognized
bad_certificate rotation certificate invalid
bad_rotation_sequence rotation sequence mismatch
counter_replay counter not increasing
bad_signature signature verification failed
bad_trust_code Trust Code mismatch
revoked_device device key revoked
revoked_group threshold group revoked
threshold_not_met insufficient valid signatures
rate_limited too many attempts
internal_error server error

Verifiers SHOULD return a generic error to unauthenticated clients:

authentication_failed

Detailed errors MAY be logged server-side.


22. Optional URI Scheme

PPS defines the following URI scheme for challenge delivery:

pulseproof://

22.1. Challenge URI

pulseproof://challenge?
  v=1
  &rp=example.com
  &nonce=base64url
  &epoch=uint
  &session=base64url
  &tx=base64url
  &policy=base64url

Parameters:

Parameter Required Description
v YES protocol version, MUST be 1
rp YES RP identifier
nonce RECOMMENDED base64url nonce
epoch OPTIONAL server time hint
session OPTIONAL base64url session identifier
tx OPTIONAL base64url transaction hash
policy OPTIONAL base64url CBOR policy object

All base64url values MUST omit padding.


23. HTTP Binding

HTTP binding is OPTIONAL.

When used, the following JSON structure is RECOMMENDED.

23.1. Challenge Response

{
  "v": 1,
  "rp": "example.com",
  "nonce": "base64url",
  "epoch": 58972480,
  "session_id": "base64url",
  "tx_hash": "base64url",
  "policy": "base64url"
}

23.2. Verification Request

{
  "session_id": "base64url",
  "pulse_token": "base64url",
  "trust_code": "49371867"
}

23.3. Verification Response

For normal success:

{
  "status": "ok"
}

For duress success, the outward response SHOULD be indistinguishable:

{
  "status": "ok"
}

The verifier MAY include a Trust Phrase after successful verification if the UI requires mutual visual confirmation.


24. Conformance

24.1. Core Authenticator

A conformant PPS Core Authenticator MUST:

  1. Support PPS-ED25519-CBOR30.
  2. Generate Ed25519 keys.
  3. Generate deterministic CBOR structures.
  4. Sign Pulse messages.
  5. Derive Trust Codes.
  6. Increment counters monotonically.
  7. Support nonce-bound online Pulses.

A conformant PPS Core Authenticator SHOULD:

  1. Support secure hardware key storage.
  2. Support forward-secure key rotation.
  3. Support duress keys.
  4. Support context and policy binding.
  5. Support AmountMark.

24.2. Core Verifier

A conformant PPS Core Verifier MUST:

  1. Support PPS-ED25519-CBOR30.
  2. Verify Ed25519 signatures.
  3. Reject non-canonical CBOR.
  4. Verify RP hash.
  5. Verify epoch and expiration.
  6. Verify counters.
  7. Verify nonces when used.
  8. Verify Trust Codes using constant-time comparison when Trust Codes are used.
  9. Implement rate limiting.

A conformant PPS Core Verifier SHOULD:

  1. Support rotation certificates.
  2. Support duress key states.
  3. Support policy and context verification.
  4. Support threshold groups.
  5. Support transaction binding.

24.3. Forward-Secure Profile

An implementation claiming PPS Forward-Secure support MUST:

  1. Rotate signing keys as specified in Section 12.
  2. Include next_pk and rot_seq in single-device Pulses.
  3. Make a best-effort secure deletion of previous private keys.
  4. Support rotation certificates for offline catch-up.

24.4. Duress Profile

An implementation claiming PPS Duress support MUST:

  1. Register a separate duress public key.
  2. Sign Honey-Pulses with the duress private key.
  3. Avoid including explicit duress indicators in Pulse payloads.
  4. Return outwardly indistinguishable success responses when duress is detected.

24.5. Threshold Profile

An implementation claiming PPS Threshold support MUST:

  1. Register threshold groups with signer keys and threshold values.
  2. Require the stored threshold value, not the client-provided value alone.
  3. Verify at least the required number of valid signatures.
  4. Reject duplicate signer signatures.
  5. Maintain a monotonic group counter.

25. Security Considerations

25.1. Shared Secret Removal

PPS removes shared symmetric secrets from the verifier.

A verifier database breach exposes public keys and metadata, not private keys.

This is a significant improvement over TOTP shared secrets.

25.2. Phishing Resistance

PPS improves phishing resistance when used with nonce-bound challenges and full Pulse Token transmission.

A Pulse is bound to:

Manual Trust Code entry alone provides weaker protection.

25.3. Trust Code Entropy

A numeric Trust Code has limited entropy.

Approximate entropy:

6 digits  = 19.9 bits
8 digits  = 26.6 bits
10 digits = 33.2 bits
12 digits = 39.9 bits

Therefore, Trust Code-only fallback MUST be protected by strict rate limiting.

For high-security operations, the full Pulse Token SHOULD be used.

25.4. Replay Protection

Replay protection is provided by:

Verifiers MUST mark nonces as used.

Verifiers MUST update counters and key states atomically.

25.5. Forward Secrecy Limitations

The PPS forward-secure ratchet depends on secure deletion of old private keys.

If an attacker continuously controls the Authenticator, forward secrecy may not protect future operations.

Secure hardware and user-presence checks are strongly recommended.

25.6. Rotation Certificate Abuse

Rotation certificates are signed authorizations to advance a key state.

If an attacker obtains a valid rotation certificate before the verifier does, they may cause the verifier to advance key state.

This can cause denial-of-service or state desynchronization but does not by itself reveal private keys.

Mitigations:

25.7. Duress Safety

Duress detection MUST remain hidden from the client response.

Verifiers SHOULD equalize:

Silent alerts SHOULD respect user safety and legal requirements.

25.8. Threshold Mode

Threshold mode increases resilience against single-device compromise.

However, if all threshold devices are compromised or coerced, threshold protection fails.

Offline threshold communication channels such as BLE, NFC, or QR should be protected against relay when possible.

25.9. Malware and User Approval

If malware can trick the user into approving a malicious transaction, PPS cannot fully prevent fraud.

Mitigations:

25.10. Clock Skew

Large clock-skew windows increase replay risk.

Verifiers SHOULD use NTP or another trusted time source.

Authenticators SHOULD synchronize time when possible.

25.11. CBOR Parsing

Implementations MUST reject non-canonical CBOR.

CBOR parsers should be tested against malformed input, duplicate keys, indefinite lengths, and oversized structures.

25.12. Post-Quantum Considerations

Ed25519 is not believed to be secure against large-scale quantum computers.

Deployments requiring long-term quantum resistance SHOULD plan migration to ML-DSA or hybrid suites.


26. Privacy Considerations

26.1. Minimal Personal Data

PPS fields SHOULD avoid personally identifiable information.

Recommended:

user_handle = opaque random identifier
kid = opaque random identifier
gid = opaque random identifier

26.2. Pairwise Identifiers

Authenticators SHOULD use pairwise kid values per RP to reduce cross-service correlation.

26.3. Context Privacy

Context fields such as IP address or user agent SHOULD be hashed or salted if stored.

Example:

ip_hash = SHA256(ip || daily_salt)

Daily salts reduce long-term correlation.

26.4. Duress Privacy

Duress events SHOULD be accessible only to authorized security personnel.

Audit logs SHOULD be protected and access-controlled.


27. IANA Considerations

This document makes the following requests.

27.1. Media Type Registration

This document requests registration of the following media type:

application/pulseproof+cbor

Purpose:

Binary encoding of PulseProof Sentinel envelopes.

27.2. URI Scheme Registration

This document requests registration of the following URI scheme:

pulseproof

Purpose:

PulseProof Sentinel challenge and authentication handoff.

27.3. Algorithm Registry

This document defines a conceptual PulseProof Sentinel Algorithm Registry.

Initial values:

PPS-ED25519-CBOR30
PPS-MLDSA44-CBOR30
PPS-MLDSA65-CBOR30
PPS-HYB-ED25519-MLDSA44-CBOR30

Future updates SHOULD specify:


28. Normative References

[RFC2119]
Bradner, S., “Key words for use in RFCs to Indicate Requirement Levels”, BCP 14, RFC 2119, March 1997.

[RFC4086]
Eastlake 3rd, D., Schiller, J., and S. Crocker, “Randomness Requirements for Security”, BCP 106, RFC 4086, June 2005.

[RFC4648]
Josefsson, S., “The Base16, Base32, and Base64 Data Encodings”, RFC 4648, October 2006.

[RFC5869]
Krawczyk, H. and P. Eronen, “HMAC-based Extract-and-Expand Key Derivation Function (HKDF)”, RFC 5869, May 2010.

[RFC8032]
Josefsson, S. and I. Liusvaara, “Edwards-Curve Digital Signature Algorithm (EdDSA)”, RFC 8032, January 2017.

[RFC8174]
Leiba, B., “Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words”, BCP 14, RFC 8174, May 2017.

[RFC8949]
Bormann, C. and P. Hoffman, “Concise Binary Object Representation (CBOR)”, STD 94, RFC 8949, December 2020.

[FIPS180-4]
National Institute of Standards and Technology, “Secure Hash Standard (SHS)”, FIPS PUB 180-4.

[FIPS204]
National Institute of Standards and Technology, “Module-Lattice-Based Digital Signature Standard”, FIPS PUB 204.


29. Informative References

[RFC8610]
Birkholz, H., Vigano, C., and C. Bormann, “Concise Data Definition Language (CDDL): A Notational Convention to Express Concise Binary Object Representation (CBOR) and JSON Data Structures”, RFC 8610, June 2019.

[WebAuthn]
W3C, “Web Authentication: An API for accessing Public Key Credentials”.

[FIDO2]
FIDO Alliance, “FIDO2: WebAuthn and CTAP”.

[RFC6238]
M’Raihi, D., Machani, S., Pei, M., and J. Rydell, “TOTP: Time-Based One-Time Password Algorithm”, RFC 6238, May 2011.

[BIP39]
Bitcoin Improvement Proposal 39, “Mnemonic code for generating deterministic keys”.


Appendix A. CDDL Summary

This appendix is informative.

uint64 = uint .size 8

alg = tstr
bstr-id = bstr .size (16..64)
rp-hash = bstr .size 32
ctx-hash = bstr .size 32
pol-hash = bstr .size 32
public-key = bstr .size 32
signature = bstr .size 64
nonce = bstr .size (16..64)
amount-mark = uint .le 99

registration-payload = {
  1: 1,
  2: 1,
  3: alg,
  4: bstr-id,
  5: rp-hash,
  6: public-key,
  7: bstr-id,
  8: nonce,
  9: uint64,
  ? 10: tstr,
  ? 11: bstr,
  ? 12: public-key
}

registration-envelope = {
  1: bstr .cbor registration-payload,
  2: signature,
  ? 3: signature
}

rotation-payload = {
  1: 1,
  2: 3,
  3: public-key,
  4: public-key,
  5: uint64,
  6: uint64,
  7: uint64,
  ? 8: rp-hash,
  ? 9: nonce
}

rotation-certificate = {
  1: bstr .cbor rotation-payload,
  2: signature
}

single-mode-fields = (
  4: bstr-id,
  9: public-key,
  10: public-key,
  15: uint64
)

threshold-mode-fields = (
  20: bstr-id,
  21: uint
)

pulse-payload = {
  1: 1,
  2: 2,
  3: alg,
  5: rp-hash,
  6: uint64,
  7: uint64,
  8: uint64,
  ? 11: nonce,
  ? 12: ctx-hash,
  ? 13: pol-hash,
  ? 14: amount-mark,
  (single-mode-fields // threshold-mode-fields)
}

signature-map = {
  * bstr-id => signature
}

pulse-envelope = {
  1: bstr .cbor pulse-payload,
  2: signature-map,
  ? 3: [ + rotation-certificate ]
}

context-object = {
  ? 1: bstr,
  ? 2: bstr,
  ? 3: bstr,
  ? 4: bstr,
  ? 5: bstr,
  ? 6: bstr,
  ? 7: bstr,
  ? 8: bstr
}

policy-object = {
  ? 1: uint,
  ? 2: bool,
  ? 3: bool,
  ? 4: uint,
  ? 5: uint64,
  ? 6: bool,
  ? 7: uint,
  ? 8: bool,
  ? 9: bool,
  ? 10: bool
}

For single-device ratchet mode:

DeviceState {
  kid
  user_handle
  current_pk
  key_seq
  last_ctr
  revoked
  created_at
  updated_at
}

For duress:

DuressState {
  kid
  duress_pk
  duress_key_seq
  duress_last_ctr
}

For threshold groups:

ThresholdGroupState {
  gid
  rp_hash
  signers: map[kid => public_key]
  threshold
  last_ctr
  revoked
  created_at
  updated_at
}

For nonces:

NonceState {
  nonce
  session_id
  created_at
  expires_at
  used
}

The following order is RECOMMENDED:

1. Parse token.
2. Check version and type.
3. Check algorithm.
4. Check RP hash.
5. Check epoch and expiration.
6. Check nonce.
7. Check context hash.
8. Check policy hash.
9. Check AmountMark.
10. Process rotation certificates, if present.
11. Verify signatures.
12. Verify counters.
13. Verify rotation sequence.
14. Verify Trust Code, if entered.
15. Update state atomically.
16. Return outward response.

Appendix D. Patent and Implementation Notice

This specification describes novel combinations of asymmetric time-bound proofs, forward-secure key ratcheting, silent duress signaling, threshold offline co-signing, policy binding, context binding, and transaction-visible trust codes.

No representation is made regarding the patent status of these techniques in any jurisdiction.

Implementers intending to patent, commercialize, or deploy this protocol in regulated environments SHOULD perform prior-art searches and obtain appropriate legal review.

This specification is experimental and has not been independently audited.