Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
b9cf0f8
agentkeys: stage 2 (#90) — P-256 verifier, on-chain K11 binding, M-of…
WildmetaAgent May 19, 2026
0762b9b
docs: stage-2 Heima Mainnet deploy + test runbook + harness fixes
WildmetaAgent May 19, 2026
ca0cf7b
harness: v2-stage2-demo as single source of truth for deploy+test
WildmetaAgent May 19, 2026
4ed5030
harness: move stage-2 helper scripts into harness/scripts/
WildmetaAgent May 19, 2026
2aef452
harness: companion daemon serves real device_key_hash + clearer step-…
WildmetaAgent May 19, 2026
ef98918
harness/scripts: shared key-resolution lib so scripts accept raw-key …
WildmetaAgent May 19, 2026
ae8bffc
fix: WebAuthn challenge double-hash + empty cred-id bytes32
WildmetaAgent May 19, 2026
0f006a6
ui: distinguish PRIMARY vs COMPANION K11 ceremony pages
WildmetaAgent May 19, 2026
9fe530c
harness: integrate full M-of-N E2E test (3 devices + 2-of-2 revoke)
WildmetaAgent May 19, 2026
f87f55f
harness: auto-version companion when previous instance is revoked
WildmetaAgent May 19, 2026
141fbac
scripts: migrate stage-1 scripts to stage-2 ABI
WildmetaAgent May 19, 2026
cf67290
ops: K3 rotation runbook + script
WildmetaAgent May 19, 2026
b6bf392
audit: tier-A Merkle relay worker + on-chain appendRoot path
WildmetaAgent May 19, 2026
19132f4
email: agentkeys-worker-email — SES send + per-actor inbox list
WildmetaAgent May 19, 2026
bd4c51c
docs: K3 rotation test verdict — 4 rounds green on Heima Mainnet
WildmetaAgent May 19, 2026
c4c35fd
ui: enrollment page + macOS Touch ID dialog readability
WildmetaAgent May 19, 2026
60e4809
docs(arch): §16.4 brief intro to K3 rotation flow
WildmetaAgent May 19, 2026
5834c1d
fix(stage-2): codex adversarial review — 7 critical/high/medium findings
WildmetaAgent May 19, 2026
da24472
deploy: stage-2 contracts with codex fixes redeployed on Heima Mainnet
WildmetaAgent May 19, 2026
3c7c1c8
issue #90: co-locate audit/email/cred/memory workers on broker host (…
WildmetaAgent May 19, 2026
390bcbd
fix: worker-{creds,memory} need REGISTRY + K3_EPOCH_COUNTER addresses
WildmetaAgent May 19, 2026
0981a88
issue #90: wire audit + email workers into stage-1 + stage-2 demos
WildmetaAgent May 19, 2026
913179a
issue #90: wire OIDC federation into cred + memory workers (Q3)
WildmetaAgent May 20, 2026
18e709b
issue #90 codex review: fix downgrade attack + secret redaction
WildmetaAgent May 20, 2026
e9926ed
issue #90 codex review: close remaining 2 deferred findings
WildmetaAgent May 20, 2026
a85101a
harness: log phase-1 acceptance for PR #92 (3-demo verification)
WildmetaAgent May 20, 2026
f26db5f
stage-3: add worker encrypt/decrypt roundtrip tests (steps 11+12)
WildmetaAgent May 20, 2026
42ffc45
isolation invariants: codify the 4-layer rule + cross-actor test (ste…
WildmetaAgent May 20, 2026
690f54c
cap-token: data-class-explicit isolation (no cross-pollution between …
WildmetaAgent May 20, 2026
c2f8638
broker: bake contract addresses into systemd env (closes step-11 502)
WildmetaAgent May 20, 2026
a9d0409
broker + worker: parse_device_entry knows the 11-field struct (codex …
WildmetaAgent May 20, 2026
f75f102
stage-3 step 11+12: pass STS creds via X-Aws-* headers (fix s3_put 502)
WildmetaAgent May 20, 2026
6488ef0
stage-3 step 11+12: mint AGENT-side STS creds (correct principal-tag …
WildmetaAgent May 20, 2026
c55ea29
stage-3: tighten pass/fail per codex adversarial review (3 findings)
WildmetaAgent May 20, 2026
5b0516b
stage-3 summary: fix `local` outside function + handle cleanup-only i…
WildmetaAgent May 20, 2026
f9e19d8
harness: log codex round-2 fix + 13/13 stage-3 strict-mode verification
WildmetaAgent May 20, 2026
f1ba2f2
stage-3 codex round-3: close skip-bypass in steps 14+15 (cross-class)
WildmetaAgent May 20, 2026
66963c9
stage-3 codex round-4: cross-class test sends X-Aws-* headers (strict…
WildmetaAgent May 20, 2026
bb9901e
arch.md: document cap-token data_class binding + 4-layer isolation in…
WildmetaAgent May 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,58 @@ Switch with `awsp <profile>`; verify with `aws sts get-caller-identity`.
### Caller-ARN matching in scripts must be case-insensitive
Lowercase the caller_arn before matching, since the remote IAM user is `agentKeys-admin` (capital K) but operator scripts canonicalize on `agentkeys-admin`. Use `tr '[:upper:]' '[:lower:]'` (portable to /bin/bash 3.2) — not `${var,,}` (bash 4+).

## Per-actor + per-data-class isolation invariants (issue #90)

The OIDC + cap-token + IAM stack enforces a defense-in-depth chain across **four layers**. Every PR that touches storage, OIDC, the broker cap-mint flow, or the worker handlers MUST verify these invariants explicitly in a demo step. A change that doesn't add a corresponding test for the layer it touches is incomplete.

| Layer | Invariant | Enforced by | Canonical test |
|---|---|---|---|
| **1. Broker cap-mint** | The session JWT's `agentkeys.omni_account` claim MUST match the request's `operator_omni`. Also: `device.operator_omni == session_omni`, `device.actor_omni == req.actor_omni`, `device.roles & ROLE_CAP_MINT`, `isServiceInScope(operator, actor, service) == true`. Returns `OperatorMismatch` / `DeviceBindingMismatch` / `DeviceRoleMissing` / `ServiceNotInScope` otherwise. | [`handlers/cap.rs`](crates/agentkeys-broker-server/src/handlers/cap.rs) — `mint_cap()` | [`harness/v2-stage3-demo.sh`](harness/v2-stage3-demo.sh) step 13 (NEGATIVE cap-mint with cross-actor `operator_omni` → HTTP 4xx) |
| **2. Worker chain-verify** | Independent re-check of layer-1 invariants from the worker's perspective — defense-in-depth against broker compromise. `verify_signature` (broker cap-sig), `check_chain_device`, `check_chain_scope`, `check_chain_k3_epoch`. | [`crates/agentkeys-worker-creds/src/verify.rs`](crates/agentkeys-worker-creds/src/verify.rs) + 26 unit tests | [`harness/v2-stage3-demo.sh`](harness/v2-stage3-demo.sh) steps 11+12 (full HTTP roundtrip exercises every verify hook) |
| **3. AWS IAM PrincipalTag scoping** | STS creds minted via `AssumeRoleWithWebIdentity` carry `PrincipalTag/agentkeys_actor_omni`. S3 resources scoped via `${aws:PrincipalTag/agentkeys_actor_omni}` resource-ARN interpolation. `s3:ListBucket` MUST carry an `s3:prefix=bots/${PrincipalTag}/<class>/*` condition (codex P2 — split-statement v3 bucket policy). | [`scripts/provision-vault-role.sh`](scripts/provision-vault-role.sh) + [`scripts/provision-memory-role.sh`](scripts/provision-memory-role.sh) + [`scripts/apply-vault-bucket-policy.sh`](scripts/apply-vault-bucket-policy.sh) + [`scripts/apply-memory-bucket-policy.sh`](scripts/apply-memory-bucket-policy.sh) | [`harness/v2-stage3-demo.sh`](harness/v2-stage3-demo.sh) steps 4-9: POSITIVE write to own prefix, NEGATIVE write + LIST to cross-actor prefix → AccessDenied |
| **4. Per-data-class bucket separation** | Vault-role's IAM permissions MUST be scoped to the vault bucket only; memory-role to the memory bucket only. Vault creds in the wrong bucket → AccessDenied; memory creds in the vault bucket → AccessDenied. Per arch.md §17.2 ("sharing one role across data classes collapses blast radius"). | Per-data-class IAM roles (`agentkeys-vault-role`, `agentkeys-memory-role`) | [`harness/v2-stage3-demo.sh`](harness/v2-stage3-demo.sh) step 10 (vault creds → memory bucket, memory creds → vault bucket, both AccessDenied) |

**Test-discipline rule**: any PR that adds a NEW worker, a NEW data class (e.g. a payments worker), or a NEW broker auth method MUST extend the stage-3 demo with negative cross-isolation tests for ALL four layers. Don't ship the feature with only POSITIVE-path tests.

### Cap-tokens are data-class-explicit (issue #90 followup)

The broker mints FOUR cap endpoints — two per data class — and the `data_class` is a SIGNED FIELD in the cap payload. Workers reject caps whose `data_class` doesn't match their bucket. This is the cap-layer isolation gate, symmetric with the AWS IAM cross-bucket gate (layer 4) but at the broker-signed capability layer.

```
POST /v1/cap/cred-store → mints CapPayload { op: Store, data_class: Credentials, ... }
POST /v1/cap/cred-fetch → mints CapPayload { op: Fetch, data_class: Credentials, ... }
POST /v1/cap/memory-put → mints CapPayload { op: Store, data_class: Memory, ... }
POST /v1/cap/memory-get → mints CapPayload { op: Fetch, data_class: Memory, ... }
```

What this prevents:

```bash
# Operator A mints a credentials Store cap:
cred_cap=$(curl -X POST $BROKER/v1/cap/cred-store -d ...)
# → CapPayload { ..., op: store, data_class: credentials }

# Tries to abuse it against the memory worker:
curl -X POST https://memory.litentry.org/v1/memory/put -d '{"cap": '"$cred_cap"', "plaintext_b64": "..."}'
# → HTTP 403 cap_data_class_mismatch
# The memory worker's verify_cap() calls check_data_class(cap, DataClass::Memory),
# sees cap.payload.data_class == Credentials, rejects.
```

The reverse (memory cap submitted to cred worker) is symmetrically blocked.

**Why two endpoints per data class, not just one + a `data_class` query param**: by making the route the source of truth, the broker can't ever mint a `Memory` cap from a request that hit `/v1/cap/cred-*` — the variant is statically derived in `handlers/cap.rs`, not from user input. Mistakes-on-the-broker-side are impossible to construct.

**Why this matters beyond the IAM layer**: AWS IAM (layer 3+4) enforces cross-actor + cross-bucket isolation at the AWS-API call site. The `data_class` cap binding enforces it at the cap-authz site — earlier in the trust chain, before the worker even calls AWS. If the AWS IAM grants were ever accidentally too broad, the cap-layer check still rejects. Defense in depth.

Verified live:

- `harness/v2-stage3-demo.sh` step 14 — cred-class cap → memory worker → `cap_data_class_mismatch`
- `harness/v2-stage3-demo.sh` step 15 — memory-class cap → cred worker → `cap_data_class_mismatch`
- Unit tests: `crates/agentkeys-worker-creds/src/verify.rs::check_data_class_rejects_cross_class` + serialization test for `DataClass`

**When a third data class lands** (e.g. payments-audit per arch.md §15.6): mint two more endpoints (`/v1/cap/payaudit-store` + `/v1/cap/payaudit-fetch`), add `DataClass::PaymentsAudit` variant, plumb to the new worker. The pattern is closed-extension: existing data classes don't need to know about the new one.

## Development Workflow (Anthropic Harness Pattern)

On every session start:
Expand Down
40 changes: 40 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ members = [
"crates/agentkeys-broker-server",
"crates/agentkeys-worker-creds",
"crates/agentkeys-worker-memory",
"crates/agentkeys-worker-audit",
"crates/agentkeys-worker-email",
]

[workspace.dependencies]
Expand Down
132 changes: 108 additions & 24 deletions crates/agentkeys-broker-server/src/handlers/cap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,19 @@ impl CapOp {
}
}

/// Data class the cap-token is bound to. Mirror of
/// `agentkeys_worker_creds::verify::DataClass`. The broker mints with
/// the right variant for each endpoint (`/v1/cap/cred-*` → Credentials,
/// `/v1/cap/memory-*` → Memory) and signs it into the payload; workers
/// reject caps whose data_class doesn't match their bucket. Issue #90
/// followup — codified in CLAUDE.md.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DataClass {
Credentials,
Memory,
}

/// Cap payload — the signed-over portion of a cap-token. The worker
/// verifies `Sha256(json(payload))` against `broker_sig` using the
/// broker's session-keypair public key before honoring the cap.
Expand All @@ -67,6 +80,9 @@ pub struct CapPayload {
pub actor_omni: String,
pub service: String,
pub op: CapOp,
/// Data class binding (issue #90 followup). REQUIRED; workers reject
/// caps whose data_class doesn't match their bucket.
pub data_class: DataClass,
pub device_key_hash: String,
pub k3_epoch: u64,
pub issued_at: u64,
Expand Down Expand Up @@ -158,15 +174,34 @@ pub async fn cap_cred_store(
headers: HeaderMap,
Json(req): Json<CapRequest>,
) -> Result<Json<CapToken>, CapError> {
mint_cap(state, headers, req, CapOp::Store).await.map(Json)
mint_cap(state, headers, req, CapOp::Store, DataClass::Credentials).await.map(Json)
}

pub async fn cap_cred_fetch(
State(state): State<SharedState>,
headers: HeaderMap,
Json(req): Json<CapRequest>,
) -> Result<Json<CapToken>, CapError> {
mint_cap(state, headers, req, CapOp::Fetch).await.map(Json)
mint_cap(state, headers, req, CapOp::Fetch, DataClass::Credentials).await.map(Json)
}

// Memory cap-mint endpoints (issue #90 followup): per-data-class
// explicit binding. The minted cap carries data_class=Memory; the cred
// worker would reject it via verify::check_data_class.
pub async fn cap_memory_put(
State(state): State<SharedState>,
headers: HeaderMap,
Json(req): Json<CapRequest>,
) -> Result<Json<CapToken>, CapError> {
mint_cap(state, headers, req, CapOp::Store, DataClass::Memory).await.map(Json)
}

pub async fn cap_memory_get(
State(state): State<SharedState>,
headers: HeaderMap,
Json(req): Json<CapRequest>,
) -> Result<Json<CapToken>, CapError> {
mint_cap(state, headers, req, CapOp::Fetch, DataClass::Memory).await.map(Json)
}

// ─── cap construction ──────────────────────────────────────────────────
Expand All @@ -176,6 +211,7 @@ async fn mint_cap(
headers: HeaderMap,
req: CapRequest,
op: CapOp,
data_class: DataClass,
) -> Result<CapToken, CapError> {
validate_hex32(&req.operator_omni, "operator_omni")?;
validate_hex32(&req.actor_omni, "actor_omni")?;
Expand Down Expand Up @@ -256,6 +292,7 @@ async fn mint_cap(
actor_omni: format!("0x{}", req_actor.clone()),
service: req.service.to_lowercase(),
op,
data_class,
device_key_hash: format!("0x{}", strip_0x_lc(&req.device_key_hash)),
k3_epoch,
issued_at: now,
Expand Down Expand Up @@ -369,18 +406,29 @@ async fn call_get_device(
/// bool revoked (word 6, right-aligned)
fn parse_device_entry(raw: &str) -> Result<DeviceEntry, CapError> {
let hex = raw.trim_start_matches("0x");
if hex.len() < 7 * 64 {
// DeviceEntry post codex H1 (SidecarRegistry.sol) has 11 ABI words:
// word 0 operatorOmni bytes32
// word 1 actorOmni bytes32
// word 2 k11CredId bytes32
// word 3 k11RpIdHash bytes32 (NEW, codex H1)
// word 4 k11PubX uint256 (NEW, codex H1)
// word 5 k11PubY uint256 (NEW, codex H1)
// word 6 tier uint8 (padded)
// word 7 roles uint8 (padded)
// word 8 registeredAt uint64 (padded)
// word 9 lastSignCount uint32 (padded)
// word 10 revoked bool (padded)
if hex.len() < 11 * 64 {
return Err(CapError::ChainRpc(format!(
"getDevice returned {} bytes; expected ≥ 7×32",
"getDevice returned {} bytes; expected ≥ 11×32 (post codex H1 struct)",
hex.len() / 2
)));
}
let operator_omni = hex[0..64].to_lowercase();
let actor_omni = hex[64..128].to_lowercase();
// word 3 = tier (skip); word 4 = roles; word 5 = registeredAt; word 6 = revoked
let roles_hex = &hex[4 * 64..5 * 64];
let registered_hex = &hex[5 * 64..6 * 64];
let revoked_hex = &hex[6 * 64..7 * 64];
let roles_hex = &hex[7 * 64..8 * 64];
let registered_hex = &hex[8 * 64..9 * 64];
let revoked_hex = &hex[10 * 64..11 * 64];
// Take last 2 hex chars (uint8) of the roles word.
let roles = u8::from_str_radix(&roles_hex[62..64], 16).unwrap_or(0);
let registered_at = u64::from_str_radix(&registered_hex[48..64], 16).unwrap_or(0);
Expand Down Expand Up @@ -582,17 +630,22 @@ mod tests {

#[test]
fn parse_device_entry_decodes_well_formed() {
// Hand-built: 7 words of 32 bytes each. operator/actor are
// `0xaa…` and `0xbb…`; tier=1, roles=7 (CAP_MINT|RECOVERY|SCOPE_MGMT),
// 11 ABI words (post codex H1): operator + actor + k11{CredId,
// RpIdHash, PubX, PubY} + tier + roles + registeredAt +
// lastSignCount + revoked. roles=7 (CAP_MINT|RECOVERY|SCOPE_MGMT),
// registeredAt=42, revoked=false.
let mut raw = String::from("0x");
raw.push_str(&"a".repeat(64)); // operatorOmni
raw.push_str(&"b".repeat(64)); // actorOmni
raw.push_str(&"0".repeat(64)); // k11CredId (zero)
raw.push_str(&format!("{:0>64x}", 1u64)); // tier=1
raw.push_str(&format!("{:0>64x}", 7u64)); // roles=7
raw.push_str(&format!("{:0>64x}", 42u64)); // registeredAt=42
raw.push_str(&"0".repeat(64)); // revoked=false
raw.push_str(&"a".repeat(64)); // operatorOmni
raw.push_str(&"b".repeat(64)); // actorOmni
raw.push_str(&"0".repeat(64)); // k11CredId
raw.push_str(&"0".repeat(64)); // k11RpIdHash
raw.push_str(&"0".repeat(64)); // k11PubX
raw.push_str(&"0".repeat(64)); // k11PubY
raw.push_str(&format!("{:0>64x}", 1u64)); // tier=1
raw.push_str(&format!("{:0>64x}", 7u64)); // roles=7
raw.push_str(&format!("{:0>64x}", 42u64)); // registeredAt=42
raw.push_str(&"0".repeat(64)); // lastSignCount=0
raw.push_str(&"0".repeat(64)); // revoked=false
let entry = parse_device_entry(&raw).unwrap();
assert_eq!(entry.operator_omni, "a".repeat(64));
assert_eq!(entry.actor_omni, "b".repeat(64));
Expand All @@ -604,13 +657,17 @@ mod tests {
#[test]
fn parse_device_entry_detects_revoked() {
let mut raw = String::from("0x");
raw.push_str(&"a".repeat(64));
raw.push_str(&"b".repeat(64));
raw.push_str(&"0".repeat(64));
raw.push_str(&format!("{:0>64x}", 1u64));
raw.push_str(&format!("{:0>64x}", 1u64));
raw.push_str(&format!("{:0>64x}", 100u64));
raw.push_str(&format!("{:0>64x}", 1u64)); // revoked=true
raw.push_str(&"a".repeat(64)); // operatorOmni
raw.push_str(&"b".repeat(64)); // actorOmni
raw.push_str(&"0".repeat(64)); // k11CredId
raw.push_str(&"0".repeat(64)); // k11RpIdHash
raw.push_str(&"0".repeat(64)); // k11PubX
raw.push_str(&"0".repeat(64)); // k11PubY
raw.push_str(&format!("{:0>64x}", 1u64)); // tier
raw.push_str(&format!("{:0>64x}", 1u64)); // roles
raw.push_str(&format!("{:0>64x}", 100u64)); // registeredAt
raw.push_str(&"0".repeat(64)); // lastSignCount
raw.push_str(&format!("{:0>64x}", 1u64)); // revoked=true
let entry = parse_device_entry(&raw).unwrap();
assert!(entry.revoked);
}
Expand All @@ -628,6 +685,7 @@ mod tests {
actor_omni: format!("0x{}", "b".repeat(64)),
service: "openrouter".into(),
op: CapOp::Store,
data_class: DataClass::Credentials,
device_key_hash: format!("0x{}", "c".repeat(64)),
k3_epoch: 1,
issued_at: 1,
Expand All @@ -637,9 +695,35 @@ mod tests {
let j = serde_json::to_string(&p).unwrap();
assert!(j.contains("\"device_key_hash\""));
assert!(j.contains("\"op\":\"store\""));
assert!(j.contains("\"data_class\":\"credentials\""));
assert!(j.contains("\"issued_at\":1"));
}

#[test]
fn cap_payload_serializes_data_class_per_endpoint() {
// The data_class is what makes the cap-token data-class-explicit;
// cred-store endpoints mint with Credentials, memory-* with Memory.
for (dc, expect) in [
(DataClass::Credentials, "credentials"),
(DataClass::Memory, "memory"),
] {
let p = CapPayload {
operator_omni: format!("0x{}", "a".repeat(64)),
actor_omni: format!("0x{}", "b".repeat(64)),
service: "openrouter".into(),
op: CapOp::Store,
data_class: dc,
device_key_hash: format!("0x{}", "c".repeat(64)),
k3_epoch: 1,
issued_at: 1,
expires_at: 100,
nonce: "00".repeat(16),
};
let j = serde_json::to_string(&p).unwrap();
assert!(j.contains(&format!("\"data_class\":\"{expect}\"")));
}
}

#[test]
fn extract_bearer_strips_prefix() {
let mut h = HeaderMap::new();
Expand Down
5 changes: 5 additions & 0 deletions crates/agentkeys-broker-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ pub fn create_router(state: SharedState) -> Router {
// doing any AES-256-GCM encrypt/decrypt + S3 PUT/GET.
.route("/v1/cap/cred-store", post(handlers::cap::cap_cred_store))
.route("/v1/cap/cred-fetch", post(handlers::cap::cap_cred_fetch))
// Per-data-class memory caps (issue #90 followup). Same shape +
// auth as cred caps but mints with data_class=Memory so the
// memory worker accepts and the cred worker rejects.
.route("/v1/cap/memory-put", post(handlers::cap::cap_memory_put))
.route("/v1/cap/memory-get", post(handlers::cap::cap_memory_get))
// Stage 7 §3.5 — pluggable auth surface.
.route(
"/v1/auth/wallet/start",
Expand Down
5 changes: 5 additions & 0 deletions crates/agentkeys-chain/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ evm_version = "london"
solc_version = "0.8.20"
optimizer = true
optimizer_runs = 200
# P256Verifier.sol uses Jacobian point ops with >16 local stack variables per
# function; legacy codegen hits "stack too deep". The IR pipeline reshuffles
# stack usage and compiles cleanly. No semantic change for the other 4
# contracts; tested 2026-05-19 against forge test --workspace.
via_ir = true
# Match arch.md §6 — events are part of the wire contract; treat them as
# strictly as we treat function signatures. Don't let solc silently elide
# unused params from event topics.
Expand Down
Loading
Loading