Signal Prekey Bundle Blueprint
Upload and retrieval of pre-key bundles combining EC signed keys, one-time EC keys, and post-quantum KEM keys for establishing end-to-end encrypted sessions
| Feature | signal-prekey-bundle |
| Category | Auth |
| Version | 1.0.0 |
| Tags | prekeys, end-to-end-encryption, ec-keys, kem-keys, double-ratchet, post-quantum, identity-keys, key-exchange |
| YAML Source | View on GitHub |
| JSON API | signal-prekey-bundle.json |
Fields
| Name | Type | Required | Label | Description |
|---|---|---|---|---|
identity_type | select | Yes | Identity Type | |
ec_one_time_prekeys | json | No | EC One-Time Pre-Keys | |
ec_signed_prekey | json | No | Signed EC Pre-Key | |
pq_one_time_prekeys | json | No | KEM One-Time Pre-Keys | |
pq_last_resort_prekey | json | No | KEM Last-Resort Pre-Key | |
target_identifier | token | No | Target Account Identifier | |
target_device_id | text | No | Target Device ID | |
unidentified_access_key | token | No | Unidentified Access Key | |
group_send_token | token | No | Group Send Endorsement Token | |
digest | token | No | Key Consistency Digest |
Rules
- signatures:
- all_signed_keys_must_validate_against_identity_key: true
- upload_limits:
- max_ec_one_time_prekeys_per_upload: 100
- max_kem_one_time_prekeys_per_upload: 100
- non_empty_ec_list_replaces_existing_pool: true
- non_empty_kem_list_replaces_existing_pool: true
- identity_key_changes:
- only_primary_device_may_change: true
- fetch_authorization:
- requires_exactly_one_of: account_credentials, unidentified_access_key, group_send_token
- group_send_token_must_be_verified_against_server_secret: true
- fetch_rate_limiting:
- per_requesting_account: true
- key_consumption:
- ec_one_time_keys_are_consumed_on_fetch: true
- kem_one_time_keys_are_consumed_on_fetch: true
- missing_signed_ec_or_kem_key_excludes_device: true
- consistency_check:
- digest_algorithm: SHA-256
- digest_covers: identity_key, signed_ec_prekey_id, signed_ec_prekey_bytes, last_resort_kem_key_id, last_resort_kem_key_bytes
Outcomes
Upload_invalid_signatures (Priority: 1) — Error: PREKEY_INVALID_SIGNATURE
Given:
- one or more submitted pre-keys have signatures that do not match the account identity key
Then:
- emit_event event:
prekey.upload_signature_invalid
Result: HTTP 422 — invalid signature; no keys stored
Upload_identity_change_non_primary (Priority: 2) — Error: PREKEY_IDENTITY_CHANGE_FORBIDDEN
Given:
- ec_signed_prekey is present
- requesting device is not the primary device
Then:
- emit_event event:
prekey.identity_change_unauthorized
Result: HTTP 403 — only the primary device may change identity keys
Upload_success (Priority: 3) | Transaction: atomic
Given:
- device is authenticated
- all submitted keys have valid signatures
Then:
- create_record target:
ec_one_time_prekey_pool— Store EC one-time pre-keys if list is non-empty - create_record target:
ec_signed_prekey_store— Replace signed EC pre-key if provided - create_record target:
kem_one_time_prekey_pool— Store KEM one-time pre-keys if list is non-empty - create_record target:
kem_last_resort_prekey_store— Replace KEM last-resort key if provided - emit_event event:
prekey.uploaded
Result: HTTP 200 — pre-keys stored successfully
Fetch_unauthenticated (Priority: 4) — Error: PREKEY_FETCH_UNAUTHORIZED
Given:
target_identifier(input) exists- requesting device is not authenticated
unidentified_access_key(input) not_existsgroup_send_token(input) not_exists
Then:
- emit_event event:
prekey.fetch_unauthorized
Result: HTTP 401 — at least one authorization mechanism required
Fetch_ambiguous_auth (Priority: 5) — Error: PREKEY_FETCH_AMBIGUOUS_AUTH
Given:
group_send_token(input) exists- ANY: requesting device is authenticated OR
unidentified_access_key(input) exists
Then:
- emit_event event:
prekey.fetch_ambiguous_auth
Result: HTTP 400 — only one authorization mechanism may be used per request
Fetch_group_token_invalid (Priority: 6) — Error: PREKEY_GROUP_TOKEN_INVALID
Given:
group_send_token(input) exists- token verification fails against server secret params or target identifier
Then:
- emit_event event:
prekey.fetch_group_token_invalid
Result: HTTP 401 — group send token is invalid or expired
Fetch_rate_limited (Priority: 7) — Error: PREKEY_FETCH_RATE_LIMITED
Given:
- authenticated account has exceeded pre-key fetch rate limit
Then:
- emit_event event:
prekey.fetch_rate_limited
Result: HTTP 429 with Retry-After header
Fetch_not_found (Priority: 8) — Error: PREKEY_NOT_FOUND
Given:
target_identifier(input) exists- target account or device does not exist or has no available pre-keys for any requested device
Then:
- emit_event event:
prekey.fetch_not_found
Result: HTTP 404 — target not found or has no keys
Fetch_success (Priority: 9)
Given:
target_identifier(input) exists- target account exists and at least one device has both a signed EC key and a KEM key
- requester holds valid authorization credentials
Then:
- delete_record — Consume one EC one-time pre-key per device if available
- delete_record — Consume one KEM one-time pre-key per device; fall back to last-resort if pool empty
- emit_event event:
prekey.fetched
Result: HTTP 200 — pre-key bundle with identity key, signed EC key, optional unsigned EC key, and KEM key per device
Consistency_check_pass (Priority: 10)
Given:
digest(input) exists- computed SHA-256 over identity key and repeated-use keys matches submitted digest
Then:
- emit_event event:
prekey.consistency_verified
Result: HTTP 200 — client and server have consistent repeated-use key views
Consistency_check_fail (Priority: 11) — Error: PREKEY_CONSISTENCY_MISMATCH
Given:
digest(input) exists- ANY: computed digest does not match submitted digest OR signed EC pre-key or last-resort KEM key not found on server
Then:
- emit_event event:
prekey.consistency_mismatch
Result: HTTP 409 — key views inconsistent; client should re-upload repeated-use keys
Errors
| Code | Status | Message | Retry |
|---|---|---|---|
PREKEY_INVALID_SIGNATURE | 422 | One or more pre-key signatures are invalid. | No |
PREKEY_IDENTITY_CHANGE_FORBIDDEN | 403 | Identity key changes can only be made from the primary device. | No |
PREKEY_FETCH_UNAUTHORIZED | 401 | Authentication required to retrieve pre-key bundles. | No |
PREKEY_FETCH_AMBIGUOUS_AUTH | 400 | Multiple authorization methods were provided. Use only one. | No |
PREKEY_GROUP_TOKEN_INVALID | 401 | Group send authorization token is invalid. | No |
PREKEY_FETCH_RATE_LIMITED | 429 | Too many key fetch requests. Please wait before trying again. | Yes |
PREKEY_NOT_FOUND | 404 | The requested account or device has no available pre-keys. | No |
PREKEY_CONSISTENCY_MISMATCH | 409 | Your device keys are out of sync. Please re-upload your pre-keys. | No |
Events
| Event | Description | Payload |
|---|---|---|
prekey.uploaded | Pre-keys successfully stored for a device | account_id, device_id, identity_type, ec_count, pq_count |
prekey.fetched | Pre-key bundle retrieved for a target account | target_identifier, device_count, ec_one_time_available, kem_one_time_available |
prekey.upload_signature_invalid | Pre-key upload rejected due to invalid signature | account_id, identity_type |
prekey.identity_change_unauthorized | Non-primary device attempted to change identity key | account_id, device_id |
prekey.consistency_verified | Client and server have consistent repeated-use key views | account_id, device_id, identity_type |
prekey.consistency_mismatch | Client and server pre-key views are inconsistent | account_id, device_id, identity_type |
prekey.fetch_rate_limited | Pre-key fetch rate limit exceeded | account_id, target_identifier |
prekey.fetch_unauthorized | Pre-key fetch attempted without authorization | target_identifier |
prekey.fetch_ambiguous_auth | Pre-key fetch provided multiple conflicting authorization mechanisms | target_identifier |
prekey.fetch_group_token_invalid | Group send token failed verification | target_identifier |
prekey.fetch_not_found | Target account or device has no available pre-keys | target_identifier, target_device_id |
Related Blueprints
| Feature | Relationship | Reason |
|---|---|---|
| phone-number-registration | required | Signed pre-keys for both identities are required at account registration time |
| one-time-prekey-replenishment | required | One-time pre-key pools must be monitored and replenished after keys are consumed during key exchange |
AGI Readiness
Goals
Reliable Signal Prekey Bundle
Upload and retrieval of pre-key bundles combining EC signed keys, one-time EC keys, and post-quantum KEM keys for establishing end-to-end encrypted sessions
Success Metrics:
| Metric | Target | Measurement |
|---|---|---|
| unauthorized_access_rate | 0% | Failed authorization attempts that succeed |
| response_time_p95 | < 500ms | 95th percentile response time |
Constraints:
- security (non-negotiable): Follow OWASP security recommendations
- security (non-negotiable): Sensitive fields must be encrypted at rest and never logged in plaintext
Autonomy
Level: supervised
Human Checkpoints:
- before modifying sensitive data fields
Escalation Triggers:
error_rate > 5consecutive_failures > 3
Verification
Invariants:
- sensitive fields are never logged in plaintext
- all data access is authenticated and authorized
- error messages never expose internal system details
Tradeoffs
| Prefer | Over | Reason |
|---|---|---|
| security | performance | authentication must prioritize preventing unauthorized access |
Coordination
Protocol: request_response
Consumes:
| Capability | From | Fallback |
|---|---|---|
phone_number_registration | phone-number-registration | fail |
one_time_prekey_replenishment | one-time-prekey-replenishment | fail |
Safety
| Action | Permission | Cooldown | Max Auto |
|---|---|---|---|
| upload_invalid_signatures | autonomous | - | - |
| upload_identity_change_non_primary | supervised | - | - |
| upload_success | autonomous | - | - |
| fetch_unauthenticated | autonomous | - | - |
| fetch_ambiguous_auth | autonomous | - | - |
| fetch_group_token_invalid | autonomous | - | - |
| fetch_rate_limited | autonomous | - | - |
| fetch_not_found | autonomous | - | - |
| fetch_success | autonomous | - | - |
| consistency_check_pass | autonomous | - | - |
| consistency_check_fail | autonomous | - | - |