lux/kms: orphan history at f2e793bf8 (was: kms: wire luxfi/mpc as REK backend)

Squashes all pre-rewrite history into a single root commit. Previous tree:
- 14506 commits including upstream Infisical fork history
- Multiple merge chains carrying pre-purge content

Force-rewrite per disk-space reclamation discipline. GHCR images
(1.9.9, 1.9.11) remain immutable; their git lineage is no longer
materialized.
This commit is contained in:
Hanzo AI
2026-06-07 14:28:09 -07:00
commit da574aa1db
8330 changed files with 2057089 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
.git
.github
k8s
k8-operator
data
*.md
LICENSE
Makefile
frontend/node_modules
**/node_modules
**/.next
docs
img
+8
View File
@@ -0,0 +1,8 @@
.git
vendor/
node_modules/
frontend/node_modules/
frontend/.next/
data/
*.tsbuildinfo
.DS_Store
+29
View File
@@ -0,0 +1,29 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
### Describe the bug
A clear and concise description of what the bug is.
### To Reproduce
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
### Expected behavior
A clear and concise description of what you expected to happen.
### Screenshots
If applicable, add screenshots to help explain your problem.
### Platform you are having the issue on:
### Additional context
Add any other context about the problem here.
+17
View File
@@ -0,0 +1,17 @@
---
name: Feature Request
about: Let us now what feature you would want to have in KMS
title: ''
labels: 'feature request'
assignees: ''
---
### Feature description
A clear and concise description of what the feature should be.
### Why would it be useful?
Why would this feature be useful for KMS users?
### Additional context
Add any other context about the problem here.
+25
View File
@@ -0,0 +1,25 @@
# Description 📣
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. Here's how we expect a pull request to be : https://lux.network/docs/contributing/getting-started/pull-requests -->
## Type ✨
- [ ] Bug fix
- [ ] New feature
- [ ] Improvement
- [ ] Breaking change
- [ ] Documentation
# Tests 🛠️
<!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. You may want to add screenshots when relevant and possible -->
```sh
# Here's some code block to paste some code snippets
```
---
- [ ] I have read the [contributing guide](https://lux.network/docs/contributing/getting-started/overview), agreed and acknowledged the [code of conduct](https://lux.network/docs/contributing/getting-started/code-of-conduct). 📝
<!-- If you have any questions regarding contribution, here's the FAQ : https://lux.network/docs/contributing/getting-started/faq -->
+79
View File
@@ -0,0 +1,79 @@
name: Build KMS
# Self-contained — calling hanzoai/.github/.github/workflows/docker-build.yml@main
# via `secrets: inherit` triggers a cross-org startup_failure (organization
# secrets aren't inherited across orgs even with `inherit`). Same pattern
# already used by build-server-self.yml.
on:
push:
branches: [main]
tags: ['v*']
workflow_dispatch:
permissions:
contents: read
packages: write
jobs:
build:
runs-on: lux-build
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- name: Compute tags
id: meta
run: |
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
VERSION="${GITHUB_REF#refs/tags/v}"
{
echo "tags<<EOF"
echo "ghcr.io/luxfi/kms:v${VERSION}"
echo "ghcr.io/luxfi/kms:${VERSION}"
echo "ghcr.io/luxfi/kms:sha-${GITHUB_SHA:0:7}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
else
{
echo "tags<<EOF"
echo "ghcr.io/luxfi/kms:sha-${GITHUB_SHA:0:7}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
fi
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
runner-amd64: luxfi-amd64
runner-arm64: luxfi-arm64
runner-deploy: lux-deploy-linux-amd64
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push (linux/amd64, with embedded SPA)
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile
platforms: linux/amd64
build-args: |
GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
runner-amd64: luxfi-amd64
runner-arm64: luxfi-arm64
runner-deploy: lux-deploy-linux-amd64
- name: Summary
run: |
echo "### Build complete" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
echo '${{ steps.meta.outputs.tags }}' >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
+53
View File
@@ -0,0 +1,53 @@
name: Build KMS Server (self-contained)
# Self-contained workflow — does NOT call hanzoai/.github reusable workflow.
# Runs directly on the amd64 ARC pool to bypass the cross-org `secrets:
# inherit` initialization failure that's blocking the standard pipeline.
on:
push:
branches: [main]
paths:
- 'cmd/**'
- 'pkg/**'
- 'go.mod'
- 'go.sum'
- 'Dockerfile.server'
- '.github/workflows/build-server-self.yml'
workflow_dispatch:
permissions:
contents: read
packages: write
jobs:
build:
runs-on: lux-build
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push (linux/amd64, headless server)
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.server
platforms: linux/amd64
build-args: |
GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}
push: true
tags: |
ghcr.io/luxfi/kms:server
ghcr.io/luxfi/kms:server-${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
+21
View File
@@ -0,0 +1,21 @@
# Binary
/kms
# Env files
.env
.env.*
!.env.example
# OS
.DS_Store
*~
*.swp
*.swo
# Editor
.vscode/
.idea/
# Data
/data/
*.json.bak
Symlink
+1
View File
@@ -0,0 +1 @@
LLM.md
Symlink
+1
View File
@@ -0,0 +1 @@
LLM.md
+421
View File
@@ -0,0 +1,421 @@
# P-Chain Atomic Key Rotation for MPC-Backed Validator Staking Keys
## Problem Statement
Validator staking on Lux requires three bound keys per NodeID:
- **TLS key** (staking certificate) -- establishes NodeID
- **BLS key** (aggregate signatures) -- ProofOfPossession in AddPermissionlessValidatorTx
- **RT key** (post-quantum / ML-DSA-65) -- RTProofOfPossession for Q-Chain validators
When these keys are managed by MPC threshold signing, two operations exist:
1. **Reshare/Refresh** -- same committee, rotated shares. Public key unchanged. No P-chain update needed.
2. **Rekey** -- committee size change, key migration, or compromise response. Public key changes. P-chain registration must update atomically.
The atomicity constraint: there must never be a window where the validator is offline (old key deregistered, new key not yet registered) or where two conflicting committees can sign for the same validator.
### When P-Chain Update IS Required
| Trigger | BLS Key Changes | RT Key Changes | TLS Key Changes |
|---------|----------------|----------------|-----------------|
| Routine reshare (same t-of-n) | No | No | No |
| Committee resize (3-of-5 to 4-of-7) | Yes | Yes | No |
| Initial migration (solo key to MPC) | Yes | Yes | Possibly |
| Emergency replacement (compromise) | Yes | Yes | Possibly |
### Invariants
1. At most one valid signing committee exists for a given NodeID at any time.
2. If the new key registration fails, the old key remains valid and the old committee retains signing authority.
3. BLS and RT keys rotate together -- never independently.
4. The old committee is deauthorized only after the new committee's registration is confirmed on-chain.
---
## Key Lifecycle State Machine
```
┌──────────────────┐
│ │
┌──────────────┤ Unmanaged │
│ migrate │ (solo key) │
│ └──────────────────┘
v
┌──────────────┐ reshare (same pubkey)
│ │◄──────────────────────────┐
│ Active │ │
│ (t-of-n) ├───────────────────────────►│
│ │ │
└──────┬───────┘ │
│ │
│ rekey requested │
│ (committee change │
│ or compromise) │
v │
┌──────────────┐ │
│ Rekeying │ new DKG runs │
│ (DKG) │ old committee still │
│ │ authoritative │
└──────┬───────┘ │
│ │
│ DKG succeeds │
│ │
v │
┌──────────────┐ │
│ Pending │ new HybridPoP computed │
│ Registration│ P-chain tx submitted │
│ │ old committee still signs │
└──────┬───────┘ │
│ │
│ P-chain tx confirmed │
│ │
v │
┌──────────────┐ │
│ Activating │ epoch boundary reached │
│ │ new committee starts │
│ │ signing consensus msgs │
└──────┬───────┘ │
│ │
│ old committee deauthorized │
│ old shares securely wiped │
│ │
└────────────────────────────────────┘
back to Active
─── Failure paths ───
Rekeying --[DKG fails]--> Active (old committee unchanged)
Pending --[tx rejected]--> Active (old committee, new shares wiped)
Activating --[epoch timeout]--> Rollback to old committee, alert
```
States:
- **Unmanaged**: Key exists outside MPC (file-based, HSM, etc.). Entry point for migration.
- **Active**: One MPC committee holds shares. Normal signing operations proceed.
- **Rekeying**: New DKG is running. Old committee remains authoritative. If DKG fails, return to Active.
- **PendingRegistration**: New keys exist but are not yet registered on P-chain. Old committee signs. New committee cannot sign consensus messages.
- **Activating**: P-chain has accepted the new keys. Waiting for epoch boundary to hand off signing authority.
- **Active** (again): New committee is sole authority. Old shares wiped.
---
## P-Chain Transaction Sequence
### Permissioned Chains (L1 Validators)
For L1 validators, the P-chain provides `RegisterL1ValidatorTx`, `SetL1ValidatorWeightTx`, and `DisableL1ValidatorTx`. The rotation sequence:
```
Time ─────────────────────────────────────────────────────────►
Old Committee (t1-of-n1) New Committee (t2-of-n2)
│ │
1. Continue signing │
│ │
2. Initiate DKG ─────────────────►│ DKG runs
│ │ New BLS + RT keys generated
│ │
3. Old committee signs │ New committee: keys exist,
RegisterL1ValidatorTx │ signing DISABLED
with new BLS PoP + RT PoP │
│ │
4. Wait for tx confirmation │
on P-chain (poll every 2s, │
timeout 5 minutes) │
│ │
5. P-chain confirms ─────────────►│ Signing ENABLED
│ │
6. Old committee signs │ New committee begins
SetL1ValidatorWeightTx │ signing consensus
weight=0 for old validator │
(or DisableL1ValidatorTx) │
│ │
7. Old shares wiped ──────────────│ Sole authority
│ │
DECOMMISSIONED │ Active
```
### Primary Network Validators
Primary network validators use `AddPermissionlessValidatorTx` which binds BLS key via `signer.Signer` (ProofOfPossession). The key is bound for the validator's staking period. Rotation requires:
1. Wait for current staking period to end (or use the re-staking window).
2. Re-register with `AddPermissionlessValidatorTx` using new HybridProofOfPossession.
3. No gap -- the new registration's start time equals old registration's end time.
For emergency rotation on primary network, the only option is to `DisableL1ValidatorTx` (if L1) or wait for staking period expiry (if permissionless). This is a protocol limitation, not a design choice.
### Q-Chain Validators (Hybrid BLS + RT)
Q-Chain validators require `HybridProofOfPossession` binding both BLS and RT keys. The rotation transaction must contain both proofs atomically. The MPC daemon generates both key types in a single DKG session (CGGMP21 for BLS, FROST for RT) and produces the combined `HybridProofOfPossession`.
---
## Operator CRD Extensions
### New Fields on MpcSpec
```rust
/// MPC configuration
pub struct MpcSpec {
// ... existing fields ...
/// Key lifecycle management
#[serde(skip_serializing_if = "Option::is_none")]
pub key_lifecycle: Option<KeyLifecycleSpec>,
}
/// Key lifecycle specification for MPC-backed validator keys
pub struct KeyLifecycleSpec {
/// Current key state (set by controller, read-only for users)
/// Values: Unmanaged, Active, Rekeying, PendingRegistration, Activating
pub state: String,
/// Active committee configuration
pub active_committee: CommitteeSpec,
/// Pending committee (populated during rekey, cleared on activation)
#[serde(skip_serializing_if = "Option::is_none")]
pub pending_committee: Option<CommitteeSpec>,
/// Rekey policy
#[serde(default)]
pub rekey_policy: RekeyPolicy,
}
/// MPC committee specification
pub struct CommitteeSpec {
/// Threshold (t in t-of-n)
pub threshold: u32,
/// Total parties
pub parties: u32,
/// MPC wallet ID (references key material in MPC daemon)
pub wallet_id: String,
/// BLS public key (hex, set after DKG)
#[serde(skip_serializing_if = "Option::is_none")]
pub bls_public_key: Option<String>,
/// RT public key (hex, set after DKG)
#[serde(skip_serializing_if = "Option::is_none")]
pub rt_public_key: Option<String>,
/// P-chain validation ID (set after registration tx confirms)
#[serde(skip_serializing_if = "Option::is_none")]
pub validation_id: Option<String>,
}
/// Rekey policy controls when and how rekeying is triggered
pub struct RekeyPolicy {
/// Allow automatic rekey when committee spec changes
#[serde(default)]
pub auto_rekey: bool,
/// Require manual approval before P-chain registration
#[serde(default = "default_true")]
pub require_approval: bool,
/// Timeout for DKG phase (seconds, default 300)
#[serde(default = "default_dkg_timeout")]
pub dkg_timeout_seconds: u64,
/// Timeout for P-chain tx confirmation (seconds, default 300)
#[serde(default = "default_tx_timeout")]
pub tx_timeout_seconds: u64,
}
```
### New Fields on LuxNetworkStatus
```rust
pub struct LuxNetworkStatus {
// ... existing fields ...
/// Per-validator key rotation status
#[serde(default)]
pub key_rotations: BTreeMap<String, KeyRotationStatus>,
}
pub struct KeyRotationStatus {
/// Current state
pub state: String,
/// Timestamp of last state transition
pub last_transition: String,
/// Active BLS public key (hex)
pub active_bls_key: Option<String>,
/// Active RT public key (hex)
pub active_rt_key: Option<String>,
/// Pending BLS public key during rotation (hex)
pub pending_bls_key: Option<String>,
/// Error message if rotation failed
pub error: Option<String>,
/// Number of failed attempts
pub failed_attempts: u32,
}
```
---
## Failure Modes and Recovery
### F1: DKG Failure (Rekeying -> Active rollback)
**Cause**: Network partition, node crash, or timeout during DKG.
**Detection**: DKG timeout (default 5 minutes) expires without all parties completing.
**Recovery**: Controller sets state back to Active. Old committee unchanged. New partial shares discarded by MPC daemon. No P-chain state affected.
**Operator action**: None. Controller retries on next reconcile if the desired committee spec still differs.
### F2: P-Chain Transaction Rejected (PendingRegistration -> Active rollback)
**Cause**: Insufficient balance, invalid PoP, concurrent registration, nonce conflict.
**Detection**: P-chain API returns error or tx not confirmed within timeout.
**Recovery**: Controller sets state back to Active. New committee shares preserved but signing disabled. Controller may retry with corrected tx parameters.
**Data cleanup**: If retry count exceeds 3, new committee shares are wiped and state returns to Active with old committee.
### F3: Epoch Activation Timeout (Activating -> Rollback)
**Cause**: New committee nodes not ready, network partition prevents consensus messages.
**Detection**: No consensus messages signed by new committee within 2 epochs (20 minutes at 10-minute epoch boundaries).
**Recovery**: Controller reverts to old committee. Submits DisableL1ValidatorTx for the new registration. Old committee resumes signing.
**Severity**: High. Requires investigation. Alerts fire.
### F4: Old Committee Compromise (Emergency Rekey)
**Cause**: Key material exposure detected.
**Detection**: External signal (security team, monitoring).
**Recovery**: Skip approval gate. Run DKG immediately. Submit registration tx with priority fee. Set weight=0 on old validator immediately after new registration confirms. Do not wait for epoch boundary.
**Trade-off**: Brief period (~30s) where both committees could theoretically sign. Acceptable because the compromised committee is being replaced, not the honest one.
### F5: Controller Crash Mid-Rotation
**Cause**: Controller pod restart during any rotation phase.
**Detection**: On startup, controller reads current state from CRD status.
**Recovery**: State machine is idempotent. Each state has a clear next action. Controller picks up where it left off:
- Rekeying: Check if DKG completed (poll MPC daemon). If yes, advance. If no, wait or timeout.
- PendingRegistration: Check if tx confirmed on P-chain. If yes, advance. If no, resubmit.
- Activating: Check if new committee is signing. If yes, advance. If no, wait or rollback.
### F6: Split Brain (Two Controllers)
**Cause**: Leader election failure.
**Prevention**: Controller uses K8s leader election (Lease). Only one controller writes status.
**Detection**: If two controllers write conflicting states, the CRD resourceVersion check causes one to fail.
**Recovery**: Failed controller retries with fresh read.
---
## MPC Daemon Changes
### New API Endpoints
```
POST /api/v1/keygen/validator
Request: { walletId, threshold, parties, nodeIds, keyTypes: ["bls", "rt"] }
Response: { walletId, blsPublicKey, rtPublicKey, blsProofOfPossession, rtProofOfPossession }
POST /api/v1/sign/validator-registration
Request: { walletId, txBytes }
Response: { signedTxBytes }
POST /api/v1/committee/deauthorize
Request: { walletId }
Response: { wiped: true }
GET /api/v1/committee/status/{walletId}
Response: { state, threshold, parties, blsPublicKey, rtPublicKey, canSign }
```
### Key Generation Changes
The existing dual keygen (CGGMP21 for ECDSA + FROST for EdDSA) must be extended:
1. **BLS keygen**: The MPC daemon runs distributed BLS key generation. Each node holds a share of the BLS private key. The combined public key and proof of possession are computed from threshold shares. This uses the `luxfi/crypto/bls` package's `SignProofOfPossession` method.
2. **RT keygen**: The MPC daemon runs distributed ML-DSA-65 key generation. Each node holds a share. The combined public key and proof of possession follow the same pattern as BLS using the Corona threshold package.
3. **Atomic dual keygen**: Both BLS and RT keygen run in the same session (like existing ECDSA+EdDSA). Both must succeed or both fail. The result is a `HybridProofOfPossession` containing both proofs.
### Signing Authority Fence
New field in key metadata stored by MPC daemon:
```go
type ValidatorKeyMeta struct {
WalletID string `json:"walletId"`
BLSPublicKey []byte `json:"blsPublicKey"`
RTPublicKey []byte `json:"rtPublicKey"`
ValidationID string `json:"validationId"` // P-chain validation ID
SigningEnabled bool `json:"signingEnabled"` // false until P-chain confirms
FencedAt int64 `json:"fencedAt"` // unix timestamp when deauthorized
CommitteeGen uint64 `json:"committeeGen"` // monotonic generation counter
}
```
The `SigningEnabled` flag prevents a new committee from signing consensus messages before P-chain registration confirms. The `FencedAt` timestamp prevents an old committee from signing after deauthorization. Both are enforced in the MPC daemon's signing handler -- if either guard rejects, the signing request returns an error.
The `CommitteeGen` counter is monotonically increasing. When the operator creates a new committee, it increments the generation. The MPC daemon only accepts signing requests where the request's generation matches the key metadata's generation.
---
## Implementation Phases
### Phase 1: State Machine in Operator (Week 1-2)
- Add `KeyLifecycleSpec` and `KeyRotationStatus` to CRD.
- Implement state machine transitions in `reconcile_network`.
- No actual MPC integration. States transition based on manual annotation updates.
- Deliverable: CRD schema, state machine tests.
### Phase 2: MPC Daemon Validator Keygen (Week 3-4)
- Add BLS threshold DKG to MPC daemon (using `luxfi/crypto/bls` threshold API).
- Add RT threshold DKG to MPC daemon (using `luxfi/Corona/threshold`).
- Add `HybridProofOfPossession` generation from threshold shares.
- Add signing authority fence (`SigningEnabled`, `CommitteeGen`).
- Deliverable: `/api/v1/keygen/validator` endpoint, unit tests with 3-node local cluster.
### Phase 3: P-Chain Transaction Builder (Week 5-6)
- Build `RegisterL1ValidatorTx` / `AddPermissionlessValidatorTx` construction from MPC-generated keys.
- P-chain tx submission and confirmation polling.
- Wire into operator reconcile loop (Rekeying -> PendingRegistration -> Activating transitions).
- Deliverable: Integration test: full rotation on devnet.
### Phase 4: Epoch Handoff and Cleanup (Week 7-8)
- Implement epoch-boundary activation (coordinate with Quasar EpochManager).
- Old committee deauthorization and share wiping.
- Emergency rekey fast path (skip approval, priority fees).
- Deliverable: E2E test: committee resize 3-of-5 to 4-of-7 on testnet with zero downtime.
### Phase 5: Production Hardening (Week 9-10)
- Failure injection testing (kill MPC node mid-DKG, kill controller mid-registration).
- Alerting integration (PagerDuty for stuck rotations, failed DKGs).
- Runbook for manual intervention.
- Dashboard UI for rotation status visibility.
- Deliverable: Production deployment on mainnet validators.
---
## Decisions
**Decision**: Two-phase registration with old-committee-signs-new-registration.
**Rationale**: The old committee is still authoritative, so it signs the P-chain tx that registers the new committee's keys. This avoids needing any out-of-band signing authority.
**Trade-off**: Requires old committee to be available during rotation. If old committee is completely unavailable (all nodes lost), manual key recovery from Shamir backup is required. Acceptable because total committee loss is a separate disaster recovery scenario.
**Decision**: Signing authority fence in MPC daemon, not in operator.
**Rationale**: The MPC daemon is the only component that actually produces signatures. Fencing at the signing layer is the only way to guarantee no unauthorized signatures. The operator controls when fences are raised/lowered, but enforcement is in the daemon.
**Trade-off**: MPC daemon becomes stateful about P-chain registration status. Adds coupling. Acceptable because the alternative (trusting the operator to never make mistakes) is worse.
**Decision**: No concurrent rotations. One rotation per NodeID at a time.
**Rationale**: Concurrent rotations create combinatorial failure modes. Sequential rotations are simpler and the time cost (minutes) is negligible for an operation that happens at most monthly.
**Trade-off**: If rotating 100 validators, they rotate sequentially. At ~5 minutes per rotation, that is ~8 hours. For fleet-wide rotation, we batch by subnet and run subnets in parallel. Acceptable.
+66
View File
@@ -0,0 +1,66 @@
# luxfi/kms — MPC-backed KMS + secrets UI on Hanzo Base
# Frontend: KMS React SPA
# Backend: Go + sqlcipher + MPC/ZAP
FROM node:22-alpine AS frontend
WORKDIR /src/frontend
COPY frontend/package.json frontend/pnpm-lock.yaml ./
RUN corepack enable pnpm && pnpm install --frozen-lockfile
COPY frontend/ .
RUN pnpm vite build
FROM golang:1.26.4-bookworm AS builder
# Auto-fetch the toolchain pinned in go.mod (avoids synctest panic with stale local toolchain)
ENV GOTOOLCHAIN=auto
RUN apt-get update && apt-get install -y --no-install-recommends \
libsqlcipher-dev gcc libc6-dev pkg-config git ca-certificates \
&& rm -rf /var/lib/apt/lists/*
ARG GITHUB_TOKEN
RUN git config --global url."https://${GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/"
ENV GOPRIVATE=github.com/luxfi/*,github.com/hanzoai/*
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Embed the React SPA into the Go binary at the embed.FS path (cmd/kms/web).
# The Makefile `copy-ui` target does this; we replicate it inline so the
# Dockerfile path is independent of `make`. Without this step, registerWebUI
# embeds an empty filesystem and `/` returns 404 even though the SPA is
# bundled at /app/frontend in the runtime layer.
COPY --from=frontend /src/frontend/dist /build/cmd/kms/web
# Per SCALE_STANDARD.md §2 (https://github.com/hanzoai/hips/blob/main/docs/SCALE_STANDARD.md)
# — every Go production Dockerfile that emits JSON to a client builds
# with GOEXPERIMENT=jsonv2. Verified -12% time / -23% allocs on the
# edge POST roundtrip vs encoding/json v1.
ARG GO_EXPERIMENT=jsonv2
ENV GOEXPERIMENT=${GO_EXPERIMENT}
# GOFLAGS=-mod=mod forces Go to fetch from the module cache instead of the
# in-tree vendor/. vendor/ is missing C headers for supranational/blst (.h
# files live in blst's source tree but `go mod vendor` strips them on the
# Go-only files filter). The cache has full module trees, so blst.h is
# resolvable. We do `go mod download` above to prime the cache.
RUN CGO_ENABLED=1 GOFLAGS=-mod=mod go build -tags "sqlite_fts5 sqlcipher" \
-ldflags="-s -w" -o /usr/local/bin/kms ./cmd/kms
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
libsqlcipher0 ca-certificates curl \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/local/bin/kms /usr/local/bin/kms
COPY --from=frontend /src/frontend/dist /app/frontend
RUN mkdir -p /data/kms
ENV KMS_FRONTEND_DIR=/app/frontend
ENV BASE_SKIP_ROOT_REDIRECT=1
ENV BASE_DISABLE_ADMIN_UI=1
EXPOSE 8080
HEALTHCHECK --interval=10s --timeout=3s --retries=3 \
CMD curl -f http://localhost:8080/healthz || exit 1
ENTRYPOINT ["kms", "serve", "--http=0.0.0.0:8080"]
+33
View File
@@ -0,0 +1,33 @@
# luxfi/kms-operator — minimal KMSSecret reconciler.
# Single static Go binary, no CGO, scratch runtime.
FROM golang:1.26.4-bookworm AS builder
ENV GOTOOLCHAIN=auto
ARG GITHUB_TOKEN
RUN git config --global url."https://${GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/"
ENV GOPRIVATE=github.com/luxfi/*,github.com/hanzoai/*
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates tzdata \
&& rm -rf /var/lib/apt/lists/* \
&& echo 'nonroot:x:65532:65532:nonroot:/home/nonroot:/sbin/nologin' >> /etc/passwd \
&& echo 'nonroot:x:65532:' >> /etc/group
WORKDIR /build
COPY go.mod go.sum ./
COPY vendor ./vendor
COPY cmd/kms-operator ./cmd/kms-operator
COPY pkg/zapclient ./pkg/zapclient
RUN CGO_ENABLED=0 go build -mod=vendor \
-ldflags="-s -w" -o /usr/local/bin/kms-operator ./cmd/kms-operator
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group
COPY --from=builder /usr/local/bin/kms-operator /usr/local/bin/kms-operator
USER 65532:65532
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/kms-operator"]
+7
View File
@@ -0,0 +1,7 @@
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates libsqlcipher0 tzdata && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY kms /usr/local/bin/kms
EXPOSE 8443 9652
ENTRYPOINT ["kms"]
CMD ["serve","--http=0.0.0.0:8443"]
+38
View File
@@ -0,0 +1,38 @@
# luxfi/kms — MPC-backed KMS server (headless, no React UI)
# Server-only build: Go + sqlcipher + MPC/ZAP. Skips the React SPA frontend
# stage so emulated cross-platform builds (M-series Mac → linux/amd64) don't
# trip QEMU's esbuild crash.
FROM golang:1.26.4-bookworm AS builder
ENV GOTOOLCHAIN=auto
RUN apt-get update && apt-get install -y --no-install-recommends \
libsqlcipher-dev gcc libc6-dev pkg-config git ca-certificates \
&& rm -rf /var/lib/apt/lists/*
ARG GITHUB_TOKEN
RUN git config --global url."https://${GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/"
ENV GOPRIVATE=github.com/luxfi/*,github.com/hanzoai/*
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=1 go build -tags "sqlite_fts5 sqlcipher" \
-ldflags="-s -w" -o /usr/local/bin/kms ./cmd/kms
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
libsqlcipher0 ca-certificates curl \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/local/bin/kms /usr/local/bin/kms
RUN mkdir -p /data/kms
ENV BASE_SKIP_ROOT_REDIRECT=1
ENV BASE_DISABLE_ADMIN_UI=1
EXPOSE 8080
HEALTHCHECK --interval=10s --timeout=3s --retries=3 \
CMD curl -f http://localhost:8080/healthz || exit 1
ENTRYPOINT ["kms", "serve", "--http=0.0.0.0:8080"]
+122
View File
@@ -0,0 +1,122 @@
Lux Ecosystem License
Version 1.2, December 2025
Copyright (c) 2020-2025 Lux Industries Inc.
All rights reserved.
TECHNOLOGY PORTFOLIO - PATENT APPLICATIONS PLANNED
Contact: licensing@lux.network
================================================================================
TERMS AND CONDITIONS
================================================================================
1. DEFINITIONS
"Lux Primary Network" means the official Lux blockchain with Network ID=1
and EVM Chain ID=96369.
"Authorized Network" means the Lux Primary Network, official testnets/devnets,
and any L1/L2/L3 chain descending from the Lux Primary Network.
"Descending Chain" means an L1/L2/L3 chain built on, anchored to, or deriving
security from the Lux Primary Network or its authorized testnets.
"Research Use" means non-commercial academic research, education, personal
study, or evaluation purposes.
"Commercial Use" means any use in connection with a product or service
offered for sale or fee, internal use by a for-profit entity, or any use
to generate revenue.
2. GRANT OF LICENSE
Subject to these terms, Lux Industries Inc grants you a non-exclusive,
royalty-free license to:
(a) Use for Research Use without restriction;
(b) Operate on the Lux Primary Network (Network ID=1, EVM Chain ID=96369);
(c) Operate on official Lux testnets and devnets;
(d) Operate L1/L2/L3 chains descending from the Lux Primary Network;
(e) Build applications within the Lux ecosystem;
(f) Contribute improvements back to the original repositories.
3. RESTRICTIONS
Without a commercial license from Lux Industries Inc, you may NOT:
(a) Fork the Lux Network or any Lux software;
(b) Create competing networks not descending from Lux Primary Network;
(c) Use for Commercial Use outside the Lux ecosystem;
(d) Sublicense or transfer rights outside the Lux ecosystem;
(e) Use to create competing blockchain networks, exchanges, custody
services, or cryptographic systems outside the Lux ecosystem.
4. NO FORKS POLICY
Lux Industries Inc maintains ZERO TOLERANCE for unauthorized forks.
Any fork or deployment on an unauthorized network constitutes:
(a) Breach of this license;
(b) Grounds for immediate legal action.
5. RIGHTS RESERVATION
All rights not explicitly granted are reserved by Lux Industries Inc.
We plan to apply for patent protection for the technology in this
repository. Any implementation outside the Lux ecosystem may require
a separate commercial license.
6. DISCLAIMER OF WARRANTY
THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
7. LIMITATION OF LIABILITY
IN NO EVENT SHALL LUX INDUSTRIES INC BE LIABLE FOR ANY CLAIM, DAMAGES
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE.
8. TERMINATION
This license terminates immediately upon any breach, including but not
limited to deployment on unauthorized networks or creation of forks.
9. GOVERNING LAW
This License shall be governed by the laws of the State of Delaware.
10. COMMERCIAL LICENSING
For commercial use outside the Lux ecosystem:
Lux Industries Inc.
Email: licensing@lux.network
Subject: Commercial License Request
================================================================================
TL;DR
================================================================================
- Research/academic use = OK
- Lux Primary Network (Network ID=1, Chain ID=96369) = OK
- L1/L2/L3 chains descending from Lux Primary Network = OK
- Commercial products outside Lux ecosystem = Contact licensing@lux.network
- Forks = Absolutely not
================================================================================
See LP-0012 for full licensing documentation:
https://github.com/luxfi/lps/blob/main/LPs/lp-0012-ecosystem-licensing.md
+18
View File
@@ -0,0 +1,18 @@
# Licensing
This repository is licensed under the **Lux Ecosystem License v1.2**
(see [LICENSE](LICENSE)). It belongs to the **patent-protected** tier
of the Lux three-tier IP strategy.
- Free for **Authorized Networks** (Lux Primary NetID=1, EVM 96369;
official testnets/devnets; Descending Chains).
- Free for **Research Use** (academic, education, evaluation).
- **Commercial use outside Authorized Networks requires a paid
commercial license**.
For the canonical Lux IP and licensing strategy and the precise
definitions of "Authorized Network", "Descending Chain", and "Research
Use", see:
<https://github.com/luxfi/.github/blob/main/profile/README.md>
For commercial licensing inquiries, contact `licensing@lux.network`.
+362
View File
@@ -0,0 +1,362 @@
# KMS — AI Assistant Knowledge Base
**Project**: Lux Key Management Service (KMS)
**Organization**: Lux Network
## One KMS per org. Env is a field, not a hostname.
There is one KMS endpoint per org. Every caller — devnet, testnet,
mainnet — points at the same `kms.lux.network` for Lux (or
`kms.hanzo.ai` for Hanzo, Zoo, Pars). The env (`dev` / `test` /
`main`) is a field on each secret, passed as `?env=` on GET/DELETE
and the `env` JSON field on POST. No `kms.dev.*` / `kms.test.*`
hostnames; that shape is removed.
## Mnemonic + key derivation
One BIP39 mnemonic is shared across all Lux-derived L1s (Lux, Hanzo,
Zoo, Pars). Each chain reads it from `providers/<org>/deploy-mnemonic`
under its own org-scoped JWT — same bytes, different KMS paths, N
independent auth boundaries. Each tenant is jurisdictionally separate
and holds its own mnemonic in `providers/liquidity/*`. See
`~/work/lux/CLAUDE.md` §"Mnemonic + Key Derivation" for the
canonical reference (paths, derivation formula, IAM apps required).
The IAM apps `lux-kms`, `hanzo-kms`, `zoo-kms`, `pars-kms` (all owner
admin, organization=<org>) MUST carry `client_credentials` in
`grant_types`. The canonical `/v1/kms/auth/login` forwards to IAM's
`/login/oauth/access_token` with that grant_type; if the app is
missing it, login returns 401 "invalid credentials". Fix by updating
the IAM `application.grant_types` JSON to include the value and
restart the IAM pod (it caches application records in-memory).
## MPC-rooted Root Encryption Key (2026-06-07)
The master-key split-brain that paused the Casibase→lux-kms-go cluster
rewrite is resolved by sourcing the Root Encryption Key (REK) from a
luxfi/mpc threshold cluster instead of a static K8s Secret env var.
### Boundary
```
KMS pod (one process)
↑ on boot, ONCE
↑ mpcrek.Bootstrap (pkg/store/mpcrek)
↑ → pkg/mpc.ZapClient.Decrypt(keyID="kms/rek/v1")
luxfi/mpc cluster (t-of-n)
- stores the wrapped REK as its own threshold record
- returns the unwrapped 32-byte REK over the AEAD-sealed ZAP wire
(X25519+ML-KEM-768 hybrid handshake)
```
The unwrapped REK lives only in the KMS pod's heap from boot to
shutdown. `defer mpcrek.Zero(rek)` in `main()` overwrites the slice on
the way out (best-effort — Go GC offers no harder guarantee). The
existing AES-256-GCM envelope (`pkg/store/crypto.go`) is untouched: it
still wraps every per-secret DEK under that 32-byte REK with AAD binding
path/name/env.
### Env contract (cmd/kms)
- `MPC_REK_ENDPOINT` — CSV of MPC `host:port`. When set, kmsd FAILS
CLOSED on any bootstrap failure (no fallback to env-var REK; that
would re-open the split-brain).
- `MPC_REK_KEY_ID` — MPC-side identifier, default `kms/rek/v1`. Bump
per epoch on reshare.
- `MPC_REK_TIMEOUT` — Go duration, default `10s`.
- `KMS_MASTER_KEY_B64` — LEGACY 32-byte master key (base64). Used only
when `MPC_REK_ENDPOINT` is unset. Slated for removal after every
deployment migrates.
### Casibase migration (`cmd/casibase-import`)
The ~50 secrets in `hanzo/kms` (Casibase Node-Fastify
`ghcr.io/hanzoai/kms:1.0.7`) are sealed under the Casibase
`ROOT_ENCRYPTION_KEY` env var. lux-kms-go uses an incompatible envelope
under an MPC-rooted REK. The one-shot bridge is `cmd/casibase-import`:
```
$ kubectl exec -n hanzo deploy/casibase-kms -- /api/v3/secrets/raw?... > dump.json
$ MPC_REK_ENDPOINT=mpc-0.lux-mpc.svc:9999,... casibase-import \
--in dump.json \
--old-key-file ./casibase-root.key \
--data-dir /data/kms \
--dry-run # verify decode first
$ casibase-import --in dump.json --old-key-file ./casibase-root.key --data-dir /data/kms
```
Status: SCAFFOLDED. The new-side (Seal under MPC-rooted REK, Put into
ZapDB) is complete. The Casibase decoder stub
(`cmd/casibase-import/main.go::decryptCasibase`) returns `not yet
implemented` until the Casibase v1.0.7 envelope format is decoded — see
the function's godoc for the extension point. The Casibase encryption
code at `hanzoai/kms@1.0.7:src/services/secret/encrypt.ts` is the
reference; once decoded here, the tool decrypts under the supplied OLD
key and re-seals under the live cluster's REK epoch.
### Re-key (REK rotation)
Out of scope for this PR. Design hook:
1. Operator triggers MPC reshare ceremony for `kms/rek/v(N+1)`.
2. Run `cmd/rek-rotate` (future): bootstrap epoch N + epoch N+1, walk
every record in ZapDB, Open under N, Seal under N+1, Put.
3. Roll KMS pods with `MPC_REK_KEY_ID=kms/rek/v(N+1)`.
Replica coordination during the migration window: every replica fetches
the same epoch N+1 from MPC (the cluster is the single source of
truth), so cross-replica consistency is automatic. There is no leader
election in KMS; the migration tool runs once from any pod or any
out-of-cluster operator with the MPC bearer.
### What this PR does NOT change
- The per-secret AES-256-GCM envelope shape (`pkg/store/crypto.go`).
- The ZapDB-at-rest encryption (`KMS_ENCRYPTION_KEY_B64`, separate
knob, controls Badger-level encryption — orthogonal to the
application envelope).
- The IAM JWT validation at the HTTP edge.
- The ZAP secrets-server wire shape (`pkg/zapserver`, `pkg/zapclient`).
### LP-103 bearer-mint (still future)
The published `pkg/zap/handshake.go` does not yet check `OpAuthHello`,
and the in-tree `pkg/mpc/zap_client.go` does not yet mint one. The
KMS↔MPC ZAP wire is currently authenticated at the K8s NetworkPolicy
layer, not the application layer. LP-103 introduces the bearer-on-
handshake check; it is a separate PR. When it lands, `mpcrek.Bootstrap`
gains a bearer parameter and `MPC_REK_ENDPOINT` becomes
mutual-auth-only. The wire-level upgrade is forward-compatible: until
MPC requires the bearer (`ZAP_AUTH_REQUIRED=true`) the existing
unauthenticated dial continues to work.
## v1.9.0 — pkg/iamclient + ZAP bearer-on-handshake (LP-103)
Pairs with luxfi/mpc v1.14.0 pkg/zapauth. KMS mints an OAuth2
client_credentials JWT against Hanzo IAM, caches per audience with
60-second early refresh, attaches it via OpAuthHello (0x00EF) BEFORE
the existing X25519+ML-KEM-768 handshake.
Env vars (all optional; if KMS_ZAP_AUTH_ENABLED unset/false the
client behaves exactly as v1.8.x):
KMS_ZAP_AUTH_ENABLED true|false (default false)
KMS_IAM_URL e.g. http://.lux.svc:8000
KMS_IAM_CLIENT_ID default ""
KMS_IAM_CLIENT_SECRET from KMS-projected universal-auth Secret
KMS_ZAP_AUDIENCE default ""
When enabled, mpc.NewZapClientWith dials, then sends OpAuthHello;
a non-2xx-shaped {"ok":true} reply fails NewZapClient — operators
must roll MPC to v1.14.0+ before flipping the flag.
## Fail-open MPC boot (v1.8.2+)
KMS no longer log.Fatalf's when MPC is unreachable at boot. If
`MPC_VAULT_ID` is set but the ZAP probe fails, KMS:
- logs a warning,
- runs in secrets-only mode (secrets-server, IAM SSO, secret routes
fully functional),
- responds 503 on `/v1/kms/keys/*` with body
`{"error":"mpc unreachable","mode":"secrets-only","detail":"..."}`,
- reports `status=degraded` on `/healthz` (still HTTP 200 — readiness
must not flap a working secrets surface out of rotation).
Each request to `/v1/kms/keys/*` re-probes MPC, so the same pod
recovers transparently when MPC comes back; no restart needed.
## Project Overview
KMS is an MPC-backed key management service for the Lux Network. It manages validator keys, threshold signing, secret storage, and key rotation using distributed Multi-Party Computation.
**No Infisical. No PostgreSQL. No Node.js.** The active server is a pure Go binary in `cmd/kms/` backed by `luxfi/mpc` for threshold cryptography and `luxfi/zapdb` for storage.
## Architecture
```
Client (ATS/BD/TA) → KMS (Go, :8080) → MPC (CGGMP21/FROST, via ZAP)
ZapDB (embedded)
ZapDB Replicator
S3 (age-encrypted)
```
### Storage: ZapDB (not SQLite, not PostgreSQL)
KMS uses `luxfi/zapdb` as its embedded storage engine. ZapDB is a Badger-derived LSM key-value store with built-in encrypted replication to S3.
**Why ZapDB over Base/SQLite:**
- Built-in `Replicator` with incremental + snapshot backup to S3 (no sidecar, no plugin)
- Age encryption on all replicated data; the active envelope is AES-256-GCM with X25519 key wrapping. X-Wing / ML-KEM-768 hybrid wrapping is roadmap, not in production.
- No WAL locking issues with single-writer — ZapDB handles concurrency natively
- Redis-compatible bindings available (`zapdb/bindings/`) for cache interop
- Eliminates the `hanzoai/base` dependency and its SQLite/Postgres abstraction layer
**S3 replication layout:**
```
s3://lux-kms-backups/kms/{node-id}/
├── snap/{timestamp}.zap.age # hourly full snapshots
└── inc/{version}.zap.age # 1s incremental backups
```
### Signing: MPC (not standalone crypto)
All key operations delegate to the MPC service at `~/work/lux/mpc/`. KMS never holds private key material — it holds metadata (validator IDs, wallet IDs, public keys, policy) and delegates all cryptographic operations to MPC.
**Two transport paths to MPC:**
- **ZAP (preferred, in-cluster):** `pkg/mpc/zap_client.go` — binary protocol over `luxfi/zap`, opcodes 0x0001-0x0031
- **HTTP (fallback, cross-cluster):** `pkg/mpc/client.go` — REST API over HTTP
**MPC operations:**
| Operation | Protocol | Opcode | HTTP Endpoint |
|-----------|----------|--------|---------------|
| Status | — | 0x0001 | GET /v1/status |
| Keygen | CGGMP21/FROST | 0x0010 | POST /v1/vaults/{id}/wallets |
| Sign | CGGMP21/FROST | 0x0011 | POST /v1/transactions |
| Reshare | CGGMP21/FROST | 0x0012 | POST /v1/wallets/{id}/reshare |
| GetWallet | — | 0x0020 | GET /v1/wallets/{id} |
| Encrypt | AES-GCM/TFHE | 0x0030 | POST /v1/fhe/encrypt |
| Decrypt | AES-GCM/TFHE | 0x0031 | POST /v1/fhe/decrypt |
### Auth: Hanzo IAM (JWKS validation)
KMS validates JWTs from Hanzo IAM via JWKS endpoint. All key management routes require superuser auth. Secret routes use per-principal access control.
### Encryption: KMS-native Transit Engine (EaaS)
The `pkg/store/crypto.go` implements envelope encryption:
- Per-secret random 256-bit DEK
- DEK wrapped under master key (AES-256-GCM)
- v2 path: ML-KEM-768 wrapping (PQ-safe)
- Threshold schemes: TFHE (secret reveal), CKKS (ML compute)
## Active code paths
| Path | Language | Purpose |
|------|----------|---------|
| `kms.go` | Go | **Canonical client API**`kms.{Get,GetSecrets,LoadEnv}` |
| `cmd/kms/` | Go | Server entrypoint |
| `pkg/keys/` | Go | Key lifecycle (generate, sign, rotate) — delegates to MPC |
| `pkg/attestation/` | Go | Composite confidential-attestation gate for epoch-key release (mirrors luxcpp/crypto/attestation C ABI) |
| `pkg/mpc/` | Go | MPC client (ZAP + HTTP transports to luxfi/mpc daemon) |
| `pkg/store/` | Go | ZapDB-backed metadata + secret store |
| `pkg/zapclient/` | Go | Low-level ZAP client (used by root `kms` package) |
| `pkg/zapserver/` | Go | ZAP server exposing SecretStore over luxfi/zap |
| `k8s/` | YAML | K8s manifests (StatefulSet + Service) |
## Canonical client usage
```go
import "github.com/luxfi/kms"
// One line at process start — populates os.Setenv with every secret.
func main() {
kms.LoadEnv()
db := os.Getenv("DATABASE_URL")
run(db)
}
// Programmatic fetch:
v, err := kms.Get(ctx, "DATABASE_URL")
all, err := kms.GetSecrets(ctx)
```
**Defaults** (override via env vars):
| Var | Default | Purpose |
|-----|---------|---------|
| `KMS_ADDR` | `zap.kms.svc.cluster.local:9999` | KMS host:port |
| `KMS_PATH` | `/` | secret path prefix |
| `KMS_ENV` | `default` | secret environment slug |
Transport is always native ZAP — there is no HTTP fallback in the Go client.
### Legacy code (not used by Go server)
| Path | Status | Notes |
|------|--------|-------|
| `backend/` | Legacy | Old Node.js/Fastify backend (from Infisical fork) |
| `frontend/` | Legacy | Old React dashboard |
## Key concepts
- **Validator Key Set**: A pair of MPC wallets (BLS secp256k1 + Corona ed25519) for a single validator
- **MPC DKG**: Distributed Key Generation — no single party ever holds the full private key
- **Threshold signing**: K-of-N parties must cooperate to produce a signature
- **Key rotation**: Reshare keys with new threshold or participant set without changing public key
- **ZapDB Replicator**: In-process encrypted streaming backup to S3 (incremental 1s + snapshot 1h)
## API routes
```
POST /v1/kms/keys/generate Generate validator key set (via MPC DKG)
GET /v1/kms/keys List all key sets
GET /v1/kms/keys/{id} Get key set by ID
POST /v1/kms/keys/{id}/sign Sign (key_type: "bls" or "Corona", delegates to MPC)
POST /v1/kms/keys/{id}/rotate Reshare with new threshold/participants (via MPC)
GET /v1/kms/status KMS + MPC cluster status
GET /healthz Health check
POST /v1/kms/auth/login Machine identity auth (IAM client_credentials)
GET /v1/kms/secrets/{name} Raw secret fetch
```
### ZAP transport (in-cluster, no HTTP)
```
OpSecretGet 0x0040 { path, name, env } → { value: base64 }
OpSecretPut 0x0041 { path, name, env, value } → { ok: true }
OpSecretList 0x0042 { path, env } → { names: [] }
OpSecretDelete 0x0043 { path, name, env } → { ok: true }
```
## Configuration (env vars)
| Var | Default | Purpose |
|-----|---------|---------|
| `MPC_ADDR` | (empty) | ZAP address (host:port); empty = mDNS discovery (dev only) |
| `MPC_VAULT_ID` | (required) | MPC vault ID for validator keys |
| `KMS_NODE_ID` | `kms-0` | ZAP node ID |
| `ZAP_PORT` | `9999` | ZAP secrets-server listen port (0 = disable) |
| `KMS_MASTER_KEY_B64` | — | 32-byte master key (base64) for SecretStore envelope |
| `KMS_DATA_DIR` | `/data/kms` | ZapDB data directory |
| `IAM_ENDPOINT` | `https://hanzo.id` | Hanzo IAM for auth |
| `REPLICATE_S3_ENDPOINT` | — | S3 endpoint for ZapDB replication |
| `REPLICATE_S3_BUCKET` | `lux-kms-backups` | S3 bucket |
| `REPLICATE_AGE_RECIPIENT` | — | Age public key for backup encryption |
| `REPLICATE_AGE_IDENTITY` | — | Age private key for restore decryption |
## K8s Deployment
StatefulSet (replicas=1) with PVC for ZapDB data. ZapDB Replicator runs in-process (no sidecar).
**Ports:**
- 8080: HTTP API (health, keys, secrets, auth)
- 9999: ZAP secrets server (in-cluster binary transport)
**Volumes:**
- `/data/kms`: ZapDB data directory (PVC, 5Gi)
**Connections:**
- MPC daemon: via ZAP (in-cluster) or HTTP (cross-cluster)
- S3: ZapDB Replicator (incremental + snapshot, age-encrypted)
- IAM: JWKS validation over HTTPS
## Integration
- **Auth**: Hanzo IAM JWT tokens (JWKS validation)
- **Callers**: ATS, BD, TA (all Go services)
- **Crypto**: luxfi/mpc (CGGMP21 for ECDSA, FROST for EdDSA)
- **Transport**: ZAP (luxfi/zap) for MPC communication and secret serving
- **Storage**: ZapDB (luxfi/zapdb) embedded — no external database
- **Replication**: ZapDB Replicator in-process (S3 + age encryption)
- **No lux/base fork**: KMS imports `luxfi/zapdb` directly for storage
## Dependencies
- `github.com/luxfi/zapdb` — embedded KV store with S3 replication
- `github.com/luxfi/zap` — binary transport protocol (MPC + secrets)
- `github.com/luxfi/age` — age encryption for S3 backups
- `github.com/luxfi/mpc` — MPC daemon (external service, not imported)
+30
View File
@@ -0,0 +1,30 @@
.PHONY: build build-ui copy-ui test clean docker dev
BINARY := kms
IMAGE := ghcr.io/luxfi/kms:latest
# Vite SPA → cmd/kms/web/ (//go:embed reads from there).
build-ui:
cd frontend && pnpm install --frozen-lockfile && pnpm build
copy-ui:
rm -rf cmd/kms/web && mkdir -p cmd/kms/web
@if [ -d frontend/dist ]; then cp -R frontend/dist/. cmd/kms/web/; \
else echo "warning: frontend/dist not found — UI will be empty in this build"; touch cmd/kms/web/.empty; fi
build: copy-ui
CGO_ENABLED=0 go build -ldflags="-w -s" -o $(BINARY) ./cmd/kms/
test:
go test -v -race ./...
clean:
rm -f $(BINARY)
rm -rf cmd/kms/web
dev: copy-ui
KMS_DATA_DIR=./data KMS_LISTEN=:8080 go run ./cmd/kms/
docker: copy-ui
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o $(BINARY) ./cmd/kms/
docker build --platform linux/amd64 -t $(IMAGE) -f Dockerfile.runtime .
+160
View File
@@ -0,0 +1,160 @@
# KMS
MPC-backed key management service for the Lux Network. Manages validator keys, threshold signing, and key rotation using distributed MPC (Multi-Party Computation).
**No Infisical. No PostgreSQL.** Pure Go server backed by [MPC](https://github.com/luxfi/mpc) for threshold cryptography and a JSON file store for metadata.
## Architecture
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Client │─────▶│ KMS │─────▶│ MPC │
│ (ATS / BD) │ HTTP │ Go Server │ HTTP │ CGGMP21 / │
│ │ │ :8080 │ │ FROST │
└─────────────┘ └──────┬──────┘ └─────────────┘
┌──────▼──────┐
│ JSON Store │
│ keys.json │
└─────────────┘
```
### How it works
1. **KMS** is the HTTP API layer — manages validator key lifecycle
2. **MPC** is the crypto backend — performs distributed key generation (DKG) and threshold signing
3. **Store** is a JSON file — maps validator IDs to MPC wallet IDs (no database needed)
### Key types
| Key | Protocol | Curve | Use |
|-----|----------|-------|-----|
| BLS | CGGMP21 | secp256k1 | Consensus signing (BLS aggregation) |
| Corona | FROST | ed25519 | Ring signatures, post-quantum prep |
## Integration with Hanzo Base
KMS runs as a standalone service alongside [Hanzo Base](https://github.com/hanzoai/base) services (ATS, BD, TA). In production:
```
┌──────────────────────┐
│ hanzoai/gateway │
└──────────┬───────────┘
┌────────────────────┼────────────────────┐
│ │ │
┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
│ ATS │ │ BD │ │ TA │
│ (Base) │ │ (Base) │ │ (Base) │
│ :8090 │ │ :8091 │ │ :8092 │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ │
└────────────────────┼────────────────────┘
┌─────▼─────┐ ┌───────────┐
│ KMS │──────▶│ MPC │
│ :8080 │ │ :8081 │
└───────────┘ └───────────┘
```
Base services call KMS for:
- **Transit encrypt/decrypt** — field-level encryption with per-customer DEKs
- **Validator key management** — generate, sign, rotate validator keys
- **MPC signing** — threshold signing for cross-chain bridge operations
### IAM Integration
KMS authenticates callers via [Hanzo IAM](https://github.com/hanzoai/iam) JWT tokens. The `Authorization: Bearer <token>` header is validated against the IAM JWKS endpoint. No API keys, no separate auth system.
## API
```
POST /api/v1/keys/generate Generate validator key set (BLS + Corona via MPC DKG)
GET /api/v1/keys List all validator key sets
GET /api/v1/keys/{id} Get validator key set by ID
POST /api/v1/keys/{id}/sign Sign message with BLS or Corona key
POST /api/v1/keys/{id}/rotate Rotate (reshare) keys with new threshold/participants
GET /api/v1/status KMS + MPC cluster status
GET /healthz Health check
```
### Generate keys
```bash
curl -X POST http://kms:8080/api/v1/keys/generate \
-H 'Content-Type: application/json' \
-d '{"validator_id": "node-0", "threshold": 3, "parties": 5}'
```
### Sign with BLS
```bash
curl -X POST http://kms:8080/api/v1/keys/node-0/sign \
-H 'Content-Type: application/json' \
-d '{"key_type": "bls", "message": "base64-encoded-message"}'
```
## Configuration
| Env Var | Flag | Default | Description |
|---------|------|---------|-------------|
| `KMS_LISTEN` | `--listen` | `:8080` | HTTP listen address |
| `MPC_URL` | `--mpc-url` | `http://mpc-api.lux-mpc.svc.cluster.local:8081` | MPC daemon URL |
| `MPC_TOKEN` | `--mpc-token` | - | MPC API auth token |
| `MPC_VAULT_ID` | `--vault-id` | - | MPC vault ID (required) |
| `KMS_STORE_PATH` | `--store` | `/data/kms/keys.json` | Key metadata store path |
## Running
```bash
# Build
go build -o kms ./cmd/kms
# Run (requires MPC daemon running)
./kms --vault-id=<your-vault-id> --mpc-url=http://localhost:8081
```
## Kubernetes
KMS deploys as a Deployment (not StatefulSet — metadata is on a PVC or ConfigMap):
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: kms
spec:
replicas: 2
template:
spec:
containers:
- name: kms
image: us-docker.pkg.dev/<project>/backend/kms:main
ports:
- containerPort: 8080
env:
- name: MPC_VAULT_ID
valueFrom:
secretKeyRef:
name: kms-secrets
key: MPC_VAULT_ID
```
## K8s Operator
The `k8-operator/` directory contains a Kubernetes operator that syncs KMS-managed secrets into K8s Secret resources. CRDs:
- `KmsSecret` — pull secrets from KMS into K8s Secrets
- `KmsDynamicSecret` — ephemeral secrets with TTL
- `KmsPushSecret` — push K8s Secrets to KMS
## Related
- [luxfi/mpc](https://github.com/luxfi/mpc) — MPC daemon (CGGMP21 + FROST)
- [luxfi/hsm](https://github.com/luxfi/hsm) — Hardware Security Module abstraction
- [hanzoai/base](https://github.com/hanzoai/base) — Go application framework (ATS, BD, TA use this)
- [hanzoai/iam](https://github.com/hanzoai/iam) — Identity and Access Management
## License
MIT — see [LICENSE](LICENSE)
+1
View File
@@ -0,0 +1 @@
1.11.0
+29
View File
@@ -0,0 +1,29 @@
steps:
- id: build
name: golang:1.26.4-bookworm
entrypoint: sh
args:
- -c
- |
apt-get update -qq && apt-get install -y -qq libsqlcipher-dev gcc libc6-dev pkg-config git >/dev/null 2>&1
git config --global url."https://${_GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/"
export GOPRIVATE=github.com/luxfi/*,github.com/hanzoai/*
go build -tags "sqlite_fts5 sqlcipher" -ldflags="-s -w" -o /workspace/kms ./cmd/kms
- id: image
name: gcr.io/cloud-builders/docker
args: [build, -t, us-docker.pkg.dev/-devnet/backend/kms:v1.3.0, -f, Dockerfile.runtime, /workspace]
waitFor: [build]
- id: push
name: gcr.io/cloud-builders/docker
args: [push, us-docker.pkg.dev/-devnet/backend/kms:v1.3.0]
waitFor: [image]
substitutions:
_GITHUB_TOKEN: '' # Pass via --substitutions at submit time or Cloud Build trigger
options:
machineType: E2_HIGHCPU_8
logging: CLOUD_LOGGING_ONLY
timeout: 900s
+256
View File
@@ -0,0 +1,256 @@
// Command casibase-import re-wraps Casibase-era secrets under the new
// MPC-rooted REK and writes them into the lux-kms-go ZapDB.
//
// # Why this exists
//
// As of 2026-05-15 the do-sfo3-hanzo-k8s cluster holds ~50 KMSSecrets
// in the Casibase Node-Fastify KMS (`hanzoai/kms:1.0.7`). Their on-disk
// envelope is Casibase's own format, sealed under the
// `ROOT_ENCRYPTION_KEY` env var on the Casibase pod. lux-kms-go uses a
// different envelope (`pkg/store/crypto.go` AES-256-GCM, REK from MPC).
// The two key domains are independent — there is no automatic rotation
// path between them.
//
// This tool is the one-shot bridge: read every Casibase secret, decrypt
// under the OLD key (operator supplies it on stdin or via file), re-seal
// under the NEW MPC-rooted REK, write into ZapDB.
//
// # Usage
//
// $ kubectl exec -n hanzo deploy/casibase-kms -- /api/v3/secrets/raw?... \
// > casibase-dump.json
// $ casibase-import \
// --in casibase-dump.json \
// --old-key-file ./casibase-root.key \
// --data-dir /data/kms \
// --dry-run
// $ casibase-import --in casibase-dump.json \
// --old-key-file ./casibase-root.key \
// --data-dir /data/kms
//
// MPC_REK_ENDPOINT + MPC_REK_KEY_ID must be set (same env contract as
// the KMS server). The tool fetches the NEW REK exactly once, the same
// way kmsd does at boot, so the re-wrap target is the live cluster's
// REK epoch.
//
// # Why this is a tool, not a server feature
//
// 1. It's one-shot: after the migration the Casibase pod is scaled to
// zero and this code is dead. Folding it into the server would
// leave permanent attack surface (a "decrypt-under-legacy-key" API)
// for a one-week event.
// 2. The OLD key has to be supplied by an operator. There's no safe
// way to embed that in the server — the only correct flow is for
// a human to mount it transiently into a one-shot pod, run this
// tool, and remove the secret.
// 3. The Casibase envelope format is foreign. Decoupling it as its
// own binary means the kmsd code does not have to maintain a
// second envelope code path.
package main
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"flag"
"fmt"
"log"
"os"
"time"
"github.com/luxfi/kms/pkg/store"
"github.com/luxfi/kms/pkg/store/mpcrek"
badger "github.com/luxfi/zapdb"
)
// casibaseDump is the on-wire shape from `GET /api/v3/secrets/raw`.
// Each entry is one secret; ciphertext is base64'd, OldKeyID identifies
// the Casibase envelope variant if Casibase ever shipped multiple.
//
// This struct mirrors the Casibase v1.0.7 export format. If the format
// changes, update the JSON tags accordingly.
type casibaseDump struct {
Secrets []casibaseSecret `json:"secrets"`
}
type casibaseSecret struct {
Workspace string `json:"workspaceSlug"`
SecretPath string `json:"secretPath"`
SecretKey string `json:"secretKey"`
Env string `json:"environment"`
// CiphertextB64 is the base64 of the Casibase-format envelope. This
// is NOT compatible with pkg/store.Open — it must be decoded by
// decryptCasibase below before re-sealing.
CiphertextB64 string `json:"ciphertextB64"`
}
func main() {
in := flag.String("in", "", "path to casibase JSON dump (required)")
oldKeyFile := flag.String("old-key-file", "", "path to base64-encoded Casibase ROOT_ENCRYPTION_KEY (required)")
dataDir := flag.String("data-dir", "/data/kms", "ZapDB data directory to write into")
dryRun := flag.Bool("dry-run", false, "decode and verify only; do not write")
flag.Parse()
if *in == "" || *oldKeyFile == "" {
flag.Usage()
os.Exit(2)
}
dump, err := readDump(*in)
if err != nil {
log.Fatalf("casibase-import: read dump: %v", err)
}
log.Printf("casibase-import: loaded %d secrets from %s", len(dump.Secrets), *in)
oldKey, err := readOldKey(*oldKeyFile)
if err != nil {
log.Fatalf("casibase-import: read old key: %v", err)
}
defer mpcrek.Zero(oldKey)
// Fetch the NEW REK from MPC. Same env contract as kmsd boot.
newREK, err := bootstrapNewREK()
if err != nil {
log.Fatalf("casibase-import: bootstrap new REK: %v", err)
}
defer mpcrek.Zero(newREK)
var db *badger.DB
var secStore *store.SecretStore
if !*dryRun {
opts := badger.DefaultOptions(*dataDir)
db, err = badger.Open(opts)
if err != nil {
log.Fatalf("casibase-import: open zapdb %s: %v", *dataDir, err)
}
defer db.Close()
secStore = store.NewSecretStore(db)
}
var ok, failed int
for _, s := range dump.Secrets {
plaintext, err := decryptCasibase(oldKey, s)
if err != nil {
log.Printf("casibase-import: FAIL decrypt workspace=%s path=%s name=%s: %v",
s.Workspace, s.SecretPath, s.SecretKey, err)
failed++
continue
}
// Re-seal under the new MPC-rooted REK using pkg/store.Seal.
// AAD bindings (path / name / env) and per-secret DEK come for
// free; we just produce the new envelope.
path := s.SecretPath
if path == "" {
path = "/" + s.Workspace
}
env := s.Env
if env == "" {
env = "default"
}
secret, err := store.Seal(newREK, path, s.SecretKey, env, plaintext)
// Zero plaintext immediately after Seal returns.
mpcrek.Zero(plaintext)
if err != nil {
log.Printf("casibase-import: FAIL seal workspace=%s name=%s: %v", s.Workspace, s.SecretKey, err)
failed++
continue
}
if *dryRun {
log.Printf("casibase-import: OK (dry-run) workspace=%s path=%s name=%s env=%s",
s.Workspace, path, s.SecretKey, env)
} else {
if err := secStore.Put(secret); err != nil {
log.Printf("casibase-import: FAIL put workspace=%s name=%s: %v", s.Workspace, s.SecretKey, err)
failed++
continue
}
log.Printf("casibase-import: OK workspace=%s path=%s name=%s env=%s",
s.Workspace, path, s.SecretKey, env)
}
ok++
}
log.Printf("casibase-import: done — %d ok, %d failed (dry-run=%v)", ok, failed, *dryRun)
if failed > 0 {
os.Exit(1)
}
}
func readDump(path string) (*casibaseDump, error) {
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var d casibaseDump
if err := json.Unmarshal(raw, &d); err != nil {
return nil, fmt.Errorf("parse: %w", err)
}
return &d, nil
}
func readOldKey(path string) ([]byte, error) {
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
// Strip trailing newlines.
for len(raw) > 0 && (raw[len(raw)-1] == '\n' || raw[len(raw)-1] == '\r') {
raw = raw[:len(raw)-1]
}
k, err := base64.StdEncoding.DecodeString(string(raw))
if err != nil {
return nil, fmt.Errorf("base64 decode: %w", err)
}
if len(k) != 32 {
return nil, fmt.Errorf("key length %d, want 32", len(k))
}
return k, nil
}
func bootstrapNewREK() ([]byte, error) {
endpoint := os.Getenv("MPC_REK_ENDPOINT")
if endpoint == "" {
return nil, errors.New("MPC_REK_ENDPOINT is required (same env contract as kmsd)")
}
keyID := os.Getenv("MPC_REK_KEY_ID")
if keyID == "" {
keyID = "kms/rek/v1"
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return mpcrek.Bootstrap(ctx, mpcrek.Config{
Endpoint: endpoint,
KeyID: keyID,
NodeID: "casibase-import",
Timeout: 10 * time.Second,
})
}
// decryptCasibase decodes a Casibase v1.0.7 envelope under the OLD
// ROOT_ENCRYPTION_KEY. SCAFFOLDED: the Casibase wire format is not
// documented in this tree. To finish, replace this function body with
// the format-correct decoder. The Casibase source is at
// hanzoai/kms@1.0.7 (Node-Fastify); the encryption code lives in its
// `src/services/secret/encrypt.ts`. As of 2026-05-15 the deployed
// envelope is AES-256-GCM with a 12-byte nonce and 16-byte tag inside
// a custom binary header.
//
// Replace stub with one of:
// - Direct base64 + AES-GCM under oldKey (if no Casibase header).
// - Casibase header parse + AES-GCM under oldKey.
//
// The Open contract: take the OLD key + the dumped record, return
// plaintext bytes. On any failure (bad tag, wrong key, malformed
// header) return a non-nil error and the caller skips this record.
func decryptCasibase(oldKey []byte, s casibaseSecret) ([]byte, error) {
if len(oldKey) != 32 {
return nil, fmt.Errorf("old key length %d, want 32", len(oldKey))
}
_ = s.CiphertextB64 // silence unused until the decoder lands
// TODO(operator): decode the Casibase envelope. See package doc.
return nil, errors.New("casibase decoder not yet implemented — see decryptCasibase godoc")
}
+252
View File
@@ -0,0 +1,252 @@
// Command kms-operator reconciles KMSSecret CRDs against KMS via ZAP.
//
// In-cluster consumers fetch secrets over the native luxfi/zap binary
// protocol (port 9999 on the KMS pod) — no HTTP, no auth tokens, no REST
// round-trips. Reconcile loop:
//
// 1. List KMSSecret CRDs via dynamic client
// 2. For each: Dial KMS at spec.hostAPI:9999 with secretsPath as default
// 3. List then Get every secret in the scope
// 4. Write/update the K8s Secret named in spec.managedSecretReference
//
// Replaces the legacy hanzoai/kms-operator (Infisical-flavor SDK over HTTP).
//
// Env:
// RESYNC_INTERVAL reconcile interval seconds (default 60)
// WATCH_NAMESPACE if set, watch only that ns; otherwise cluster-wide
// ZAP_PORT port appended to KMSSecret hostAPI host (default 9999)
package main
import (
"context"
"fmt"
"log"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
"github.com/luxfi/kms/pkg/zapclient"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)
var kmsSecretGVR = schema.GroupVersionResource{
Group: "secrets.lux.network", Version: "v1alpha1", Resource: "kmssecrets",
}
func main() {
resync := envInt("RESYNC_INTERVAL", 60)
zapPort := envInt("ZAP_PORT", 9999)
ns := os.Getenv("WATCH_NAMESPACE")
cfg, err := getRestConfig()
if err != nil {
log.Fatalf("k8s config: %v", err)
}
dyn, err := dynamic.NewForConfig(cfg)
if err != nil {
log.Fatalf("dynamic client: %v", err)
}
kc, err := kubernetes.NewForConfig(cfg)
if err != nil {
log.Fatalf("kubernetes client: %v", err)
}
if ns == "" {
log.Printf("kms-operator: ZAP transport, watching cluster-wide, resync=%ds zap_port=%d", resync, zapPort)
} else {
log.Printf("kms-operator: ZAP transport, watching ns=%s, resync=%ds zap_port=%d", ns, resync, zapPort)
}
go func() {
http.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`{"status":"ok"}`))
})
log.Print("kms-operator: liveness on :8080/healthz")
log.Fatal(http.ListenAndServe(":8080", nil))
}()
for {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(resync)*time.Second-5*time.Second)
reconcileAll(ctx, dyn, kc, ns, zapPort)
cancel()
time.Sleep(time.Duration(resync) * time.Second)
}
}
func reconcileAll(ctx context.Context, dyn dynamic.Interface, kc kubernetes.Interface, ns string, zapPort int) {
var list dynamic.ResourceInterface
if ns == "" {
list = dyn.Resource(kmsSecretGVR)
} else {
list = dyn.Resource(kmsSecretGVR).Namespace(ns)
}
objs, err := list.List(ctx, metav1.ListOptions{})
if err != nil {
log.Printf("kms-operator: list KMSSecrets: %v", err)
return
}
log.Printf("kms-operator: reconciling %d KMSSecret(s)", len(objs.Items))
for i := range objs.Items {
o := &objs.Items[i]
if err := reconcile(ctx, kc, o.Object, zapPort); err != nil {
log.Printf("kms-operator: %s/%s: %v", o.GetNamespace(), o.GetName(), err)
}
}
}
func reconcile(ctx context.Context, kc kubernetes.Interface, obj map[string]any, zapPort int) error {
meta, _ := obj["metadata"].(map[string]any)
spec, _ := obj["spec"].(map[string]any)
if meta == nil || spec == nil {
return fmt.Errorf("malformed: missing metadata or spec")
}
name := str(meta, "name")
ns := str(meta, "namespace")
hostAPI := str(spec, "hostAPI")
if hostAPI == "" {
hostAPI = "http://zap.kms.svc.cluster.local"
}
zapAddr := zapAddrFromHost(hostAPI, zapPort)
auth, _ := spec["authentication"].(map[string]any)
ua, _ := auth["universalAuth"].(map[string]any)
scope, _ := ua["secretsScope"].(map[string]any)
if scope == nil {
return fmt.Errorf("missing authentication.universalAuth.secretsScope")
}
env := strDefault(scope, "envSlug", "default")
path := strDefault(scope, "secretsPath", "/")
target, _ := spec["managedSecretReference"].(map[string]any)
tName := str(target, "secretName")
tNS := strDefault(target, "secretNamespace", ns)
tType := strDefault(target, "secretType", "Opaque")
if tName == "" {
return fmt.Errorf("missing managedSecretReference.secretName")
}
zc, err := zapclient.Dial(ctx, zapAddr, path)
if err != nil {
return fmt.Errorf("zap dial %s: %w", zapAddr, err)
}
defer zc.Close()
names, err := zc.ListAt(ctx, path, env)
if err != nil {
return fmt.Errorf("zap list path=%s env=%s: %w", path, env, err)
}
data := make(map[string][]byte, len(names))
for _, n := range names {
v, err := zc.GetAt(ctx, path, n, env)
if err != nil {
log.Printf("kms-operator: %s/%s: skip %s: %v", ns, name, n, err)
continue
}
data[n] = []byte(v)
}
desired := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: tName,
Namespace: tNS,
Labels: map[string]string{
"app.kubernetes.io/managed-by": "lux-kms-operator",
"kmssecret.secrets.lux.network/source": name,
},
},
Type: corev1.SecretType(tType),
Data: data,
}
existing, err := kc.CoreV1().Secrets(tNS).Get(ctx, tName, metav1.GetOptions{})
switch {
case apierrors.IsNotFound(err):
if _, err := kc.CoreV1().Secrets(tNS).Create(ctx, desired, metav1.CreateOptions{}); err != nil {
return fmt.Errorf("create %s/%s: %w", tNS, tName, err)
}
log.Printf("kms-operator: %s/%s -> created %s/%s with %d keys (zap)", ns, name, tNS, tName, len(data))
case err != nil:
return fmt.Errorf("get %s/%s: %w", tNS, tName, err)
default:
existing.Data = desired.Data
existing.Type = desired.Type
if existing.Labels == nil {
existing.Labels = desired.Labels
} else {
for k, v := range desired.Labels {
existing.Labels[k] = v
}
}
if _, err := kc.CoreV1().Secrets(tNS).Update(ctx, existing, metav1.UpdateOptions{}); err != nil {
return fmt.Errorf("update %s/%s: %w", tNS, tName, err)
}
log.Printf("kms-operator: %s/%s -> updated %s/%s with %d keys (zap)", ns, name, tNS, tName, len(data))
}
return nil
}
// zapAddrFromHost extracts the host from a URL or bare host string and
// joins it with the ZAP port. e.g. "http://api.kms.svc:80" + 9999
//
// → "api.kms.svc:9999".
func zapAddrFromHost(hostAPI string, port int) string {
host := hostAPI
if u, err := url.Parse(hostAPI); err == nil && u.Host != "" {
host = u.Hostname()
} else if i := strings.Index(host, "://"); i >= 0 {
host = host[i+3:]
if j := strings.IndexAny(host, "/:"); j >= 0 {
host = host[:j]
}
} else if j := strings.IndexAny(host, "/:"); j >= 0 {
host = host[:j]
}
return fmt.Sprintf("%s:%d", host, port)
}
func getRestConfig() (*rest.Config, error) {
if cfg, err := rest.InClusterConfig(); err == nil {
return cfg, nil
}
return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
clientcmd.NewDefaultClientConfigLoadingRules(),
&clientcmd.ConfigOverrides{},
).ClientConfig()
}
func envInt(key string, def int) int {
if v := os.Getenv(key); v != "" {
if n, err := strconv.Atoi(v); err == nil {
return n
}
}
return def
}
func str(m map[string]any, k string) string {
if m == nil {
return ""
}
s, _ := m[k].(string)
return s
}
func strDefault(m map[string]any, k, def string) string {
if v := str(m, k); v != "" {
return v
}
return def
}
+4
View File
@@ -0,0 +1,4 @@
# `web/` is populated at build time by `make build` from frontend/dist.
# Keep the dir present (.gitkeep) but ignore the copied artefacts.
web/*
!web/.gitkeep
+244
View File
@@ -0,0 +1,244 @@
// JWT-backed authorization for the secrets surface.
//
// Every /v1/kms/orgs/{org}/secrets/* request must carry an
// `Authorization: Bearer <jwt>` header. The JWT is validated against
// IAM's JWKS (signature, issuer, expiry) and the `owner` claim must
// equal the {org} path param. Service tokens minted by IAM via
// client_credentials carry the application's owner — that's the org
// boundary we enforce.
//
// kms-admin role override: a token whose `roles` includes "kms-admin"
// can read/write any org. The role is granted in IAM, not configured
// here.
//
// Public endpoints (no Bearer required) are wired in main.go and stay
// public: /healthz, /health, /v1/kms/health{,z}, /v1/admin/config,
// /v1/kms/auth/login, the OIDC routes, and the embedded SPA. The
// middleware below only wraps the secrets handlers.
package main
import (
"context"
"errors"
"fmt"
"log"
"net/http"
"strings"
"time"
gojose "github.com/go-jose/go-jose/v4"
"github.com/go-jose/go-jose/v4/jwt"
)
// orgClaims is the minimal JWT shape we authorize secrets reads/writes
// against. The org binding follows two flavors of IAM JWT:
//
// 1. User tokens (Google OAuth, password) — `owner` IS the org slug.
// Example: a user signs in to organization=liquidity, the JWT
// carries owner="liquidity", name="<username>".
// 2. Application tokens (client_credentials) — `owner`="admin"
// (the parent record), and the application's `name` carries the
// org as a prefix per the documented `<org>-<service>` naming
// convention. Example: kms-app for liquidity is name="kms",
// owner="admin", type="application".
//
// `roles` carries the IAM role list (kms-admin override).
type orgClaims struct {
jwt.Claims
Owner string `json:"owner"`
Name string `json:"name"`
Type string `json:"type"`
Tag string `json:"tag"`
Roles []string `json:"roles"`
}
// orgs returns the set of org slugs this token is allowed to act for.
// Empty result = unauthenticated principal.
//
// Resolution order, first non-empty wins:
// - Tag (operators set this on cross-cutting service apps when they
// don't follow the <org>-<service> convention).
// - For application tokens: prefix of Name up to the first '-'.
// - Owner (the user-token case).
func (c *orgClaims) orgs() []string {
out := []string{}
if c.Tag != "" {
out = append(out, c.Tag)
}
if c.Type == "application" && c.Name != "" {
if i := strings.Index(c.Name, "-"); i > 0 {
out = append(out, c.Name[:i])
}
}
if c.Owner != "" && c.Owner != "admin" && c.Owner != "built-in" {
out = append(out, c.Owner)
}
return out
}
// orgJWTAuth verifies tokens against the IAM JWKS. Issuer is checked
// (must equal expectedIssuer). Audience is NOT enforced because IAM
// client_credentials grants don't pin an audience to the resource
// server today — we rely on issuer + signature + owner-equals-org.
//
// jwksURL and expectedIssuer often differ in production: jwksURL is
// the in-cluster IAM URL (e.g. `http://iam.<namespace>.svc:8000/...`)
// for cheap fetches, expectedIssuer is the public host the JWT `iss`
// claim was minted with (e.g. `https://iam.<env>.<tenant>.example.com`).
// Splitting them is what keeps validation working when KMS sits behind
// a gateway that rewrites Host.
type orgJWTAuth struct {
jwksURL string
issuer string
cache *jwksCache
}
// newOrgJWTAuth wires the validator from env. iamEndpoint is the URL
// KMS will fetch JWKS from (in-cluster); expectedIssuer is the
// `iss` claim value to enforce. If expectedIssuer is empty the
// iamEndpoint is used (matches the simple single-URL deployment).
func newOrgJWTAuth(iamEndpoint, expectedIssuer string) *orgJWTAuth {
iam := strings.TrimRight(iamEndpoint, "/")
iss := strings.TrimRight(expectedIssuer, "/")
if iss == "" {
iss = iam
}
jwksURL := iam + "/.well-known/jwks"
return &orgJWTAuth{
jwksURL: jwksURL,
issuer: iss,
cache: &jwksCache{
url: jwksURL,
ttl: 5 * time.Minute,
client: &http.Client{Timeout: 10 * time.Second},
},
}
}
func (a *orgJWTAuth) validate(ctx context.Context, raw string) (*orgClaims, error) {
if raw == "" {
return nil, errors.New("empty token")
}
tok, err := jwt.ParseSigned(raw, []gojose.SignatureAlgorithm{
gojose.RS256, gojose.RS384, gojose.RS512,
gojose.ES256, gojose.ES384, gojose.ES512,
})
if err != nil {
return nil, fmt.Errorf("parse: %w", err)
}
keys, err := a.cache.get(ctx)
if err != nil {
return nil, fmt.Errorf("jwks: %w", err)
}
// DEBUG: log JWKS state + token kid for diagnosis
tokKid := ""
if len(tok.Headers) > 0 {
tokKid = tok.Headers[0].KeyID
}
kidList := make([]string, 0, len(keys.Keys))
for _, k := range keys.Keys {
kidList = append(kidList, k.KeyID)
}
log.Printf("kms: validate: tok kid=%s jwks kids=%v", tokKid, kidList)
var claims orgClaims
verified := false
var lastErr error
for _, k := range keys.Keys {
if err := tok.Claims(k.Key, &claims); err == nil {
verified = true
break
} else {
log.Printf("kms: validate: kid=%s err=%v", k.KeyID, err)
lastErr = err
}
}
if !verified {
if lastErr == nil {
lastErr = errors.New("no key matched")
}
return nil, fmt.Errorf("verify: %w", lastErr)
}
exp := jwt.Expected{
Time: time.Now(),
Issuer: a.issuer,
}
if err := claims.Claims.Validate(exp); err != nil {
return nil, fmt.Errorf("validate: %w", err)
}
if len(claims.orgs()) == 0 {
return nil, errors.New("token has no resolvable org")
}
return &claims, nil
}
// requireOrgJWT wraps a handler, ensuring the request carries a valid
// IAM-signed JWT whose `owner` matches the {org} path param (or whose
// `roles` includes "kms-admin"). On failure the request is rejected
// with 401 (missing/malformed) or 403 (token belongs to another org).
//
// nil receiver short-circuits to 503 — operators who haven't wired
// IAM_ENDPOINT can see the misconfiguration in their logs and probes
// instead of a wide-open store.
func (a *orgJWTAuth) requireOrgJWT(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if a == nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]any{
"statusCode": 503, "message": "auth not configured",
})
return
}
raw := bearerToken(r)
if raw == "" {
writeJSON(w, http.StatusUnauthorized, map[string]any{
"statusCode": 401, "message": "missing bearer token",
})
return
}
claims, err := a.validate(r.Context(), raw)
if err != nil {
// Log the actual reason so operators can see *why* a token
// was rejected (signature mismatch vs issuer mismatch vs
// expiry vs etc.). Body stays opaque to clients.
log.Printf("kms: auth reject: %v (path=%s)", err, r.URL.Path)
writeJSON(w, http.StatusUnauthorized, map[string]any{
"statusCode": 401, "message": "invalid token",
})
return
}
org := r.PathValue("org")
if org == "" {
// Should never happen — the route pattern requires {org}.
writeJSON(w, http.StatusBadRequest, map[string]any{
"statusCode": 400, "message": "org required",
})
return
}
allowed := false
for _, o := range claims.orgs() {
if o == org {
allowed = true
break
}
}
if !allowed && !hasRole(claims.Roles, "kms-admin") {
writeJSON(w, http.StatusForbidden, map[string]any{
"statusCode": 403, "message": "token does not authorize this org",
})
return
}
next(w, r)
}
}
func bearerToken(r *http.Request) string {
h := r.Header.Get("Authorization")
if h == "" {
return ""
}
const prefix = "Bearer "
if !strings.HasPrefix(h, prefix) {
return ""
}
return strings.TrimSpace(h[len(prefix):])
}
+422
View File
@@ -0,0 +1,422 @@
// Tests for the org-scoped JWT middleware on the secrets surface.
package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
gojose "github.com/go-jose/go-jose/v4"
"github.com/go-jose/go-jose/v4/jwt"
)
// signOrgClaims returns a serialized JWT for the given orgClaims under signer.
func signOrgClaims(t *testing.T, signer gojose.Signer, claims orgClaims) string {
t.Helper()
out, err := jwt.Signed(signer).Claims(claims).Serialize()
if err != nil {
t.Fatalf("jwt.Signed.Serialize: %v", err)
}
return out
}
// jwksHandler returns a stub IAM that serves the given JWKS.
func jwksHandler(jwks *gojose.JSONWebKeySet) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/.well-known/jwks") {
json.NewEncoder(w).Encode(jwks)
return
}
http.NotFound(w, r)
})
}
func TestRequireOrgJWT_missingBearer(t *testing.T) {
auth := newOrgJWTAuth("https://iam.example.com", "")
mux := http.NewServeMux()
mux.HandleFunc("GET /v1/kms/orgs/{org}/secrets/{rest...}", auth.requireOrgJWT(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest(http.MethodGet, "/v1/kms/orgs/liquidity/secrets/iam/X", nil)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Errorf("got %d want 401; body=%s", rec.Code, rec.Body.String())
}
}
func TestRequireOrgJWT_invalidBearerShape(t *testing.T) {
auth := newOrgJWTAuth("https://iam.example.com", "")
mux := http.NewServeMux()
mux.HandleFunc("GET /v1/kms/orgs/{org}/secrets/{rest...}", auth.requireOrgJWT(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest(http.MethodGet, "/v1/kms/orgs/liquidity/secrets/iam/X", nil)
req.Header.Set("Authorization", "Token abc")
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Errorf("got %d want 401; body=%s", rec.Code, rec.Body.String())
}
}
func TestRequireOrgJWT_validTokenForCorrectOrg(t *testing.T) {
signer, jwks := newTestSigner(t)
iam := httptest.NewServer(jwksHandler(jwks))
defer iam.Close()
auth := newOrgJWTAuth(iam.URL, "")
tok := signOrgClaims(t, signer, orgClaims{
Claims: jwt.Claims{
Issuer: iam.URL,
Subject: "user-z",
Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)),
},
Owner: "liquidity", // user-token case: owner IS the org
})
called := false
mux := http.NewServeMux()
mux.HandleFunc("GET /v1/kms/orgs/{org}/secrets/{rest...}", auth.requireOrgJWT(func(w http.ResponseWriter, r *http.Request) {
called = true
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest(http.MethodGet, "/v1/kms/orgs/liquidity/secrets/iam/X", nil)
req.Header.Set("Authorization", "Bearer "+tok)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("got %d want 200; body=%s", rec.Code, rec.Body.String())
}
if !called {
t.Error("inner handler not called")
}
}
func TestRequireOrgJWT_crossOrgRejected(t *testing.T) {
signer, jwks := newTestSigner(t)
iam := httptest.NewServer(jwksHandler(jwks))
defer iam.Close()
auth := newOrgJWTAuth(iam.URL, "")
// Token for org=mlc, request for org=liquidity → 403.
tok := signOrgClaims(t, signer, orgClaims{
Claims: jwt.Claims{
Issuer: iam.URL,
Subject: "mlc-app",
Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)),
},
Owner: "mlc",
})
mux := http.NewServeMux()
mux.HandleFunc("GET /v1/kms/orgs/{org}/secrets/{rest...}", auth.requireOrgJWT(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("inner handler must NOT be called for cross-org token")
}))
req := httptest.NewRequest(http.MethodGet, "/v1/kms/orgs/liquidity/secrets/iam/X", nil)
req.Header.Set("Authorization", "Bearer "+tok)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden {
t.Errorf("got %d want 403; body=%s", rec.Code, rec.Body.String())
}
}
func TestRequireOrgJWT_kmsAdminCrossesOrgs(t *testing.T) {
signer, jwks := newTestSigner(t)
iam := httptest.NewServer(jwksHandler(jwks))
defer iam.Close()
auth := newOrgJWTAuth(iam.URL, "")
// kms-admin role lets the holder reach across orgs.
tok := signOrgClaims(t, signer, orgClaims{
Claims: jwt.Claims{
Issuer: iam.URL,
Subject: "ops",
Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)),
},
Owner: "operator-org",
Roles: []string{"kms-admin"},
})
mux := http.NewServeMux()
mux.HandleFunc("GET /v1/kms/orgs/{org}/secrets/{rest...}", auth.requireOrgJWT(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest(http.MethodGet, "/v1/kms/orgs/liquidity/secrets/iam/X", nil)
req.Header.Set("Authorization", "Bearer "+tok)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("got %d want 200 (kms-admin); body=%s", rec.Code, rec.Body.String())
}
}
func TestRequireOrgJWT_expiredRejected(t *testing.T) {
signer, jwks := newTestSigner(t)
iam := httptest.NewServer(jwksHandler(jwks))
defer iam.Close()
auth := newOrgJWTAuth(iam.URL, "")
tok := signOrgClaims(t, signer, orgClaims{
Claims: jwt.Claims{
Issuer: iam.URL,
Subject: "old",
Expiry: jwt.NewNumericDate(time.Now().Add(-time.Hour)),
},
Owner: "liquidity",
})
mux := http.NewServeMux()
mux.HandleFunc("GET /v1/kms/orgs/{org}/secrets/{rest...}", auth.requireOrgJWT(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("must reject expired token")
}))
req := httptest.NewRequest(http.MethodGet, "/v1/kms/orgs/liquidity/secrets/iam/X", nil)
req.Header.Set("Authorization", "Bearer "+tok)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Errorf("got %d want 401 (expired); body=%s", rec.Code, rec.Body.String())
}
}
func TestRequireOrgJWT_wrongIssuerRejected(t *testing.T) {
signer, jwks := newTestSigner(t)
iam := httptest.NewServer(jwksHandler(jwks))
defer iam.Close()
auth := newOrgJWTAuth(iam.URL, "")
// Issuer mismatch — claim says some-other-iam, validator expects iam.URL.
tok := signOrgClaims(t, signer, orgClaims{
Claims: jwt.Claims{
Issuer: "https://other-iam.example.com",
Subject: "u",
Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)),
},
Owner: "liquidity",
})
mux := http.NewServeMux()
mux.HandleFunc("GET /v1/kms/orgs/{org}/secrets/{rest...}", auth.requireOrgJWT(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("must reject wrong issuer")
}))
req := httptest.NewRequest(http.MethodGet, "/v1/kms/orgs/liquidity/secrets/iam/X", nil)
req.Header.Set("Authorization", "Bearer "+tok)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Errorf("got %d want 401 (wrong issuer); body=%s", rec.Code, rec.Body.String())
}
}
// IAM client_credentials JWT — owner="admin" (record parent),
// type="application", name="<org>-<service>". The org is derived from
// the name prefix per the documented IAM naming convention.
func TestRequireOrgJWT_applicationToken_orgFromName(t *testing.T) {
signer, jwks := newTestSigner(t)
iam := httptest.NewServer(jwksHandler(jwks))
defer iam.Close()
auth := newOrgJWTAuth(iam.URL, "")
tok := signOrgClaims(t, signer, orgClaims{
Claims: jwt.Claims{
Issuer: iam.URL,
Subject: "admin/kms",
Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)),
},
Owner: "admin",
Name: "kms",
Type: "application",
})
mux := http.NewServeMux()
mux.HandleFunc("GET /v1/kms/orgs/{org}/secrets/{rest...}", auth.requireOrgJWT(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest(http.MethodGet, "/v1/kms/orgs/liquidity/secrets/iam/X", nil)
req.Header.Set("Authorization", "Bearer "+tok)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("got %d want 200 (app token, org from name); body=%s", rec.Code, rec.Body.String())
}
}
// An application named "kms" must NOT be able to read mlc's
// secrets — the name prefix is the only org binding, no fall-through
// to other orgs.
func TestRequireOrgJWT_applicationToken_crossOrgRejected(t *testing.T) {
signer, jwks := newTestSigner(t)
iam := httptest.NewServer(jwksHandler(jwks))
defer iam.Close()
auth := newOrgJWTAuth(iam.URL, "")
tok := signOrgClaims(t, signer, orgClaims{
Claims: jwt.Claims{
Issuer: iam.URL,
Subject: "admin/kms",
Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)),
},
Owner: "admin",
Name: "kms",
Type: "application",
})
mux := http.NewServeMux()
mux.HandleFunc("GET /v1/kms/orgs/{org}/secrets/{rest...}", auth.requireOrgJWT(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("must reject app token reaching across orgs")
}))
req := httptest.NewRequest(http.MethodGet, "/v1/kms/orgs/mlc/secrets/iam/X", nil)
req.Header.Set("Authorization", "Bearer "+tok)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden {
t.Errorf("got %d want 403 (cross-org app token); body=%s", rec.Code, rec.Body.String())
}
}
// Operator-set Tag overrides org derivation — used for cross-cutting
// service apps that don't follow <org>-<service>.
func TestRequireOrgJWT_tagPicksOrg(t *testing.T) {
signer, jwks := newTestSigner(t)
iam := httptest.NewServer(jwksHandler(jwks))
defer iam.Close()
auth := newOrgJWTAuth(iam.URL, "")
tok := signOrgClaims(t, signer, orgClaims{
Claims: jwt.Claims{
Issuer: iam.URL,
Subject: "admin/some-app",
Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)),
},
Owner: "admin",
Name: "some-app",
Type: "application",
Tag: "liquidity",
})
mux := http.NewServeMux()
mux.HandleFunc("GET /v1/kms/orgs/{org}/secrets/{rest...}", auth.requireOrgJWT(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest(http.MethodGet, "/v1/kms/orgs/liquidity/secrets/iam/X", nil)
req.Header.Set("Authorization", "Bearer "+tok)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("got %d want 200 (Tag override); body=%s", rec.Code, rec.Body.String())
}
}
func TestRequireOrgJWT_missingOwnerClaimRejected(t *testing.T) {
signer, jwks := newTestSigner(t)
iam := httptest.NewServer(jwksHandler(jwks))
defer iam.Close()
auth := newOrgJWTAuth(iam.URL, "")
tok := signOrgClaims(t, signer, orgClaims{
Claims: jwt.Claims{
Issuer: iam.URL,
Subject: "u",
Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)),
},
// Owner deliberately empty AND no Tag/Name/Type — orgs() yields
// nothing, so the token is rejected.
})
mux := http.NewServeMux()
mux.HandleFunc("GET /v1/kms/orgs/{org}/secrets/{rest...}", auth.requireOrgJWT(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("must reject token with no resolvable org")
}))
req := httptest.NewRequest(http.MethodGet, "/v1/kms/orgs/liquidity/secrets/iam/X", nil)
req.Header.Set("Authorization", "Bearer "+tok)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Errorf("got %d want 401 (missing owner); body=%s", rec.Code, rec.Body.String())
}
}
// In production KMS fetches JWKS from the in-cluster IAM URL but the
// JWT `iss` claim was minted with the public hostname. The validator
// must accept tokens whose `iss` matches the configured expectedIssuer
// even when that string differs from the JWKS host.
func TestRequireOrgJWT_splitJwksAndIssuer(t *testing.T) {
signer, jwks := newTestSigner(t)
jwksHost := httptest.NewServer(jwksHandler(jwks))
defer jwksHost.Close()
publicIssuer := "https://iam.dev.example.com"
auth := newOrgJWTAuth(jwksHost.URL, publicIssuer)
tok := signOrgClaims(t, signer, orgClaims{
Claims: jwt.Claims{
Issuer: publicIssuer, // minted with public host
Subject: "admin/kms",
Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)),
},
Owner: "admin",
Name: "kms",
Type: "application",
})
mux := http.NewServeMux()
mux.HandleFunc("GET /v1/kms/orgs/{org}/secrets/{rest...}", auth.requireOrgJWT(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest(http.MethodGet, "/v1/kms/orgs/liquidity/secrets/iam/X", nil)
req.Header.Set("Authorization", "Bearer "+tok)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("got %d want 200 (split jwksURL vs issuer); body=%s", rec.Code, rec.Body.String())
}
}
func TestBearerToken_extracts(t *testing.T) {
cases := []struct {
header, want string
}{
{"", ""},
{"Bearer ", ""},
{"Bearer abc", "abc"},
{"Bearer spaced ", "spaced"},
{"bearer abc", ""}, // case-sensitive prefix per RFC 6750
{"Token abc", ""},
}
for _, c := range cases {
req := httptest.NewRequest(http.MethodGet, "/", nil)
if c.header != "" {
req.Header.Set("Authorization", c.header)
}
got := bearerToken(req)
if got != c.want {
t.Errorf("header=%q got=%q want=%q", c.header, got, c.want)
}
}
}
+199
View File
@@ -0,0 +1,199 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// consensus.go — wires the kmsd to its consensus authority.
//
// The kmsd asks consensus "is NodeID X a member of authority Y?" on
// every request. Authority data lives upstream (luxd); the kmsd never
// runs its own ACL. This file holds the boot-time wiring.
//
// Today the wiring reads a consensus snapshot from env / file. The
// snapshot is produced by the kms-operator, which is the component
// that talks to luxd directly. When luxd later exposes a wire-level
// authorization RPC, this file gains a second wiring (push-style:
// kmsd dials luxd, pulls the current validator + operator sets at the
// configured TTL). The ConsensusAuthorizer interface does not change.
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"strings"
"time"
"github.com/luxfi/ids"
"github.com/luxfi/kms/pkg/zapserver"
)
const (
envValidators = "KMS_CONSENSUS_VALIDATORS"
envOperators = "KMS_CONSENSUS_OPERATORS"
envFile = "KMS_CONSENSUS_FILE"
envTTL = "KMS_CONSENSUS_TTL"
// Nonce ledger knobs. Both have package-default fallbacks; the env
// vars exist so the kms-operator can tune memory pressure in
// high-cardinality deployments without a rebuild.
envNonceLedgerTTL = "KMS_NONCE_LEDGER_TTL"
envNonceLedgerGC = "KMS_NONCE_LEDGER_GC"
defaultConsensusTTL = 30 * time.Second
)
// buildNonceLedger returns the canonical in-process anti-replay ledger.
// Honours KMS_NONCE_LEDGER_TTL and KMS_NONCE_LEDGER_GC env vars; zero /
// missing values fall back to the package defaults (TTL = MaxClockSkew +
// 1m = 6m, GC = TTL/4 = 90s).
//
// The ledger is wired into zapserver.Server at boot. The kms-operator
// owns the values in production: services with high envelope volume
// (e.g. paas/platform) typically run with KMS_NONCE_LEDGER_GC=30s for
// tighter memory bounds.
func buildNonceLedger() (zapserver.NonceLedger, error) {
cfg := zapserver.MemoryNonceLedgerConfig{}
if v := strings.TrimSpace(os.Getenv(envNonceLedgerTTL)); v != "" {
d, err := time.ParseDuration(v)
if err != nil {
return nil, fmt.Errorf("%s: %w", envNonceLedgerTTL, err)
}
cfg.TTL = d
}
if v := strings.TrimSpace(os.Getenv(envNonceLedgerGC)); v != "" {
d, err := time.ParseDuration(v)
if err != nil {
return nil, fmt.Errorf("%s: %w", envNonceLedgerGC, err)
}
cfg.GCInterval = d
}
return zapserver.NewMemoryNonceLedger(cfg), nil
}
// consensusSnapshot is the wire shape of the JSON file the operator
// drops into the kmsd container. Same shape as the env vars (one
// authority per slice) so the operator can pick either delivery.
type consensusSnapshot struct {
Validators []string `json:"validators"`
Operators []string `json:"operators"`
}
// buildConsensusAuthorizer constructs the ConsensusAuthorizer wired
// at boot. Reads either KMS_CONSENSUS_FILE or the two NodeID env
// vars; refuses to boot if neither is present, since the ZAP server
// is fail-closed by construction.
func buildConsensusAuthorizer() (zapserver.ConsensusAuthorizer, error) {
validators, operators, err := loadConsensusSnapshot()
if err != nil {
return nil, err
}
if len(validators) == 0 {
return nil, errors.New("consensus validator authority is empty (refusing to boot fail-open)")
}
if len(operators) == 0 {
return nil, errors.New("consensus operator authority is empty (refusing to boot fail-open)")
}
ttl := defaultConsensusTTL
if v := strings.TrimSpace(os.Getenv(envTTL)); v != "" {
d, err := time.ParseDuration(v)
if err != nil {
return nil, fmt.Errorf("%s: %w", envTTL, err)
}
ttl = d
}
az, err := zapserver.NewInProcessAuthorizer(zapserver.InProcessAuthorizerConfig{
Validators: zapserver.NewStaticAuthorityProvider(validators),
Operator: zapserver.NewStaticAuthorityProvider(operators),
CacheTTL: ttl,
})
if err != nil {
return nil, err
}
// Probe both providers once so a malformed snapshot surfaces here
// (in the fatal path) rather than on the first inbound request.
if _, err := az.Authorize(context.Background(), zapserver.Identity{
NodeID: validators[0],
}, "self-test", zapserver.OpAuthGet); err != nil {
return nil, fmt.Errorf("authorizer self-test: %w", err)
}
return az, nil
}
// loadConsensusSnapshot returns the (validators, operators) NodeID
// sets, sourcing from KMS_CONSENSUS_FILE first then falling back to
// KMS_CONSENSUS_VALIDATORS + KMS_CONSENSUS_OPERATORS env vars.
func loadConsensusSnapshot() ([]ids.NodeID, []ids.NodeID, error) {
if path := strings.TrimSpace(os.Getenv(envFile)); path != "" {
data, err := os.ReadFile(path)
if err != nil {
return nil, nil, fmt.Errorf("%s: %w", envFile, err)
}
var snap consensusSnapshot
if err := json.Unmarshal(data, &snap); err != nil {
return nil, nil, fmt.Errorf("%s: %w", envFile, err)
}
validators, err := parseNodeIDs(snap.Validators)
if err != nil {
return nil, nil, fmt.Errorf("%s validators: %w", envFile, err)
}
operators, err := parseNodeIDs(snap.Operators)
if err != nil {
return nil, nil, fmt.Errorf("%s operators: %w", envFile, err)
}
return validators, operators, nil
}
validators, err := parseNodeIDs(splitLines(os.Getenv(envValidators)))
if err != nil {
return nil, nil, fmt.Errorf("%s: %w", envValidators, err)
}
operators, err := parseNodeIDs(splitLines(os.Getenv(envOperators)))
if err != nil {
return nil, nil, fmt.Errorf("%s: %w", envOperators, err)
}
return validators, operators, nil
}
// splitLines splits on newlines, commas, and whitespace. Empty
// elements are dropped. Lets the operator pick whichever delimiter is
// convenient for its template engine.
func splitLines(raw string) []string {
if raw == "" {
return nil
}
out := make([]string, 0, 8)
field := strings.Builder{}
for _, r := range raw {
if r == '\n' || r == ',' || r == ' ' || r == '\t' || r == '\r' {
if field.Len() > 0 {
out = append(out, field.String())
field.Reset()
}
continue
}
field.WriteRune(r)
}
if field.Len() > 0 {
out = append(out, field.String())
}
return out
}
// parseNodeIDs returns the parsed NodeID slice; any malformed entry
// is a hard failure (refuses to boot with a typo in the snapshot).
func parseNodeIDs(items []string) ([]ids.NodeID, error) {
out := make([]ids.NodeID, 0, len(items))
for _, s := range items {
s = strings.TrimSpace(s)
if s == "" {
continue
}
id, err := ids.NodeIDFromString(s)
if err != nil {
return nil, fmt.Errorf("parse %q: %w", s, err)
}
out = append(out, id)
}
return out, nil
}
+361
View File
@@ -0,0 +1,361 @@
// Tests for the boot-time MPC fail-open behaviour added in v1.8.2.
//
// Before v1.8.2, KMS called log.Fatalf on any MPC ZAP init or status
// failure — a transient MPC outage took the secrets surface down with
// it. v1.8.2 logs a warning, sets mpcAvailable=false, and lets the
// secrets-only routes keep serving. /v1/kms/keys/* responds 503 with a
// re-probe attempt so the same pod can recover when MPC comes back up.
//
// These tests pin both behaviours: the 503 with re-probe, and the
// transparent recovery once a re-probe succeeds.
package main
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strings"
"sync"
"sync/atomic"
"testing"
"github.com/luxfi/kms/pkg/keys"
"github.com/luxfi/kms/pkg/mpc"
)
// fakeBackend is a minimal MPCBackend used only by the route tests.
// statusErr is consulted on every call so a single test can transition
// from "down" to "up" and verify the re-probe heals the cached flag.
//
// statusErr uses a sync.Mutex + plain error rather than atomic.Value
// because atomic.Value rejects typed-nil error; the route handlers are
// sequential per request so contention is negligible.
type fakeBackend struct {
statusCalls atomic.Int32
mu sync.Mutex
statusErr error
}
func (f *fakeBackend) currentErr() error {
f.mu.Lock()
defer f.mu.Unlock()
return f.statusErr
}
func (f *fakeBackend) setErr(err error) {
f.mu.Lock()
defer f.mu.Unlock()
f.statusErr = err
}
func (f *fakeBackend) Status(ctx context.Context) (*mpc.ClusterStatus, error) {
f.statusCalls.Add(1)
if err := f.currentErr(); err != nil {
return nil, err
}
return &mpc.ClusterStatus{Ready: true, ConnectedPeers: 3, ExpectedPeers: 3, Mode: "consensus"}, nil
}
func (f *fakeBackend) Keygen(context.Context, string, mpc.KeygenRequest) (*mpc.KeygenResult, error) {
return nil, errors.New("keygen unused in failopen tests")
}
func (f *fakeBackend) Sign(context.Context, mpc.SignRequest) (*mpc.SignResult, error) {
return nil, errors.New("sign unused in failopen tests")
}
func (f *fakeBackend) Reshare(context.Context, string, mpc.ReshareRequest) error {
return errors.New("reshare unused in failopen tests")
}
func (f *fakeBackend) GetWallet(context.Context, string) (*mpc.Wallet, error) {
return nil, errors.New("getwallet unused in failopen tests")
}
func (f *fakeBackend) Encrypt(context.Context, string, []byte) (*mpc.EncryptResult, error) {
return nil, errors.New("encrypt unused in failopen tests")
}
func (f *fakeBackend) Decrypt(context.Context, string, []byte) (*mpc.DecryptResult, error) {
return nil, errors.New("decrypt unused in failopen tests")
}
// requireMPC must short-circuit signing routes with 503 and a structured
// error body when MPC is unreachable. Without this, a degraded KMS would
// quietly invoke MPC and surface 500s with cryptic ZAP errors instead of
// the well-known "secrets-only" mode signal callers branch on.
func TestRegisterKMSRoutes_SignReturns503WhenMPCDown(t *testing.T) {
backend := &fakeBackend{}
backend.setErr(errors.New("connection reset by peer"))
mgr := keys.NewManager(backend, nil, "vault-1")
mux := http.NewServeMux()
mpcAvailable := false
registerKMSRoutes(mux, mgr, backend, &mpcAvailable)
srv := httptest.NewServer(mux)
defer srv.Close()
resp, err := http.Post(srv.URL+"/v1/kms/keys/v-1/sign", "application/json", strings.NewReader(`{"key_type":"bls","message":"aGVsbG8="}`))
if err != nil {
t.Fatalf("post sign: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusServiceUnavailable {
t.Fatalf("status: got %d want 503", resp.StatusCode)
}
var body map[string]string
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
t.Fatalf("decode: %v", err)
}
if body["error"] != "mpc unreachable" {
t.Errorf("error field: got %q want %q", body["error"], "mpc unreachable")
}
if body["mode"] != "secrets-only" {
t.Errorf("mode field: got %q want secrets-only", body["mode"])
}
if !strings.Contains(body["detail"], "connection reset by peer") {
t.Errorf("detail field: got %q; want underlying error included", body["detail"])
}
}
// keygen + rotate must also gate on requireMPC. A regression that
// exposes either of these endpoints to a down MPC is a 500 instead of a
// 503 — a different SLO / on-call signal — so this test pins both.
func TestRegisterKMSRoutes_KeygenAndRotateAlsoGated(t *testing.T) {
backend := &fakeBackend{}
backend.setErr(errors.New("dial tcp: i/o timeout"))
mgr := keys.NewManager(backend, nil, "vault-1")
mux := http.NewServeMux()
mpcAvailable := false
registerKMSRoutes(mux, mgr, backend, &mpcAvailable)
srv := httptest.NewServer(mux)
defer srv.Close()
keygenResp, err := http.Post(srv.URL+"/v1/kms/keys/generate", "application/json",
strings.NewReader(`{"validator_id":"v-1","threshold":2,"parties":3}`))
if err != nil {
t.Fatalf("post generate: %v", err)
}
defer keygenResp.Body.Close()
if keygenResp.StatusCode != http.StatusServiceUnavailable {
t.Fatalf("generate status: got %d want 503", keygenResp.StatusCode)
}
rotateResp, err := http.Post(srv.URL+"/v1/kms/keys/v-1/rotate", "application/json",
strings.NewReader(`{"new_threshold":3}`))
if err != nil {
t.Fatalf("post rotate: %v", err)
}
defer rotateResp.Body.Close()
if rotateResp.StatusCode != http.StatusServiceUnavailable {
t.Fatalf("rotate status: got %d want 503", rotateResp.StatusCode)
}
}
// requireMPC re-probes MPC on each call so a pod that booted into
// degraded mode can recover transparently when MPC comes back. The
// healed mpcAvailable flag flips to true and subsequent calls skip the
// re-probe entirely. This is the second-half guarantee the operator
// runbook documents — without it, a single MPC blip would require a KMS
// pod restart to clear the degraded state.
func TestRegisterKMSRoutes_RecoversAfterMPCComesUp(t *testing.T) {
backend := &fakeBackend{}
backend.setErr(errors.New("temporary outage"))
mgr := keys.NewManager(backend, nil, "vault-1")
mux := http.NewServeMux()
mpcAvailable := false
registerKMSRoutes(mux, mgr, backend, &mpcAvailable)
// Wrap with a recovery middleware so the inner mgr.SignWithBLS panic
// (nil store, expected for this minimal test rig) doesn't kill the
// test server. We only care that the request crossed requireMPC; the
// status code is enough signal.
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
w.WriteHeader(http.StatusInternalServerError)
}
}()
mux.ServeHTTP(w, r)
}))
defer srv.Close()
// First call: still down, expect 503 + a re-probe.
resp1, err := http.Post(srv.URL+"/v1/kms/keys/v-1/sign", "application/json",
strings.NewReader(`{"key_type":"bls","message":"aGVsbG8="}`))
if err != nil {
t.Fatalf("first post: %v", err)
}
resp1.Body.Close()
if resp1.StatusCode != http.StatusServiceUnavailable {
t.Fatalf("first call: got %d want 503", resp1.StatusCode)
}
if got := backend.statusCalls.Load(); got != 1 {
t.Fatalf("status probe count after first call: got %d want 1", got)
}
if mpcAvailable {
t.Fatal("mpcAvailable must remain false after a failed re-probe")
}
// MPC comes back. Next call probes once, flips the flag, then runs
// the actual signing handler (which fails inside the fake — the
// route reaches mgr.SignWithBLS and returns 500 from the fake's
// Sign error). Either way the request crossed requireMPC, which is
// what we're pinning.
backend.setErr(nil)
resp2, err := http.Post(srv.URL+"/v1/kms/keys/v-1/sign", "application/json",
strings.NewReader(`{"key_type":"bls","message":"aGVsbG8="}`))
if err != nil {
t.Fatalf("second post: %v", err)
}
resp2.Body.Close()
if resp2.StatusCode == http.StatusServiceUnavailable {
t.Fatalf("second call: got 503 after MPC recovered; want any non-503")
}
if got := backend.statusCalls.Load(); got != 2 {
t.Fatalf("status probe count after recovery: got %d want 2", got)
}
if !mpcAvailable {
t.Fatal("mpcAvailable must flip to true after a successful re-probe")
}
// Third call: flag is now true, requireMPC must skip the re-probe.
resp3, err := http.Post(srv.URL+"/v1/kms/keys/v-1/sign", "application/json",
strings.NewReader(`{"key_type":"bls","message":"aGVsbG8="}`))
if err != nil {
t.Fatalf("third post: %v", err)
}
resp3.Body.Close()
if got := backend.statusCalls.Load(); got != 2 {
t.Fatalf("status probe count must NOT increase after flag is up: got %d want 2", got)
}
}
// When NewZapClient itself fails at boot, no MPC client exists and
// registerKMSRoutes is never called — the keys/* routes would otherwise
// fall through to a 404 (not registered) or 405 (POST against an
// implicit GET-only SPA catch-all). registerStubKMSRoutes exists so
// callers see a uniform 503 signal across both degraded paths.
func TestRegisterStubKMSRoutes_All503WithBootError(t *testing.T) {
mux := http.NewServeMux()
registerStubKMSRoutes(mux, errors.New("dial tcp: i/o timeout"))
srv := httptest.NewServer(mux)
defer srv.Close()
cases := []struct {
method string
path string
body string
}{
{http.MethodPost, "/v1/kms/keys/generate", `{"validator_id":"x","threshold":2,"parties":3}`},
{http.MethodGet, "/v1/kms/keys", ""},
{http.MethodGet, "/v1/kms/keys/v-1", ""},
{http.MethodPost, "/v1/kms/keys/v-1/sign", `{"key_type":"bls","message":"aGVsbG8="}`},
{http.MethodPost, "/v1/kms/keys/v-1/rotate", `{"new_threshold":3}`},
{http.MethodGet, "/v1/kms/status", ""},
}
for _, c := range cases {
t.Run(c.method+" "+c.path, func(t *testing.T) {
req, err := http.NewRequest(c.method, srv.URL+c.path, strings.NewReader(c.body))
if err != nil {
t.Fatalf("build req: %v", err)
}
if c.body != "" {
req.Header.Set("Content-Type", "application/json")
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("do: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusServiceUnavailable {
t.Fatalf("status: got %d want 503", resp.StatusCode)
}
var body map[string]string
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
t.Fatalf("decode: %v", err)
}
if body["error"] != "mpc unreachable" {
t.Errorf("error: got %q want mpc unreachable", body["error"])
}
if body["mode"] != "secrets-only" {
t.Errorf("mode: got %q want secrets-only", body["mode"])
}
if !strings.Contains(body["detail"], "i/o timeout") {
t.Errorf("detail: got %q; want underlying boot error", body["detail"])
}
})
}
}
// healthHandler must report status=ok when secrets-only mode is the
// intended config (vaultID empty), and degrade to status=degraded when
// MPC was wired in but is unreachable.
func TestHealthHandler(t *testing.T) {
cases := []struct {
name string
vaultID string
mpcAvailable *bool
wantStatus string
wantMPCField bool
}{
{"secrets-only mode", "", nil, "ok", false},
{"mpc up", "vault-1", boolp(true), "ok", false},
{"mpc down", "vault-1", boolp(false), "degraded", true},
{"mpc nil flag treated as down", "vault-1", nil, "degraded", true},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
h := healthHandler(c.vaultID, c.mpcAvailable)
rr := httptest.NewRecorder()
h.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/healthz", nil))
if rr.Code != http.StatusOK {
t.Errorf("status code: got %d want 200", rr.Code)
}
var body map[string]string
if err := json.Unmarshal(rr.Body.Bytes(), &body); err != nil {
t.Fatalf("decode: %v", err)
}
if body["status"] != c.wantStatus {
t.Errorf("status field: got %q want %q", body["status"], c.wantStatus)
}
_, hasMPC := body["mpc"]
if hasMPC != c.wantMPCField {
t.Errorf("mpc field present: got %v want %v", hasMPC, c.wantMPCField)
}
})
}
}
func boolp(v bool) *bool { return &v }
// nil pointer must short-circuit cleanly; this is the legacy code path
// (mpcAvailable wasn't passed in earlier versions). Keep this test as a
// guard for any future caller that passes nil.
func TestRegisterKMSRoutes_NilFlagFallsBackToProbe(t *testing.T) {
backend := &fakeBackend{}
backend.setErr(errors.New("down"))
mgr := keys.NewManager(backend, nil, "vault-1")
mux := http.NewServeMux()
registerKMSRoutes(mux, mgr, backend, nil)
srv := httptest.NewServer(mux)
defer srv.Close()
resp, err := http.Post(srv.URL+"/v1/kms/keys/v-1/sign", "application/json",
strings.NewReader(`{"key_type":"bls","message":"aGVsbG8="}`))
if err != nil {
t.Fatalf("post: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusServiceUnavailable {
t.Fatalf("nil flag: got %d want 503", resp.StatusCode)
}
}
+971
View File
@@ -0,0 +1,971 @@
// Command kms starts the KMS server backed by ZapDB + MPC.
//
// Configuration precedence: flags > env vars > defaults.
//
// Boot is fail-open: if MPC_VAULT_ID is set but the MPC daemon is
// unreachable, KMS logs a warning and continues in secrets-only mode.
// /healthz reports `status=degraded` and any /v1/kms/keys/* request
// returns 503 with body `{"error":"mpc unreachable","mode":"secrets-only"}`.
// Each request re-probes MPC, so the same pod recovers transparently
// once MPC comes back up — no restart required.
//
// Env vars:
// MPC_ADDR - ZAP address (host:port); empty = mDNS discovery
// MPC_VAULT_ID - MPC vault ID for validator keys (required for MPC)
// KMS_NODE_ID - ZAP node ID (default "kms-0")
// ZAP_PORT - ZAP secrets-server listen port (default 9999, 0 = disable)
// MPC_REK_ENDPOINT - MPC ZAP CSV for threshold-rooted Root Encryption Key
// fetch. When set, kmsd refuses to start unless the
// MPC cluster returns a 32-byte REK. Takes precedence
// over KMS_MASTER_KEY_B64.
// MPC_REK_KEY_ID - MPC-side identifier of the wrapped REK record
// (default "kms/rek/v1"). Bump alongside reshare.
// MPC_REK_TIMEOUT - REK bootstrap timeout (Go duration, default "10s").
// KMS_MASTER_KEY_B64 - LEGACY 32-byte master key (base64). Used only when
// MPC_REK_ENDPOINT is unset. Slated for removal once
// every KMS deployment ships with MPC-rooted REK.
// KMS_DATA_DIR - ZapDB data directory (default "/data/kms")
// KMS_LISTEN - HTTP listen address (default ":8080")
// IAM_ENDPOINT - Hanzo IAM endpoint for auth (default "https://hanzo.id")
// KMS_CONSENSUS_VALIDATORS - newline-separated NodeIDs of the validator
// authority (consensus read set). Required for
// ZAP. Format: one cb58 NodeID per line.
// KMS_CONSENSUS_OPERATORS - newline-separated NodeIDs of the operator
// authority (consensus write set). Required
// for ZAP. Same format.
// KMS_CONSENSUS_FILE - optional file path supplying the same two
// sets (JSON: {"validators":[...],"operators":[...]}).
// When set, env vars above are ignored.
// KMS_CONSENSUS_TTL - per-authority refresh TTL (Go duration,
// default 30s). The kms-operator re-applies
// the snapshot at this cadence.
// KMS_NONCE_LEDGER_TTL - anti-replay nonce TTL (Go duration,
// default MaxClockSkew+1m=6m). Captured
// envelopes are rejected as replays while
// their (NodeID,Nonce) tuple is in the
// ledger; after TTL they fail clock-skew
// anyway. Tune up only if MaxClockSkew is
// tuned up.
// KMS_NONCE_LEDGER_GC - background GC sweep cadence (Go duration,
// default TTL/4 = 90s). Smaller values
// reduce peak memory; larger values reduce
// CPU burn. The ledger correctness does
// NOT depend on this — it's a memory/CPU
// trade only.
//
// S3 replication (ZapDB Replicator):
// REPLICATE_S3_ENDPOINT - S3 endpoint (empty = replication disabled)
// REPLICATE_S3_BUCKET - S3 bucket (default "lux-kms-backups")
// REPLICATE_S3_REGION - S3 region (default "us-central1")
// REPLICATE_S3_ACCESS_KEY
// REPLICATE_S3_SECRET_KEY
// REPLICATE_AGE_RECIPIENT - age public key for backup encryption
// REPLICATE_AGE_IDENTITY - age private key for restore
// REPLICATE_PATH - S3 key prefix (default "kms/{KMS_NODE_ID}")
package main
import (
"context"
"embed"
"encoding/base64"
"encoding/json"
"fmt"
"io/fs"
"log"
"log/slog"
"net/http"
"net/url"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"time"
badger "github.com/luxfi/zapdb"
"github.com/luxfi/kms/pkg/keys"
"github.com/luxfi/kms/pkg/mpc"
"github.com/luxfi/kms/pkg/store"
"github.com/luxfi/kms/pkg/store/mpcrek"
"github.com/luxfi/kms/pkg/zapserver"
luxlog "github.com/luxfi/log"
"github.com/luxfi/zap"
)
// One binary, one image. The KMS admin UI ships inside this binary.
// The `web/` directory at this package root is populated by `make build`
// (it copies `frontend/dist/` from the repo root before `go build`); the
// embed picks up the static Vite output and serves it at `/` with a SPA
// fallback so React Router routes resolve. API routes (`/v1/*`,
// `/healthz`, `/health`) take precedence and are registered before the
// catch-all. If `web/` is empty (e.g. fresh clone, no UI build yet), the
// fallback returns 404 cleanly — the API still works.
//
//go:embed all:web
var frontendDist embed.FS
func main() {
mpcAddr := envOr("MPC_ADDR", "")
vaultID := envOr("MPC_VAULT_ID", "")
nodeID := envOr("KMS_NODE_ID", "kms-0")
iamEndpoint := envOr("IAM_ENDPOINT", "https://hanzo.id")
dataDir := envOr("KMS_DATA_DIR", "/data/kms")
listen := envOr("KMS_LISTEN", ":8080")
// Open ZapDB.
if err := os.MkdirAll(dataDir, 0o700); err != nil {
log.Fatalf("kms: create data dir %s: %v", dataDir, err)
}
dbOpts := badger.DefaultOptions(dataDir).
WithLogger(zapdbLogger{}).
WithEncryptionKey(masterKeyFromEnv()).
WithIndexCacheSize(64 << 20) // 64MB index cache
db, err := badger.Open(dbOpts)
if err != nil {
log.Fatalf("kms: open zapdb at %s: %v", dataDir, err)
}
defer db.Close()
log.Printf("kms: zapdb opened at %s", dataDir)
// Start ZapDB Replicator if S3 is configured.
replicator := startReplicator(db, nodeID)
if replicator != nil {
defer replicator.Stop()
}
mux := http.NewServeMux()
// MPC availability flag. Flips to true after a successful ZAP probe at
// boot. Read by /healthz to surface degraded mode and by the keys
// routes to short-circuit with 503 when MPC is unreachable. The
// pointer-to-bool is set once in main and read concurrently — atomic
// load isn't necessary because the value never changes after boot.
var mpcAvailable bool
// Health probes — wired in every shape callers might try:
// /healthz / /health — root, for direct/standalone probes
// /v1/kms/healthz / /v1/kms/health — gateway-routed, no prefix strip
// All return the same shape so probes are interchangeable.
healthOK := healthHandler(vaultID, &mpcAvailable)
mux.HandleFunc("GET /healthz", healthOK)
mux.HandleFunc("GET /health", healthOK)
mux.HandleFunc("GET /v1/kms/healthz", healthOK)
mux.HandleFunc("GET /v1/kms/health", healthOK)
// SPA bootstrap config. The Infisical-derived React frontend in
// frontend/src/hooks/api/admin/queries.ts:fetchServerConfig fetches
// `/v1/admin/config` at first paint and refuses to render when the
// payload is missing — the user sees `["server-config"] data is
// undefined`. We don't run the full Infisical admin surface, so
// this returns a minimal-but-complete shape: signups via IAM, no
// invite-only gating, no SMTP, instance is initialized. Fields the
// SPA reads: initialized, allowSignUp, allowedSignUpDomain, etc.
// defaultAuthOrgSlug drives the OIDC "Login with <Brand>" button in
// the SPA: when set, the SPA bypasses the org-picker and goes straight
// to /v1/sso/oidc/login?orgSlug=<this>. Read from env so the same image
// white-labels per-deployment via KMS_DEFAULT_ORG_SLUG.
defaultOrgSlug := envOr("KMS_DEFAULT_ORG_SLUG", "")
mux.HandleFunc("GET /v1/admin/config", func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{
"config": map[string]any{
"initialized": true,
"allowSignUp": true,
"allowedSignUpDomain": nil,
"trustSamlEmails": false,
"trustLdapEmails": false,
"trustOidcEmails": true,
"defaultAuthOrgId": "",
"defaultAuthOrgSlug": defaultOrgSlug,
"isSecretScanningDisabled": false,
"isMigrationModeOn": false,
"enabledLoginMethods": []string{"oidc"},
"slackClientId": "",
"isSmtpConfigured": false,
"isSecretApprovalDisabled": false,
"identityRevocationEnabled": true,
},
})
})
// Machine identity auth via IAM.
mux.HandleFunc("POST /v1/kms/auth/login", func(w http.ResponseWriter, r *http.Request) {
var req struct {
ClientID string `json:"clientId"`
ClientSecret string `json:"clientSecret"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.ClientID == "" || req.ClientSecret == "" {
writeJSON(w, http.StatusBadRequest, map[string]any{"statusCode": 400, "message": "clientId and clientSecret required"})
return
}
form := url.Values{
"grant_type": {"client_credentials"},
"client_id": {req.ClientID},
"client_secret": {req.ClientSecret},
}
// Canonical OAuth2 path on hanzoai/iam — no `/api/` prefix (killed in
// v2.381.0). `/login/oauth/access_token` is mounted at root per the
// OAuth2 spec; `/oauth/access_token` is also wired as an alias.
resp, err := http.PostForm(iamEndpoint+"/login/oauth/access_token", form)
if err != nil {
writeJSON(w, http.StatusBadGateway, map[string]any{"statusCode": 502, "message": "identity provider unreachable"})
return
}
defer resp.Body.Close()
var tok map[string]any
json.NewDecoder(resp.Body).Decode(&tok)
at, _ := tok["access_token"].(string)
if at == "" {
writeJSON(w, http.StatusUnauthorized, map[string]any{"statusCode": 401, "message": "invalid credentials"})
return
}
writeJSON(w, http.StatusOK, map[string]any{"accessToken": at, "expiresIn": 86400, "tokenType": "Bearer"})
})
// Secret store — ZapDB-backed, encrypted at rest.
secStore := store.NewSecretStore(db)
// JWT-backed authorization for the secrets surface. Every request to
// /v1/kms/orgs/{org}/secrets/* must carry an IAM-signed bearer token
// whose `owner` claim equals {org} (or whose roles include
// kms-admin). Public endpoints (health, /v1/kms/auth/login, OIDC
// SSO, the SPA) are NOT wrapped — they remain reachable without a
// token. See auth.go.
//
// JWKS is fetched from KMS_IAM_URL (in-cluster) and the JWT `iss`
// claim is validated against KMS_EXPECTED_ISSUER (public hostname).
// When the deployment uses a single URL for both, fall through to
// IAM_ENDPOINT.
jwksFrom := envOr("KMS_IAM_URL", iamEndpoint)
expectedIss := envOr("KMS_EXPECTED_ISSUER", iamEndpoint)
auth := newOrgJWTAuth(jwksFrom, expectedIss)
// GET /v1/kms/orgs/{org}/secrets/{path...}/{name}
// Matches the ATS kmsclient.Get() URL pattern.
mux.HandleFunc("GET /v1/kms/orgs/{org}/secrets/{rest...}", auth.requireOrgJWT(func(w http.ResponseWriter, r *http.Request) {
rest := r.PathValue("rest")
idx := strings.LastIndex(rest, "/")
if idx < 0 {
writeJSON(w, http.StatusBadRequest, map[string]any{"message": "path and name required"})
return
}
path, name := rest[:idx], rest[idx+1:]
env := r.URL.Query().Get("env")
if env == "" {
env = "default"
}
sec, err := secStore.Get(path, name, env)
if err != nil {
writeJSON(w, http.StatusNotFound, map[string]any{"message": "not found"})
return
}
writeJSON(w, http.StatusOK, map[string]any{
"secret": map[string]any{"value": string(sec.Ciphertext)},
})
}))
// POST /v1/kms/orgs/{org}/secrets — create a secret.
mux.HandleFunc("POST /v1/kms/orgs/{org}/secrets", auth.requireOrgJWT(func(w http.ResponseWriter, r *http.Request) {
var req struct {
Path string `json:"path"`
Name string `json:"name"`
Env string `json:"env"`
Value string `json:"value"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Name == "" {
writeJSON(w, http.StatusBadRequest, map[string]any{"message": "name and value required"})
return
}
if req.Env == "" {
req.Env = "default"
}
sec := &store.Secret{
Name: req.Name,
Path: req.Path,
Env: req.Env,
Ciphertext: []byte(req.Value),
}
if err := secStore.Put(sec); err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]any{"message": err.Error()})
return
}
writeJSON(w, http.StatusCreated, map[string]any{"ok": true})
}))
// DELETE /v1/kms/orgs/{org}/secrets/{rest...}/{name}
mux.HandleFunc("DELETE /v1/kms/orgs/{org}/secrets/{rest...}", auth.requireOrgJWT(func(w http.ResponseWriter, r *http.Request) {
rest := r.PathValue("rest")
idx := strings.LastIndex(rest, "/")
if idx < 0 {
writeJSON(w, http.StatusBadRequest, map[string]any{"message": "path and name required"})
return
}
path, name := rest[:idx], rest[idx+1:]
env := r.URL.Query().Get("env")
if env == "" {
env = "default"
}
if err := secStore.Delete(path, name, env); err != nil {
writeJSON(w, http.StatusNotFound, map[string]any{"message": "not found"})
return
}
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
}))
// Legacy: env-backed secret fetch.
mux.HandleFunc("GET /v1/kms/secrets/{name}", func(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name")
if name == "" {
writeJSON(w, http.StatusBadRequest, map[string]any{"message": "secret name required"})
return
}
val := os.Getenv(name)
if val == "" {
writeJSON(w, http.StatusNotFound, map[string]any{"message": "not found"})
return
}
writeJSON(w, http.StatusOK, map[string]any{
"secret": map[string]any{"secretKey": name, "secretValue": val},
})
})
// MPC key management (only when MPC_VAULT_ID is set).
//
// Boot is fail-open: every error path here logs a warning and degrades
// to secrets-only mode instead of exiting. The previous behaviour
// (log.Fatalf on any ZAP init or status failure) meant a transient MPC
// outage took down KMS too — the secrets surface is independent and
// must keep serving. Routes that require MPC return 503 via the
// mpcAvailable flag wired into healthOK / registerKMSRoutes.
if vaultID != "" {
// Trust at the network boundary (NetworkPolicy + ZAP wire).
zapClient, err := mpc.NewZapClient(nodeID, mpcAddr)
switch {
case err != nil:
log.Printf("kms: WARNING: mpc zap client init failed: %v — degrading to secrets-only mode", err)
// Even though no client could be built, the key routes must
// still respond with a clear 503 — without this, callers see
// 405/404 and can't distinguish "MPC down" from "wrong path"
// or "wrong method". registerStubKMSRoutes installs handlers
// that always 503 with the same body shape as registerKMSRoutes.
registerStubKMSRoutes(mux, err)
default:
keyStore, ksErr := store.New(db)
if ksErr != nil {
log.Printf("kms: WARNING: mpc key store init failed: %v — degrading to secrets-only mode", ksErr)
zapClient.Close()
registerStubKMSRoutes(mux, ksErr)
break
}
checkCtx, checkCancel := context.WithTimeout(context.Background(), 5*time.Second)
status, statusErr := zapClient.Status(checkCtx)
checkCancel()
if statusErr != nil {
log.Printf("kms: WARNING: mpc unreachable via ZAP: %v — degrading to secrets-only mode", statusErr)
// Keep the client + key routes wired even though the probe
// failed: MPC may come up later in the same pod lifetime
// (e.g. NetworkPolicy applied after KMS started). Routes
// re-probe on every call; if MPC is up by then they
// succeed transparently.
} else {
log.Printf("kms: mpc ready=%v peers=%d/%d mode=%s",
status.Ready, status.ConnectedPeers, status.ExpectedPeers, status.Mode)
mpcAvailable = true
}
mgr := keys.NewManager(zapClient, keyStore, vaultID)
registerKMSRoutes(mux, mgr, zapClient, &mpcAvailable)
}
}
if vaultID == "" {
log.Printf("kms: MPC_VAULT_ID not set — running in secrets-only mode (no threshold signing)")
}
// ZAP secrets server — exposes the SecretStore over luxfi/zap on its own
// port so in-cluster callers can fetch with zero REST round-trip.
//
// The master key (Root Encryption Key) protecting every per-secret DEK is
// resolved by loadREK below. Preferred source is a luxfi/mpc threshold
// cluster (MPC_REK_ENDPOINT); fallback is KMS_MASTER_KEY_B64 for the
// migration window. If MPC_REK_ENDPOINT is set, kmsd FAILS CLOSED on any
// fetch error — there is no env-var fallback in MPC mode, since the
// whole point of MPC-rooting the REK is that it must not be derivable
// from anything on the pod.
zapPortStr := envOr("ZAP_PORT", "9999")
zapPort, _ := strconv.Atoi(zapPortStr)
masterKey := loadREK()
defer mpcrek.Zero(masterKey)
if masterKey != nil && zapPort > 0 {
n := zap.NewNode(zap.NodeConfig{
NodeID: nodeID + "-secrets",
ServiceType: "_kms._tcp",
Port: zapPort,
})
if err := n.Start(); err != nil {
log.Printf("kms: ZAP secrets-server failed to start on :%d: %v", zapPort, err)
} else {
authorizer, err := buildConsensusAuthorizer()
if err != nil {
// Fail-closed: a misconfigured authorizer is a wire-
// reachable security hole. Refuse to boot.
log.Fatalf("kms: consensus authorizer init failed: %v", err)
}
nonceLedger, err := buildNonceLedger()
if err != nil {
// Same fail-closed posture: a misconfigured ledger
// silently re-opens the replay window. Refuse to
// boot rather than ship a downgrade.
log.Fatalf("kms: nonce ledger init failed: %v", err)
}
zs := zapserver.New(zapserver.Config{
Store: secStore,
MasterKey: masterKey,
Authorizer: authorizer,
NonceLedger: nonceLedger,
Logger: luxlog.New("component", "kms-zapserver"),
})
zs.Register(n)
log.Printf("kms: ZAP secrets-server listening on :%d (service=_kms._tcp)", zapPort)
}
} else {
log.Printf("kms: ZAP secrets-server disabled (set MPC_REK_ENDPOINT or KMS_MASTER_KEY_B64 and ZAP_PORT to enable)")
}
// IAM OIDC SSO — /v1/sso/oidc/{login,callback}, /v1/sso/whoami, /v1/sso/logout.
// Registered before the SPA catch-all; if OIDC isn't configured the
// handlers return 503 so misconfiguration is observable.
registerOIDCRoutes(mux)
// Mount the embedded admin UI under the root catch-all. Registered last
// so explicit handlers (`/healthz`, `/health`, `/v1/*`) win the route
// match. SPA fallback: any GET that doesn't match a real file falls
// back to index.html so React Router can resolve the path client-side.
registerWebUI(mux)
// Start HTTP server.
srv := &http.Server{
Addr: listen,
Handler: mux,
ReadTimeout: 30 * time.Second,
WriteTimeout: 60 * time.Second,
IdleTimeout: 120 * time.Second,
}
go func() {
log.Printf("kms: HTTP listening on %s", listen)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("kms: http: %v", err)
}
}()
// Graceful shutdown.
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
log.Println("kms: shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv.Shutdown(ctx)
}
// registerStubKMSRoutes installs 503-only handlers for every /v1/kms/keys/*
// route. Used when the boot-time MPC client could not be constructed at
// all (e.g. dial timed out, ZAP node init failed). Without this, GET
// /v1/kms/keys/{id} fell through to the SPA catch-all and returned the
// admin UI HTML; POST returned 405 from the muxer's "no method match"
// path. Both are useless to callers — they need a single, parseable
// signal that MPC is down so retry / circuit-break logic kicks in.
//
// The body shape matches registerKMSRoutes' requireMPC 503 exactly so
// callers don't need to branch on "stub vs gated" — they see one
// `{"error":"mpc unreachable","mode":"secrets-only","detail":"..."}`
// response across both code paths.
func registerStubKMSRoutes(mux *http.ServeMux, bootErr error) {
stub := func(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{
"error": "mpc unreachable",
"detail": bootErr.Error(),
"mode": "secrets-only",
})
}
mux.HandleFunc("POST /v1/kms/keys/generate", stub)
mux.HandleFunc("GET /v1/kms/keys", stub)
mux.HandleFunc("GET /v1/kms/keys/{id}", stub)
mux.HandleFunc("POST /v1/kms/keys/{id}/sign", stub)
mux.HandleFunc("POST /v1/kms/keys/{id}/rotate", stub)
mux.HandleFunc("GET /v1/kms/status", stub)
}
// healthHandler returns the /healthz handler.
//
// Health is intentionally HTTP 200 even when MPC is unreachable: the
// secrets surface (ZAP secrets-server, /v1/kms/orgs/.../secrets, IAM
// SSO, /v1/kms/auth/login) is fully functional in secrets-only mode.
// Routes that require MPC (/v1/kms/keys/*) self-report 503 instead.
// K8s readiness gating off /healthz would otherwise pull a working
// secrets-only KMS out of rotation purely because MPC is in upgrade.
//
// When MPC is enabled in spec but unreachable, the body switches to
// `{"status":"degraded","mpc":"unreachable","detail":"..."}` so probes
// that scrape the body still observe the degraded mode without
// flapping the pod out of service.
func healthHandler(vaultID string, mpcAvailable *bool) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
body := map[string]string{"status": "ok", "service": "kms"}
if vaultID != "" && (mpcAvailable == nil || !*mpcAvailable) {
body["status"] = "degraded"
body["mpc"] = "unreachable"
body["detail"] = "secrets-only mode; signing routes return 503"
}
writeJSON(w, http.StatusOK, body)
}
}
func registerKMSRoutes(mux *http.ServeMux, mgr *keys.Manager, mpcBackend keys.MPCBackend, mpcAvailable *bool) {
// KMS key routes are unprotected at the HTTP layer — auth is enforced
// by the Gateway (JWT validation + X-IAM-Roles header injection).
// In-cluster callers (ATS, BD, TA) go through Gateway; direct access
// is blocked by K8s NetworkPolicy (only Gateway can reach port 8080).
//
// requireMPC short-circuits with 503 + a re-probe attempt when the
// boot-time MPC handshake failed. The re-probe lets KMS recover
// transparently if MPC came up after KMS did (common during rollout
// or NetworkPolicy reconcile races); a single 5s status call is
// cheap relative to keygen/sign latency.
requireMPC := func(w http.ResponseWriter, r *http.Request) bool {
if mpcAvailable != nil && *mpcAvailable {
return true
}
probeCtx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
if _, err := mpcBackend.Status(probeCtx); err != nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{
"error": "mpc unreachable",
"detail": err.Error(),
"mode": "secrets-only",
})
return false
}
if mpcAvailable != nil {
*mpcAvailable = true
}
return true
}
mux.HandleFunc("POST /v1/kms/keys/generate", func(w http.ResponseWriter, r *http.Request) {
if !requireMPC(w, r) {
return
}
var req keys.GenerateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
return
}
if req.ValidatorID == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "validator_id is required"})
return
}
if req.Threshold < 2 {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "threshold must be >= 2"})
return
}
if req.Parties < req.Threshold {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "parties must be >= threshold"})
return
}
if req.Threshold == req.Parties {
log.Printf("kms: WARNING: keygen threshold==parties (%d) for validator=%s — no fault tolerance",
req.Threshold, req.ValidatorID)
}
ks, err := mgr.GenerateValidatorKeys(r.Context(), req)
if err != nil {
log.Printf("kms: audit: keygen FAILED validator_id=%s error=%v", req.ValidatorID, err)
if strings.Contains(err.Error(), "already exists") {
writeJSON(w, http.StatusConflict, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
log.Printf("kms: audit: keygen OK validator_id=%s bls_wallet=%s corona_wallet=%s threshold=%d parties=%d",
ks.ValidatorID, ks.BLSWalletID, ks.CoronaWalletID, ks.Threshold, ks.Parties)
writeJSON(w, http.StatusCreated, ks)
})
mux.HandleFunc("GET /v1/kms/keys", func(w http.ResponseWriter, r *http.Request) {
list := mgr.List()
if list == nil {
list = []*keys.ValidatorKeySet{}
}
writeJSON(w, http.StatusOK, list)
})
mux.HandleFunc("GET /v1/kms/keys/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
ks, err := mgr.Get(id)
if err != nil {
writeJSON(w, http.StatusNotFound, map[string]string{"error": "validator key set not found"})
return
}
writeJSON(w, http.StatusOK, ks)
})
mux.HandleFunc("POST /v1/kms/keys/{id}/sign", func(w http.ResponseWriter, r *http.Request) {
if !requireMPC(w, r) {
return
}
id := r.PathValue("id")
var req keys.SignRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
return
}
if len(req.Message) == 0 {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "message is required"})
return
}
var resp *keys.SignResponse
var err error
switch req.KeyType {
case "bls":
resp, err = mgr.SignWithBLS(r.Context(), id, req.Message)
case "corona":
resp, err = mgr.SignWithCorona(r.Context(), id, req.Message)
default:
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "key_type must be 'bls' or 'corona'"})
return
}
if err != nil {
log.Printf("kms: audit: sign FAILED validator_id=%s key_type=%s error=%v", id, req.KeyType, err)
if strings.Contains(err.Error(), "not found") {
writeJSON(w, http.StatusNotFound, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
log.Printf("kms: audit: sign OK validator_id=%s key_type=%s", id, req.KeyType)
writeJSON(w, http.StatusOK, resp)
})
mux.HandleFunc("POST /v1/kms/keys/{id}/rotate", func(w http.ResponseWriter, r *http.Request) {
if !requireMPC(w, r) {
return
}
id := r.PathValue("id")
var req keys.RotateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
return
}
if req.NewThreshold == 0 && len(req.NewParticipants) == 0 {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "new_threshold or new_participants required"})
return
}
ks, err := mgr.Rotate(r.Context(), id, req)
if err != nil {
log.Printf("kms: audit: rotate FAILED validator_id=%s error=%v", id, err)
if strings.Contains(err.Error(), "not found") {
writeJSON(w, http.StatusNotFound, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
log.Printf("kms: audit: rotate OK validator_id=%s new_threshold=%d new_parties=%d",
id, ks.Threshold, ks.Parties)
writeJSON(w, http.StatusOK, ks)
})
mux.HandleFunc("GET /v1/kms/status", func(w http.ResponseWriter, r *http.Request) {
status, err := mpcBackend.Status(r.Context())
if err != nil {
writeJSON(w, http.StatusOK, map[string]string{
"kms": "ok",
"mpc": "unreachable",
"details": err.Error(),
})
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"kms": "ok",
"mpc": status,
})
})
}
// startReplicator initializes ZapDB S3 replication if configured.
//
// REPLICATE_S3_ENDPOINT is normalized to bare host:port — s3.New
// rejects fully-qualified URLs ("Endpoint url cannot have fully
// qualified paths") and silently disables replication when an operator
// passes a scheme- or path-bearing value. We parse defensively and
// drop both the scheme and any path component, logging a warning so
// the misconfiguration is visible.
func startReplicator(db *badger.DB, nodeID string) *badger.Replicator {
rawEndpoint := os.Getenv("REPLICATE_S3_ENDPOINT")
if rawEndpoint == "" {
log.Printf("kms: S3 replication disabled (set REPLICATE_S3_ENDPOINT to enable)")
return nil
}
endpoint, useSSL := normalizeS3Endpoint(rawEndpoint)
// Backwards-compatible env reads: AWS SDK names take precedence,
// REPLICATE_S3_* legacy names are honoured, and the historical
// REPLICATE_S3_ACCESS_KEY / _SECRET_KEY shorthand still wins as a
// last resort. Operators should pick one — we accept all three so
// a stale chart cannot silently zero-out credentials.
access := firstNonEmpty(
os.Getenv("REPLICATE_S3_ACCESS_KEY_ID"),
os.Getenv("AWS_ACCESS_KEY_ID"),
os.Getenv("REPLICATE_S3_ACCESS_KEY"),
)
secret := firstNonEmpty(
os.Getenv("REPLICATE_S3_SECRET_ACCESS_KEY"),
os.Getenv("AWS_SECRET_ACCESS_KEY"),
os.Getenv("REPLICATE_S3_SECRET_KEY"),
)
// Backup interval. Default 60s — every-second backups generated
// continuous allocation pressure (~3 KiB streamed per cycle plus
// the encrypter's working buffers, ~150 MiB sustained heap on
// quiet KMS pods). 60s catches up to mainnet write rate (~10
// secret rotations/day) with plenty of headroom; operators can
// tune via REPLICATE_INTERVAL=<go-duration>.
intv := time.Minute
if v := os.Getenv("REPLICATE_INTERVAL"); v != "" {
if d, err := time.ParseDuration(v); err == nil && d > 0 {
intv = d
}
}
cfg := badger.ReplicatorConfig{
Endpoint: endpoint,
Bucket: envOr("REPLICATE_S3_BUCKET", "lux-kms-backups"),
Region: envOr("REPLICATE_S3_REGION", "us-central1"),
AccessKey: access,
SecretKey: secret,
UseSSL: useSSL,
Path: envOr("REPLICATE_S3_PATH", envOr("REPLICATE_PATH", fmt.Sprintf("kms/%s", nodeID))),
Interval: intv,
}
// Age encryption for backups.
recipientStr := os.Getenv("REPLICATE_AGE_RECIPIENT")
if recipientStr != "" {
// Age recipient parsing happens inside the Replicator via the age package.
// The ReplicatorConfig accepts the raw recipient/identity interfaces.
log.Printf("kms: S3 replication with age encryption enabled")
}
replicator, err := badger.NewReplicator(db, cfg)
if err != nil {
log.Printf("kms: WARNING: S3 replicator init failed: %v — replication disabled", err)
return nil
}
go replicator.Start(context.Background())
log.Printf("kms: S3 replication started → %s/%s/%s", endpoint, cfg.Bucket, cfg.Path)
return replicator
}
// loadREK returns the 32-byte Root Encryption Key used by the ZAP secrets
// server to seal every per-secret DEK.
//
// Preference order:
// 1. MPC_REK_ENDPOINT set → fetch via mpcrek.Bootstrap. FAIL-CLOSED on
// any error: log.Fatalf rather than silently falling back to the
// env-var path. This is the point of MPC-rooting the REK — if the
// pod can shrug off MPC unavailability and still come up with a
// key, we have re-introduced the static-secret weakness we set out
// to remove.
// 2. KMS_MASTER_KEY_B64 set → decode and return. Logged WARN so the
// deployment knows it's still on the legacy path.
// 3. Neither set → nil (ZAP secrets-server stays disabled).
//
// The returned slice is the live REK for the process lifetime. The
// caller MUST keep it alive until shutdown and arrange to zero it via
// mpcrek.Zero on the way out (see the defer in main).
func loadREK() []byte {
if endpoint := envOr("MPC_REK_ENDPOINT", ""); endpoint != "" {
keyID := envOr("MPC_REK_KEY_ID", "kms/rek/v1")
timeoutStr := envOr("MPC_REK_TIMEOUT", "10s")
timeout, perr := time.ParseDuration(timeoutStr)
if perr != nil {
log.Fatalf("kms: MPC_REK_TIMEOUT %q invalid: %v", timeoutStr, perr)
}
nodeID := envOr("KMS_NODE_ID", "kms-0") + "-rek-bootstrap"
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
log.Printf("kms: bootstrapping REK from MPC endpoint=%q keyID=%q", endpoint, keyID)
rek, err := mpcrek.Bootstrap(ctx, mpcrek.Config{
Endpoint: endpoint,
KeyID: keyID,
NodeID: nodeID,
Timeout: timeout,
})
if err != nil {
// Fail-closed. The deployment asked for MPC-rooted REK and
// MPC said no — refuse to start with a static key, that's
// the split-brain we are escaping.
log.Fatalf("kms: MPC REK bootstrap failed: %v", err)
}
log.Printf("kms: REK bootstrapped from MPC (32 bytes)")
return rek
}
b64 := envOr("KMS_MASTER_KEY_B64", "")
if b64 == "" {
return nil
}
mk, err := base64.StdEncoding.DecodeString(b64)
if err != nil || len(mk) != 32 {
log.Printf("kms: KMS_MASTER_KEY_B64 invalid (need 32 bytes base64); ZAP secrets-server disabled")
return nil
}
log.Printf("kms: WARNING: using legacy KMS_MASTER_KEY_B64 env-var REK; set MPC_REK_ENDPOINT to migrate to MPC-rooted REK")
return mk
}
// masterKeyFromEnv returns a 32-byte encryption key for ZapDB at-rest encryption,
// or nil to disable (dev only).
func masterKeyFromEnv() []byte {
b64 := os.Getenv("KMS_ENCRYPTION_KEY_B64")
if b64 == "" {
return nil
}
key, err := base64.StdEncoding.DecodeString(b64)
if err != nil || len(key) != 32 {
log.Printf("kms: KMS_ENCRYPTION_KEY_B64 invalid (need 32 bytes base64); at-rest encryption disabled")
return nil
}
return key
}
func writeJSON(w http.ResponseWriter, code int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(v)
}
func envOr(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
func envBool(key string, fallback bool) bool {
v := strings.ToLower(strings.TrimSpace(os.Getenv(key)))
switch v {
case "":
return fallback
case "1", "true", "yes", "on":
return true
case "0", "false", "no", "off":
return false
default:
return fallback
}
}
// firstNonEmpty returns the first argument with non-zero length, or "" if
// all are empty. Used to fall back across the AWS / REPLICATE_S3 env-name
// variants without picking a default that could silently mask a typo.
func firstNonEmpty(values ...string) string {
for _, v := range values {
if v != "" {
return v
}
}
return ""
}
// normalizeS3Endpoint strips scheme + path from a REPLICATE_S3_ENDPOINT
// value so s3.New receives the bare host:port it expects. Returns
// (host[:port], useSSL) where useSSL is true unless the operator
// explicitly specified an http:// scheme. A non-empty path is logged as
// a warning so the misconfiguration is visible — the SDK rejects path-
// bearing endpoints with "Endpoint url cannot have fully qualified
// paths." and the historical behaviour was to silently disable
// replication.
func normalizeS3Endpoint(raw string) (host string, useSSL bool) {
useSSL = !strings.HasPrefix(raw, "http://")
// strings.HasPrefix above already covers "http://"; treat the bare
// "https://" case explicitly so the parse path stays simple.
if strings.Contains(raw, "://") {
u, err := url.Parse(raw)
if err != nil || u.Host == "" {
log.Printf("kms: REPLICATE_S3_ENDPOINT %q failed to parse — using as-is, replication may fail", raw)
return raw, useSSL
}
if u.Path != "" && u.Path != "/" {
log.Printf("kms: REPLICATE_S3_ENDPOINT %q has a path component (%q) — stripping; put the bucket in REPLICATE_S3_BUCKET", raw, u.Path)
}
if u.RawQuery != "" {
log.Printf("kms: REPLICATE_S3_ENDPOINT %q has a query string — stripping", raw)
}
return u.Host, useSSL
}
// Already bare host:port. Trim a trailing "/" if any operator added one.
return strings.TrimRight(raw, "/"), useSSL
}
// zapdbLogger adapts slog to ZapDB's Logger interface.
type zapdbLogger struct{}
func (zapdbLogger) Errorf(format string, args ...interface{}) {
slog.Error(fmt.Sprintf(format, args...))
}
func (zapdbLogger) Warningf(format string, args ...interface{}) {
slog.Warn(fmt.Sprintf(format, args...))
}
func (zapdbLogger) Infof(format string, args ...interface{}) {
slog.Info(fmt.Sprintf(format, args...))
}
func (zapdbLogger) Debugf(format string, args ...interface{}) {
slog.Debug(fmt.Sprintf(format, args...))
}
// registerWebUI mounts the embedded Vite SPA at `/` with a SPA fallback.
// API routes are registered earlier on the same mux and win the route
// match (Go 1.22 ServeMux specificity rules); this only catches the
// remainder. If `web/` is empty (not built yet), every fallthrough returns
// 404 — the API still works, just no UI.
func registerWebUI(mux *http.ServeMux) {
sub, err := fs.Sub(frontendDist, "web")
if err != nil {
log.Printf("kms: web UI disabled — embed sub: %v", err)
return
}
// If index.html is missing, treat the UI as not-built and return early.
// (`make build` populates `web/` from frontend/dist before `go build`.)
if _, err := fs.Stat(sub, "index.html"); err != nil {
log.Printf("kms: web UI not built — `make build` populates cmd/kms/web from frontend/dist")
return
}
fileSrv := http.FileServer(http.FS(sub))
mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
// Trim leading "/" once for fs.Stat.
clean := strings.TrimPrefix(r.URL.Path, "/")
if clean == "" {
clean = "index.html"
}
if _, err := fs.Stat(sub, clean); err != nil {
// Not a real asset — serve index.html so React Router takes over.
r2 := r.Clone(r.Context())
r2.URL.Path = "/"
fileSrv.ServeHTTP(w, r2)
return
}
fileSrv.ServeHTTP(w, r)
})
log.Printf("kms: web UI mounted at /")
}
+60
View File
@@ -0,0 +1,60 @@
package main
import "testing"
// TestNormalizeS3Endpoint covers the silent-disable bug fixed in
// startReplicator. A path-bearing or scheme-bearing endpoint must be
// reduced to bare host:port; minio.New rejects anything else with
// "Endpoint url cannot have fully qualified paths." and the historical
// behaviour was to silently disable replication.
func TestNormalizeS3Endpoint(t *testing.T) {
cases := []struct {
name string
in string
wantHost string
wantSSL bool
}{
{"bare host:port", "s3.example:9000", "s3.example:9000", true},
{"http scheme stripped, no SSL", "http://s3.example:9000", "s3.example:9000", false},
{"https scheme stripped, SSL on", "https://s3.example:9000", "s3.example:9000", true},
{"path stripped after http", "http://s3.example:9000/somebucket", "s3.example:9000", false},
{"path stripped after https", "https://s3.example:9000/x/y/z", "s3.example:9000", true},
{"trailing slash trimmed", "s3.example:9000/", "s3.example:9000", true},
{"k8s svc DNS http", "http://s3.lux.svc.cluster.local:9000", "s3.lux.svc.cluster.local:9000", false},
{"query stripped", "https://s3.example:9000/?versioning=on", "s3.example:9000", true},
// Defensive: malformed input — return as-is so the operator sees
// the original error from minio rather than a silently-mangled
// host. Replication will still fail, but visibly.
{"malformed scheme", "://broken", "://broken", true},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
gotHost, gotSSL := normalizeS3Endpoint(c.in)
if gotHost != c.wantHost {
t.Errorf("host: got %q want %q", gotHost, c.wantHost)
}
if gotSSL != c.wantSSL {
t.Errorf("useSSL: got %v want %v", gotSSL, c.wantSSL)
}
})
}
}
// TestFirstNonEmpty covers the credential-resolution fallback. The
// historical env var was REPLICATE_S3_ACCESS_KEY; AWS SDK chains and
// the operator-injected REPLICATE_S3_ACCESS_KEY_ID variant must take
// precedence in that order without one zeroing out another.
func TestFirstNonEmpty(t *testing.T) {
if got := firstNonEmpty("", "", ""); got != "" {
t.Errorf("all empty: got %q want empty", got)
}
if got := firstNonEmpty("a", "b", "c"); got != "a" {
t.Errorf("first wins: got %q want a", got)
}
if got := firstNonEmpty("", "b", "c"); got != "b" {
t.Errorf("second wins when first empty: got %q want b", got)
}
if got := firstNonEmpty("", "", "c"); got != "c" {
t.Errorf("third wins when first two empty: got %q want c", got)
}
}
+880
View File
@@ -0,0 +1,880 @@
// OIDC SSO handlers — Hanzo IAM authorization-code flow.
//
// Flow:
//
// 1. GET /v1/sso/oidc/login?orgSlug=... → 302 IAM /login/oauth/authorize
// Sets HttpOnly+Secure state cookie (HMAC-signed, 5-min expiry, single-use).
// 2. GET /v1/sso/oidc/callback?code&state → exchange code, set session cookie, 302 /
// Validates state (signature + expiry + cookie match + server-side nonce
// blacklist), Origin/Referer (parsed-Host equality, no prefix extension).
// 3. GET /v1/sso/whoami → echo session subject (used by SPA after redirect)
// 4. POST /v1/sso/logout → clear session cookie, 204
// 5. POST /v1/kms/credentials → mint M2M client (kms-admin role + JWKS-validated session required)
//
// State and session cookies are independent: the state cookie is short-lived
// (5 min) and consumed exactly once on callback; the session cookie holds the
// IAM access token for subsequent KMS API calls.
//
// CSRF protection on /callback:
// - state parameter is HMAC-signed, expires in 5 min, single-use (matched
// against the kms_oidc_state cookie value AND a server-side nonce LRU
// so a replica fleet can't double-redeem)
// - Referer/Origin (if present) must be from IAM_ENDPOINT or this Host —
// compared by parsed URL.Host equality, not prefix.
//
// Mint authorization (POST /v1/kms/credentials):
// - Session JWT validated against IAM JWKS (sig, iss, aud, exp, owner)
// - Caller must have `kms-admin` or `superadmin` in `roles` claim
// - Per-subject rate limit: 5 mints / hour
// - Audited: subject + name + outcome
package main
import (
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strings"
"sync"
"time"
gojose "github.com/go-jose/go-jose/v4"
"github.com/go-jose/go-jose/v4/jwt"
)
const (
// stateCookie holds the HMAC-signed state nonce. Single-use.
stateCookie = "kms_oidc_state"
// sessionCookie holds the IAM access token after successful login.
sessionCookie = "kms_session"
// stateTTL bounds the OAuth round-trip — IAM must redirect back inside
// this window or the user re-clicks "Login".
stateTTL = 5 * time.Minute
// sessionTTL matches the IAM access-token lifetime we accept (24h).
// IAM enforces its own expiry inside the JWT; this is the cookie cap.
sessionTTL = 24 * time.Hour
// mintRateLimit caps mint-credential requests per subject per window.
mintRateLimit = 5
mintRateWindow = time.Hour
// Roles permitted to mint M2M client credentials.
roleKMSAdmin = "kms-admin"
roleSuperadmin = "superadmin"
)
// oidcConfig captures the static OIDC settings for KMS. The clientID is
// safe to bake into env (it's public per OAuth2 spec); clientSecret stays
// in env-only and is supplied via KMS_OIDC_CLIENT_SECRET (KMSSecret CRD).
type oidcConfig struct {
iamEndpoint string // e.g. https://iam.dev.example.com
iamHost string // parsed Host of iamEndpoint (for origin comparisons)
clientID string // e.g. lux-kms
clientSecret string // KMSSecret-injected
stateSecret []byte // HMAC key for state-nonce signing (>=32 bytes)
owner string // KMS_IAM_OWNER — required JWT `owner` claim value
cookieDomain string // optional; empty = host-only cookie (recommended)
// Server-side single-use nonce blacklist. Replicated boxes share state
// only via cookie+HMAC; this LRU prevents intra-replica replay before
// the cookie clear lands on the client.
nonces *nonceLRU
// Per-subject mint rate limiter.
mintLimiter *rateLimiter
// JWT validator for session tokens (mint-credential gate).
jwtValidator *sessionJWTValidator
}
// loadOIDCConfig reads the OIDC settings. Returns nil if OIDC is not
// configured — the handlers register a 503 in that case so the deployment
// is observably misconfigured rather than silently broken.
func loadOIDCConfig() *oidcConfig {
iam := envOr("IAM_ENDPOINT", "")
cid := envOr("KMS_OIDC_CLIENT_ID", "")
cs := envOr("KMS_OIDC_CLIENT_SECRET", "")
ss := envOr("KMS_STATE_SECRET", "")
owner := envOr("KMS_IAM_OWNER", "lux")
if iam == "" || cid == "" || cs == "" || ss == "" {
return nil
}
// State HMAC key: minimum 32 bytes of entropy. The operator wires
// this from KMS (KMSSecret CRD); reject anything shorter so a typo
// can't reduce the search space for a forger.
if len(ss) < 32 {
log.Printf("kms: KMS_STATE_SECRET must be >= 32 bytes; OIDC disabled")
return nil
}
cookieDomain := envOr("KMS_COOKIE_DOMAIN", "")
// Reject leading-dot wildcard cookie domains. Setting Domain=.foo.com
// makes the session cookie visible to every subdomain — leakage we don't
// want. Operator must use host-only (the empty default) or an exact host.
if strings.HasPrefix(cookieDomain, ".") {
log.Printf("kms: KMS_COOKIE_DOMAIN=%q starts with '.' (wildcard); OIDC disabled — set host-only or exact host", cookieDomain)
return nil
}
iamURL, err := url.Parse(strings.TrimRight(iam, "/"))
if err != nil || iamURL.Host == "" {
log.Printf("kms: IAM_ENDPOINT %q failed to parse; OIDC disabled", iam)
return nil
}
return &oidcConfig{
iamEndpoint: strings.TrimRight(iam, "/"),
iamHost: iamURL.Host,
clientID: cid,
clientSecret: cs,
stateSecret: []byte(ss),
owner: owner,
cookieDomain: cookieDomain,
nonces: newNonceLRU(10000, stateTTL),
mintLimiter: newRateLimiter(mintRateLimit, mintRateWindow),
jwtValidator: newSessionJWTValidator(strings.TrimRight(iam, "/"), cid, owner),
}
}
// signState builds an opaque, tamper-evident state token:
//
// base64url(nonce(16B) | be64(unix-expiry) | hmac(secret, nonce|expiry|orgSlug))
//
// orgSlug is bound into the MAC so an attacker cannot relay a state token
// minted for one org to a different org's callback.
func (c *oidcConfig) signState(orgSlug string) (string, error) {
nonce := make([]byte, 16)
if _, err := rand.Read(nonce); err != nil {
return "", err
}
exp := time.Now().Add(stateTTL).Unix()
buf := make([]byte, 0, 32)
buf = append(buf, nonce...)
buf = appendBE64(buf, exp)
mac := hmac.New(sha256.New, c.stateSecret)
mac.Write(buf)
mac.Write([]byte(orgSlug))
buf = append(buf, mac.Sum(nil)...)
return base64.RawURLEncoding.EncodeToString(buf), nil
}
// verifyState confirms the state token was issued by us, hasn't expired,
// and is bound to the given orgSlug. Returns the 16-byte nonce on success
// for caller-side single-use enforcement.
func (c *oidcConfig) verifyState(token, orgSlug string) ([]byte, error) {
raw, err := base64.RawURLEncoding.DecodeString(token)
if err != nil {
return nil, fmt.Errorf("state: bad encoding")
}
// 16-byte nonce + 8-byte expiry + 32-byte HMAC = 56 bytes minimum.
if len(raw) != 16+8+sha256.Size {
return nil, fmt.Errorf("state: bad length")
}
nonce := raw[:16]
exp := readBE64(raw[16:24])
gotMAC := raw[24:]
mac := hmac.New(sha256.New, c.stateSecret)
mac.Write(nonce)
mac.Write(raw[16:24])
mac.Write([]byte(orgSlug))
wantMAC := mac.Sum(nil)
if !hmac.Equal(gotMAC, wantMAC) {
return nil, fmt.Errorf("state: bad signature")
}
if time.Now().Unix() > exp {
return nil, fmt.Errorf("state: expired")
}
return nonce, nil
}
// callbackURL returns the absolute callback URL. KMS runs behind
// hanzoai/ingress with TLS termination; the scheme is always https in
// production. Hardcoded to avoid X-Forwarded-Proto misconfig regressions
// silently producing http:// redirect_uri values that IAM rejects.
func callbackURL(r *http.Request) string {
return "https://" + r.Host + "/v1/sso/oidc/callback"
}
// registerOIDCRoutes wires the SSO endpoints onto the given mux. If OIDC
// isn't configured the handlers return 503 so misconfiguration is visible.
func registerOIDCRoutes(mux *http.ServeMux) {
cfg := loadOIDCConfig()
if cfg == nil {
mux.HandleFunc("GET /v1/sso/oidc/login", oidcUnconfigured)
mux.HandleFunc("GET /v1/sso/oidc/callback", oidcUnconfigured)
mux.HandleFunc("GET /v1/sso/whoami", oidcUnconfigured)
mux.HandleFunc("POST /v1/sso/logout", oidcUnconfigured)
mux.HandleFunc("POST /v1/kms/credentials", oidcUnconfigured)
log.Printf("kms: OIDC SSO disabled (set IAM_ENDPOINT, KMS_OIDC_CLIENT_ID, KMS_OIDC_CLIENT_SECRET, KMS_STATE_SECRET)")
return
}
log.Printf("kms: OIDC SSO enabled — issuer=%s client_id=%s owner=%s", cfg.iamEndpoint, cfg.clientID, cfg.owner)
mux.HandleFunc("GET /v1/sso/oidc/login", cfg.handleLogin)
mux.HandleFunc("GET /v1/sso/oidc/callback", cfg.handleCallback)
mux.HandleFunc("GET /v1/sso/whoami", cfg.handleWhoami)
mux.HandleFunc("POST /v1/sso/logout", cfg.handleLogout)
mux.HandleFunc("POST /v1/kms/credentials", cfg.handleMintClientCredential)
}
func oidcUnconfigured(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusServiceUnavailable, map[string]any{
"statusCode": 503,
"message": "OIDC SSO not configured",
})
}
// handleLogin: mint a state token, set the state cookie, redirect to IAM.
func (c *oidcConfig) handleLogin(w http.ResponseWriter, r *http.Request) {
orgSlug := r.URL.Query().Get("orgSlug")
state, err := c.signState(orgSlug)
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]any{"message": "state mint failed"})
return
}
http.SetCookie(w, &http.Cookie{
Name: stateCookie,
Value: state,
Path: "/v1/sso/oidc/callback",
Domain: c.cookieDomain,
MaxAge: int(stateTTL / time.Second),
HttpOnly: true,
Secure: true,
// SameSite=Lax allows the cookie to ride along the IAM→KMS
// top-level GET redirect; Strict would drop it.
SameSite: http.SameSiteLaxMode,
})
q := url.Values{
"client_id": {c.clientID},
"redirect_uri": {callbackURL(r)},
"response_type": {"code"},
"scope": {"openid profile email"},
"state": {state},
}
if orgSlug != "" {
// Hanzo IAM honours `application` as the org-app scoping hint.
q.Set("application", orgSlug)
}
http.Redirect(w, r, c.iamEndpoint+"/login/oauth/authorize?"+q.Encode(), http.StatusFound)
}
// handleCallback: validate state + Origin/Referer, exchange code, set session.
func (c *oidcConfig) handleCallback(w http.ResponseWriter, r *http.Request) {
// CSRF: if the browser sent Origin or Referer, it must match this host
// or IAM. Comparison is parsed-URL.Host equality (case-insensitive),
// not prefix — `iam.dev.example.com.evil.com` does not match
// `iam.dev.example.com`.
if !originLooksOK(r, c.iamHost, r.Host) {
log.Printf("kms: oidc callback rejected — bad origin/referer")
http.Error(w, "forbidden", http.StatusForbidden)
return
}
state := r.URL.Query().Get("state")
code := r.URL.Query().Get("code")
orgSlug := r.URL.Query().Get("orgSlug") // optional; may be empty
if state == "" || code == "" {
writeJSON(w, http.StatusBadRequest, map[string]any{"message": "state and code required"})
return
}
// State must equal the cookie minted on /login (strict pinning).
cookie, err := r.Cookie(stateCookie)
if err != nil || cookie.Value == "" {
writeJSON(w, http.StatusBadRequest, map[string]any{"message": "missing state cookie"})
return
}
if !hmac.Equal([]byte(cookie.Value), []byte(state)) {
writeJSON(w, http.StatusBadRequest, map[string]any{"message": "state mismatch"})
return
}
nonce, err := c.verifyState(state, orgSlug)
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]any{"message": err.Error()})
return
}
// Single-use: cookie clear (race-prone across replicas) + server-side
// nonce blacklist (authoritative). If the same nonce arrives a second
// time on this replica it's rejected here.
if !c.nonces.consume(nonce) {
writeJSON(w, http.StatusBadRequest, map[string]any{"message": "state: already used"})
return
}
http.SetCookie(w, &http.Cookie{
Name: stateCookie, Value: "", Path: "/v1/sso/oidc/callback",
Domain: c.cookieDomain, MaxAge: -1, HttpOnly: true, Secure: true, SameSite: http.SameSiteLaxMode,
})
tok, err := c.exchangeCode(r.Context(), code, callbackURL(r))
if err != nil {
log.Printf("kms: oidc token exchange failed: %v", err)
writeJSON(w, http.StatusBadGateway, map[string]any{"message": "token exchange failed"})
return
}
if tok == "" {
writeJSON(w, http.StatusUnauthorized, map[string]any{"message": "no access_token returned"})
return
}
// Session cookie = IAM access token. We validate it ourselves on the
// mint-credential gate; SPA-only routes treat it as an opaque presence
// signal.
http.SetCookie(w, &http.Cookie{
Name: sessionCookie,
Value: tok,
Path: "/",
Domain: c.cookieDomain,
MaxAge: int(sessionTTL / time.Second),
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
})
// Strict redirect target: hardcoded to "/" so a tampered query param
// can't turn this into an open redirect.
http.Redirect(w, r, "/", http.StatusFound)
}
// handleWhoami echoes the session token presence so the SPA can decide
// whether to render the dashboard. Token contents are NOT returned.
func (c *oidcConfig) handleWhoami(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie(sessionCookie)
if err != nil || cookie.Value == "" {
writeJSON(w, http.StatusUnauthorized, map[string]any{"authenticated": false})
return
}
writeJSON(w, http.StatusOK, map[string]any{"authenticated": true})
}
// handleMintClientCredential creates a new KMS service application
// (clientId/clientSecret pair) via Hanzo IAM admin API and returns the
// secret to the caller exactly once.
//
// Authz chain (every check fail-closed):
// 1. session cookie present
// 2. session JWT verified against IAM JWKS (sig, iss, aud, exp)
// 3. JWT `owner` claim equals KMS_IAM_OWNER
// 4. JWT `roles` claim contains kms-admin or superadmin
// 5. per-subject rate limit (5 / hour)
//
// On success the IAM admin credential (env-only) is used to create the
// app; the generated secret is forwarded to the caller exactly once.
func (c *oidcConfig) handleMintClientCredential(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie(sessionCookie)
if err != nil || cookie.Value == "" {
writeJSON(w, http.StatusUnauthorized, map[string]any{"message": "session required"})
return
}
claims, err := c.jwtValidator.validate(r.Context(), cookie.Value)
if err != nil {
log.Printf("kms: audit: mint-credential rejected — jwt: %v", err)
writeJSON(w, http.StatusUnauthorized, map[string]any{"message": "invalid session"})
return
}
if !hasRole(claims.Roles, roleKMSAdmin) && !hasRole(claims.Roles, roleSuperadmin) {
log.Printf("kms: audit: mint-credential subject=%s ok=false reason=role", claims.Subject)
writeJSON(w, http.StatusForbidden, map[string]any{"message": "kms-admin role required"})
return
}
if !c.mintLimiter.allow(claims.Subject) {
log.Printf("kms: audit: mint-credential subject=%s ok=false reason=rate", claims.Subject)
writeJSON(w, http.StatusTooManyRequests, map[string]any{"message": "rate limited"})
return
}
adminID := envOr("KMS_IAM_ADMIN_CLIENT_ID", "")
adminSecret := envOr("KMS_IAM_ADMIN_CLIENT_SECRET", "")
if adminID == "" || adminSecret == "" {
writeJSON(w, http.StatusServiceUnavailable, map[string]any{
"message": "IAM admin credentials not configured",
})
return
}
var req struct {
Name string `json:"name"`
Description string `json:"description"`
}
_ = json.NewDecoder(r.Body).Decode(&req) // body optional
if req.Name == "" {
// Default name: kms-client-{8 hex} so duplicates can't collide.
nb := make([]byte, 4)
_, _ = rand.Read(nb)
req.Name = fmt.Sprintf("kms-client-%x", nb)
}
clientID, clientSecret, err := c.callIAMAddApplication(r.Context(), adminID, adminSecret, req.Name, req.Description)
if err != nil {
log.Printf("kms: audit: mint-credential subject=%s name=%s ok=false reason=iam: %v", claims.Subject, req.Name, err)
writeJSON(w, http.StatusBadGateway, map[string]any{"message": err.Error()})
return
}
log.Printf("kms: audit: mint-credential subject=%s name=%s ok=true", claims.Subject, req.Name)
writeJSON(w, http.StatusCreated, map[string]any{
"name": req.Name,
"clientId": clientID,
"clientSecret": clientSecret,
})
}
// callIAMAddApplication posts the create request to Hanzo IAM and parses
// the response. Validates HTTP status AND the IAM envelope `status` so a
// 200-with-error body is treated as the failure it is.
func (c *oidcConfig) callIAMAddApplication(ctx context.Context, adminID, adminSecret, name, description string) (string, string, error) {
body := map[string]any{
"owner": c.owner,
"name": name,
"displayName": name,
"description": description,
"organization": c.owner,
// Mark as M2M (IAM "Service" type) so it doesn't appear in
// the user-facing app picker.
"clientId": "", // server-generated
"clientSecret": "", // server-generated
}
bodyJSON, _ := json.Marshal(body)
q := url.Values{
"clientId": {adminID},
"clientSecret": {adminSecret},
}
adminURL := c.iamEndpoint + "/api/add-application?" + q.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, adminURL, strings.NewReader(string(bodyJSON)))
if err != nil {
return "", "", err
}
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", "", fmt.Errorf("iam unreachable: %w", err)
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 64<<10))
if resp.StatusCode/100 != 2 {
return "", "", fmt.Errorf("iam status=%d", resp.StatusCode)
}
// Hanzo IAM returns the generated app under either `data` or `data2`
// depending on version — try both shapes.
var iam struct {
Status string `json:"status"`
Msg string `json:"msg"`
Data json.RawMessage `json:"data"`
Data2 json.RawMessage `json:"data2"`
}
if err := json.Unmarshal(respBody, &iam); err != nil {
return "", "", fmt.Errorf("iam: bad json: %w", err)
}
if iam.Status != "" && iam.Status != "ok" {
return "", "", fmt.Errorf("iam: %s", iam.Msg)
}
type appShape struct {
ClientID string `json:"clientId"`
ClientSecret string `json:"clientSecret"`
}
var app appShape
for _, raw := range []json.RawMessage{iam.Data2, iam.Data} {
if len(raw) == 0 || string(raw) == "null" {
continue
}
var a appShape
if err := json.Unmarshal(raw, &a); err == nil && a.ClientID != "" {
app = a
break
}
}
if app.ClientID == "" || app.ClientSecret == "" {
return "", "", errors.New("iam returned no credentials")
}
return app.ClientID, app.ClientSecret, nil
}
// handleLogout clears the session cookie. State cookie is already cleared
// by the callback; nothing else to do server-side.
func (c *oidcConfig) handleLogout(w http.ResponseWriter, _ *http.Request) {
http.SetCookie(w, &http.Cookie{
Name: sessionCookie, Value: "", Path: "/", Domain: c.cookieDomain,
MaxAge: -1, HttpOnly: true, Secure: true, SameSite: http.SameSiteLaxMode,
})
w.WriteHeader(http.StatusNoContent)
}
// exchangeCode posts the auth code to IAM's token endpoint and returns
// the access_token. The IAM endpoint is canonical Hanzo IAM at
// /login/oauth/access_token (no /api/ prefix — killed in iam v2.381).
func (c *oidcConfig) exchangeCode(ctx context.Context, code, redirectURI string) (string, error) {
form := url.Values{
"grant_type": {"authorization_code"},
"code": {code},
"client_id": {c.clientID},
"client_secret": {c.clientSecret},
"redirect_uri": {redirectURI},
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
c.iamEndpoint+"/login/oauth/access_token",
strings.NewReader(form.Encode()))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 64<<10))
if err != nil {
return "", err
}
if resp.StatusCode/100 != 2 {
return "", fmt.Errorf("iam status=%d", resp.StatusCode)
}
var tok struct {
AccessToken string `json:"access_token"`
Error string `json:"error"`
}
if err := json.Unmarshal(body, &tok); err != nil {
return "", err
}
if tok.Error != "" {
return "", fmt.Errorf("iam error: %s", tok.Error)
}
return tok.AccessToken, nil
}
// originLooksOK returns true if the request's Origin/Referer is empty
// (no header sent — direct nav) or its parsed Host equals one of the
// allowed hosts (case-insensitive). Hosts only — no prefix matching — so
// `iam.dev.example.com.evil.com` cannot impersonate `iam.dev.example.com`.
// Schemes other than https are rejected.
func originLooksOK(r *http.Request, allowedHosts ...string) bool {
for _, h := range []string{r.Header.Get("Origin"), r.Header.Get("Referer")} {
if h == "" {
continue
}
u, err := url.Parse(h)
if err != nil || u.Host == "" {
return false
}
if u.Scheme != "https" {
return false
}
matched := false
for _, host := range allowedHosts {
if host == "" {
continue
}
if strings.EqualFold(u.Host, host) {
matched = true
break
}
}
if !matched {
return false
}
}
return true
}
func appendBE64(b []byte, v int64) []byte {
return append(b,
byte(v>>56), byte(v>>48), byte(v>>40), byte(v>>32),
byte(v>>24), byte(v>>16), byte(v>>8), byte(v),
)
}
func readBE64(b []byte) int64 {
_ = b[7]
return int64(b[0])<<56 | int64(b[1])<<48 | int64(b[2])<<40 | int64(b[3])<<32 |
int64(b[4])<<24 | int64(b[5])<<16 | int64(b[6])<<8 | int64(b[7])
}
// hasRole returns true if any element of roles equals want (case-sensitive,
// matches IAM convention).
func hasRole(roles []string, want string) bool {
for _, r := range roles {
if r == want {
return true
}
}
return false
}
// ---------------------------------------------------------------------------
// JWKS-validated session JWTs.
// ---------------------------------------------------------------------------
// sessionClaims is the subset of IAM claims we authorize on.
// `owner` scopes to the org slug; `roles` carries the role list the
// kms-admin gate consumes.
type sessionClaims struct {
jwt.Claims
Owner string `json:"owner"`
Roles []string `json:"roles"`
}
// sessionJWTValidator verifies session JWTs against the IAM JWKS. TTL
// cache for keys (5 min); fail-stale on refresh error so brief IAM blips
// don't break the mint-credential gate.
type sessionJWTValidator struct {
jwksURL string
issuer string
audience string
owner string
cache *jwksCache
}
func newSessionJWTValidator(iamEndpoint, audience, owner string) *sessionJWTValidator {
return &sessionJWTValidator{
jwksURL: iamEndpoint + "/.well-known/jwks",
issuer: iamEndpoint,
audience: audience,
owner: owner,
cache: &jwksCache{
url: iamEndpoint + "/.well-known/jwks",
ttl: 5 * time.Minute,
client: &http.Client{Timeout: 10 * time.Second},
},
}
}
func (v *sessionJWTValidator) validate(ctx context.Context, raw string) (*sessionClaims, error) {
if raw == "" {
return nil, errors.New("empty token")
}
tok, err := jwt.ParseSigned(raw, []gojose.SignatureAlgorithm{
gojose.RS256, gojose.RS384, gojose.RS512,
gojose.ES256, gojose.ES384, gojose.ES512,
})
if err != nil {
return nil, fmt.Errorf("parse: %w", err)
}
keys, err := v.cache.get(ctx)
if err != nil {
return nil, fmt.Errorf("jwks: %w", err)
}
var claims sessionClaims
verified := false
var lastErr error
for _, k := range keys.Keys {
if err := tok.Claims(k.Key, &claims); err == nil {
verified = true
break
} else {
lastErr = err
}
}
if !verified {
if lastErr == nil {
lastErr = errors.New("no key matched")
}
return nil, fmt.Errorf("verify: %w", lastErr)
}
exp := jwt.Expected{
Time: time.Now(),
Issuer: v.issuer,
AnyAudience: jwt.Audience{v.audience},
}
if err := claims.Claims.Validate(exp); err != nil {
return nil, fmt.Errorf("validate: %w", err)
}
if claims.Owner != v.owner {
return nil, fmt.Errorf("owner: got %q want %q", claims.Owner, v.owner)
}
return &claims, nil
}
// jwksCache holds a TTL-bounded copy of IAM's JWKS. fail-stale: if any
// keys have been fetched and a refresh fails, the stale set is returned.
type jwksCache struct {
mu sync.RWMutex
keys *gojose.JSONWebKeySet
fetchedAt time.Time
ttl time.Duration
url string
client *http.Client
}
func (c *jwksCache) get(ctx context.Context) (*gojose.JSONWebKeySet, error) {
c.mu.RLock()
if c.keys != nil && time.Since(c.fetchedAt) < c.ttl {
k := c.keys
c.mu.RUnlock()
return k, nil
}
c.mu.RUnlock()
c.mu.Lock()
defer c.mu.Unlock()
if c.keys != nil && time.Since(c.fetchedAt) < c.ttl {
return c.keys, nil
}
keys, err := c.fetch(ctx)
if err != nil {
if c.keys != nil {
return c.keys, nil
}
return nil, err
}
c.keys = keys
c.fetchedAt = time.Now()
return keys, nil
}
func (c *jwksCache) fetch(ctx context.Context) (*gojose.JSONWebKeySet, error) {
rctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(rctx, http.MethodGet, c.url, nil)
if err != nil {
return nil, err
}
resp, err := c.client.Do(req)
if err != nil {
log.Printf("kms: jwks fetch: req err: %v (url=%s)", err, c.url)
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Printf("kms: jwks fetch: status %d (url=%s)", resp.StatusCode, c.url)
return nil, fmt.Errorf("status %d", resp.StatusCode)
}
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
log.Printf("kms: jwks fetch: read err: %v", err)
return nil, err
}
log.Printf("kms: jwks fetch: %d bytes from %s", len(body), c.url)
var ks gojose.JSONWebKeySet
if err := json.Unmarshal(body, &ks); err != nil {
log.Printf("kms: jwks fetch: unmarshal err: %v body=%.200q", err, string(body))
return nil, err
}
kids := make([]string, 0, len(ks.Keys))
for _, k := range ks.Keys {
kids = append(kids, k.KeyID)
}
log.Printf("kms: jwks fetch: parsed %d keys: %v", len(ks.Keys), kids)
return &ks, nil
}
// ---------------------------------------------------------------------------
// Server-side single-use nonce blacklist.
// ---------------------------------------------------------------------------
// nonceLRU is a fixed-size hash set with TTL eviction. Keys = 16-byte
// state nonces. Provides at-most-once redemption inside the process; with
// >1 replica behind ingress the request must hit the same replica twice
// for the LRU to fire — sticky sessions or shared-state would fully close
// the multi-replica gap, but for KMS (StatefulSet replicas=1 in prod) this
// is sufficient and zero-dep.
type nonceLRU struct {
mu sync.Mutex
cap int
ttl time.Duration
entries map[[16]byte]time.Time
}
func newNonceLRU(cap int, ttl time.Duration) *nonceLRU {
if cap < 1 {
cap = 1
}
return &nonceLRU{
cap: cap,
ttl: ttl,
entries: make(map[[16]byte]time.Time, cap),
}
}
// consume returns true if nonce is unseen (and records it) or false if
// already used. Expired entries are evicted on access.
func (l *nonceLRU) consume(nonce []byte) bool {
if len(nonce) != 16 {
return false
}
var k [16]byte
copy(k[:], nonce)
l.mu.Lock()
defer l.mu.Unlock()
now := time.Now()
if t, ok := l.entries[k]; ok {
if now.Sub(t) < l.ttl {
return false
}
}
// Evict if at capacity (drop expired first; if none, drop oldest).
if len(l.entries) >= l.cap {
var oldestKey [16]byte
var oldestT time.Time
first := true
for ek, et := range l.entries {
if now.Sub(et) >= l.ttl {
delete(l.entries, ek)
continue
}
if first || et.Before(oldestT) {
oldestKey = ek
oldestT = et
first = false
}
}
if len(l.entries) >= l.cap && !first {
delete(l.entries, oldestKey)
}
}
l.entries[k] = now
return true
}
// ---------------------------------------------------------------------------
// Per-subject rate limiter (in-memory window count).
// ---------------------------------------------------------------------------
type rateLimiter struct {
mu sync.Mutex
limit int
window time.Duration
buckets map[string][]time.Time
}
func newRateLimiter(limit int, window time.Duration) *rateLimiter {
return &rateLimiter{
limit: limit,
window: window,
buckets: make(map[string][]time.Time),
}
}
// allow returns true if the subject is under its window quota and
// records the hit; false otherwise.
func (rl *rateLimiter) allow(subject string) bool {
if subject == "" {
return false
}
rl.mu.Lock()
defer rl.mu.Unlock()
now := time.Now()
cutoff := now.Add(-rl.window)
hits := rl.buckets[subject]
// Drop expired hits.
keep := hits[:0]
for _, t := range hits {
if t.After(cutoff) {
keep = append(keep, t)
}
}
if len(keep) >= rl.limit {
rl.buckets[subject] = keep
return false
}
rl.buckets[subject] = append(keep, now)
return true
}
+720
View File
@@ -0,0 +1,720 @@
package main
import (
"bytes"
"context"
"crypto/hmac"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/tls"
"encoding/base64"
"encoding/json"
"io"
"log"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
gojose "github.com/go-jose/go-jose/v4"
"github.com/go-jose/go-jose/v4/jwt"
)
func newTestCfg(t *testing.T, iamEndpoint string) *oidcConfig {
t.Helper()
u, _ := url.Parse(strings.TrimRight(iamEndpoint, "/"))
return &oidcConfig{
iamEndpoint: strings.TrimRight(iamEndpoint, "/"),
iamHost: u.Host,
clientID: "lux-kms",
clientSecret: "test-secret",
stateSecret: []byte("test-secret-test-secret-test-32!"),
owner: "lux",
nonces: newNonceLRU(1024, stateTTL),
mintLimiter: newRateLimiter(mintRateLimit, mintRateWindow),
}
}
// TestSsoOidcLogin_redirectsToIAM asserts the login handler 302s to the
// IAM authorize URL with all required OAuth2 params.
func TestSsoOidcLogin_redirectsToIAM(t *testing.T) {
cfg := newTestCfg(t, "https://iam.dev.example.com")
req := httptest.NewRequest(http.MethodGet, "/v1/sso/oidc/login?orgSlug=lux", nil)
req.Host = "kms.dev.example.com"
req.TLS = &tls.ConnectionState{}
rec := httptest.NewRecorder()
cfg.handleLogin(rec, req)
if rec.Code != http.StatusFound {
t.Fatalf("status: got %d want 302", rec.Code)
}
loc := rec.Header().Get("Location")
if !strings.HasPrefix(loc, "https://iam.dev.example.com/login/oauth/authorize?") {
t.Fatalf("Location: %q does not target IAM authorize", loc)
}
u, err := url.Parse(loc)
if err != nil {
t.Fatalf("parse Location: %v", err)
}
q := u.Query()
for _, k := range []string{"client_id", "redirect_uri", "response_type", "scope", "state"} {
if q.Get(k) == "" {
t.Errorf("missing query param %q", k)
}
}
if q.Get("client_id") != "lux-kms" {
t.Errorf("client_id: got %q want lux-kms", q.Get("client_id"))
}
if q.Get("response_type") != "code" {
t.Errorf("response_type: got %q want code", q.Get("response_type"))
}
if q.Get("redirect_uri") != "https://kms.dev.example.com/v1/sso/oidc/callback" {
t.Errorf("redirect_uri: got %q", q.Get("redirect_uri"))
}
if q.Get("application") != "lux" {
t.Errorf("application: got %q want lux", q.Get("application"))
}
}
// TestSsoOidcLogin_includesStateNonce asserts a state cookie is set,
// HttpOnly+Secure, and the value verifies under our HMAC.
func TestSsoOidcLogin_includesStateNonce(t *testing.T) {
cfg := newTestCfg(t, "https://iam.dev.example.com")
req := httptest.NewRequest(http.MethodGet, "/v1/sso/oidc/login?orgSlug=lux", nil)
req.Host = "kms.dev.example.com"
req.TLS = &tls.ConnectionState{}
rec := httptest.NewRecorder()
cfg.handleLogin(rec, req)
resp := rec.Result()
defer resp.Body.Close()
var stateVal string
for _, c := range resp.Cookies() {
if c.Name != stateCookie {
continue
}
stateVal = c.Value
if !c.HttpOnly {
t.Error("state cookie not HttpOnly")
}
if !c.Secure {
t.Error("state cookie not Secure")
}
if c.SameSite != http.SameSiteLaxMode {
t.Error("state cookie not SameSite=Lax")
}
if c.MaxAge != int(stateTTL/time.Second) {
t.Errorf("state cookie MaxAge=%d want %d", c.MaxAge, int(stateTTL/time.Second))
}
}
if stateVal == "" {
t.Fatal("no state cookie set")
}
u, _ := url.Parse(resp.Header.Get("Location"))
if u.Query().Get("state") != stateVal {
t.Error("state cookie value does not match state query param")
}
if _, err := cfg.verifyState(stateVal, "lux"); err != nil {
t.Errorf("state verify: %v", err)
}
if _, err := cfg.verifyState(stateVal, "evil"); err == nil {
t.Error("state verify accepted wrong orgSlug — binding broken")
}
}
// TestSsoOidcCallback_validatesState covers four cases:
// 1. missing state cookie → 400
// 2. mismatch between cookie and ?state= → 400
// 3. expired state → 400
// 4. good state → token exchange + 302 /
func TestSsoOidcCallback_validatesState(t *testing.T) {
iam := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/login/oauth/access_token" {
t.Errorf("unexpected IAM path: %s", r.URL.Path)
}
body, _ := io.ReadAll(r.Body)
form, _ := url.ParseQuery(string(body))
if form.Get("grant_type") != "authorization_code" {
t.Errorf("grant_type: got %q", form.Get("grant_type"))
}
if form.Get("code") != "good-code" {
t.Errorf("code: got %q", form.Get("code"))
}
json.NewEncoder(w).Encode(map[string]string{"access_token": "iam-jwt-token"})
}))
defer iam.Close()
cfg := newTestCfg(t, iam.URL)
good, err := cfg.signState("lux")
if err != nil {
t.Fatalf("signState: %v", err)
}
// 1) No state cookie.
req := httptest.NewRequest(http.MethodGet,
"/v1/sso/oidc/callback?state="+good+"&code=good-code&orgSlug=lux", nil)
req.Host = "kms.dev.example.com"
req.TLS = &tls.ConnectionState{}
rec := httptest.NewRecorder()
cfg.handleCallback(rec, req)
if rec.Code != http.StatusBadRequest {
t.Errorf("missing-cookie: got %d want 400", rec.Code)
}
// 2) Cookie mismatch.
req = httptest.NewRequest(http.MethodGet,
"/v1/sso/oidc/callback?state="+good+"&code=good-code&orgSlug=lux", nil)
req.Host = "kms.dev.example.com"
req.TLS = &tls.ConnectionState{}
req.AddCookie(&http.Cookie{Name: stateCookie, Value: "different-value"})
rec = httptest.NewRecorder()
cfg.handleCallback(rec, req)
if rec.Code != http.StatusBadRequest {
t.Errorf("mismatched-cookie: got %d want 400", rec.Code)
}
// 3) Expired.
stale := mintState(t, cfg.stateSecret, "lux", time.Now().Add(-1*time.Minute).Unix())
req = httptest.NewRequest(http.MethodGet,
"/v1/sso/oidc/callback?state="+stale+"&code=good-code&orgSlug=lux", nil)
req.Host = "kms.dev.example.com"
req.TLS = &tls.ConnectionState{}
req.AddCookie(&http.Cookie{Name: stateCookie, Value: stale})
rec = httptest.NewRecorder()
cfg.handleCallback(rec, req)
if rec.Code != http.StatusBadRequest {
t.Errorf("expired: got %d want 400", rec.Code)
}
// 4) Happy path.
req = httptest.NewRequest(http.MethodGet,
"/v1/sso/oidc/callback?state="+good+"&code=good-code&orgSlug=lux", nil)
req.Host = "kms.dev.example.com"
req.TLS = &tls.ConnectionState{}
req.AddCookie(&http.Cookie{Name: stateCookie, Value: good})
rec = httptest.NewRecorder()
cfg.handleCallback(rec, req)
if rec.Code != http.StatusFound {
t.Fatalf("happy: got %d want 302; body=%s", rec.Code, rec.Body.String())
}
if loc := rec.Header().Get("Location"); loc != "/" {
t.Errorf("happy: Location=%q want /", loc)
}
var session *http.Cookie
for _, c := range rec.Result().Cookies() {
if c.Name == sessionCookie {
session = c
}
}
if session == nil {
t.Fatal("session cookie not set on happy path")
}
if session.Value != "iam-jwt-token" {
t.Errorf("session value: got %q want iam-jwt-token", session.Value)
}
if !session.HttpOnly || !session.Secure {
t.Error("session cookie missing HttpOnly/Secure")
}
}
// TestSsoOidcCallback_csrfProtection asserts a callback whose
// Origin/Referer points elsewhere is rejected with 403.
func TestSsoOidcCallback_csrfProtection(t *testing.T) {
cfg := newTestCfg(t, "https://iam.dev.example.com")
good, _ := cfg.signState("lux")
req := httptest.NewRequest(http.MethodGet,
"/v1/sso/oidc/callback?state="+good+"&code=c&orgSlug=lux", nil)
req.Host = "kms.dev.example.com"
req.TLS = &tls.ConnectionState{}
req.AddCookie(&http.Cookie{Name: stateCookie, Value: good})
req.Header.Set("Origin", "https://evil.example")
rec := httptest.NewRecorder()
cfg.handleCallback(rec, req)
if rec.Code != http.StatusForbidden {
t.Errorf("evil origin: got %d want 403", rec.Code)
}
}
// TestOriginGuard_rejectsPrefixExtension confirms the parsed-Host
// equality check rejects suffix-based smuggling like
// `iam.dev.example.com.evil.com` — the previous prefix-match accepted it.
func TestOriginGuard_rejectsPrefixExtension(t *testing.T) {
cfg := newTestCfg(t, "https://iam.dev.example.com")
good, _ := cfg.signState("lux")
req := httptest.NewRequest(http.MethodGet,
"/v1/sso/oidc/callback?state="+good+"&code=c&orgSlug=lux", nil)
req.Host = "kms.dev.example.com"
req.TLS = &tls.ConnectionState{}
req.AddCookie(&http.Cookie{Name: stateCookie, Value: good})
// Smuggled host: `iam.dev.example.com` is the prefix but the actual
// host is attacker-controlled.
req.Header.Set("Origin", "https://iam.dev.example.com.evil.com")
rec := httptest.NewRecorder()
cfg.handleCallback(rec, req)
if rec.Code != http.StatusForbidden {
t.Fatalf("prefix-extension: got %d want 403", rec.Code)
}
}
// TestOriginGuard_rejectsHTTP confirms http:// origins are rejected even
// if the host matches.
func TestOriginGuard_rejectsHTTP(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/", nil)
r.Header.Set("Origin", "http://iam.dev.example.com")
if originLooksOK(r, "iam.dev.example.com", "kms.dev.example.com") {
t.Error("http:// origin accepted; want rejected")
}
}
// TestStateNonce_singleUseAcrossReplicas mints a state, redeems it once,
// then attempts to replay it (cookie still attached, e.g. before the
// Set-Cookie clear lands). The second redemption must be rejected.
func TestStateNonce_singleUseAcrossReplicas(t *testing.T) {
iam := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"access_token": "iam-jwt-token"})
}))
defer iam.Close()
cfg := newTestCfg(t, iam.URL)
good, _ := cfg.signState("lux")
// First redemption — should succeed (302 /).
req := httptest.NewRequest(http.MethodGet,
"/v1/sso/oidc/callback?state="+good+"&code=c&orgSlug=lux", nil)
req.Host = "kms.dev.example.com"
req.TLS = &tls.ConnectionState{}
req.AddCookie(&http.Cookie{Name: stateCookie, Value: good})
rec := httptest.NewRecorder()
cfg.handleCallback(rec, req)
if rec.Code != http.StatusFound {
t.Fatalf("first redeem: got %d want 302", rec.Code)
}
// Replay — same state, same cookie still attached. LRU must reject.
req = httptest.NewRequest(http.MethodGet,
"/v1/sso/oidc/callback?state="+good+"&code=c&orgSlug=lux", nil)
req.Host = "kms.dev.example.com"
req.TLS = &tls.ConnectionState{}
req.AddCookie(&http.Cookie{Name: stateCookie, Value: good})
rec = httptest.NewRecorder()
cfg.handleCallback(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("replay: got %d want 400", rec.Code)
}
if !strings.Contains(rec.Body.String(), "already used") {
t.Errorf("replay body: got %q want contains 'already used'", rec.Body.String())
}
}
// TestSsoOidcLogout clears the session cookie.
func TestSsoOidcLogout(t *testing.T) {
cfg := newTestCfg(t, "https://iam.dev.example.com")
req := httptest.NewRequest(http.MethodPost, "/v1/sso/logout", nil)
rec := httptest.NewRecorder()
cfg.handleLogout(rec, req)
if rec.Code != http.StatusNoContent {
t.Errorf("status: got %d want 204", rec.Code)
}
var cleared bool
for _, c := range rec.Result().Cookies() {
if c.Name == sessionCookie && c.MaxAge < 0 {
cleared = true
}
}
if !cleared {
t.Error("session cookie not cleared")
}
}
// TestMintClientCredential_requiresSession asserts the endpoint refuses
// unauthenticated callers.
func TestMintClientCredential_requiresSession(t *testing.T) {
cfg := newTestCfg(t, "https://iam.dev.example.com")
req := httptest.NewRequest(http.MethodPost, "/v1/kms/credentials", strings.NewReader(`{}`))
rec := httptest.NewRecorder()
cfg.handleMintClientCredential(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Errorf("no-session: got %d want 401", rec.Code)
}
}
// TestMintClientCredential_validatesJWT covers four invalid-JWT cases —
// each must result in 401:
// 1. unsigned/garbage
// 2. signature key mismatch
// 3. expired
// 4. wrong audience
func TestMintClientCredential_validatesJWT(t *testing.T) {
signer, jwks := newTestSigner(t)
wrongSigner, _ := newTestSigner(t)
iam := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/.well-known/jwks") {
json.NewEncoder(w).Encode(jwks)
return
}
http.NotFound(w, r)
}))
defer iam.Close()
cfg := newTestCfg(t, iam.URL)
cfg.jwtValidator = newSessionJWTValidator(iam.URL, cfg.clientID, cfg.owner)
cases := []struct {
name string
token string
}{
{
name: "garbage",
token: "not-a-jwt",
},
{
name: "wrong-signer",
token: signClaims(t, wrongSigner, sessionClaims{
Claims: jwt.Claims{
Issuer: iam.URL,
Subject: "u1",
Audience: jwt.Audience{cfg.clientID},
Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)),
},
Owner: cfg.owner,
Roles: []string{roleKMSAdmin},
}),
},
{
name: "expired",
token: signClaims(t, signer, sessionClaims{
Claims: jwt.Claims{
Issuer: iam.URL,
Subject: "u1",
Audience: jwt.Audience{cfg.clientID},
Expiry: jwt.NewNumericDate(time.Now().Add(-1 * time.Hour)),
},
Owner: cfg.owner,
Roles: []string{roleKMSAdmin},
}),
},
{
name: "wrong-aud",
token: signClaims(t, signer, sessionClaims{
Claims: jwt.Claims{
Issuer: iam.URL,
Subject: "u1",
Audience: jwt.Audience{"some-other-app"},
Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)),
},
Owner: cfg.owner,
Roles: []string{roleKMSAdmin},
}),
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/v1/kms/credentials", strings.NewReader(`{}`))
req.AddCookie(&http.Cookie{Name: sessionCookie, Value: c.token})
rec := httptest.NewRecorder()
cfg.handleMintClientCredential(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Errorf("got %d want 401; body=%s", rec.Code, rec.Body.String())
}
})
}
}
// TestMintClientCredential_requiresKmsAdminRole — a JWT with a non-admin
// role list must produce 403.
func TestMintClientCredential_requiresKmsAdminRole(t *testing.T) {
signer, jwks := newTestSigner(t)
iam := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/.well-known/jwks") {
json.NewEncoder(w).Encode(jwks)
return
}
http.NotFound(w, r)
}))
defer iam.Close()
cfg := newTestCfg(t, iam.URL)
cfg.jwtValidator = newSessionJWTValidator(iam.URL, cfg.clientID, cfg.owner)
tok := signClaims(t, signer, sessionClaims{
Claims: jwt.Claims{
Issuer: iam.URL,
Subject: "u-no-roles",
Audience: jwt.Audience{cfg.clientID},
Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)),
},
Owner: cfg.owner,
Roles: []string{"viewer"}, // not admin
})
req := httptest.NewRequest(http.MethodPost, "/v1/kms/credentials", strings.NewReader(`{}`))
req.AddCookie(&http.Cookie{Name: sessionCookie, Value: tok})
rec := httptest.NewRecorder()
cfg.handleMintClientCredential(rec, req)
if rec.Code != http.StatusForbidden {
t.Errorf("got %d want 403; body=%s", rec.Code, rec.Body.String())
}
}
// TestMintClientCredential_rateLimits — 6th mint within 1h from the same
// subject must produce 429.
func TestMintClientCredential_rateLimits(t *testing.T) {
signer, jwks := newTestSigner(t)
iam := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/.well-known/jwks") {
json.NewEncoder(w).Encode(jwks)
return
}
// add-application: return generated app under data2 envelope.
json.NewEncoder(w).Encode(map[string]any{
"status": "ok",
"data2": map[string]any{
"clientId": "generated-client-id",
"clientSecret": "generated-client-secret",
},
})
}))
defer iam.Close()
cfg := newTestCfg(t, iam.URL)
cfg.jwtValidator = newSessionJWTValidator(iam.URL, cfg.clientID, cfg.owner)
t.Setenv("KMS_IAM_ADMIN_CLIENT_ID", "admin-id")
t.Setenv("KMS_IAM_ADMIN_CLIENT_SECRET", "admin-secret")
tok := signClaims(t, signer, sessionClaims{
Claims: jwt.Claims{
Issuer: iam.URL,
Subject: "u-burst",
Audience: jwt.Audience{cfg.clientID},
Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)),
},
Owner: cfg.owner,
Roles: []string{roleKMSAdmin},
})
// First 5 must succeed.
for i := 0; i < mintRateLimit; i++ {
req := httptest.NewRequest(http.MethodPost, "/v1/kms/credentials", strings.NewReader(`{}`))
req.AddCookie(&http.Cookie{Name: sessionCookie, Value: tok})
rec := httptest.NewRecorder()
cfg.handleMintClientCredential(rec, req)
if rec.Code != http.StatusCreated {
t.Fatalf("mint #%d: got %d want 201; body=%s", i+1, rec.Code, rec.Body.String())
}
}
// 6th must be 429.
req := httptest.NewRequest(http.MethodPost, "/v1/kms/credentials", strings.NewReader(`{}`))
req.AddCookie(&http.Cookie{Name: sessionCookie, Value: tok})
rec := httptest.NewRecorder()
cfg.handleMintClientCredential(rec, req)
if rec.Code != http.StatusTooManyRequests {
t.Fatalf("mint #6: got %d want 429", rec.Code)
}
}
// TestMintClientCredential_auditLogs captures log output and asserts the
// audit line carries subject and name.
func TestMintClientCredential_auditLogs(t *testing.T) {
signer, jwks := newTestSigner(t)
iam := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/.well-known/jwks") {
json.NewEncoder(w).Encode(jwks)
return
}
json.NewEncoder(w).Encode(map[string]any{
"status": "ok",
"data2": map[string]any{
"clientId": "generated-client-id",
"clientSecret": "generated-client-secret",
},
})
}))
defer iam.Close()
cfg := newTestCfg(t, iam.URL)
cfg.jwtValidator = newSessionJWTValidator(iam.URL, cfg.clientID, cfg.owner)
t.Setenv("KMS_IAM_ADMIN_CLIENT_ID", "admin-id")
t.Setenv("KMS_IAM_ADMIN_CLIENT_SECRET", "admin-secret")
tok := signClaims(t, signer, sessionClaims{
Claims: jwt.Claims{
Issuer: iam.URL,
Subject: "u-audit",
Audience: jwt.Audience{cfg.clientID},
Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)),
},
Owner: cfg.owner,
Roles: []string{roleKMSAdmin},
})
var buf bytes.Buffer
prevW := log.Writer()
prevF := log.Flags()
log.SetOutput(&buf)
log.SetFlags(0)
t.Cleanup(func() {
log.SetOutput(prevW)
log.SetFlags(prevF)
})
body := strings.NewReader(`{"name":"my-test-app"}`)
req := httptest.NewRequest(http.MethodPost, "/v1/kms/credentials", body)
req.AddCookie(&http.Cookie{Name: sessionCookie, Value: tok})
rec := httptest.NewRecorder()
cfg.handleMintClientCredential(rec, req)
if rec.Code != http.StatusCreated {
t.Fatalf("mint: got %d want 201; body=%s", rec.Code, rec.Body.String())
}
logs := buf.String()
if !strings.Contains(logs, "subject=u-audit") {
t.Errorf("audit log missing subject; got: %s", logs)
}
if !strings.Contains(logs, "name=my-test-app") {
t.Errorf("audit log missing name; got: %s", logs)
}
if !strings.Contains(logs, "ok=true") {
t.Errorf("audit log missing ok=true; got: %s", logs)
}
}
// TestCookieDomain_rejectsLeadingDot asserts loadOIDCConfig refuses to
// boot when KMS_COOKIE_DOMAIN is a wildcard subdomain (leading dot).
func TestCookieDomain_rejectsLeadingDot(t *testing.T) {
t.Setenv("IAM_ENDPOINT", "https://iam.dev.example.com")
t.Setenv("KMS_OIDC_CLIENT_ID", "lux-kms")
t.Setenv("KMS_OIDC_CLIENT_SECRET", "test-secret")
t.Setenv("KMS_STATE_SECRET", "test-secret-test-secret-test-32!")
t.Setenv("KMS_COOKIE_DOMAIN", ".example.com")
if cfg := loadOIDCConfig(); cfg != nil {
t.Fatal("loadOIDCConfig accepted leading-dot KMS_COOKIE_DOMAIN; want nil")
}
// Sanity: removing the dot succeeds.
t.Setenv("KMS_COOKIE_DOMAIN", "kms.dev.example.com")
if cfg := loadOIDCConfig(); cfg == nil {
t.Error("loadOIDCConfig rejected exact host KMS_COOKIE_DOMAIN; want accepted")
}
}
// TestIAMErrorEnvelope_returnsError — IAM returns 200 + status:error
// envelope. callIAMAddApplication must convert to error with Msg.
func TestIAMErrorEnvelope_returnsError(t *testing.T) {
iam := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]any{
"status": "error",
"msg": "duplicate name",
})
}))
defer iam.Close()
cfg := newTestCfg(t, iam.URL)
_, _, err := cfg.callIAMAddApplication(context.Background(), "admin", "secret", "dup", "")
if err == nil {
t.Fatal("expected error from status:error envelope; got nil")
}
if !strings.Contains(err.Error(), "duplicate name") {
t.Errorf("error: got %q want to contain 'duplicate name'", err.Error())
}
}
// TestIAMErrorEnvelope_non2xxFails — non-2xx HTTP must fail without
// trying to parse the body envelope.
func TestIAMErrorEnvelope_non2xxFails(t *testing.T) {
iam := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "down", http.StatusServiceUnavailable)
}))
defer iam.Close()
cfg := newTestCfg(t, iam.URL)
_, _, err := cfg.callIAMAddApplication(context.Background(), "admin", "secret", "n", "")
if err == nil {
t.Fatal("expected error from 503; got nil")
}
if !strings.Contains(err.Error(), "status=503") {
t.Errorf("error: got %q want to contain 'status=503'", err.Error())
}
}
// TestCallbackURL_alwaysHTTPS asserts the callback URL is https:// even
// when the request lacks TLS metadata. Production runs behind ingress;
// http:// would produce a redirect_uri IAM rejects.
func TestCallbackURL_alwaysHTTPS(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/v1/sso/oidc/login", nil)
r.Host = "kms.dev.example.com"
if got := callbackURL(r); got != "https://kms.dev.example.com/v1/sso/oidc/callback" {
t.Errorf("callbackURL = %q; want https://...", got)
}
}
// TestSsoOidcUnconfigured returns 503 when env vars are missing.
func TestSsoOidcUnconfigured(t *testing.T) {
rec := httptest.NewRecorder()
oidcUnconfigured(rec, httptest.NewRequest(http.MethodGet, "/v1/sso/oidc/login", nil))
if rec.Code != http.StatusServiceUnavailable {
t.Errorf("unconfigured: got %d want 503", rec.Code)
}
}
// mintState fabricates a state token with an arbitrary expiry — used to
// build an expired token for the negative test.
func mintState(t *testing.T, secret []byte, orgSlug string, exp int64) string {
t.Helper()
nonce := make([]byte, 16)
for i := range nonce {
nonce[i] = byte(i + 1)
}
buf := append([]byte(nil), nonce...)
buf = appendBE64(buf, exp)
mac := hmac.New(sha256.New, secret)
mac.Write(buf)
mac.Write([]byte(orgSlug))
buf = append(buf, mac.Sum(nil)...)
return base64.RawURLEncoding.EncodeToString(buf)
}
// newTestSigner returns an RSA signer + JWKS containing the public key.
func newTestSigner(t *testing.T) (gojose.Signer, *gojose.JSONWebKeySet) {
t.Helper()
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("rsa.GenerateKey: %v", err)
}
signer, err := gojose.NewSigner(
gojose.SigningKey{Algorithm: gojose.RS256, Key: key},
(&gojose.SignerOptions{}).WithType("JWT").WithHeader("kid", "test-key"),
)
if err != nil {
t.Fatalf("NewSigner: %v", err)
}
jwks := &gojose.JSONWebKeySet{
Keys: []gojose.JSONWebKey{{
Key: &key.PublicKey,
KeyID: "test-key",
Algorithm: string(gojose.RS256),
Use: "sig",
}},
}
return signer, jwks
}
// signClaims returns a serialized JWT for the given claims under signer.
func signClaims(t *testing.T, signer gojose.Signer, claims sessionClaims) string {
t.Helper()
out, err := jwt.Signed(signer).Claims(claims).Serialize()
if err != nil {
t.Fatalf("jwt.Signed.Serialize: %v", err)
}
return out
}
+324
View File
@@ -0,0 +1,324 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// smoke-replay is a one-shot proof that the kmsd anti-replay ledger
// closes the wall-clock-window replay gap. The harness boots an
// in-process kmsd (with a 2-second nonce ledger TTL so we can exercise
// the post-TTL path in real time), opens a ZAP transport, then exercises
// three scenarios in order:
//
// 1. Submit a freshly-signed envelope — expect statusOK ("first OK").
// 2. Submit the SAME envelope bytes again within TTL — expect
// statusForbid with reason "envelope: replay-detected" ("replay
// rejected").
// 3. Sleep > ledger TTL, submit a freshly-signed envelope with a new
// nonce — expect statusOK ("post-TTL fresh nonce OK").
//
// The harness exits non-zero on any deviation and prints the verbatim
// outcome at every step.
//
// Run:
//
// go run ./cmd/smoke-replay
//
// Expected output (the canonical PASS):
//
// smoke: boot listenAddr=127.0.0.1:NNNNN ledgerTTL=2s
// smoke: identity nodeID=NodeID-... path=hanzo/commerce
// smoke: scenario-1 first OK status=0x00 body={"value":"c21va2Utc2VlZA=="}
// smoke: scenario-2 replay rejected status=0x03 body={"error":"envelope: replay-detected"}
// smoke: scenario-3 post-TTL fresh nonce OK status=0x00 body={"value":"c21va2Utc2VlZA=="}
// smoke: PASS
//
// Any deviation is FAIL with a non-zero exit code.
package main
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"net"
"os"
"strconv"
"strings"
"time"
"github.com/luxfi/ids"
"github.com/luxfi/keys"
"github.com/luxfi/kms/pkg/envelope"
"github.com/luxfi/kms/pkg/store"
"github.com/luxfi/kms/pkg/zapserver"
"github.com/luxfi/log"
"github.com/luxfi/zap"
badger "github.com/luxfi/zapdb"
)
// The smoke harness is its own binary, so we keep parameters as
// constants. ledgerTTL is deliberately small (2s) so the post-TTL
// scenario runs in a few seconds end-to-end. Production kmsd uses
// envelope.DefaultNonceLedgerTTL = MaxClockSkew + 1m = 6m.
const (
ledgerTTL = 2 * time.Second
ledgerGC = 500 * time.Millisecond
smokeMnemonic = "abandon abandon abandon abandon abandon abandon " +
"abandon abandon abandon abandon abandon about"
smokeServicePath = "hanzo/commerce"
smokeSecretPath = "hanzo/commerce"
smokeSecretName = "ping"
smokeSecretEnv = "smoke"
smokeSecretValue = "smoke-seed"
)
func main() {
if err := run(); err != nil {
fmt.Fprintf(os.Stderr, "smoke: FAIL %v\n", err)
os.Exit(1)
}
fmt.Println("smoke: PASS")
}
func run() error {
ident, err := keys.NewServiceIdentity(smokeMnemonic, smokeServicePath)
if err != nil {
return fmt.Errorf("identity: %w", err)
}
defer ident.Wipe()
srv, addr, cleanup, err := bootServer(ident.NodeID)
if err != nil {
return fmt.Errorf("boot: %w", err)
}
defer cleanup()
_ = srv
fmt.Printf("smoke: boot listenAddr=%s ledgerTTL=%s\n", addr, ledgerTTL)
fmt.Printf("smoke: identity nodeID=%s path=%s\n", ident.NodeID.String(), smokeServicePath)
// Scenario 1: fresh envelope → expect statusOK.
envBytes1, err := signEnvelope(ident, time.Now(), randomNonce())
if err != nil {
return fmt.Errorf("sign env1: %w", err)
}
status1, body1, err := callDirect(addr, envBytes1)
if err != nil {
return fmt.Errorf("scenario-1: call: %w", err)
}
fmt.Printf("smoke: scenario-1 first OK status=0x%02X body=%s\n", status1, string(body1))
if status1 != 0x00 {
return fmt.Errorf("scenario-1 expected status=0x00, got 0x%02X body=%s", status1, body1)
}
// Scenario 2: replay same bytes → expect statusForbid + replay-detected.
status2, body2, err := callDirect(addr, envBytes1)
if err != nil {
return fmt.Errorf("scenario-2: call: %w", err)
}
fmt.Printf("smoke: scenario-2 replay rejected status=0x%02X body=%s\n", status2, string(body2))
if status2 != 0x03 {
return fmt.Errorf("scenario-2 expected status=0x03 (forbid), got 0x%02X body=%s", status2, body2)
}
if !strings.Contains(string(body2), "replay-detected") {
return fmt.Errorf("scenario-2 expected body to contain replay-detected, got %q", body2)
}
// Scenario 3: sleep past TTL, sign fresh envelope, expect statusOK.
sleep := ledgerTTL + 500*time.Millisecond
fmt.Printf("smoke: scenario-3 sleep %s past TTL...\n", sleep)
time.Sleep(sleep)
envBytes3, err := signEnvelope(ident, time.Now(), randomNonce())
if err != nil {
return fmt.Errorf("sign env3: %w", err)
}
status3, body3, err := callDirect(addr, envBytes3)
if err != nil {
return fmt.Errorf("scenario-3: call: %w", err)
}
fmt.Printf("smoke: scenario-3 post-TTL fresh nonce OK status=0x%02X body=%s\n", status3, string(body3))
if status3 != 0x00 {
return fmt.Errorf("scenario-3 expected status=0x00, got 0x%02X body=%s", status3, body3)
}
return nil
}
// bootServer brings up a real luxfi/zap.Node + zapserver.Server backed by
// an in-memory secret store and a tiny-TTL nonce ledger so the smoke
// harness can exercise replay defence in seconds. Returns the listen
// address and a cleanup func.
func bootServer(identity ids.NodeID) (*zapserver.Server, string, func(), error) {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return nil, "", nil, fmt.Errorf("listen: %w", err)
}
port := l.Addr().(*net.TCPAddr).Port
_ = l.Close()
n := zap.NewNode(zap.NodeConfig{
NodeID: "kms-smoke-" + strconv.Itoa(port),
ServiceType: "_kms._tcp",
Port: port,
NoDiscovery: true,
})
if err := n.Start(); err != nil {
return nil, "", nil, fmt.Errorf("zap start: %w", err)
}
opts := badger.DefaultOptions("").WithInMemory(true)
db, err := badger.Open(opts)
if err != nil {
n.Stop()
return nil, "", nil, fmt.Errorf("zapdb: %w", err)
}
mk := make([]byte, 32)
if _, err := rand.Read(mk); err != nil {
db.Close()
n.Stop()
return nil, "", nil, fmt.Errorf("rand mk: %w", err)
}
secStore := store.NewSecretStore(db)
sealed, err := store.Seal(mk, smokeSecretPath, smokeSecretName, smokeSecretEnv, []byte(smokeSecretValue))
if err != nil {
db.Close()
n.Stop()
return nil, "", nil, fmt.Errorf("seal: %w", err)
}
if err := secStore.Put(sealed); err != nil {
db.Close()
n.Stop()
return nil, "", nil, fmt.Errorf("store.Put: %w", err)
}
authz, err := zapserver.NewInProcessAuthorizer(zapserver.InProcessAuthorizerConfig{
Validators: zapserver.NewStaticAuthorityProvider([]ids.NodeID{identity}),
Operator: zapserver.NewStaticAuthorityProvider(nil),
})
if err != nil {
db.Close()
n.Stop()
return nil, "", nil, fmt.Errorf("authz: %w", err)
}
ledger := zapserver.NewMemoryNonceLedger(zapserver.MemoryNonceLedgerConfig{
TTL: ledgerTTL,
GCInterval: ledgerGC,
})
srv := zapserver.New(zapserver.Config{
Store: secStore,
MasterKey: mk,
Authorizer: authz,
NonceLedger: ledger,
Logger: log.NewNoOpLogger(),
})
srv.Register(n)
cleanup := func() {
ledger.Stop()
n.Stop()
db.Close()
}
return srv, "127.0.0.1:" + strconv.Itoa(port), cleanup, nil
}
// signEnvelope builds an OpSecretGet envelope for the smoke secret.
// Pure function in (identity, time, nonce) so the harness can re-sign
// or replay verbatim by passing the same arguments.
func signEnvelope(ident *keys.ServiceIdentity, now time.Time, nonce string) ([]byte, error) {
hdr := envelope.IdentityHeader{
NodeID: ident.NodeID,
FullDigest: ident.FullDigest,
ServicePath: ident.ServicePath,
PublicKey: ident.PublicKey,
}
inner, err := json.Marshal(map[string]string{
"path": smokeSecretPath,
"name": smokeSecretName,
"env": smokeSecretEnv,
})
if err != nil {
return nil, err
}
env, err := envelope.Build(hdr, ident, 0x0040, inner, nonce, now)
if err != nil {
return nil, err
}
return json.Marshal(env)
}
// randomNonce returns a base64-encoded 16-byte random nonce. Same shape
// the canonical zapclient.call produces.
func randomNonce() string {
var buf [16]byte
_, _ = rand.Read(buf[:])
return base64.StdEncoding.EncodeToString(buf[:])
}
// callDirect dials the kmsd via a fresh luxfi/zap.Node, submits the
// pre-built envelope bytes as an OpSecretGet payload, and parses the
// status byte + JSON body from the response.
//
// The harness uses SkipHandshake → no hybrid session, no AEAD-seal of
// the body. That keeps the wire path simple for the smoke proof; the
// nonce ledger sits behind the verifyAndAuthorize chain and fires
// identically with or without a session.
func callDirect(addr string, envBytes []byte) (byte, []byte, error) {
n := zap.NewNode(zap.NodeConfig{
NodeID: "smoke-client-" + strconv.FormatInt(time.Now().UnixNano(), 10),
ServiceType: "_kms._tcp",
Port: 0,
NoDiscovery: true,
})
if err := n.Start(); err != nil {
return 0, nil, fmt.Errorf("client start: %w", err)
}
defer n.Stop()
if err := n.ConnectDirect(addr); err != nil {
return 0, nil, fmt.Errorf("connect: %w", err)
}
var peerID string
for _, p := range n.Peers() {
peerID = p
break
}
if peerID == "" {
return 0, nil, errors.New("peer not resolved")
}
// Wire body: opcode(2 LE) || envelope bytes. Universal mux on the
// server side dispatches via the first 2 bytes when no per-opcode
// handler matches the message type.
payload := make([]byte, 2+len(envBytes))
binary.LittleEndian.PutUint16(payload[:2], 0x0040)
copy(payload[2:], envBytes)
b := zap.NewBuilder(len(payload) + 16)
b.WriteBytes(payload)
raw := b.Finish()
msg, err := zap.Parse(raw)
if err != nil {
return 0, nil, fmt.Errorf("build msg: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := n.Call(ctx, peerID, msg)
if err != nil {
return 0, nil, fmt.Errorf("call: %w", err)
}
root := resp.Root()
body := root.Bytes(0)
if len(body) < 1 {
b := resp.Bytes()
if len(b) <= zap.HeaderSize {
return 0, nil, errors.New("empty response")
}
body = b[zap.HeaderSize:]
}
return body[0], body[1:], nil
}
+25
View File
@@ -0,0 +1,25 @@
services:
kms:
image: ghcr.io/luxfi/kms:latest
ports:
- "8080:8080"
environment:
MPC_ADDR: "mpc:9000"
MPC_VAULT_ID: "${MPC_VAULT_ID}"
volumes:
- kms-data:/data/kms
depends_on:
- mpc
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8080/healthz"]
interval: 10s
timeout: 3s
retries: 3
mpc:
image: ghcr.io/luxfi/mpc:latest
ports:
- "8081:8081"
volumes:
kms-data:
+41
View File
@@ -0,0 +1,41 @@
# KMS configuration.
#
# All values can be overridden by env vars or CLI flags.
# Env vars take precedence over this file; flags take precedence over env vars.
#
# Server:
# KMS_LISTEN - HTTP listen address (default ":8080")
# KMS_DATA_DIR - ZapDB data directory (default "/data/kms")
#
# MPC (signing backend):
# MPC_ADDR - ZAP address (host:port); empty = mDNS discovery (dev only)
# MPC_VAULT_ID - MPC vault for validator keys (keep in K8s secret, not here)
# KMS_NODE_ID - ZAP node ID (default "kms-0")
#
# Secrets server (ZAP transport):
# ZAP_PORT - ZAP listen port (default 9999, 0 = disable)
# KMS_MASTER_KEY_B64 - 32-byte master key (base64) for envelope encryption
#
# ZapDB at-rest encryption:
# KMS_ENCRYPTION_KEY_B64 - 32-byte key (base64) for ZapDB encryption
#
# ZapDB S3 replication:
# REPLICATE_S3_ENDPOINT - S3 endpoint (empty = replication disabled)
# REPLICATE_S3_BUCKET - S3 bucket (default "lux-kms-backups")
# REPLICATE_S3_REGION - S3 region (default "us-central1")
# REPLICATE_S3_ACCESS_KEY
# REPLICATE_S3_SECRET_KEY
# REPLICATE_AGE_RECIPIENT - age public key for backup encryption
# REPLICATE_AGE_IDENTITY - age private key for restore
# REPLICATE_PATH - S3 key prefix (default "kms/{KMS_NODE_ID}")
#
# Auth:
# IAM_ENDPOINT - Hanzo IAM endpoint (default "https://hanzo.id")
server:
addr: ":8080"
data_dir: "/data/kms"
mpc:
# ZAP direct connect address. Leave empty for mDNS discovery (dev only).
addr: ""
+142
View File
@@ -0,0 +1,142 @@
# KMS architecture — MPC-rooted Root Encryption Key
## Threat model
- Adversary: anyone with `kubectl exec`, anyone who can dump pod env or
read a static K8s Secret, anyone who can read the KMS pod's heap via
core file or pprof.
- Goal: prevent any of the above from recovering the AEAD key that
protects per-secret DEKs in ZapDB.
- Out of scope: a fully-compromised KMS process (it has plaintext in
memory by design) and a fully-compromised MPC quorum.
## Construction
```
+-----------------------------+
| luxfi/mpc cluster |
| (t-of-n; default 2-of-3) |
| |
| stores wrapped REK at |
| keyID="kms/rek/v<epoch>" |
| |
| share set persisted in |
| each MPC node's BadgerDB |
| under encdb (ChaCha20) |
+-------------+---------------+
^
| ZAP wire (OpDecrypt 0x0031)
| over X25519+ML-KEM-768 AEAD
|
+--------+--------+
| KMS pod (boot) |
| |
| mpcrek.Bootstrap|
| (single fetch) |
+--------+--------+
|
v
+--------------------+------------------+
| pkg/store.Seal / Open |
| |
| per-secret AES-256-GCM envelope: |
| DEK := rand(32) |
| ct := AES-GCM(DEK, plaintext, |
| AAD=path/name/env) |
| wrap:= AES-GCM(REK, DEK, |
| AAD=name) |
+--------------------+------------------+
|
v
+--------+--------+
| ZapDB (PVC) |
| on-disk LSM |
| |
| each Secret = |
| {ct, wrap, |
| meta...} |
+-----------------+
```
## Security argument
### Assumptions
1. AES-256-GCM is IND-CCA2 in the multi-user / multi-key setting up to
the standard birthday bound at 2^32 messages per key (Bellare-Tackmann
CRYPTO 2016). Each per-secret DEK encrypts exactly one plaintext, so
the inner layer is well below the bound.
2. The REK is unpredictable from the adversary's view: it was sampled
in an MPC-internal DKG and is held as t-of-n shares; recovering it
without t cooperating shareholders requires solving the underlying
threshold hardness assumption (CGGMP21 ECDSA / FROST EdDSA — discrete
log on secp256k1 / ed25519).
3. The MPC ZAP wire is AEAD-sealed under an X25519+ML-KEM-768 hybrid
handshake. Both layers are post-quantum-hardened.
### Claims
- Confidentiality at rest. A snapshot of the ZapDB PVC reveals only
`(ct, wrap)` pairs. Without the REK, `wrap` is an AES-GCM ciphertext
the adversary cannot open; without the unwrapped DEK, `ct` is
unopenable.
- Replay / swap resistance. The AAD on each layer binds the envelope
to its name and (path, name, env). A swap of `wrap_A` into record
`B` fails AEAD verification.
- No env-var attack surface in MPC mode. With `MPC_REK_ENDPOINT` set,
the REK is never in pod env, K8s Secrets, kubeconfig, or kubectl
exec'd shells. It exists only on the t-of-n MPC nodes (as shares)
and transiently in the KMS pod's heap.
### Non-claims
- The KMS pod's heap is not a vault. A `gcore` dump of a running KMS
pod recovers the REK. Mitigations: pod security context drops all
capabilities, no debugfs, no ptrace, K8s NetworkPolicy isolates the
pod, and `mpcrek.Zero` overwrites the REK on graceful shutdown.
- An adversary who compromises t MPC nodes can recover the REK. That
is the threshold-trust assumption, not a flaw of this design.
## Wire details
### REK fetch (mpcrek.Bootstrap)
- Endpoint: CSV of MPC `host:port` (e.g. all replicas).
- Method: `mpc.ZapClient.Decrypt(ctx, keyID, nil)`. The empty
ciphertext means "decrypt the stored record under keyID via your own
t-of-n share set", as opposed to "decrypt this caller-supplied
ciphertext".
- Response: `mpc.DecryptResult{Plaintext: <32 bytes>}`.
- Rejections: any non-32-byte plaintext; an all-zero plaintext (rejected
via `crypto/subtle.ConstantTimeCompare`).
- Timeout: 10 s default; configurable via `MPC_REK_TIMEOUT`.
### Per-secret envelope (pkg/store.Seal)
Unchanged from pre-MPC. Documented here for completeness:
```
DEK = rand(32)
Nonce = rand(12)
Ciphertext = 0x01 || Nonce || AES-GCM-Seal(DEK, Nonce, plaintext, AAD=path/name/env)
WrappedDEK = 0x01 || Nonce'|| AES-GCM-Seal(REK, Nonce', DEK, AAD=name)
```
`pkg/store.Open` verifies AAD on both layers and zeroes DEK after use.
## Operational notes
- Replicas. All KMS replicas fetch the same epoch from MPC; the
cluster is the single source of truth. No leader election needed.
- Cluster size. Default 3-of-5 for production (matches the
`lux-mpc` consensus-mode StatefulSet). 2-of-3 acceptable for non-
production. Operator chooses on MPC bootstrap.
- Backups. ZapDB Replicator continues to back up to S3 with age
encryption (`REPLICATE_*` env). The backup is double-wrapped: each
Secret in the backup blob is already AES-GCM under the MPC-rooted
REK; the blob is then age-encrypted with the operator's public key.
A leak of the S3 bucket reveals nothing without BOTH the age
identity AND the MPC quorum.
- Re-key cadence. Manual, on incident or quarterly. Run
`cmd/rek-rotate` (future) with both old and new epoch live, then
roll deployments.
+2
View File
@@ -0,0 +1,2 @@
node_modules
dist
+4
View File
@@ -0,0 +1,4 @@
node_modules/
dist/
.vite/
*.tsbuildinfo
+10
View File
@@ -0,0 +1,10 @@
{
"singleQuote": false,
"printWidth": 100,
"trailingComma": "none",
"tabWidth": 2,
"semi": true,
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindStylesheet": "./src/index.css",
"tailwindFunctions": ["clsx", "twMerge"]
}
@@ -0,0 +1,12 @@
import { useEffect } from "react";
import type { Decorator } from "@storybook/react-vite";
export const DocumentDecorator: Decorator = (Story) => {
useEffect(() => {
const root = document.documentElement;
root.setAttribute("class", "overflow-visible");
}, []);
return <Story />;
};
@@ -0,0 +1,17 @@
import { useMemo } from "react";
import type { Decorator } from "@storybook/react-vite";
import { createRootRoute, createRouter, RouterProvider } from "@tanstack/react-router";
export const RouterDecorator: Decorator = (Story, params) => {
const router = useMemo(() => {
const routeTree = createRootRoute({
component: Story
});
return createRouter({
routeTree
});
}, [Story, params]);
return <RouterProvider router={router as any} />;
};
+2
View File
@@ -0,0 +1,2 @@
export * from "./DocumentDecorator";
export * from "./RouterDecorator";
+14
View File
@@ -0,0 +1,14 @@
import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = {
stories: [
"../src/components/v3/**/*.mdx",
"../src/components/v3/**/*.stories.@(js|jsx|mjs|ts|tsx)"
],
addons: ["@storybook/addon-docs", "@storybook/addon-a11y"],
framework: {
name: "@storybook/react-vite",
options: {}
}
};
export default config;
+36
View File
@@ -0,0 +1,36 @@
import type { Preview } from "@storybook/react-vite";
import { DocumentDecorator, RouterDecorator } from "./decorators";
import "../src/index.css";
const preview: Preview = {
decorators: [DocumentDecorator, RouterDecorator],
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i
}
},
docs: {
backgroundColor: "var(--background)"
},
a11y: {
test: "todo"
},
backgrounds: {
default: "dark",
options: {
dark: { name: "Dark", value: "var(--background)" }
}
}
},
initialGlobals: {
backgrounds: {
value: "dark"
}
}
};
export default preview;
+17
View File
@@ -0,0 +1,17 @@
# Base layer
FROM node:20-alpine
# Set the working directory
WORKDIR /app
# Copy over dependency files
COPY package.json ./
COPY package-lock.json ./
# Install
RUN npm install --ignore-scripts
# Copy all files
COPY . .
CMD ["npm", "run", "dev"]
+50
View File
@@ -0,0 +1,50 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
export default tseslint.config({
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
- Optionally add `...tseslint.configs.stylisticTypeChecked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
```js
// eslint.config.js
import react from 'eslint-plugin-react'
export default tseslint.config({
// Set the react version
settings: { react: { version: '18.3' } },
plugins: {
// Add the react plugin
react,
},
rules: {
// other rules...
// Enable its recommended rules
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
},
})
```
+141
View File
@@ -0,0 +1,141 @@
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
import storybook from "eslint-plugin-storybook";
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import eslintPluginPrettier from "eslint-plugin-prettier/recommended";
import simpleImportSort from "eslint-plugin-simple-import-sort";
import tseslint from "typescript-eslint";
import { FlatCompat } from "@eslint/eslintrc";
import stylisticPlugin from "@stylistic/eslint-plugin";
import importPlugin from "eslint-plugin-import";
import pluginRouter from "@tanstack/eslint-plugin-router";
const compat = new FlatCompat({
baseDirectory: import.meta.dirname
});
export default tseslint.config(
{ ignores: ["dist"] },
{
extends: [
...pluginRouter.configs["flat/recommended"],
js.configs.recommended,
tseslint.configs.recommended,
...compat.extends("airbnb"),
...compat.extends("@kesills/airbnb-typescript"),
eslintPluginPrettier
],
files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname
}
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
"simple-import-sort": simpleImportSort,
import: importPlugin
},
settings: {
"import/resolver": {
typescript: {
project: ["./tsconfig.json"]
}
}
},
rules: {
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": "off",
"@typescript-eslint/only-throw-error": "off",
"@typescript-eslint/no-empty-function": "off",
quotes: ["error", "double", { avoidEscape: true }],
"comma-dangle": ["error", "only-multiline"],
"react/react-in-jsx-scope": "off",
"import/prefer-default-export": "off",
"react-hooks/exhaustive-deps": "off",
"@typescript-eslint/ban-ts-comment": "warn",
"react/jsx-props-no-spreading": "off", // switched off for component building
// TODO: This rule will be switched ON after complete revamp of frontend
"@typescript-eslint/no-explicit-any": "off",
"jsx-a11y/control-has-associated-label": "off",
"import/no-extraneous-dependencies": [
"error",
{
devDependencies: true
}
],
"no-console": "off",
"arrow-body-style": "off",
"no-underscore-dangle": [
"error",
{
allow: ["_id"]
}
],
"jsx-a11y/anchor-is-valid": "off",
// all those <a> tags must be converted to label or a p component
//
"react/require-default-props": "off",
"react/jsx-filename-extension": [
1,
{
extensions: [".tsx", ".ts"]
}
],
// TODO: turn this rule ON after migration. everything should use arrow functions
"react/function-component-definition": [
0,
{
namedComponents: "arrow-function"
}
],
"react/no-unknown-property": [
"error",
{
ignore: ["jsx"]
}
],
"@typescript-eslint/no-non-null-assertion": "off",
"simple-import-sort/exports": "warn",
"simple-import-sort/imports": [
"warn",
{
groups: [
// Node.js builtins. You could also generate this regex if you use a `.js` config.
// For example: `^(${require("module").builtinModules.join("|")})(/|$)`
// Note that if you use the `node:` prefix for Node.js builtins,
// you can avoid this complexity: You can simply use "^node:".
[
"^(assert|buffer|child_process|cluster|console|constants|crypto|dgram|dns|domain|events|fs|http|https|module|net|os|path|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|tty|url|util|vm|zlib|freelist|v8|process|async_hooks|http2|perf_hooks)(/.*|$)"
],
// Packages `react` related packages
["^react", "^next", "^@?\\w"],
["^@app"],
// Internal packages.
["^~(/.*|$)"],
// Relative imports
["^\\.\\.(?!/?$)", "^\\.\\./?$", "^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"],
// Style imports.
["^.+\\.?(css|scss)$"]
]
}
],
"import/first": "error",
"import/newline-after-import": "error",
"import/no-duplicates": "error"
}
},
{
rules: Object.fromEntries(
Object.keys(stylisticPlugin.configs["all-flat"].rules ?? {}).map((key) => [key, "off"])
)
},
storybook.configs["flat/recommended"]
);
+41
View File
@@ -0,0 +1,41 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
http-equiv="Content-Security-Policy"
content="
default-src 'self';
connect-src 'self' http://127.0.0.1:* https://checkout.stripe.com https://api.stripe.com https://api.pwnedpasswords.com https://hcaptcha.com https://*.hcaptcha.com;
script-src 'self' https://js.stripe.com https://api.stripe.com https://hcaptcha.com https://*.hcaptcha.com 'unsafe-inline' 'unsafe-eval';
style-src 'self' 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com;
child-src https://api.stripe.com;
frame-src https://js.stripe.com/ https://api.stripe.com https://hcaptcha.com https://*.hcaptcha.com;
img-src 'self' https://*.stripe.com data:;
media-src 'self';
font-src 'self' https://fonts.gstatic.com;
"
/>
<title>KMS</title>
<script>
// White-label by hostname. Brand mapping comes from runtime-ui-env.js
// (window.__BRAND_DOMAINS__) loaded next; default to Lux. Never bake
// tenant-specific domains into source.
(function () {
var h = (location.hostname || "").toLowerCase();
var b = { name: "Lux", title: "Lux KMS" };
if (/(^|\.)hanzo\.ai$/.test(h)) b = { name: "Hanzo", title: "Hanzo KMS" };
else if (/(^|\.)zoo\.(ngo|network)$/.test(h)) b = { name: "Zoo", title: "Zoo KMS" };
document.title = b.title;
window.__BRAND__ = b;
})();
</script>
<script src="/runtime-ui-env.js"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+16039
View File
File diff suppressed because it is too large Load Diff
+178
View File
@@ -0,0 +1,178 @@
{
"name": "@hanzo/kms-frontend",
"description": "Hanzo KMS Frontend Dashboard",
"private": true,
"version": "1.0.0",
"packageManager": "pnpm@10.33.4+sha512.1c67b3b359b2d408119ba1ed289f34b8fc3c6873412bec6fd264fbdc82489e510fcbecb9ce9d22dae7f3b76269d8441046014bdca53b9979cd7a561ad631b800",
"repository": {
"type": "git",
"url": "git+https://github.com/hanzoai/kms.git",
"directory": "frontend"
},
"author": "Hanzo AI",
"homepage": "https://kms.hanzo.ai",
"type": "module",
"pnpm": {
"onlyBuiltDependencies": [
"@swc/core",
"core-js",
"esbuild",
"protobufjs",
"unrs-resolver"
]
},
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"lint": "eslint ./src",
"lint:fix": "eslint --fix ./src",
"type:check": "tsc --noEmit --project ./tsconfig.app.json",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"overrides": {
"sha.js": "2.4.12"
},
"dependencies": {
"@casl/ability": "^6.7.2",
"@casl/react": "^4.0.0",
"@dagrejs/dagre": "^1.1.4",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@fontsource/inter": "^5.1.0",
"@fortawesome/fontawesome-svg-core": "^6.7.1",
"@fortawesome/free-brands-svg-icons": "^6.7.1",
"@fortawesome/free-regular-svg-icons": "^6.7.1",
"@fortawesome/free-solid-svg-icons": "^6.7.1",
"@fortawesome/react-fontawesome": "^0.2.2",
"@hcaptcha/react-hcaptcha": "^1.11.0",
"@headlessui/react": "^1.7.19",
"@hookform/resolvers": "^3.9.1",
"@lexical/react": "^0.29.0",
"@lottiefiles/dotlottie-react": "^0.12.0",
"@lottiefiles/dotlottie-web": "^0.38.2",
"@octokit/rest": "^21.0.2",
"@peculiar/x509": "^1.12.3",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-alert-dialog": "^1.1.3",
"@radix-ui/react-checkbox": "^1.1.3",
"@radix-ui/react-collapsible": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.3",
"@radix-ui/react-dropdown-menu": "^2.1.3",
"@radix-ui/react-hover-card": "^1.1.3",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-popover": "^1.1.3",
"@radix-ui/react-popper": "^1.2.1",
"@radix-ui/react-progress": "^1.1.1",
"@radix-ui/react-radio-group": "^1.2.2",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.1.3",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-toast": "^1.2.3",
"@radix-ui/react-tooltip": "^1.1.5",
"@simplewebauthn/browser": "^13.2.2",
"@sindresorhus/slugify": "^2.2.1",
"@tanstack/react-query": "^5.62.7",
"@tanstack/react-router": "^1.168.10",
"@tanstack/virtual-file-routes": "^1.87.6",
"@tanstack/zod-adapter": "^1.91.0",
"@types/dagre": "^0.7.52",
"@types/nprogress": "^0.2.3",
"@ucast/mongo2js": "^1.3.4",
"@xyflow/react": "^12.4.4",
"argon2-browser": "^1.18.0",
"axios": "^1.12.0",
"buffer": "^6.0.3",
"classnames": "^2.5.1",
"clsx": "^2.1.1",
"cva": "npm:class-variance-authority@^0.7.1",
"date-fns": "^4.1.0",
"dompurify": "^3.2.4",
"file-saver": "^2.0.5",
"framer-motion": "^11.14.1",
"i18next": "^24.1.0",
"i18next-browser-languagedetector": "^8.0.2",
"i18next-http-backend": "^3.0.1",
"jspdf": "^3.0.2",
"jsrp": "^0.2.4",
"jwt-decode": "^4.0.0",
"lexical": "^0.29.0",
"lucide-react": "^0.544.0",
"ms": "^2.1.3",
"nprogress": "^0.2.0",
"picomatch": "^4.0.2",
"posthog-js": "^1.198.0",
"qrcode": "^1.5.4",
"react": "^18.3.1",
"react-code-input": "^3.10.1",
"react-day-picker": "^9.4.3",
"react-dom": "^18.3.1",
"react-helmet": "^6.1.0",
"react-hook-form": "^7.56.3",
"react-i18next": "^15.2.0",
"react-icons": "^5.4.0",
"react-markdown": "^10.0.1",
"react-select": "^5.9.0",
"react-toastify": "^10.0.6",
"redaxios": "^0.5.1",
"rehype-raw": "^7.0.0",
"tailwind-merge": "^2.5.5",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1",
"yaml": "^2.6.1",
"zod": "^3.24.1",
"zustand": "^5.0.2"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.15.0",
"@kesills/eslint-config-airbnb-typescript": "^20.0.0",
"@storybook/addon-a11y": "^9.1.9",
"@storybook/addon-docs": "^9.1.9",
"@storybook/react-vite": "^9.1.9",
"@stylistic/eslint-plugin": "^2.12.1",
"@tailwindcss/postcss": "^4.1.14",
"@tailwindcss/typography": "^0.5.15",
"@tanstack/eslint-plugin-router": "^1.87.6",
"@tanstack/router-devtools": "^1.87.9",
"@tanstack/router-plugin": "^1.167.12",
"@types/argon2-browser": "^1.18.4",
"@types/file-saver": "^2.0.7",
"@types/jsrp": "^0.2.6",
"@types/ms": "^0.7.34",
"@types/picomatch": "^3.0.1",
"@types/qrcode": "^1.5.5",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@types/react-helmet": "^6.1.11",
"@vitejs/plugin-react-swc": "^3.5.0",
"eslint": "^8.57.1",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.7.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.14",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-storybook": "^9.1.9",
"globals": "^15.12.0",
"postcss": "^8.4.49",
"prettier": "3.4.2",
"prettier-plugin-tailwindcss": "^0.6.14",
"tailwindcss": "^4.1.14",
"tw-animate-css": "^1.4.0",
"typescript": "~5.6.2",
"typescript-eslint": "^8.15.0",
"vite": "^6.2.0",
"vite-plugin-node-polyfills": "^0.23.0",
"vite-plugin-top-level-await": "^1.4.4",
"vite-plugin-wasm": "^3.4.0",
"vite-tsconfig-paths": "^5.1.4"
}
}
+11852
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
},
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect width="100" height="100" fill="black"/>
<path d="M50 85 L15 25 L85 25 Z" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 169 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 109 KiB

+235
View File
@@ -0,0 +1,235 @@
<svg width="1096" height="1024" viewBox="0 0 1096 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M652.84 516.37L655.57 496.81H661.39L663.89 486.77H667.21L669.61 476.51C669.61 476.51 712.12 478.93 749.44 484.55C786.76 490.17 826.43 499.87 826.43 499.87C826.43 499.87 830.12 453.85 831.88 427.3C833.63 400.75 841.09 361.75 841.09 361.75L847.03 381.57L854.24 352.91L867.19 387.39C867.19 387.39 867.68 364.88 871.36 357.72C875.04 350.56 875.72 339.86 876.39 341.44C877.06 343.02 902.81 414.74 920.98 445.7C939.15 476.66 947.96 494.14 947.96 494.14C947.96 494.14 955.48 504.33 958.31 510.78C961.14 517.24 959.89 522.41 958.92 525.31C957.95 528.21 958 533.77 950.07 540.91C942.15 548.04 933.07 549.85 879.61 540.42C826.15 530.99 774.34 526.59 742.93 524.83C711.52 523.07 652.86 516.38 652.86 516.38L652.84 516.37Z" fill="#E5F7B7"/>
<path d="M655.09 514.47C655.09 514.47 698.6 509.96 773.53 518.61C848.46 527.26 890.16 538.4 912.52 540.9C934.88 543.41 944.76 538.47 944.76 538.47C944.76 538.47 949.79 536.8 952.68 532.69C955.57 528.58 956.14 523.64 956.27 521.98C956.4 520.31 956.68 518.69 956.27 517.45C955.86 516.21 952.45 510.6 951.59 509.6C950.73 508.6 950.12 507.13 950.12 507.13C950.12 507.13 953.73 510.22 956.48 514.16C959.23 518.1 959.39 523.39 958.89 525.3C958.39 527.21 958.15 540.12 943.24 545.98C933.61 549.76 917.62 547.09 871.8 539.07C825.98 531.05 793.78 527.77 777.34 526.55C760.9 525.33 728.21 522.38 713.15 521.1C698.09 519.82 655.08 514.46 655.08 514.46L655.09 514.47Z" fill="#B4BF8F"/>
<path d="M702.2 888.02C702.2 888.02 576.88 875.18 575.97 874.24C575.06 873.29 573.81 874.27 575.24 866.6C576.67 858.93 610.77 685.18 616.71 641.48C622.65 597.78 636.23 541.36 638.48 529.91C640.73 518.46 639.62 515.85 647.67 516.03C655.72 516.21 739.33 524.73 773.75 527.61C808.17 530.49 894.49 543.5 912.38 546.01C930.27 548.52 945.04 547.55 952.5 541.21C959.96 534.87 965.23 526.39 965.59 517.28C965.95 508.17 962.71 497.6 962.71 497.6L967.15 493.31C967.15 493.31 974.07 505.43 968.81 526.58C963.55 547.72 920.27 678.91 896.02 778.85C871.78 878.79 863.96 894.74 864.14 899.4C864.32 904.06 858.14 909.92 843.14 910.51C828.14 911.09 823.58 910.18 819.72 905.03C815.86 899.88 702.18 888.02 702.18 888.02H702.2Z" fill="url(#paint0_linear_121_754)"/>
<path d="M950.06 540.9C956.45 534.8 957.48 529.26 958.91 525.3C960.34 521.35 960.29 514.3 957.21 508.41C955.51 505.15 938.98 479.83 927.55 458.57C916.12 437.31 895.58 391.31 895.58 391.31C895.58 391.31 906.51 352.41 907.19 350.81C907.87 349.21 909.75 353.7 912.65 362.23C915.55 370.76 936.81 423.13 943.46 437.88C950.11 452.63 955.33 462.79 955.33 462.79C955.33 462.79 961.09 491.47 963.53 500.62C965.97 509.76 967.4 518.52 964 525.31C960.6 532.1 960.14 533.92 956.32 537.56C952.5 541.2 950.05 540.9 950.05 540.9H950.06Z" fill="url(#paint1_linear_121_754)"/>
<path d="M940 480.33C940 480.33 955.17 462.92 963.15 453.35C971.13 443.77 978.18 441.09 985.36 441.48C992.54 441.87 999.94 449.97 1008.96 464.17C1017.98 478.37 1021.23 482.04 1021.23 482.04C1021.23 482.04 1012.53 469.81 998.42 470.88C984.31 471.95 978.66 474.74 974.5 482.04C970.33 489.33 956.32 506.78 956.32 506.78L940 482.17V480.34V480.33Z" fill="#DEEB34"/>
<path d="M1048.16 768.08C1048.16 768.08 1061.91 744.67 1063.97 734.72C1066.04 724.77 1068.1 712.8 1064.72 701.98C1061.34 691.16 1052.28 678.21 1052.28 678.21C1052.28 678.21 1075.23 682.53 1086.88 704.5C1098.52 726.47 1092.33 742.05 1089.32 753.13C1086.32 764.21 1079.74 782.23 1079.74 782.23C1079.74 782.23 1080.49 762.7 1079.74 755.38C1078.99 748.06 1077.67 744.3 1070.91 749.37C1064.15 754.44 1051.2 767.39 1051.2 767.39L1048.15 768.08H1048.16Z" fill="#DEEB34"/>
<path d="M1000.21 471.41C1000.21 471.41 1008.15 471.23 1016.68 478.54C1025.21 485.86 1025.5 495.62 1022.56 510.77C1019.63 525.92 1010.68 560.26 1008.56 576.38C1006.44 592.5 1001.9 612.83 1009.63 634.86C1017.36 656.89 1038.81 670.03 1038.81 670.03C1038.81 670.03 1001.4 651.73 992.04 632.15C982.69 612.57 980.25 596.24 985.47 570.71C990.69 545.19 998.42 516.97 1001.7 500.93C1004.99 484.89 1004.21 477.56 1002.86 475.43C1001.51 473.3 1000.2 471.39 1000.2 471.39L1000.21 471.41Z" fill="#DEEB34"/>
<path d="M1003.79 497.89C1004.96 503.21 1014.23 528.72 1009.61 558.42C1005 588.12 1021.23 519.51 1023.08 507.97C1024.93 496.44 1026.24 488.41 1019.19 481.05C1012.14 473.69 1004.64 471.81 1002.43 471.61L1000.21 471.41C1000.21 471.41 1003.53 475.9 1004.15 482.42C1004.76 488.94 1003.79 497.89 1003.79 497.89V497.89Z" fill="#AAAE11"/>
<path d="M943.41 478.52L958.16 500.89L954.94 505.85L940 480.33L943.41 478.52Z" fill="#AAAE11"/>
<path d="M983.57 442.52L1001.12 471.42C1001.12 471.42 1008.92 473.64 1013.96 476.4C1019 479.16 1017.9 476.82 1016.67 475.45C1015.44 474.08 998.61 446.45 996.02 445.7C993.43 444.95 986.21 443.56 985.78 442.52C985.35 441.48 983.56 442.52 983.56 442.52H983.57Z" fill="#AAAE11"/>
<path d="M987.14 615.44C989.43 621.88 993.41 627.85 999.81 635.72C1006.21 643.59 1015.88 647.36 1015.88 647.36L1025.94 661.42C1025.94 661.42 1006.91 651.69 997.13 638.43C987.35 625.17 987.14 615.44 987.14 615.44V615.44Z" fill="#AAAE11"/>
<path d="M1087.14 753.97C1089.82 743.76 1086.5 723.73 1077.95 711.87C1069.4 700.01 1064.55 696.94 1064.55 696.94L1055.24 678.2C1055.24 678.2 1074.61 684.02 1084.89 701.07C1095.17 718.12 1094.36 731.04 1093.15 737.31C1091.93 743.58 1087.14 753.96 1087.14 753.96V753.97Z" fill="#AAAE11"/>
<path d="M663.11 494.49C663.11 494.49 693.21 493.65 740.1 497.72C786.99 501.79 880.38 524.04 910.43 533.48C910.43 533.48 826.19 514.19 796.33 509.37C766.47 504.54 699.89 496.46 682.16 496.68C664.42 496.9 663.12 494.49 663.12 494.49H663.11Z" fill="#B4BF8F"/>
<path d="M870.81 367.6C870.81 367.6 878.9 401.49 895.58 431.57C912.25 461.65 943.52 499.9 947.27 503.92C951.02 507.94 926.63 481.64 915.5 468.77C904.37 455.9 883.1 416.83 876.66 407.89C870.22 398.94 867.26 384.98 867.26 384.98C867.26 384.98 868.92 367.47 869.24 367.6C869.56 367.73 870.81 367.6 870.81 367.6Z" fill="#B4BF8F"/>
<path d="M850.87 369.64C850.87 369.64 862.73 403.37 880.18 430.63C897.63 457.89 938.14 501.85 938.14 501.85C938.14 501.85 908.67 476.15 876.07 430.63C843.47 385.12 847.76 378.65 847.76 378.65L850.88 369.63L850.87 369.64Z" fill="#B4BF8F"/>
<path d="M934.45 530.61L940.81 506.78C940.81 506.78 949.74 513.68 950.15 517.88C950.56 522.09 950.05 527.02 943.27 529.69C936.49 532.36 934.44 530.61 934.44 530.61H934.45Z" fill="#B4BF8F"/>
<path d="M4.32007 678.12C4.32007 678.12 12.2001 604.24 32.8901 544.94C53.5801 485.64 72.8001 435.61 101.45 389.31C130.1 343.01 180.92 284.36 206.21 262.03C231.5 239.7 240.77 233.22 254.68 233.08C268.58 232.94 271.24 244.93 272.74 262.03C274.25 279.14 283.28 334.28 288.09 356.38C292.9 378.48 299.63 407.57 299.63 407.57C299.63 407.57 315.06 348.37 339.66 299.3C364.26 250.23 388.38 226.79 400.36 222.99C412.34 219.19 420.25 221.01 429.33 226.2C438.41 231.39 448.75 249.11 448.75 249.11C448.75 249.11 464.7 234.94 474.2 225.6C483.7 216.26 502.13 185.22 502.13 185.22C502.13 185.22 458.61 169.33 426.14 132.18C393.67 95.03 380.17 66.95 379.66 58.54L379.15 50.13C379.15 50.13 410.82 78.46 446.68 100.33C482.54 122.2 504.02 125.53 519.05 132.42C534.08 139.31 537.07 137.41 538.53 142.89C539.99 148.37 542.4 152.56 542.4 152.56C542.4 152.56 553.11 145.17 574 145.35C594.89 145.53 597.95 147.95 597.95 147.95C597.95 147.95 580.33 126.46 566.14 91.4C551.95 56.34 548.35 31.77 547.84 25.4C547.33 19.03 546.64 9.45996 546.64 9.45996C546.64 9.45996 571.28 59.04 601.86 90.38C632.44 121.72 642.58 118.49 646.2 127.84C649.81 137.19 645.18 144.7 644.42 149.52C643.66 154.34 644.49 162.29 644.49 162.29C644.49 162.29 643.9 163.1 650.01 169.09C656.12 175.08 673.12 191.87 678.77 203.51C684.42 215.14 683.39 220.74 680.68 224.11C677.97 227.48 677.82 227.29 678.26 229C678.7 230.71 681.45 233.34 681.06 239.26C680.67 245.18 679.25 255.27 679.86 257.33C680.47 259.39 691.4 269.61 703.58 280.13C715.76 290.65 720.64 294.86 720.64 294.86C720.64 294.86 728.68 286.96 738.26 290.28C747.85 293.6 753.23 300.13 751.72 314.62C750.21 329.11 742.5 340.64 742.5 340.64C742.5 340.64 741.54 347.97 738.26 355.37C734.98 362.77 730.49 366.31 730.49 366.31L731.16 353.55C731.16 353.55 726.75 361.63 724.62 366.68C722.49 371.73 711.54 379.98 697.43 380.9C683.32 381.83 680.12 381.21 680.12 381.21L677.59 382.87L677 378.97C677 378.97 660.44 373.73 653.14 360C645.84 346.27 633.44 330.26 621.74 319.58C610.04 308.9 611.63 307.76 598.97 308.28C586.31 308.8 557.97 319.19 543.43 306.73C528.89 294.27 511.52 264.64 511.52 264.64C511.52 264.64 471.7 313.6 460.34 393.35C448.98 473.1 458.06 556.12 486 648.88C513.95 741.64 522.2 757.54 529.53 796.59C536.86 835.64 539.67 862.09 539.67 862.09C539.67 862.09 567.37 824.2 567.87 794.79C568.37 765.38 556.55 731.07 551.51 709.5C546.47 687.94 550.11 672.68 553.51 662.25C556.91 651.82 561.75 646.79 564.26 645C566.77 643.21 571.25 641.5 571.25 641.5L566.59 653.56C566.59 653.56 574.91 655.28 580.39 659.25C585.87 663.22 589.31 671.48 588.24 676.36C587.17 681.24 587.02 687.11 587.02 687.11C587.02 687.11 589.53 679.94 598.49 672.6C607.45 665.25 613.25 662.4 613.25 662.4L574.49 870.52C574.49 870.52 573.8 874.4 577.98 875C582.16 875.6 625.21 881.4 625.21 881.4L702.17 888.05C702.17 888.05 703.17 884.06 704.63 878.27C706.09 872.48 714 856.51 714 856.51L714.95 871.41C714.95 871.41 719.69 872.79 723.82 874.62C727.95 876.45 731.92 878.27 731.92 878.27C731.92 878.27 734.67 867.28 737.58 863.62C740.48 859.95 746.41 853.74 746.41 853.74L742.04 870.04C742.04 870.04 746.47 870.8 751.96 874.32C757.45 877.84 763.06 885.17 763.06 885.17C763.06 885.17 766.98 879.67 769.56 877.53C772.13 875.39 774.54 874.63 774.54 874.63L778.1 862.86L782.69 876.01C782.69 876.01 791.56 877.46 794.77 884.07C797.98 890.68 800.58 897.54 800.58 897.54C800.58 897.54 806.63 897.56 809.72 899.55C812.81 901.54 814.19 907.35 812.5 911.47C810.82 915.6 801.02 930.12 788.88 929.66C776.73 929.2 767.41 922.8 767.41 922.8C767.41 922.8 762.81 933.48 749.21 943.11C735.61 952.74 712.07 960.08 710.39 959.93C708.71 959.78 709.79 990.12 706.8 1005.45C703.81 1020.78 699.71 1036.66 699.71 1036.66H160.76C160.76 1036.66 177.03 995.13 214.36 955.09C251.69 915.05 280.88 884.87 310.37 823.7C339.87 762.54 342.19 707.19 347.71 675.91C353.23 644.63 352.3 575.71 352.3 575.71C352.3 575.71 333.93 565.74 321.48 555.6C309.03 545.46 300.72 542.18 300.72 542.18C300.72 542.18 292.35 554.55 283.61 575.06C274.86 595.57 264.21 617.67 264.21 617.67C264.21 617.67 265.01 597.97 267.91 573.12C270.81 548.27 275.4 520.48 275.4 520.48C275.4 520.48 258 506.46 242.77 520C227.54 533.53 220.54 555.53 214.74 577.04C208.94 598.55 202.68 625.64 202.68 625.64C202.68 625.64 199.27 572.19 196.25 549.74C193.23 527.28 186.99 517.62 176.06 521.46C165.13 525.3 155.03 536.71 143.19 575.08C131.35 613.45 127.48 653.9 126.51 658.73C125.54 663.56 124.09 684.11 124.09 684.11C124.09 684.11 114.07 639.16 111.83 612.57C109.59 585.99 107.17 558.19 104.75 550.22C102.33 542.24 93.4401 532.1 80.3401 540.07C67.2401 548.04 53.7401 565.98 35.8701 601.94C18.0001 637.9 4.30005 678.17 4.30005 678.17L4.32007 678.12Z" fill="#A6CF3D"/>
<path d="M524.59 1033.99C524.59 1033.99 532.39 1015.81 533.63 1010.65C534.88 1005.48 537.02 999.07 537.02 999.07L556.09 982.32L551.1 998.54C551.1 998.54 572.48 987.8 591.73 961.63C610.97 935.46 614.36 925.66 619.35 909.62C624.34 893.58 625.24 881.38 625.24 881.38L700.6 887.89C700.6 887.89 706.99 916.06 708.08 945.17C709.17 974.28 708.29 997.41 704.66 1014.4C701.02 1031.39 699.71 1036.64 699.71 1036.64L524.59 1034V1033.99Z" fill="#DEEB34"/>
<path d="M538.95 859.6C538.95 859.6 560.82 827.85 564.76 798.73C568.7 769.61 563.31 746.97 555.52 726.02C547.73 705.07 547.11 687.06 550.32 674.64C553.53 662.22 558.78 652.69 561.53 648.83C564.28 644.97 571.27 641.47 571.27 641.47C571.27 641.47 568.42 647.29 567.26 649.67C566.1 652.05 566.61 653.53 566.61 653.53C566.61 653.53 575.74 654.15 580.41 659.22C585.08 664.29 584.81 668.84 586.54 672.59C588.26 676.34 587.16 684.72 587.16 684.72C587.16 684.72 589.2 680.14 596.57 674.25C603.94 668.35 613.26 662.38 613.26 662.38L620.19 629.93C620.19 629.93 594.57 575.85 585.59 555.64C576.61 535.43 563.87 497.59 560.64 463.58C557.41 429.57 555.12 414.34 559.07 383.24C563.03 352.15 581.15 316.44 581.15 316.44C581.15 316.44 557.53 317.56 543.44 306.73C529.35 295.9 524.76 288.11 520.06 278.1C515.35 268.09 511.8 262.17 511.8 262.17C511.8 262.17 477.72 311.52 466.27 363.09C454.82 414.66 452.48 484.72 462.47 547.32C472.46 609.92 489.46 660.72 499.74 690.58C510.02 720.44 518.31 743.52 526.32 781.08C534.33 818.64 538.94 859.62 538.94 859.62L538.95 859.6Z" fill="#DEEB34"/>
<path d="M518.45 130.95C518.9 131 535.86 130.11 550.89 134.13C565.92 138.15 576.19 143.8 576.19 143.8C576.19 143.8 565.38 142.65 557.74 145.82C550.1 148.99 541.3 150.37 541.3 150.37C541.3 150.37 542.57 142.31 536.47 139.04C530.37 135.77 518.47 130.95 518.47 130.95H518.45Z" fill="#30923A"/>
<path d="M415.98 235.65C415.98 235.65 424.57 222.64 437.19 210.97C449.81 199.3 468.93 191.23 477.15 188.54C485.38 185.85 494.2 183.16 494.2 183.16L502.87 186.69C502.87 186.69 487.17 203.76 463.71 241.87C440.24 279.98 418.11 312.24 400.45 360.08C382.8 407.93 371.04 454.26 366.35 479.5C361.66 504.74 358.28 535.61 358.28 535.61L345.68 513.16L325.33 474.67C325.33 474.67 329.83 465.52 341.05 455.08C352.27 444.64 363.93 434.85 369.19 419.17C374.45 403.49 382.34 384.77 389.21 366.82C396.08 348.87 399.3 340.14 396.65 336.16C393.99 332.18 385.08 334.83 375.79 341.47C366.5 348.11 356.69 360.08 356.69 360.08C356.69 360.08 366.69 336.54 376.93 324.97C387.17 313.4 395.33 307.33 404.81 302.02C414.29 296.71 424.94 287.42 431.95 276.23C438.95 265.04 454.46 238.25 459.44 232.04C464.42 225.82 466.17 218.87 461.94 217.02C457.7 215.17 443.32 216.63 435.55 221.68C427.77 226.73 416 235.66 416 235.66L415.98 235.65Z" fill="#30923A"/>
<path d="M263.62 775.68C263.87 775.22 272.1 761.44 283.58 754.65C295.05 747.85 304.54 747.73 307.24 755.66C309.94 763.59 306.78 781.89 298.52 805.24C290.26 828.59 276.19 854.73 258.08 879.18C239.97 903.63 228.88 917.89 221.41 924C213.94 930.11 185.47 940.06 165.88 951.69C146.29 963.32 135.27 971.28 129.14 981.39C123.02 991.49 117.81 1008.4 117.81 1008.4C117.81 1008.4 129.1 995.53 140.37 988.18C151.64 980.83 162.03 985.38 166.4 988.13C170.77 990.88 167.88 1002.8 166.3 1011.96C164.72 1021.11 162.22 1026.22 162.22 1026.22C162.22 1026.22 193.09 978.06 210.48 959.3C227.88 940.54 261.39 903.88 284.96 869.46C308.53 835.05 326.6 790.04 335.17 757.59C343.74 725.14 343.04 678.42 348.29 642.23C353.54 606.04 355.12 575.9 355.12 575.9L337.45 569.66C337.45 569.66 336.09 579.41 333.33 613.7C330.57 647.99 327.22 682.92 308.53 707.99C289.84 733.07 285.85 729.72 275.16 747.18C264.47 764.64 263.61 775.67 263.61 775.67L263.62 775.68Z" fill="#30923A"/>
<path d="M563.23 227.79C563.23 227.79 570.52 223.95 576.28 223.8C582.05 223.65 592.19 224.66 601.99 230.3C611.8 235.94 621.81 233.84 623.64 240.66C625.46 247.48 629.15 266.45 629.15 266.45C629.15 266.45 625.63 260.5 621.75 260.29C617.87 260.08 599.05 263.31 583.57 251.98C568.09 240.65 563.23 227.79 563.23 227.79Z" fill="white"/>
<path d="M665.63 246.95C665.63 246.95 667.96 234.5 668.4 233.47C668.84 232.44 676.88 227.93 677.2 227.72C677.52 227.51 678.16 230.62 677.2 235.38C676.24 240.14 670.32 251.39 670.32 251.39L665.63 246.96V246.95Z" fill="#D4D4D4"/>
<path d="M493.29 377.45C489.48 400.94 485.16 467.06 495.97 525.31C506.77 583.56 524.59 622.86 524.59 622.86C524.59 622.86 512.76 571.59 507.21 542.39C501.65 513.19 498.08 483.09 495.97 452.58C493.87 422.06 493.29 377.45 493.29 377.45V377.45Z" fill="white"/>
<path d="M716.24 334.84C716.24 334.84 719.05 338.57 726.74 339.8C734.43 341.03 740.89 334.23 743.34 328.44C745.79 322.65 751.35 309.34 747.4 298.62C743.45 287.9 753.9 304.15 751.74 314.6C749.58 325.05 741.88 342.76 741.4 343.96C740.92 345.16 736.96 349.78 732.58 348.88C728.2 347.98 721.84 345.94 719.44 343C717.04 340.06 716.24 334.85 716.24 334.85V334.84Z" fill="#30923A"/>
<path d="M718.15 356.64C719.12 353.88 720.32 352.38 722.26 353.13C724.2 353.88 722.86 358.66 721.21 362.1C719.57 365.54 718.37 368.06 714.86 370.01C711.35 371.96 702.26 373.18 702.26 373.18C702.26 373.18 694.77 375.83 685.8 375.09L680.72 379.8C680.72 379.8 699.3 381.79 710.61 377.04C721.92 372.29 724.5 364.63 727.42 360.75C730.34 356.87 731.86 351.38 731.91 351.18C731.96 350.98 733.7 348.95 730.27 347.68C726.83 346.41 723.18 343.07 720.9 345.75C718.62 348.43 718.17 356.65 718.17 356.65L718.15 356.64Z" fill="#30923A"/>
<path d="M701.96 370.19C699.28 369.86 697.01 368.78 697.06 367.31C697.11 365.84 698.43 364 703.21 364.79C707.99 365.58 701 360.81 698.01 360.64C695.02 360.47 691.34 361.48 691.23 361.69C691.12 361.9 688.5 368.72 688.5 368.72C688.5 368.72 690.02 367.31 693.33 368.72C696.64 370.14 700.95 371.14 700.95 371.14L701.96 370.18V370.19Z" fill="#30923A"/>
<path d="M683.61 380.22C685.08 379.45 690.97 374.83 693.33 368.73C695.69 362.63 689.7 363.74 689.7 363.74C689.7 363.74 685.5 373.99 683.61 376.03C681.72 378.08 680.19 379.71 680.19 379.71L683.61 380.21V380.22Z" fill="#30923A"/>
<path d="M716.24 334.84C716.24 334.84 711.04 345.78 696.66 350.97C682.28 356.15 670.04 349.6 664.87 342.91C659.7 336.22 662.27 324.59 662.27 324.59L653.44 327.75C653.44 327.75 651.36 339.85 657.42 346.92C663.47 354 680.07 359.88 679.87 360.99C679.67 362.1 685.67 363.77 689.22 362.48C692.77 361.19 703.28 358.24 707.98 352.97C712.68 347.7 714.09 343.36 715.11 340.74C716.13 338.12 716.24 334.84 716.24 334.84V334.84Z" fill="#30923A"/>
<path d="M674.03 312.34C672.33 313.33 670.94 314.83 671.04 317.67C671.14 320.51 673.88 322.49 677.49 322.04C681.1 321.59 684.25 321.71 686.57 322.04C688.89 322.37 691.48 322.64 691.48 322.64C691.48 322.64 686.37 319.11 681.26 316.59C676.15 314.06 674.03 312.34 674.03 312.34Z" fill="#30923A"/>
<path d="M731.18 319.57C731.18 319.57 731.51 313.99 732.56 310.43C733.61 306.87 735.72 303.72 739.32 302.87C742.93 302.02 745.39 304.77 745.34 307.33C745.28 309.89 741.58 308.09 738.82 310.44C736.07 312.79 731.18 319.58 731.18 319.58V319.57Z" fill="#30923A"/>
<path d="M668.37 327.92C668.37 327.92 660.76 326.67 657.17 322.64C653.58 318.61 654.13 309.28 659.4 303.03C664.67 296.77 659.4 303.89 659.31 310.43C659.22 316.97 659.45 317.14 661.27 320.59C663.09 324.04 668.37 327.92 668.37 327.92Z" fill="#30923A"/>
<path d="M598.58 287.33C606.21 287.09 612.73 286.49 619.68 287.77C626.63 289.06 655.53 313.65 655.53 313.65C655.53 313.65 657.19 320.7 659.73 322.64C662.27 324.58 661.27 326.11 661.27 326.11L657.5 330.74L651.58 327.92C651.58 327.92 621.22 296.26 614.36 292.49C607.5 288.72 599.26 288.2 599.26 288.2L598.58 287.33V287.33Z" fill="#30923A"/>
<path d="M631.04 271.34C631.04 271.34 601.67 269.76 582.99 253.84C564.31 237.92 578.12 248.31 578.12 248.31C578.12 248.31 590.32 260.12 603.62 260.3C616.92 260.48 623.77 259.82 626.09 261.26C628.41 262.7 631.04 271.34 631.04 271.34V271.34Z" fill="#30923A"/>
<path d="M640.16 251.83C638.76 252.41 635.59 253.43 632.89 258.15C630.19 262.88 631.04 271.33 631.04 271.33C631.04 271.33 630.05 262.93 627.74 254.65C625.43 246.36 621.29 239.19 621.29 239.19C621.29 239.19 625.44 238.54 630.03 235.38C634.61 232.22 637 226 637 226C637 226 637.73 230.81 635.85 235.89C633.96 240.98 634.55 244.71 635.4 247.16C636.54 250.47 640.17 251.83 640.17 251.83H640.16Z" fill="#30923A"/>
<path d="M543.45 219.94C543.45 219.94 549.96 212.62 566.15 211.5C582.34 210.37 597.23 217.45 609.98 222.12C622.72 226.78 627.34 227.65 630.73 227.46C634.12 227.27 636.3 223.94 636.2 222C636.2 222 637.81 235.38 625.15 235.89C612.49 236.4 598.84 228.29 586.47 222.76C574.1 217.23 568.61 214.54 559.99 215.44C551.37 216.34 543.46 219.95 543.46 219.95L543.45 219.94Z" fill="#30923A"/>
<path d="M543.45 219.94C543.45 219.94 556.42 228.11 567.26 239.55C578.1 250.99 562.18 231.16 562.49 229.43C562.8 227.7 566.96 227.78 572.75 225.12C578.54 222.46 579.33 218.81 570.55 217.06C561.77 215.3 553.27 216.01 550.02 217.06C546.77 218.11 543.44 219.95 543.44 219.95L543.45 219.94Z" fill="#30923A"/>
<path d="M634.66 157.89C634.66 157.89 645.57 166.32 659.68 187.01C673.79 207.7 675.39 224.62 675.56 226.6C675.73 228.58 668.4 231.13 668.4 231.13C668.4 231.13 664.24 206.5 655.11 190.17C645.99 173.84 634.66 157.89 634.66 157.89V157.89Z" fill="#30923A"/>
<path d="M669.06 192.25C673.17 198.25 676.32 209.98 674.27 219.02C672.22 228.07 674.73 228.3 677.71 226.2C680.69 224.1 683.35 225.48 681.73 215.95C680.11 206.42 678.91 204.06 675.57 198.71C672.23 193.35 669.06 192.25 669.06 192.25V192.25Z" fill="#30923A"/>
<path d="M670.55 251.91C670.55 251.91 673.85 248.32 675.57 241.62C677.29 234.91 678.43 229.43 678.43 229.43C678.43 229.43 680.82 237.57 680.57 244.97C680.32 252.37 679.26 260.62 679.26 260.62L670.55 251.91Z" fill="#30923A"/>
<path d="M545.62 155.85C549.04 153.37 564.17 145.31 581.54 146.76C598.9 148.21 580.18 144.29 569.77 144.75C559.36 145.21 546.26 147.62 544.28 148.52C542.31 149.42 541.59 151 541.59 151L545.62 155.84V155.85Z" fill="#30923A"/>
<path d="M566.15 155.12C566.15 155.12 572.56 154.2 583.46 155.12C594.36 156.04 607.24 160.83 607.24 160.83C607.24 160.83 607.32 168.57 600.16 165.54C592.99 162.52 574.7 156.65 574.39 156.57C574.08 156.49 566.15 155.11 566.15 155.11V155.12Z" fill="#30923A"/>
<path d="M649.05 248.98C653.78 247.91 659.05 245.71 661.25 240.66C663.45 235.61 660.37 217.02 654.43 203.04C648.49 189.07 639.23 174.76 628.24 161.45C617.24 148.14 596.16 127.86 596.16 127.86C596.16 127.86 618.5 141.02 629.93 154.36C641.36 167.71 654.12 188 661.05 203.44C667.97 218.88 669.57 231.29 668.42 237.58C667.27 243.87 664.81 247.43 659.95 248.3C655.08 249.16 649.07 248.97 649.07 248.97L649.05 248.98Z" fill="#30923A"/>
<path d="M592.3 141.7L600.86 142.89L603.91 150.29L596.15 148.57L592.3 141.7Z" fill="#30923A"/>
<path d="M623.24 145.82C627.21 146.52 633.77 148.47 636.25 145.82C638.73 143.17 639.75 134.39 638.73 130.9C637.71 127.42 622.4 118.23 608.55 104.23C594.7 90.23 568.33 63.95 548.97 18.15C548.97 18.15 568.99 51.65 587.95 74.49C606.91 97.33 633.39 116.23 641.03 121.7C648.67 127.17 648.6 133.87 647.15 139.37C645.7 144.86 644.69 162.99 644.69 162.99C644.69 162.99 637.59 158.14 632.19 155.12C626.79 152.1 623.41 148.98 623.41 148.98L623.22 145.81L623.24 145.82Z" fill="#30923A"/>
<path d="M396.57 90.5701C405.92 105.09 420.54 123.34 448.75 144.72C476.96 166.1 522.9 190.17 522.9 190.17C522.9 190.17 472.44 175.53 450.43 155.12C428.42 134.71 412.4 121.48 407.04 111.32C401.68 101.16 396.58 90.5701 396.58 90.5701H396.57Z" fill="#30923A"/>
<path d="M522.9 190.17C522.9 190.17 512.05 188.74 503.58 198.15C495.11 207.56 453.48 260.57 431.96 304.98C410.43 349.39 395.98 384.91 380.19 448.84C364.4 512.78 365.09 542.39 365.09 542.39L358.78 533.56C358.78 533.56 364.01 483.93 372.01 453.5C380.01 423.07 396.47 366.72 406.69 344.26C416.91 321.8 435.01 281.79 451.09 259.06C467.17 236.33 502.14 185.21 502.14 185.21C502.14 185.21 510.4 186.81 511.05 186.68C511.7 186.55 522.9 190.16 522.9 190.16V190.17Z" fill="#30923A"/>
<path d="M275.59 527.71C277.42 539.98 281.94 549.15 284.34 553.38C286.74 557.61 288.96 561.56 288.96 561.56L299.83 541.4L275.4 520.44L275.6 527.71H275.59Z" fill="#30923A"/>
<path d="M264.94 610.23C264.94 610.23 271.64 588.55 277.84 573.09C284.05 557.62 291.24 547.04 291.24 547.04L295.19 551.27L264.94 610.23V610.23Z" fill="#30923A"/>
<path d="M316.48 391.19C316.48 391.19 338.04 347.92 355.31 320.59C372.58 293.26 386.76 287.1 396.01 285.66C405.26 284.22 412.57 291.91 414.11 294.5C414.11 294.5 421.72 291.1 427.97 280.93C434.21 270.76 443.69 271.06 427.97 263.46C412.25 255.86 385.51 252.87 374.47 268.01C363.44 283.14 346.44 311.03 339.66 330.89C332.88 350.75 316.49 391.2 316.49 391.2L316.48 391.19Z" fill="#30923A"/>
<path d="M377.53 260.75C383.15 254.76 393.48 246.34 406.1 242.77C418.73 239.2 427.18 245.11 431.95 249.1C436.71 253.1 441.34 260.74 441.34 260.74L435.65 266.2C435.65 266.2 423.45 251.08 411.08 250.17C398.71 249.26 377.53 260.74 377.53 260.74V260.75Z" fill="#30923A"/>
<path d="M410.49 222.54C414.49 222.67 423.7 224.61 430.71 233.05C437.72 241.48 443.41 255.13 443.41 255.13L448.83 248.35C448.83 248.35 440.83 232.06 429.66 226.39C418.49 220.72 411.09 220.98 411.09 220.98L410.49 222.54Z" fill="#30923A"/>
<path d="M332.23 461.78C334.44 449.97 346.52 404.35 351.53 387.39C356.54 370.43 362.14 368.96 363.9 369.25C365.66 369.54 369.04 380.32 369.04 397.19C369.04 414.06 369.21 425.23 369.21 425.23C369.21 425.23 352.12 452.59 344.46 459.83C336.8 467.06 332.23 461.79 332.23 461.79V461.78Z" fill="#30923A"/>
<path d="M307.03 426.42L314.44 401.78C314.44 401.78 314.22 444.47 314.44 450.2C314.66 455.93 307.03 426.43 307.03 426.43V426.42Z" fill="#30923A"/>
<path d="M65.6899 481.9C65.6899 481.9 87.3999 448.99 110.51 413.17C133.63 377.35 160.55 343.32 178.59 325.79C196.63 308.26 211.1 298.61 225.84 296.58C240.57 294.55 248.96 305.6 253.53 317.6C258.1 329.6 263.18 352.79 273.6 385.52C284.01 418.25 291.28 435.78 300.73 457.88C310.18 479.98 338.88 523.04 338.88 523.04C338.88 523.04 294.14 440.04 283 391.39C271.86 342.74 264.47 306.4 258.11 292.72C251.75 279.04 243.87 272.14 226.1 274.73C208.33 277.32 184.54 302.72 160.31 329.97C136.07 357.21 113.55 394.66 102.65 411.14C91.75 427.62 65.7 481.9 65.7 481.9H65.6899Z" fill="#30923A"/>
<path d="M111.84 514.59C111.84 514.59 127.02 485.18 133.32 472.91C139.62 460.64 145.09 449.69 147.08 449.69C149.07 449.69 149.4 454.17 147.08 462.79C144.76 471.41 139.11 500.39 139.11 500.39C139.11 500.39 154.33 438.99 161.87 424.49C169.42 409.99 170.47 402.98 167.81 404.26C165.14 405.54 164.33 406.56 156.7 417.53C149.07 428.5 131.46 467.56 127.26 476.93C123.06 486.31 111.83 514.59 111.83 514.59H111.84Z" fill="#30923A"/>
<path d="M194.05 474.61C194.05 474.61 203.62 437.83 207.45 424.34C211.27 410.85 214.49 411.57 216.07 413.11C217.65 414.65 218.01 422.74 219.08 431.89C220.15 441.04 224.69 474.6 224.69 474.6C224.69 474.6 221.37 409.63 222.55 394.66C223.73 379.69 223.16 369.01 220.84 371.55C218.53 374.09 214.8 382.84 211.91 392.16C209.02 401.48 202.59 429.58 200.62 439.3C198.65 449.02 194.05 474.6 194.05 474.6V474.61Z" fill="#30923A"/>
<path d="M6.72998 670.28C6.72998 670.28 13.42 640.41 28.44 600.71C43.47 561.01 59.73 541.39 73.12 530.82C86.51 520.25 104.13 523.74 108.83 533.86C113.53 543.98 111.13 572.47 114.27 611.64C117.42 650.81 108.36 580.4 107.42 565.83C106.48 551.26 102.72 539.99 92.38 537.4C82.04 534.82 71.47 545.86 59.02 564.42C46.57 582.98 34.5799 602.01 28.4799 617.99C22.3699 633.97 6.72998 670.28 6.72998 670.28V670.28Z" fill="#30923A"/>
<path d="M123.01 669.16C123.01 669.16 121.53 651.83 127.4 611.65C133.27 571.47 141.07 551.62 150.9 535.61C160.2 520.45 167.21 513.87 176.76 512.32C188.29 510.45 194.7 521 197.01 538.23C199.33 555.46 193.7 530.74 188.16 524.44C182.62 518.14 169.93 519.78 164.76 528.48C159.59 537.17 144.32 564.19 138.92 590.04C133.52 615.88 127.75 648.21 126.53 658.69C125.31 669.18 123.02 669.16 123.02 669.16H123.01Z" fill="#30923A"/>
<path d="M382.09 584.38C361.19 576.12 336.13 562.47 318.98 545.63C301.83 528.8 295.23 521.27 281.57 512.1C267.91 502.93 253.79 502.29 240.69 510.43C227.59 518.57 221.17 530.36 214.76 555.76C208.34 581.16 204.22 611.72 204.22 611.72C204.22 611.72 217.34 558.79 228.38 539.32C239.42 519.85 247.51 513.75 260.38 514.23C273.25 514.72 285.92 527.55 302.11 542.76C318.3 557.97 334.04 567.26 349.55 574.13C365.05 580.99 382.1 584.38 382.1 584.38H382.09Z" fill="#30923A"/>
<path d="M528.13 802.64C529.43 815.43 530.94 842.38 530.19 853.35C529.44 864.32 528.13 873.18 528.13 873.18L540.95 860.26C540.95 860.26 540.55 849.22 536.94 833.52C533.33 817.82 528.13 802.64 528.13 802.64V802.64Z" fill="#30923A"/>
<path d="M582.76 875.63C582.76 875.63 581.18 883.85 581.04 885.11C580.9 886.37 582.16 886.92 585.5 887.48C588.85 888.04 622.86 892.5 622.86 892.5L625.29 878.99L582.76 875.63Z" fill="#30923A"/>
<path d="M609.71 884.06C609.71 884.06 606.22 908.52 593.21 937.22C580.2 965.92 554.88 993.52 554.88 993.52C554.88 993.52 576.38 981.66 590.89 962.74C605.4 943.82 615.29 924.04 618.81 911.33C622.33 898.62 623.55 888.65 623.55 888.65L609.71 884.05V884.06Z" fill="#30923A"/>
<path d="M567.85 957.74C567.85 957.74 544.24 986.93 497.29 1002.11C450.35 1017.3 411.12 1024.18 387.08 1023.43C363.04 1022.68 430.68 1033.59 450.08 1031.76C469.48 1029.93 524.58 1036.63 524.58 1036.63L538.18 997.19C538.18 997.19 549.93 994.08 554.87 986.24C559.81 978.4 561.64 972.91 563.67 969.18C565.7 965.45 567.84 957.75 567.84 957.75L567.85 957.74Z" fill="#30923A"/>
<path d="M764.01 925.88C755.35 934.14 741.43 940.15 729.8 941.83C718.17 943.52 708.8 941.83 708.8 941.83L701.56 892.49C701.56 892.49 704.91 909.59 719.51 915.91C734.11 922.23 738.25 920.93 738.25 920.93C738.25 920.93 742.39 925.5 750.45 925.71C758.51 925.93 764.01 925.88 764.01 925.88V925.88Z" fill="#30923A"/>
<path d="M735.49 918.99C727.89 913.86 723.72 902.71 723.72 890.7C723.72 878.69 725.11 878.99 722.83 877.31C720.55 875.62 713.37 873.47 713.37 873.47C713.37 873.47 718.89 873.06 724.43 874.84C729.97 876.62 731.66 880.19 731.66 880.19C731.66 880.19 729.08 892.39 731.16 902.11C733.24 911.83 735.49 918.99 735.49 918.99V918.99Z" fill="#30923A"/>
<path d="M707.37 895.76C708.59 899.36 711.22 904.74 719.21 910.01C727.2 915.27 735.49 914.88 735.49 914.88L738.26 920.93C738.26 920.93 726.33 921.11 717.95 914.19C709.57 907.27 707.38 895.75 707.38 895.75L707.37 895.76Z" fill="#30923A"/>
<path d="M736.3 913.54C738.16 916.69 743.16 919.67 749.22 920.93C755.28 922.19 761.74 918.58 763.09 916.43C764.44 914.29 766.76 921.59 766.76 921.59C766.76 921.59 760.19 928.12 749.22 924.52C738.25 920.92 735.49 914.87 735.49 914.87L736.31 913.54H736.3Z" fill="#30923A"/>
<path d="M754.86 878.56C754.86 878.56 751.34 884.85 752.94 898.8C754.79 914.98 761.26 922.5 762.64 924.2C764.02 925.9 765.71 920.54 765.71 920.54C765.71 920.54 758.3 902.41 760.93 895.1C763.56 887.79 764.46 884.62 763.09 884.04C761.72 883.46 754.86 878.57 754.86 878.57V878.56Z" fill="#30923A"/>
<path d="M786.41 879.99C791.77 884.16 793.64 892.93 795.12 902.19C796.6 911.45 794.53 922.6 789.9 924.52C785.27 926.44 772.35 926.44 766.76 916.12C761.17 905.8 766.56 923.12 775.81 927.67C785.06 932.22 795.38 929.71 801.76 924.77C808.14 919.83 813.06 916.87 812.08 909.78C811.09 902.68 808.43 899.63 805.77 899.23C803.11 898.84 800.15 898.44 800.15 898.44C800.15 898.44 799.95 893.4 795.12 886.9C790.29 880.4 786.41 879.98 786.41 879.98V879.99Z" fill="#30923A"/>
<path d="M522.9 233.47C522.9 233.47 523.44 241.77 525.72 249.33C528 256.89 533.86 264.69 533.86 264.69H547.69C547.69 264.69 538.95 257.68 533.28 250.04C527.61 242.4 522.9 233.48 522.9 233.48V233.47Z" fill="#30923A"/>
<path d="M527.4 261.91C527.4 261.91 531.32 273.73 545.2 284.29C559.08 294.85 576.23 298.17 586.46 299.04C596.69 299.91 607.24 297.45 616.07 304.54C624.89 311.63 633.99 320.67 640.16 327.03C646.33 333.39 654.89 345.36 654.89 345.36L651.59 327.93C651.59 327.93 625.2 302.49 616.63 295.11C608.06 287.73 594.61 290.68 587.22 289.94C579.83 289.2 560.62 287.28 551.75 281.66C542.88 276.04 533.42 268.06 533.42 268.06L527.41 261.92L527.4 261.91Z" fill="#30923A"/>
<path d="M644.36 344.13C648.99 350.77 655.83 359.79 662.72 365.2C669.61 370.61 679.08 373.57 679.08 373.57L678.4 379.47C678.4 379.47 668.7 376.46 661.82 370.39C654.94 364.31 648.64 352.23 648.64 352.23L644.36 344.13V344.13Z" fill="#30923A"/>
<path d="M650.79 339.82C651.95 342.54 652.49 347.18 659.18 355.23C665.87 363.28 678.41 366.67 678.41 366.67L679.88 360.98C679.88 360.98 665.32 358.69 660 350.89C654.68 343.09 650.79 339.82 650.79 339.82V339.82Z" fill="#30923A"/>
<path d="M627.36 321.65C622.51 316.16 614.77 308.16 614.22 306.71C613.67 305.26 615.62 303.77 621.63 309.22C627.63 314.67 621.49 304.5 610.48 299.32C599.47 294.14 580.16 298.28 580.16 298.28C580.16 298.28 586.69 298.99 587.13 301.85C587.57 304.71 572.92 305.83 560.3 304.79C547.68 303.74 538.79 295.9 530.03 284.52C521.27 273.14 504.5 248.3 504.5 248.3C504.5 248.3 520.25 278.12 527.41 290.96C534.57 303.8 548 313.72 563.24 312.81C578.48 311.9 595.02 307.89 602.01 308.17C609 308.45 610.96 309.44 616.45 313.65C621.94 317.86 627.36 321.65 627.36 321.65V321.65Z" fill="#30923A"/>
<path d="M551.95 187.15C551.95 187.15 550.33 165.92 549.74 160.48C549.15 155.04 552.86 151.49 557.97 149.97C563.08 148.45 554.17 149.42 548.97 151.52C543.77 153.62 541.73 152.25 543.54 156.79C545.35 161.33 551.95 187.15 551.95 187.15V187.15Z" fill="#30923A"/>
<path d="M504.49 295.48C507.08 291.49 512.44 290.76 516.95 296.49C521.46 302.22 530.95 311.48 542.35 317.57C553.75 323.66 570.58 324.8 575.74 324.19C575.74 324.19 581.29 316.56 578.17 316.45C575.05 316.34 547.62 313.83 537.17 303.14C526.72 292.45 519.29 280.08 517.11 273.57C514.93 267.05 510.75 263.75 510.75 263.75C501.82 273.19 493.3 289.62 493.3 289.62L504.5 295.49L504.49 295.48Z" fill="#AAAE11"/>
<path d="M504.49 295.48C499.84 302.19 484.54 326.83 474.6 369.22C464.66 411.61 462.21 451.03 465.69 503.92C469.17 556.81 479.28 593.49 490.85 632.6C502.42 671.71 513.75 699.54 524.59 737.04C535.43 774.54 544.3 816.94 546.7 846.92L539.2 857.09C539.2 857.09 532.58 793.72 518.67 749.44C504.76 705.16 486.18 653.43 474.83 608.21C463.49 562.99 456.65 521.97 456.16 496.48C455.67 470.99 452.7 425.11 458.53 398.13C464.36 371.15 477.85 324.75 484.79 310.43C491.73 296.11 500.83 283.27 500.83 283.27L504.48 295.48H504.49Z" fill="#AAAE11"/>
<path d="M565.74 323.95C568.64 324.19 568.77 327.85 567.63 330.74C566.48 333.62 554.51 358.42 551.39 410.82C548.28 463.22 559.08 508.09 568.8 532.38C578.51 556.66 590.99 583.66 601.24 605.61C611.49 627.56 616.71 641.47 616.71 641.47L620.21 629.91C620.21 629.91 591.4 570.9 582.16 548.6C572.92 526.3 563.27 493.75 560.06 465.82C556.85 437.89 556.52 404.54 559.09 384.05C561.66 363.56 570.69 337.28 573.21 330.73C575.74 324.18 581.56 315.72 581.56 315.72L565.75 323.94L565.74 323.95Z" fill="#AAAE11"/>
<path d="M482.56 341.48C481.36 345.06 483.72 344.39 489.7 342.3C495.68 340.21 507.93 336.42 521.38 335.82C534.83 335.22 544.59 336.42 551.27 339.11C557.95 341.8 560.37 344.69 561.11 344.93C561.85 345.17 562.37 344.85 562.83 343.46C563.29 342.06 565.01 342.57 563.75 346.23C562.49 349.89 561.23 352.26 561.23 352.26C561.23 352.26 557.15 343.47 539.11 341.64C521.07 339.8 510.95 341.59 497.3 346.29C483.65 350.99 473.18 357.83 471.23 355.77C469.27 353.72 473.63 342.77 473.63 342.77L482.56 341.5V341.48Z" fill="#AAAE11"/>
<path d="M465.44 427.89C465.27 430.17 467.25 431.89 471.85 429.59C476.45 427.29 484.04 423.14 501.3 419.18C518.55 415.22 527.07 415.33 535.12 416.25C543.17 417.17 546.87 418.77 548.25 419.18C549.63 419.59 550.73 419.16 551.08 417.12C551.43 415.08 552.57 415.84 552.85 420.25C553.13 424.66 553.13 426.04 553.13 426.04C553.13 426.04 537.49 418.31 522.89 420.2C508.29 422.1 492.4 425.71 479.95 431.5C467.5 437.29 460.16 444.55 458.86 441.96C457.56 439.37 465.44 427.89 465.44 427.89Z" fill="#AAAE11"/>
<path d="M467.56 525.59C467.87 528.42 469.77 530.74 473.07 529.24C476.37 527.74 490.04 520.72 509.33 517.63C528.62 514.54 543.98 513.88 552.69 515.19C561.4 516.5 562.93 515.49 562.09 512.49C561.26 509.49 566.46 514.12 567.58 518.26C568.7 522.4 568.42 523.04 568.42 523.04C568.42 523.04 547.06 515.82 520.47 521.09C493.88 526.36 482.46 531.62 475.8 535.04C469.13 538.47 465.03 542.91 464.65 540.57C464.28 538.23 467.56 525.58 467.56 525.58V525.59Z" fill="#AAAE11"/>
<path d="M494.03 643.12C495.13 646.63 498.12 648.35 503.78 645.03C509.44 641.72 520.66 634.67 548.43 627.94C576.2 621.21 594.68 620.57 600.34 619.93C606 619.29 607.23 618.75 606.16 616.38C605.09 614.01 608.88 617.58 610.59 620.46C612.3 623.34 610.59 624.09 610.59 624.09C610.59 624.09 576.48 623.59 548.85 632.6C521.22 641.61 503.3 653.15 499.74 655.6C496.19 658.06 492.02 662.08 491.92 659.21C491.81 656.34 494.03 643.11 494.03 643.11V643.12Z" fill="#AAAE11"/>
<path d="M535.37 778.84C536.11 782.15 537.78 785.68 543.01 784.54C548.24 783.4 565.29 778.84 565.29 778.84V784.76L532.14 793.08L535.37 778.84Z" fill="#AAAE11"/>
<path d="M607.45 942.59C608.4 943.02 628.41 937.5 655.09 933.68C681.77 929.87 706.13 930.06 706.13 930.06L707.6 936.34C707.6 936.34 682.95 935.91 646.92 942.62C610.89 949.32 597.1 954.2 597.1 954.2L607.44 942.6L607.45 942.59Z" fill="#AAAE11"/>
<path d="M645.63 883.13C645.63 883.13 641.5 918.53 616.56 959C591.62 999.46 548.32 1027.1 548.32 1027.1L526.23 1031.87L536.75 1000.55L554.88 986.23L550.49 999.24C550.49 999.24 581.75 976.49 595.99 956.69C610.23 936.89 620.49 911.84 623.54 898.91C626.59 885.98 625.28 878.98 625.28 878.98L645.62 883.13H645.63Z" fill="#AAAE11"/>
<path d="M632.68 893.94C633.29 893.94 702.89 901.54 702.89 901.54L699.73 886.57L633.51 880.67L632.68 893.95V893.94Z" fill="#AAAE11"/>
<path d="M691.94 896.22C691.94 896.22 699.61 922.27 700.25 955.43C700.89 988.59 694.17 1036.54 694.17 1036.54L701.57 1029.14C701.57 1029.14 708.52 991.24 707.37 957.73C706.22 924.22 701.57 892.49 701.57 892.49L691.95 896.21L691.94 896.22Z" fill="#AAAE11"/>
<path d="M591.78 990.55C585.5 996.88 587.84 1006.88 609.74 1005.51C631.64 1004.14 675.17 996.31 685.84 993.52C696.52 990.73 699.58 986.59 699.92 975.38C700.26 964.17 702.78 976.49 701.57 999.22C700.35 1021.94 694.17 1036.54 694.17 1036.54L524.59 1036.62L543.76 1013.99L591.78 990.54V990.55Z" fill="#AAAE11"/>
<path d="M597.34 678.8C600.23 675.95 601.21 683.05 598.56 707.26C595.91 731.47 588.26 795.38 588.26 795.38L613.27 662.37C613.27 662.37 599.21 670.44 598.05 673.08C596.89 675.72 597.34 678.81 597.34 678.81V678.8Z" fill="#30923A"/>
<path d="M564.41 654.58C563.91 657.61 564.03 661.02 566.69 661.66C569.35 662.3 574.54 664.95 577.08 673.69C579.61 682.43 579.36 698.64 578.85 701.56C578.34 704.48 587.62 690.27 587.36 682.32C587.1 674.37 589.44 668.63 582.78 662.37C576.12 656.11 569.56 655.64 568.09 654.58C566.62 653.52 565.3 654.54 566.66 651.34C568.02 648.13 568.94 646.25 568.94 646.25L564.41 654.58Z" fill="#30923A"/>
<path d="M363.28 579.33C363.28 579.33 359.9 631.33 355.28 692.16C350.66 752.99 328.13 824.45 295.76 873.44C263.39 922.43 237.94 946.48 209.97 984.01C182 1021.54 177.04 1040.66 177.04 1040.66L157.75 1036.63C157.75 1036.63 190.49 984.8 219.53 951.48C248.56 918.16 287.96 872.13 303.67 843.1C319.38 814.07 336.37 759.54 342.39 718.83C348.41 678.11 349.98 634.21 350.72 611.73C351.46 589.26 353.69 575.88 353.69 575.88L363.28 579.34V579.33Z" fill="#30923A"/>
<path d="M668.37 327.92C668.37 327.92 663.5 322.63 664.18 316.23C664.86 309.84 668.35 305.82 675.45 303.63C682.54 301.44 690.49 301.59 700.2 308.11C700.2 308.11 696.87 300.6 683.24 298.05C669.61 295.5 661.93 301.17 660.05 305.86C658.17 310.54 657.87 311.64 658.87 316.24C659.87 320.84 660.6 322.03 661.73 323.13C662.86 324.23 668.37 327.92 668.37 327.92V327.92Z" fill="#E4FFA4"/>
<path d="M743.72 294.21C740.61 290.71 734.24 290.42 729.09 293.42C723.24 296.83 719.52 304.3 718.33 306.99C718.33 306.99 724.11 301.75 728.03 298.38C731.95 295.01 733.8 294.15 736.61 293.49C740.81 292.5 743.71 294.21 743.71 294.21H743.72Z" fill="#E4FFA4"/>
<path d="M672.18 335.03C674.81 337.7 680.02 340.02 685.83 339.82C691.64 339.62 697.45 336.1 697.45 336.1C697.45 336.1 692.08 343.76 685.51 343.76C675.03 343.76 672.18 335.03 672.18 335.03Z" fill="#E4FFA4"/>
<path d="M522.9 233.47C522.9 233.47 528.73 241.19 540.99 250C553.25 258.81 575.53 265.91 591.15 268.36C591.15 268.36 580.34 271.89 562.86 271.34C545.38 270.79 536.1 267.55 536.1 267.55C536.1 267.55 543.29 266.45 546.1 265.73C548.91 265.01 546.62 261.83 543.46 260.29C540.3 258.75 532.15 249.88 529.25 245.62C526.35 241.36 522.91 233.47 522.91 233.47H522.9Z" fill="#E4FFA4"/>
<path d="M650.94 276.3C650.94 276.3 655.95 286.67 658.3 292.23C660.65 297.79 660.78 300.46 659.4 303.03C658.02 305.6 665.29 301.09 665.75 298.34C666.21 295.59 656.82 284.31 656.09 283.67C655.36 283.03 650.94 276.3 650.94 276.3V276.3Z" fill="#E4FFA4"/>
<path d="M608.56 203.46C600.04 198.45 585.83 196.11 574.85 197.11C574.85 197.11 577.71 195.18 582.01 193.72C586.31 192.26 587.48 188.69 586.17 185.91C584.86 183.14 579.6 176.57 573.62 172.12C573.62 172.12 582.09 176.39 587.26 180.58C597.95 189.24 608.56 203.45 608.56 203.45V203.46Z" fill="#E4FFA4"/>
<path d="M381.62 55.95C381.62 55.95 395.62 73.93 428.75 98.1C461.88 122.27 487.95 133.73 507.21 139.15C526.47 144.57 535 143.23 538.24 151.44C541.48 159.66 542.83 147.48 531.89 140.38C520.96 133.28 469.58 113.73 448.75 102.9C427.92 92.07 398.96 69.56 391.87 63.1C384.79 56.64 381.62 55.96 381.62 55.96V55.95Z" fill="#E4FFA4"/>
<path d="M403.21 436.53C405.98 415.71 415.42 376.47 427.56 350.24C439.69 324.01 451.41 311.46 451.41 311.46C451.41 311.46 435.35 341.91 424.28 377.45C413.2 412.99 404.12 457.7 402.54 478.6C402.54 478.6 400.96 453.49 403.22 436.52L403.21 436.53Z" fill="#E4FFA4"/>
<path d="M380.03 704.74C380.03 704.74 398.01 718.42 407.49 734.48C416.97 750.54 415.2 753.12 413.7 754.48C412.2 755.84 410.16 753.39 402.81 753.66C395.46 753.93 391.52 757.33 389.47 763.73C389.47 763.73 392.02 756.83 399.53 758.42C407.05 760 411.78 759.11 420.63 769.58C429.47 780.06 436.43 791.9 436.43 791.9C436.43 791.9 432.63 762.5 419.89 743.66C407.16 724.82 396.36 716.68 392.94 714.04C389.52 711.4 380.01 704.74 380.01 704.74H380.03Z" fill="#E4FFA4"/>
<path d="M388.99 770.33C390.01 777.81 394.66 788.58 398.96 796.51C403.26 804.44 404.63 808.64 403.61 810.11C402.59 811.58 399.53 810.59 396.69 809.39C393.85 808.19 386.6 807.75 382.57 809.66C382.57 809.66 389.46 806.94 399.08 814.74C408.69 822.53 417.86 834.42 418.02 834.37C418.18 834.32 417.44 816.31 405.44 797.95C393.44 779.6 388.99 770.33 388.99 770.33Z" fill="#E4FFA4"/>
<path d="M459.24 906.97C459.24 906.97 494.13 909.53 523.86 889.62C553.59 869.71 563.4 848.01 569.81 822.35C576.7 794.77 570.37 763.62 563.1 739.16C555.82 714.7 568.65 764.75 565.28 795.38C561.92 826.01 550.81 853.47 532.13 869.16C513.45 884.85 502.1 895.41 489.1 898.66C476.09 901.91 459.24 906.97 459.24 906.97Z" fill="#E4FFA4"/>
<path d="M567.24 645.6C567.24 645.6 558.62 660.9 557.39 679.03C556.16 697.17 557.25 718.37 561.85 734.82C566.44 751.27 553.82 720.79 551.63 708.85C549.44 696.9 547.26 684.05 551.68 669.92C556.1 655.79 563.49 645.6 563.49 645.6H567.25H567.24Z" fill="#E4FFA4"/>
<path d="M773.86 913.54C771.37 910.13 768.96 905.22 768.59 897.5C768.22 889.78 769.93 884.21 772.1 882.04C773.86 880.28 774.41 879.99 774.41 879.99C774.41 879.99 774.41 883.19 774.78 886.91C774.78 886.91 773.67 890.35 772.93 896.22C772.19 902.09 773.85 913.55 773.85 913.55L773.86 913.54Z" fill="#E4FFA4"/>
<path d="M742.94 858.55C742.94 858.55 739.29 869.25 739.06 883.15C738.83 897.06 741 907.58 741 907.58C741 907.58 732.3 897.73 732.79 884.03C733.28 870.33 736.52 863.55 739.73 861.05L742.94 858.55Z" fill="#E4FFA4"/>
<path d="M34.51 544.94C43 520.25 61.42 470.18 88.97 421.11C116.52 372.04 144.41 338 173.74 303.83C203.07 269.65 229.04 251.99 240.02 245.24C251 238.49 257.88 240.88 261.28 243.74C264.68 246.6 268.77 249.97 270.76 264.92C272.75 279.87 274 252.27 270.09 241.67C266.18 231.07 256.09 228.62 247.18 232.28C238.27 235.94 207.57 258.27 187.39 280.39C167.21 302.5 136.64 340.87 116.69 368.98C96.74 397.08 84.5 418.88 73.62 441.11C62.74 463.34 57.38 484.41 48.93 500.45C40.47 516.49 34.51 544.94 34.51 544.94Z" fill="#E4FFA4"/>
<path d="M187.24 370.01C171.98 383.96 140.56 428.52 130.24 454.92C119.92 481.32 111.84 514.59 111.84 514.59C111.84 514.59 124.19 483.37 133.69 463.57C143.19 443.77 154.69 418.83 162.45 407.63C170.21 396.43 180.24 382.38 180.24 382.38L187.24 370.01Z" fill="#E4FFA4"/>
<path d="M194.05 474.61C194.05 474.61 196.53 439.2 200.76 415.26C204.99 391.32 218.11 348.76 228.38 332.15C228.38 332.15 224.11 355.48 218.52 370.95C212.94 386.42 201.6 432.94 201.6 432.94L194.04 474.61H194.05Z" fill="#E4FFA4"/>
<path d="M325.97 926.16C325.34 933.67 323.98 945.23 322.17 949.77C320.36 954.31 317.29 951.47 314.91 949.09C312.53 946.71 303.69 944.78 298.47 953.4C293.25 962.02 297.16 961.56 297.16 961.56C297.16 961.56 301.53 949.66 306.18 950.79C310.83 951.92 313.21 955.12 316.5 963.44C319.79 971.76 320.43 976.79 320.43 976.79C320.43 976.79 325.73 966.46 327.01 955.67C328.29 944.89 327.01 940.85 327.01 937.18C327.01 933.51 325.97 926.15 325.97 926.15V926.16Z" fill="#E4FFA4"/>
<path d="M425.18 614.18C436.28 622.92 445.31 633.47 452.26 647.12C459.21 660.77 460.07 675.46 460.07 675.46C460.07 675.46 454.8 661.27 450.51 655.11C446.22 648.95 444.43 650.5 443.59 651.8C442.75 653.1 444.24 654.99 445.42 657.47C446.6 659.95 449.79 664.9 453.11 675.01C456.44 685.12 456.7 693.27 456.7 693.27C456.7 693.27 450.84 674.04 442.01 662.22C433.18 650.4 420.91 639.56 411.76 634.78C411.76 634.78 417.69 635.61 425.18 638.73C432.67 641.85 436.1 644.5 438.39 643.95C440.68 643.41 439.26 638.65 437.04 633.53C434.02 626.56 425.19 614.17 425.19 614.17L425.18 614.18Z" fill="#E4FFA4"/>
<path d="M388.34 956.57C413.9 960.1 446.2 957.69 467.03 952.37C489.39 946.65 504.8 933.78 504.8 933.78C504.8 933.78 493.12 938.23 477.91 939.71C462.71 941.19 453.62 939.71 453.62 939.71C453.62 939.71 469.38 937.67 484.4 931.92C499.42 926.17 514.55 913.57 514.55 913.57C514.55 913.57 504.32 918.54 476.84 924.39C449.36 930.24 418.08 926.14 404.05 922.64C404.05 922.64 414.25 928.11 423.31 931.52C432.37 934.94 437.46 937.07 437.6 940.09C437.83 945 430.05 948.64 416.26 951.68C403.38 954.52 388.34 956.57 388.34 956.57V956.57Z" fill="#E4FFA4"/>
<path d="M391.97 564.33C358.09 550.8 337.88 506.45 322.9 475.16C299.15 422.8 285.14 366.6 277.07 309.82C273.99 289.78 271.38 269.67 268.78 249.62C268.43 246.64 267.71 243.25 266.13 240.76C261.75 233.47 252.1 233.67 244.84 236.82C240.66 238.52 237.09 241.11 233.42 243.83C158.8 302.24 102.08 382.29 63.7801 468.54C36.9901 529.31 16.93 593.48 8.01004 659.39C6.81004 668.81 5.85002 678.25 5.21002 687.72C5.21002 687.72 1.68003 687.13 1.69003 687.13C9.35003 660.21 18.7601 633.85 30.3401 608.37C39.0801 589.3 48.7101 570.17 62.1501 553.8C66.8401 548.33 71.6 542.93 77.39 538.56C83.83 533.61 93.34 531.69 100.1 537.5C105.19 541.81 107.45 547.83 108.46 554.06C110.33 567.82 111.76 585.76 113.16 599.69C115.56 627.36 119.44 654.92 125.87 681.95L121.69 682.24C122.82 670.08 124.24 658.01 125.94 645.94C131.16 609.68 138.43 573.3 154.55 540.07C168.83 512.2 193.4 507.63 197.95 543.89C201.42 571.08 203.17 598.44 205.29 625.7L201.76 625.54C203.63 614.11 205.89 602.8 208.51 591.53C212.52 574.68 217.25 557.68 224.99 542.07C228.88 534.22 233.1 526.18 239.42 519.85C246.18 512.81 255.84 510.73 265.24 512.79C271.29 514.1 276.48 517.55 281.24 521.21C294.81 532 306.79 544.38 320.55 554.87C334.21 565.11 350.01 572.3 365.87 578.56C371.21 580.63 376.57 582.63 382.09 584.37C376.29 583.96 370.61 582.6 365.04 580.99C348.34 575.94 332.56 568 318.29 557.97C304.34 547.79 291.95 535.46 278.57 524.67C271.9 519.28 264.81 515.74 256.34 516.39C247.98 516.72 241.79 522.41 237.11 529.19C224.35 548.14 217.71 570.4 212.31 592.44C208.43 608.11 205.28 625.21 202.8 641.21C200.8 612.04 198.68 582.47 195.1 553.49C194.56 549.34 193.82 544.25 192.95 540.19C191.22 532.08 186.32 518.96 175.86 523.79C167.99 527.3 162.96 534.78 158.91 542.37C153.42 552.97 149.48 564.46 145.79 575.85C138.23 598.84 133.84 622.66 130.32 646.58C127.88 662.77 126.17 679.84 124.55 696.15C119.16 671.33 114.13 646.12 111.43 620.9C108.89 599.36 107.46 577.58 103.53 556.25C103.14 553.88 102.48 551.45 101.58 549.26C97.59 539.33 89.39 536.75 80.7 543.21C75.4 547.25 70.7701 552.34 66.3401 557.35C52.9801 573.15 43.23 591.55 34.37 610.25C20.23 640.47 9.07004 672.49 0.540039 704.75L1.62003 687.52C3.41003 658.87 8.01003 630.47 14.34 602.49C44.5 472.47 107.67 347.46 207.1 256.79C214.23 250.35 221.59 244.17 229.34 238.36C233.3 235.4 237.53 232.37 242.22 230.48C252.25 226.11 265.85 226.57 272.02 237.22C274.86 241.78 275.33 247.15 275.96 252.29C277.14 261.77 278.17 271.23 279.28 280.69C286.1 346.78 299.12 412.7 326.55 473.53C338.26 499.3 351.2 525.02 370.54 545.92C376.96 552.83 383.87 559.37 391.97 564.33V564.33Z" fill="#0E1016"/>
<path d="M339.66 524.44C326.32 504.71 315.55 483.01 305.86 461.26C280.66 403.67 264.92 342.46 253.68 280.76C251.74 274.35 249.14 264.77 241.09 265.55C229.81 266.62 220.11 274.06 211.55 281.16C166.59 320 130.66 368.53 99.9199 419.13C87.6899 439.56 76.32 460.53 65.7 481.91C84.46 437.97 107.73 395.75 136.08 357.22C157.61 328.45 181.22 301.04 208.33 277.33C214.76 272.03 221.36 266.43 228.85 262.44C246.84 253.07 256.79 260.23 259.69 279.46C262.21 294.1 264.64 309.04 267.62 323.52C274.79 358.47 284.81 392.83 296.38 426.56C308.29 460.18 322.16 493.32 339.67 524.44H339.66Z" fill="#0E1016"/>
<path d="M224.69 474.61C220.42 443.69 220.09 412.36 221.11 381.2C221.43 370.79 221.94 360.43 222.8 349.99L226.51 350.77C216.52 381.18 207.28 411.89 200.49 443.19C198.21 453.62 196 464.08 194.06 474.6C195.3 463.97 196.96 453.39 198.91 442.87C205.78 405.39 216.74 368.41 228.39 332.14C223.41 379.34 221.88 427.21 224.7 474.6L224.69 474.61Z" fill="#0E1016"/>
<path d="M139.12 500.39C143.78 479.77 149.73 459.54 155.82 439.29C159.01 429.19 162.76 419.3 166.71 409.48C170.65 399.66 174.91 390 179.34 380.38L181.68 381.83C151.37 421.92 131.04 468.43 111.84 514.59C115.75 502.64 120.43 490.95 125.31 479.37C139.97 444.58 156.63 410.33 179.51 380.12L187.25 370.02L181.86 381.57C173.11 400.68 164.85 420.13 158.07 440.03C151.51 460.05 145.86 480.41 139.13 500.4L139.12 500.39Z" fill="#0E1016"/>
<path d="M640.16 251.83C645.12 249.62 650.99 248.55 656.27 247.37C657.61 247.05 660.17 246.6 661.34 246.11C662.68 245.55 663.82 244.53 664.62 243.19C667.14 238.68 666.57 233.17 665.76 228.1C665.11 224.57 664.23 220.95 663.28 217.46C658.54 199.8 649.45 183.66 638.3 169.23C624.97 151.76 608.65 136.51 590.69 123.91L593.11 121.61C599.81 134.2 605.65 147.11 610.24 160.61C611.77 165.12 613.14 169.7 614.45 174.37C614.86 175.84 614.21 175.09 612.74 175.51C611.08 176 611.23 175.8 611.12 174.08C611.01 172.91 608.78 172.98 608.61 171.82C606.83 159.23 602.01 147.15 596.65 135.67C593.78 129.44 589.98 123.01 586.45 117.15C604.43 128.25 620.78 141.98 634.65 157.89C651.99 177.94 665.74 200.87 669.47 227.52C670.49 234.91 670.8 245.68 662.59 249.25C660.55 249.97 658.86 250.14 656.86 250.52C651.31 251.39 645.83 252.16 640.15 251.83H640.16Z" fill="#0E1016"/>
<path d="M539.99 148.37C557.57 141.03 588.78 140.7 605.8 149.57C606.18 149.75 606.01 150 605.88 150.59C605.68 151.42 604.96 152.54 604.36 152.97C604.24 153.03 604.19 152.94 604.11 152.92C603.3 152.57 601.74 151.95 600.9 151.6C587.24 145.84 571.66 145.2 557.18 147.82C551.99 148.83 546.71 150.38 542.03 152.74C539.09 154.14 536.99 149.76 539.99 148.37V148.37Z" fill="#0E1016"/>
<path d="M566.15 155.12C589.73 156.54 616.43 167.04 626.15 190.17C622.1 185.66 618.24 181.27 613.64 177.63C611.6 176.02 608.68 173.94 606.59 172.48C594.43 164.02 580.51 158.77 566.16 155.12H566.15Z" fill="#0E1016"/>
<path d="M515.04 130.01C526.18 129.04 537.44 130.14 548.43 132.11C559.34 134.13 570.17 137.8 579.9 143.2C582.64 144.71 581.7 146.45 578.86 145.06C578.84 145.05 577.45 145.83 577.43 145.82L576.76 145.23L574 145.35C561.02 138.26 546.35 134.66 531.7 133.1C526.44 132.56 521.13 132.31 515.85 132.45L514.98 132.22C513.84 131.95 513.88 130.22 515.04 130.01Z" fill="#0E1016"/>
<path d="M623.55 147.6C644.23 160.3 663.2 176.56 676.62 196.97C680.57 203.28 684.87 210.34 684.53 218.26C684.25 223.93 679.79 228.03 675.54 231.14C673.5 232.65 671.43 233.97 669.11 235.17C668.17 235.65 667.02 235.29 666.53 234.35C666.04 233.41 666.42 232.27 667.33 231.78C671.21 229.48 675.18 226.52 678.14 223.33C687.98 212.61 663.62 184.57 655.77 176.46C646.08 166.6 635.28 157.81 623.77 150.13L623.01 149.63L622.63 149.38C622.45 149.26 622.23 149.1 622.05 148.95C621.36 148.41 621.95 147.2 622.8 147.44L623.54 147.61L623.55 147.6Z" fill="#0E1016"/>
<path d="M543.45 219.94C556.98 210.44 568.7 211.97 583.25 218.05C591.54 221.55 599.43 225.78 607.57 229.44C612.87 231.7 618.29 234.12 624.06 234.37C631.21 234.61 635.57 229.36 636.19 221.99C640.09 233.78 629.44 241.17 618.65 238.8C609.6 236.97 601.43 232.51 593.1 228.86C584.12 224.91 575.33 220.21 565.79 218.05C558.24 216.28 550.98 217.97 543.45 219.94V219.94Z" fill="#0E1016"/>
<path d="M590.59 225.6C581.19 225.14 571.67 227.34 562.96 230.7L564.36 227.36C569.67 238.95 579.57 249.07 590.77 255.04C593.65 256.52 596.71 257.56 599.85 258.17C609.18 260.5 623.21 254.82 630 263.33C630.05 263.35 626.34 264.57 626.35 264.58C625.87 254.66 622.91 244.32 616.5 236.54C616.12 236.11 616.17 235.45 616.62 235.09C617.03 234.76 617.63 234.81 617.98 235.2C619.79 237.18 621.5 239.27 622.87 241.58C627.08 248.53 629.49 256.31 630.37 264.41L631.04 271.35C631.04 271.35 626.71 265.65 626.7 265.65C625.37 263.45 622.55 262.38 619.42 262.01C612.88 261.26 605.92 262.49 599.3 261.05C585.66 258.13 574.75 248.32 565.94 238.04C563.1 234.64 560.57 230.9 558.55 226.91C561.83 225.87 565.34 224.94 568.63 224.36C575.93 223.02 583.46 222.47 590.82 223.62C592.08 223.82 591.85 225.71 590.59 225.61V225.6Z" fill="#0E1016"/>
<path d="M596.15 258.76C596.15 258.76 597.41 253.82 600.82 248.85C604.23 243.88 609.94 240.66 609.94 240.66C609.94 240.66 610.23 247.82 609.21 252.94C608.19 258.06 607.16 260.29 607.16 260.29C607.16 260.29 603.28 260.3 600.82 259.82C598.36 259.34 596.15 258.76 596.15 258.76V258.76Z" fill="#0E1016"/>
<path d="M718.72 296.83C700.05 281.07 680.8 264.11 662.53 247.85C661.61 247.03 662.8 245.53 663.81 246.31C677.96 257.3 693.7 269.78 707.67 280.99C712.5 284.89 717.32 288.81 722.11 292.76C723.55 294 721.68 293.86 720.65 294.84C719.79 295.66 718.71 296.83 718.71 296.83H718.72Z" fill="#0E1016"/>
<path d="M695.01 360.47C695.01 360.47 697.57 360.62 700.74 362.74C703.91 364.86 711.47 364.74 714.72 360.72C719.65 354.63 717.24 349.04 724.64 346.37" stroke="#0E1016" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round"/>
<path d="M697.72 361.16C697.72 361.16 704.39 365.86 709.78 363.66C715.17 361.46 716.98 357.71 717.69 354.64C718.4 351.57 718.71 350.13 719.43 349.38C720.15 348.63 725.46 346.95 723.22 345.59C719.69 343.45 716.14 338.12 716.14 338.12C716.14 338.12 712.93 347.58 707.65 352.31C702.37 357.04 697.46 359.67 697.46 359.67L697.72 361.16Z" fill="#0E1016"/>
<path d="M731.18 319.57C733.29 315.24 735.97 309.19 739.07 306.6C742.17 304.02 744.35 304.85 745.13 306.6C746 308.58 744.57 310.3 741.95 311.47C739.8 312.43 738.27 313.52 736.27 315.01C734.55 316.29 731.18 319.56 731.18 319.56V319.57Z" fill="#0E1016"/>
<path d="M691.49 322.64C689.09 319.24 684.72 314.79 679.85 312.61C674.98 310.43 672.41 312.79 672.19 315.3C672.01 317.32 674.75 319.55 678.48 319.37C682.21 319.19 685.57 320.59 688 321.42C690.43 322.25 691.49 322.64 691.49 322.64V322.64Z" fill="#0E1016"/>
<path d="M668.37 327.92C653.27 322.99 652.79 303.72 666.54 296.68C678.34 291.11 694.47 296.6 700.2 308.12C692.68 299.62 678.9 296.5 668.92 301.44C662.92 304.24 659.08 311.07 660.68 317.68C661.73 321.95 664.76 325.28 668.37 327.93V327.92Z" fill="#0E1016"/>
<path d="M666.99 300.14L658.84 288.31C656.5 284.81 653.07 279.88 650.95 276.3C652.52 278.1 654.18 279.82 655.83 281.54C658.97 284.81 667.62 293.5 670.94 296.83C672.34 298.32 671.27 298.95 670.03 299.74C669.01 300.39 668.01 301.49 667 300.14H666.99Z" fill="#0E1016"/>
<path d="M522.9 233.47C527.55 241.95 533.98 249.28 541.12 255.7C545.1 259.24 549.54 262.5 553.67 265.95C546.18 266.95 542.36 268.37 531.08 267.14L533.46 264.05C533.79 264.54 534.13 265.15 534.54 265.65C541.08 274.14 550.88 279.47 560.98 282.7C569.24 285.22 577.96 286.8 586.61 287.24C594.12 287.71 601.51 286.86 609.09 287.77C613.87 289.09 617.45 292.64 621.14 295.59C627.93 301.48 634.29 307.83 640.57 314.23C644.91 318.69 649.23 323.15 653.45 327.74C653.45 329.73 653.72 331.89 654.14 333.88C656.7 347.6 667.98 356.84 681.4 359.16C681.79 367.38 681.1 375.6 678.73 383.49L676.55 382.25C677.97 380.73 679.35 379.09 680.64 377.44C684.64 372.52 687.04 366.65 689.19 360.74C689.19 360.74 689.53 359.75 689.54 359.74C703.15 359.41 712.39 346.77 716.24 334.82L717.31 337.15C720.09 343 726.73 346.72 733.15 346.25C734.74 353.12 732.95 360.06 731.49 366.76C731.52 366.86 729.29 365.6 729.28 365.63C735.48 358.7 738.94 349.68 740.34 340.58C746.65 329.2 753.08 312.43 747.92 299.73C745.87 294.69 741.03 291.48 735.5 291.31C728.65 290.9 722.83 294.7 717.1 298.03C720.64 291.42 728.1 287.52 735.57 287.28C749.08 287.12 755.39 299.63 754.34 311.73C753.41 322.6 749.37 332.94 743.6 342.12L743.85 341.46C742.85 346.29 741.45 350.88 739.39 355.35C736.79 361.13 732.56 366.07 728.14 370.58L728.94 366.23C729.88 360.4 731.18 354.28 729.95 348.44L731.71 349.79H731.67C726.15 349.8 721.02 346.84 717.81 342.5C716.76 341.05 715.94 339.56 715.39 337.76H717.29C713.27 349.71 704.16 361.58 690.76 362.86L692.18 361.8C689.8 367.93 687.05 374.1 682.75 379.13C680.6 381.89 677.56 384.81 675.13 387.36C675.87 384.25 676.78 380.45 677.24 377.33C678.14 371.9 678.07 366.28 677.65 360.82L679.18 362.54C671.14 360.94 663.65 357.14 658.07 351.04C652.52 344.89 649.53 336.75 649.32 328.51L649.85 329.91C639.9 319 629.7 308.21 618.51 298.59C615.31 296.08 612.2 293.03 608.38 291.82C604.16 291.35 599.79 292.02 595.42 292.22C586.36 292.69 577.12 291.69 568.26 289.74C553.38 286.48 537.84 279.49 529.6 266.03L527.36 261.9L531.99 262.94C537.9 264.28 544.28 263.94 550.33 263.71L549.82 266.2C546.39 264.83 543.27 262.81 540.42 260.49C534.8 255.81 530.31 249.88 526.8 243.52C525.12 240.32 523.6 236.98 522.86 233.45L522.9 233.47Z" fill="#0E1016"/>
<path d="M680.73 378.53C688.47 380.17 696.45 379.18 704.03 377.17C711.43 375.35 719.09 372.4 723.2 365.62C725.92 361.19 728.32 356.19 730.98 351.64C731.31 351.02 732.09 350.79 732.71 351.13C733.29 351.45 733.51 352.17 733.23 352.76C732.09 355.21 730.93 357.65 729.74 360.07C728.62 362.29 727.28 365.28 725.92 367.35C719.97 377.42 707.4 380.14 696.73 381.92C691.23 382.7 685.56 382.47 680.15 381.2C679.41 381.03 678.96 380.29 679.13 379.56C679.3 378.84 680.01 378.39 680.73 378.53V378.53Z" fill="#0E1016"/>
<path d="M504.49 248.3C513.12 260.4 520.21 273.87 527.3 286.78C536.33 301.79 550.26 310.86 568.07 309.39C577.76 309.07 587.85 307.62 597.51 306.33C607.28 304.73 612.59 306.32 619.76 313.28C633.81 327.68 644.56 344.74 656.15 361.01C661.78 368.78 669.45 375.21 678.76 377.86C680.56 378.36 680.05 381.31 677.94 380.84C661.2 376.31 653.7 364.28 644.42 351.08C638.66 343.02 632.74 335.07 626.44 327.43C621.16 321.44 613.54 310.88 605.15 310.57C601.3 310.47 594.79 311.75 590.78 312.27C583.35 313.36 575.82 314.35 568.28 314.72C560.48 315.31 552.28 314.36 545.18 310.87C535.61 306.4 528.11 298.32 523.04 289.26C521.18 285.96 519.69 282.39 518.08 279.05C513.45 268.87 508.75 258.72 504.48 248.31L504.49 248.3Z" fill="#0E1016"/>
<path d="M693.26 368.72C696.91 369.54 700.47 370.16 704.13 370.36C707.79 370.66 711.15 370.85 714.86 370.01C709.1 376 698.94 373.68 693.26 368.72V368.72Z" fill="#0E1016"/>
<path d="M551.95 187.15C546.13 173.19 541.66 158.74 535.51 145.06C535.16 144.32 534.57 143.27 534.12 142.68C534.14 142.68 533.98 142.56 533.86 142.46C533.76 142.36 533.51 142.23 533.35 142.11C527.4 138.87 520.26 136.81 513.75 134.38C509.18 132.75 504.6 131.09 500.03 129.36C481.71 122.57 464.07 113.62 447.2 103.8C421.88 89.04 397.44 72.34 376.9 51.21L380.22 49.6C381.07 54.73 382.78 60.11 384.51 65.06C395.87 95.63 416.53 122.15 441.03 143.42C442.63 144.72 446.94 148.24 448.6 149.59C450.32 150.85 454.79 154.16 456.44 155.4C458.26 156.72 462.61 159.6 464.5 160.91C482.85 172.74 502.45 182.39 522.9 190.16C514.1 189.02 505.53 186.52 497.19 183.49C445.55 164.26 398.93 118.62 380.46 66.47C378.75 61.11 377.16 55.8 376.34 50.09L375.49 44.3C390.26 59.55 407.19 72.6001 425.03 84.1701C449.39 99.8301 475.02 113.43 502.37 123.06C511.44 126.44 520.58 129.47 529.77 133.26C532.35 134.4 535.24 135.57 537.79 137.52C539.87 139.15 540.82 141.89 541.83 144.23C546.96 158.15 550.06 172.49 551.95 187.14V187.15Z" fill="#0E1016"/>
<path d="M448.75 130.95C460.21 141.49 473.47 149.97 487.52 156.54C497.11 161.16 506.69 165.5 517.17 167.97C515.5 168.14 513.82 168.1 512.14 168C487.47 166.09 463.95 149.84 448.74 130.95H448.75Z" fill="#0E1016"/>
<path d="M641.11 161.28C641.65 155.48 642.37 149.75 644.03 144.13C646.65 136 648.09 128.37 640.05 123.19C618.88 108.96 599.25 92.19 583.29 72.21C578.61 66.13 573.47 58.55 569.3 52.12C560.47 38.43 552.21 24.44 544.56 10.02L548.22 9.02C549.69 33.74 555.65 58.27 564.35 81.38C568.54 92.98 573.6 104.39 579.13 115.47C584.67 126.39 590.59 137.37 598.39 146.83C599.12 147.57 599.09 148.78 598.3 149.46C597.58 150.1 596.48 150.05 595.82 149.37C594.6 148.12 593.59 146.95 592.57 145.7C589.56 142 586.86 138.07 584.27 134.08C577.51 123.41 571.77 112.17 566.64 100.66C564.16 94.98 561.53 88.98 559.51 83.13C552.37 62.23 548.26 40.5 545.37 18.67C544.56 13.17 544.1 5.6 543.54 0L548.01 8.24C559.28 29.56 571.63 50.67 586.14 69.98C601.12 88.88 619.84 104.72 639.67 118.32C641.17 119.31 643.51 120.77 644.84 122.04C651.93 128.12 650.45 137.38 648.01 145.31C647.34 147.62 646.99 150.04 646.69 152.47C646.22 156.39 646.04 160.45 646.1 164.37C646.03 167.95 641.02 162.25 641.11 161.27V161.28Z" fill="#0E1016"/>
<path d="M586.56 312.01C567.05 338.8 561.04 378.45 559.94 411C558.38 460.61 566.29 508.56 587.13 553.81C598.13 578.29 610.32 602.42 620.94 627.14C621.42 628.25 618.44 633.65 617.91 632.41C612.39 619.5 606.66 606.67 600.75 593.94C582.29 555.97 564.6 516.66 559.16 474.35C556.09 453.32 555.18 432.09 555.63 410.87C556.26 389.61 558.83 368.35 564.91 347.88C568.49 335.93 573.16 324.15 579.92 313.59C580.97 312.02 581.93 310.57 583.18 309.07C583.97 308.12 585.38 307.99 586.33 308.78C587.31 309.59 587.41 311.06 586.56 312V312.01Z" fill="#0E1016"/>
<path d="M514.11 266.45C485.87 308.03 468.64 356.86 461.51 406.48C459.76 419.24 458.73 432.11 458.49 444.99C457.69 483.8 459.86 522.76 466.61 561C473.26 599.33 484.58 636.65 497.13 673.43C519.52 734.28 536.42 798.59 542.35 863.24C542.35 863.24 541.3 866.42 539.62 866.58C537.94 866.75 536.44 865.53 536.28 863.85C534.18 838.03 530.32 812.35 525.11 786.96C512.8 722.96 485.08 663.45 469.55 600.25C459.88 562.26 454.54 523.19 453.01 484.05C449.74 423.11 456.99 367.39 481.52 311.09C489.46 292.95 498.49 275.31 509.93 259.07C509.93 259.07 514.76 265.52 514.12 266.45H514.11Z" fill="#0E1016"/>
<path d="M459.04 904.98C482.04 900.38 505.07 891.22 522.31 874.99C545.17 852 562.25 818.34 563.59 785.63C564 768.64 560.5 751.84 555.68 735.58C553.79 728.9 551.27 722.39 549.66 715.61C546.07 699.71 544.47 678.15 550.75 662.82C555.25 653.12 561.57 643.58 571.39 638.66L576.35 636.19C572.85 641.66 568.83 647.43 567.8 653.92L566.11 652.06C567.4 651.94 568.45 652 569.6 652.13C587.72 654.95 591.79 673.13 588.57 688.61C588.33 689.74 583.16 693.52 583.73 690.93C585.67 681.73 586.36 670.89 580.58 663C577.27 658.55 571.9 655.52 566.36 655.24L564.43 654.59L564.66 653.38C565.49 648.19 568.2 643.62 570.83 639.21L572.85 641.28C570.03 643.01 567.53 645.16 565.32 647.6C560.97 652.55 557.72 658.62 555.23 664.69C551.91 674.03 551.94 684.34 552.6 694.23C553.31 702.61 554.56 710.96 557.03 718.97C558.59 723.78 560.26 729.01 561.64 733.85C565.53 747.31 568.42 761.12 569.4 775.15C571.77 810.58 559.4 846.95 534.85 872.8C525.29 883.39 513.09 891.62 500 897.08C493.48 899.82 486.79 902.03 479.97 903.67C473.15 905.33 466.28 906.52 459.25 906.97C458.7 907 458.22 906.58 458.19 906.03C458.16 905.5 458.54 905.05 459.05 904.98H459.04Z" fill="#0E1016"/>
<path d="M573.75 729.21C573.63 722.22 574.18 715.26 575.58 708.38C577.12 701.4 579.86 694.91 583.01 688.53C588.63 677.02 597.32 667.47 608.84 661.63C610.44 660.79 612 660.03 613.75 659.32C615.22 658.72 612.67 660.91 613.27 662.37C613.83 663.74 614.19 665.72 613.18 666.35C608.59 669.23 604.08 672.39 600.22 675.95C591.79 683.71 585.5 694.17 580.62 704.61C577.2 712.41 575.38 720.75 573.74 729.21H573.75Z" fill="#0E1016"/>
<path d="M912.2 547.03C825.76 532.1 738.08 525.18 650.65 518.85L644.68 518.42C643.62 518.2 642.81 518.73 642.58 519.53L640.45 531.08C620.73 640.81 597.82 757.12 577.17 866.31L576.61 869.25L576.33 870.72C576.16 871.45 576.52 872.09 577.13 872.43C577.23 872.48 577.33 872.52 577.44 872.55C577.57 872.58 577.59 872.59 577.9 872.62L579.39 872.78C582.99 873.14 605 875.57 609.14 876L704.36 886.2C705.3 886.3 705.98 887.15 705.88 888.09C705.78 889.03 704.94 889.71 704 889.61C680.78 887.19 599.32 878.65 577.5 876.41C574.42 876.25 571.84 873.1 572.56 870.02C572.73 869.1 573.2 866.56 573.38 865.61C594.07 753.38 615.42 633.7 637.22 521.35L637.5 519.88L637.64 519.15C637.91 517.24 639.03 515.44 640.7 514.45C641.8 513.56 643.91 513.42 645.05 513.55C670.24 515.57 697.54 517.7 722.61 520.08C770.33 524.36 818.01 529.39 865.32 537.07C881.1 539.56 896.84 542.18 912.56 545.07C913.1 545.17 913.46 545.69 913.36 546.23C913.26 546.77 912.74 547.13 912.2 547.03V547.03Z" fill="#0E1016"/>
<path d="M953.48 463.55C939.98 430.93 924.04 399.22 912.58 365.76C911.4 362.5 908.33 353.97 907.19 350.81L906.97 350.19C907.13 350.7 907.88 350.82 908.2 350.31C908.23 350.25 908.26 350.21 908.27 350.15C908.27 350.15 908.26 350.19 908.25 350.21C908.12 350.77 907.82 351.88 907.68 352.46L897.28 393.51C897.06 394.48 896.08 395.08 895.12 394.83C894.17 394.59 893.59 393.61 893.84 392.66L904.63 351.64L904.97 350.36L905.14 349.72L905.22 349.4C905.75 347.05 909.14 346.88 909.9 349.15L910.12 349.77C911.22 352.91 914.23 361.51 915.37 364.77C926.52 398.31 942.12 430.04 955.33 462.79C955.54 463.3 955.29 463.88 954.78 464.09C954.27 464.3 953.69 464.05 953.48 463.54V463.55Z" fill="#0E1016"/>
<path d="M949.97 467.69C940.93 450.23 933.4 431.96 926.24 413.64C919.21 395.28 912.05 376.84 907.38 357.72C916.41 385.77 928.48 412.76 940.16 439.78C944.07 448.79 948.11 457.71 951.79 466.87C952.24 468.01 950.55 468.85 949.96 467.69H949.97Z" fill="#0E1016"/>
<path d="M912.51 545.05C924.16 546.1 940.02 547.76 950.06 540.9C954.47 537.77 957.93 533.4 960.51 528.64C964.04 522.2 965.64 515.05 964.16 507.73C963.59 504.09 962.66 500.48 961.61 496.9C961.45 496.37 961.75 495.81 962.29 495.65C962.81 495.5 963.35 495.79 963.52 496.29C964.73 499.88 965.84 503.53 966.57 507.31C969.27 518.93 964.2 531.03 956.18 539.42C950.75 545.29 942.83 547.95 935.01 548.33C927.34 548.9 919.74 548.13 912.26 547.05C911 546.84 911.21 544.95 912.52 545.07L912.51 545.05Z" fill="#0E1016"/>
<path d="M937.06 550.47C928.1 551.42 918.83 551.01 909.99 549.27C909.99 549.28 909.99 549.29 909.98 549.31C859.75 541.95 809.49 534.16 758.87 529.75C727.22 526.95 694.93 524.37 663.21 521.9C660.14 521.66 651.79 521.02 648.86 520.8L646.47 520.62L646.17 520.6C645.27 520.46 644.41 521.15 644.31 522.04C641.77 535.01 639.42 548 638.49 561.21C642.15 548.66 644.44 535.85 646.55 522.98C648.41 523.12 652.07 523.41 653.48 523.52C688.36 526.25 723.87 529.09 758.67 532.2C806.84 536.53 854.65 544 902.48 551.21C906 551.74 908.23 555.25 907.22 558.67C902.16 575.75 897.23 592.87 892.37 610.01C875.69 669.3 858.77 729.41 845.06 789.5C867.02 713.97 889.43 635.43 910.14 559.54C911.19 555.69 914.91 553.27 918.88 553.72C927.32 554.69 935.93 554.34 944.11 552C950.66 550.17 956.63 546.09 960.29 540.33C953.68 545.96 945.76 549.67 937.07 550.49L937.06 550.47Z" fill="#0E1016"/>
<path d="M739.86 922.63C722.13 924.3 705.57 910.66 702.69 893.22C701.71 887.48 702.29 881.44 703.61 875.88C704.47 872.21 705.42 868.38 707.04 864.92C709.24 859.78 713.05 855.12 716.91 851.05C716.59 853.58 715.92 857.75 715.79 860.24C715.44 864.01 715.94 867.71 716.81 871.36L715.3 869.9C716.95 870.3 718.43 870.75 719.97 871.25C724.54 872.77 728.98 874.9 732.84 877.8C734.66 879.12 732.75 881.87 730.87 880.61C729.76 879.82 728.29 878.72 727.09 878.09C722.89 875.73 718.09 874.16 713.34 873.46C711.91 867.7 711.77 861.74 712.87 855.92L715.76 857.3C711.25 862.84 708.68 869.71 707.37 876.71C705.76 883.65 705.57 890.86 707.92 897.59C712.07 910.19 724.28 919.71 737.63 918.94C738.54 918.89 743.46 922.16 740.85 922.49C740.85 922.49 740.17 922.57 739.84 922.6L739.86 922.63Z" fill="#0E1016"/>
<path d="M768.68 922.84C757.24 932.65 738.74 927.91 732.68 914.35C727.79 902.74 727.99 886.66 731.04 874.56C733.6 865.85 738.33 857.79 745.56 852.16L749.3 849.28C747.77 853.3 745.91 857.81 744.77 861.89C744.04 864.59 743.65 867.51 743.41 870.31L741.98 868.76C747.03 869.04 751.56 871.59 755.54 874.5C758.95 877.07 761.85 880.34 764.12 883.96C764.44 884.48 764.31 886.36 763.32 886.92C762.33 887.48 762.02 888.55 761.47 887.56C757.63 880.03 750.53 873.1 741.99 871.63L740.43 871.51L740.56 870.08C740.99 864.06 742.83 858.24 745.14 852.72L747.17 854.27C743.98 856.87 741.4 860.3 739.26 863.9C733.67 873.04 732.4 883.88 732.7 894.39C733.14 904.29 734.23 916.3 743.74 921.73C750.56 925.87 760.31 925.28 765.61 919.5C767.9 917.17 771.2 920.77 768.68 922.86V922.84Z" fill="#0E1016"/>
<path d="M967.59 491.45C967.23 490.53 966.08 490.14 965.2 490.76C964.61 491.18 964.43 491.98 964.69 492.65C967.81 500.63 970.57 508.99 968.87 517.55C967.65 524.38 963.27 536.21 961.27 542.71C927.6 636.55 887.35 802.17 861.38 900.43C860.82 901.31 859.57 902.5 858.65 903.19C849.68 910.02 837.2 910.05 827.13 905.71C824.63 904.74 820.94 901.88 818.91 899.23C818.35 898.5 818.14 897.56 818.37 896.66C818.37 896.66 818.37 896.66 818.37 896.65C822.55 879.6 825.49 862.28 828.51 844.99C827 849.44 825.64 853.93 824.36 858.44C821.16 869.53 818.27 880.72 815.99 892.04C815.64 893.79 813.4 897.84 808.21 897.4C805.83 897.2 803.48 896.76 801.11 896.47C800.41 894.25 799.58 892.06 798.68 889.94C795.72 883.53 791.18 876.36 783.82 874.77C783.34 873.24 779.59 861.17 779.59 861.17L778.03 856.17L776.19 861.1C774.76 865.13 773.65 869.08 772.5 873.14C767.58 875.14 763.45 878.75 761.21 883.65C758.06 891.07 757.51 899.33 758.84 907.21C759.29 909.81 759.91 912.4 760.71 914.92C765.3 928.46 781.34 935.41 794.58 930.87C805.27 927.37 812.48 915.91 817.15 905.56C822.38 909.89 829.17 911.6 835.68 912.89C844.02 914.1 852.83 912.55 859.93 907.86C862.21 906.24 864.16 904.8 865.63 902.19C884.62 831.06 926.57 662.18 948.86 594.49C951.52 586 966.54 538.39 968.74 531.15C974.11 514.94 974.14 507.45 967.59 491.45V491.45ZM804.35 918.72C799.96 924.27 793.61 927.86 786.46 927.67C777.41 927.86 768.43 921.82 765.52 913.28C763.45 906.49 762.41 899.23 763.59 892.2C764.69 884.23 767.99 879.01 775.44 875.64C776.24 872.9 777.05 870.1 777.92 867.34C779.39 871.57 781.14 876.63 781.14 876.63L781.35 877.22C789.67 880.17 793.99 888.62 797.7 896.19C801.11 903.15 801.43 907.12 802.94 911.05C803.34 907.32 802.94 903.59 802.09 899.95C802.26 899.99 802.45 900.03 802.65 900.07C808.78 901.32 811.74 908.35 808.26 913.55C807.05 915.35 805.76 917.08 804.36 918.73L804.35 918.72Z" fill="#0E1016"/>
<path d="M865.83 386.9C866.77 369.54 871.27 352.64 875.87 335.95L877.47 340.53C887.95 370.54 899.4 400.33 913.11 429.03C917.33 437.83 923.17 448.56 927.95 457.15C937.15 473.89 946.81 490.42 956.97 506.6C960.69 511.95 962.08 519.49 958.9 525.31C960.18 520.14 959.33 514.86 956.83 510.39C943.44 489.94 930.74 468.98 919.01 447.52C908.79 428.81 900.01 409.27 892.12 389.51C885.8 373.67 879.84 357.72 874.59 341.48C874.59 341.48 877.5 341.42 877.49 341.43C874.32 352.62 871.52 364 869.7 375.49C869.12 379.32 868.64 383.17 868.48 386.99C868.39 388.73 865.8 388.61 865.83 386.89V386.9Z" fill="#0E1016"/>
<path d="M846.49 380.06C847.15 377.3 853.86 349.76 854.34 347.78C857.04 357.03 860.24 366.21 863.71 375.22C869.92 391.32 877.2 407.04 885.61 422.11C897.18 442.43 911.13 461.29 925.92 479.39C933.63 488.86 941.73 498.01 950.05 506.94C952.89 509.83 955.71 513.31 956.31 517.45C954.82 513.64 951.9 510.7 948.93 508.03C940.26 499.35 932.13 490.17 924.19 480.82C903.15 455.82 884.18 428.7 870.7 398.82C863.88 383.91 857.87 368.68 853.24 352.92L855.68 352.89L848.78 381.97C848.44 383.24 846.18 381.32 846.48 380.06H846.49Z" fill="#0E1016"/>
<path d="M650.82 515.66C650.94 514.76 653.32 494.57 653.34 494.49C660.22 494.02 667.03 494.11 673.8 494.25C716.72 495.48 759.4 501.24 801.6 508.82C837.76 515.39 873.71 523.15 909.11 533.07C915.05 534.72 920.75 536.84 926.68 537.85C932.68 539.09 938.65 538.74 944.77 538.61C938.94 540.87 932.46 540.61 926.35 539.91C921.13 539.34 913.56 536.91 908.42 535.46C873.11 525.62 837.18 518.01 801.14 511.39C759.1 503.95 716.51 498.5 673.76 498.39C667.7 498.42 661.54 498.55 655.62 499.01L657.76 496.94L655.73 516.2C655.33 519.42 650.52 518.94 650.83 515.65L650.82 515.66Z" fill="#0E1016"/>
<path d="M660.09 496.26C660.19 495 661.14 485.47 661.21 484.55C674.48 485.1 687.67 486.07 700.86 487.25C738.83 490.62 776.8 496.24 814.01 504.61C848.16 512.11 882.34 519.92 915.74 530.38C920.91 532.11 926.1 533.94 931.59 534.25C942.81 535.32 951.02 532.55 956.31 521.93C953.87 534.36 942.72 537.82 931.38 536.87C925.67 536.59 920.26 534.79 914.91 532.95C881.62 522.44 847.5 514.84 813.4 507.41C776.29 499.15 738.44 493.63 700.57 490.41C687.93 489.32 675.26 488.42 662.62 487.91L664.31 486.41L663.61 496.57C663.43 498.9 659.9 498.66 660.08 496.28L660.09 496.26Z" fill="#0E1016"/>
<path d="M665.99 486.24C666.1 485.35 667.53 476.13 667.62 475.45C692.02 476.86 716.3 479.72 740.51 482.88C799.98 490.39 858.46 504.93 914.72 525.54C920.3 527.52 925.83 529.55 931.76 530.1C937.25 530.64 943.72 530.29 947.53 525.95C947.88 525.52 948.53 525.47 948.95 525.83C949.37 526.19 949.41 526.81 949.06 527.23C944.73 532.26 937.64 532.76 931.54 532.21C925.48 531.7 919.66 529.64 913.97 527.66C846.5 503.19 775.77 489.16 704.48 481.81C692.6 480.54 680.7 479.37 668.81 478.47L670.41 477.19L668.93 486.68C668.64 488.59 665.71 488.19 665.98 486.23L665.99 486.24Z" fill="#0E1016"/>
<path d="M954.27 504.94C958.67 498.89 963.2 492.98 967.97 487.21C972.84 481.44 977.81 475.14 984.95 472.03C999.68 464.91 1017.31 472.87 1024.3 487.01C1024.86 488.17 1023.15 489.07 1022.53 487.93C1020.85 484.76 1018.51 481.94 1015.82 479.56C1004.45 469.18 988.27 469.83 977.65 480.91C972.61 486.23 968.03 492.12 963.55 497.95C961.29 500.89 959.05 503.85 956.86 506.84C956.18 507.75 955.68 507.15 955.04 506.57C954.5 506.08 953.69 505.73 954.26 504.94H954.27Z" fill="#0E1016"/>
<path d="M997.61 469.75C1007.64 471.62 1005.3 489.77 1004.05 497.11C1002.42 505.32 999.38 517.1 997.38 525.35C992.85 544.03 988.2 562.77 986.07 581.87C981.76 613.91 989.97 636.79 1017.42 654.93C1025.32 660.22 1033.83 664.62 1042.46 668.89C1043.44 669.38 1043.85 670.57 1043.36 671.55C1042.87 672.54 1041.67 672.94 1040.68 672.45C1032.03 668.15 1023.36 663.65 1015.23 658.18C998.04 646.95 985.8 631.48 982.37 610.81C979.64 591.22 983.78 571.83 987.64 552.8C990.61 539.12 994.5 524.19 997.98 510.58C1000.27 501.36 1003.06 492.06 1002.67 482.52C1002.38 478.48 1001.93 472.73 997.28 471.71C996.03 471.52 996.28 469.55 997.6 469.74L997.61 469.75Z" fill="#0E1016"/>
<path d="M1046.37 672.02C1049.19 673.55 1053.37 673.93 1055.36 676.16C1056.62 677.57 1057.75 679.07 1058.78 680.59C1066.7 692.42 1068.77 706.89 1067.87 720.83C1066.48 739.32 1056.5 755.59 1048.27 771.72L1046.08 770.01C1050.58 765.85 1055.07 761.67 1059.52 757.47C1063.05 754.35 1067.82 748.94 1071.9 746.66C1073.53 745.69 1075.74 745.39 1077.57 746.23C1080.22 747.67 1080.72 750.84 1081.13 753.33C1082.05 762.67 1081.27 771.85 1081.15 781.16L1078.11 780.52C1083 767.16 1087.67 753.53 1090.51 739.59C1093.65 725.89 1089.36 711.54 1081.49 700.14C1077.52 694.45 1072.57 689.43 1066.71 685.72C1056.46 679.53 1045.27 675.57 1034.94 668.97C1020.2 659.91 1009.65 644.64 1005.89 627.79C1001.16 606.3 1004.51 584.13 1008.63 562.88C1010.52 553.45 1012.69 544.09 1014.95 534.76C1017.66 523.11 1021.08 511.67 1022.7 499.9C1023.09 496.49 1023.3 493.1 1022.47 489.83C1021.44 486.54 1019.55 483.63 1017.89 480.58C1013.11 472.37 1007.91 464.33 1002.38 456.59C999.67 452.96 996.97 448.93 993.55 446.04C989.81 443.79 985.15 443.07 980.85 443.63C971.31 444.78 965.55 453.36 959.39 459.9C953.07 467.06 946.92 474.4 940.87 481.79C940.21 482.6 939.7 481.96 939.09 481.46C938.48 480.96 937.76 480.61 938.41 479.79C947.61 468.64 956.66 457.45 966.86 447.15C973.87 440.01 985.58 437.98 994.49 442.75C996.02 443.6 997.29 444.87 998.31 446.02C1000.76 448.75 1002.94 451.69 1005.08 454.64C1010.7 462.49 1015.93 470.55 1020.81 478.89C1022.59 482.11 1024.66 485.35 1025.74 488.94C1026.71 492.65 1026.51 496.6 1026.08 500.29C1024.46 512.27 1021.01 523.9 1018.31 535.56C1016.07 544.86 1013.92 554.18 1012.06 563.55C1007.02 590.7 1002.06 621.51 1017.39 646.24C1029.55 666.52 1049.65 671.34 1068.78 682.46C1083.42 691.83 1093.12 708.24 1095.02 725.45C1096.04 735.27 1093.08 745.02 1090.49 754.35C1087.76 763.61 1084.54 772.66 1081.05 781.65L1077.55 790.67C1077.92 781.54 1078.57 771.7 1078.64 762.68C1078.54 759.83 1079.33 748.96 1075.67 748.51C1072.77 748.33 1070.32 751.37 1068.14 753.19C1061.66 759.25 1054.53 766.12 1047.99 772.05L1041.71 777.83L1045.8 770.34C1052.07 758.47 1058.68 746.52 1062.5 733.63C1066.82 717.39 1065.37 696.89 1055.14 683.1C1052.61 679.67 1049.44 676.67 1045.89 675.12C1042.59 673.53 1042.27 670.03 1045.51 671.66L1046.37 671.99V672.02Z" fill="#0E1016"/>
<path d="M356.7 532.55C364.18 473.86 377.64 415.86 398.09 360.3C420.06 303.5 451.46 250.78 487.25 201.68C492.08 195.14 496.95 188.7 502.12 182.33C503.2 181 503.42 183.37 504.75 184.44C506.1 185.54 508.01 184.98 506.87 186.3C501.77 192.4 496.79 198.78 491.93 205.18C448.48 262.97 411.35 326.6 390.32 396.09C375.75 442.22 366.08 489.81 361.56 537.97C361.41 539.48 356.5 534.16 356.71 532.55H356.7Z" fill="#0E1016"/>
<path d="M357.9 576.12C357.13 587.42 355.17 614.17 354.42 624.96C353.19 641.28 352.76 657.56 351.5 673.92C348.47 717.63 340.21 761.15 323.69 801.86C311.41 832.41 297 862.29 276.84 888.56C250.47 922.35 218.67 954.52 192.68 988.46C179.49 1005.59 167.12 1023.37 157.58 1042.86C157.34 1043.36 156.74 1043.57 156.24 1043.32C155.78 1043.1 155.57 1042.57 155.73 1042.09C159.3 1031.7 164.1 1021.74 169.53 1012.18C180.3 993.18 193.72 975.42 207.62 958.6C228.31 933.37 251.12 909.86 271.51 884.45C291.64 858.98 306.51 829.84 318.76 799.87C331.16 769.82 339.19 738.15 343.26 705.91C347.6 673.74 347.9 640.71 350.26 608.31L352.29 575.67C352.39 574.12 353.77 575.84 355.32 575.93C356.9 576.03 358.04 574.53 357.89 576.11L357.9 576.12Z" fill="#0E1016"/>
<path d="M339.82 568.03C337.92 582.56 336.3 597.12 335.47 611.75C333.86 645.84 330.29 682.43 310.15 711.33C300.13 725.21 285.11 734.69 276.77 749.77C270.82 760.07 266.36 771.51 263.84 783.11L260.35 782.09C264.05 774.45 268.15 767.18 273.69 760.69C278.01 755.85 283.27 751.53 289.61 749.57C297.07 747.25 304.47 749.38 307.82 756.85C311.76 765.15 310.48 774.14 308.65 782.69C298.76 827.7 272.69 867.15 244.12 902.49C239.75 907.83 234.63 913.94 230.09 919.11C218.26 933.01 201.31 940.62 185.01 947.56C170.15 953.67 154.36 959.11 142.44 970.19C136.56 975.51 131.69 981.96 127.38 988.8C123.18 995.53 119.4 1002.81 117.19 1010.4L113.3 1008.24C119.55 1002.37 125.99 996.92 132.98 991.97C136.57 989.49 140.23 987.08 144.34 985.45C153.14 981.66 165.56 982.72 169.33 992.95C172.14 1001.86 169.63 1011.12 167.11 1019.69C165.74 1023.9 164.22 1027.94 161.86 1032.14C160.68 1034.28 157.97 1035.06 155.83 1033.86C153.47 1032.56 152.84 1029.35 154.53 1027.26C156.82 1024.3 158.7 1020.64 160.25 1017.13C163.04 1010.34 168.08 996.55 161.52 990.8C157.23 987.3 151.16 987.89 145.99 989.76C142.23 991.12 138.82 993.37 135.51 995.62C126.45 1001.75 118.21 1009.65 110.64 1017.52C111.71 1013.27 113.1 1007.02 114.65 1003.15C120.07 989.35 127.9 976.52 138.73 966.18C148.01 957.42 159.65 951.6 171.35 946.9C189.72 939.09 209.74 932.02 224.11 917.84C229.64 911.83 235.02 905.61 240.19 899.29C265.8 867.67 288.77 832.64 300.46 793.41C302.58 785.7 304.66 777.85 305.2 769.86C305.3 763.92 304.48 756.21 298.66 753.51C294.28 752.07 289.65 753.3 285.55 755.73C275.2 761.95 268.89 772.95 263.69 783.62L258.52 796.1L260.2 782.6C262.41 767.71 267.78 753.23 276.08 740.66C284.38 728.44 297.01 719.35 305.81 708.07C326.83 680.77 330.02 644.88 331.56 611.55C332.39 596.83 333.8 582.15 335.51 567.52C335.65 566.33 336.76 567.23 337.95 567.37C339.15 567.51 339.98 566.85 339.82 568.05V568.03Z" fill="#0E1016"/>
<path d="M334.31 490.92C341.11 485.9 348.18 487.71 348.85 496.77C349.42 502.08 348.12 508.92 347.67 514.14C347.27 517.1 342.91 516.63 343.16 513.65C343.74 508.87 345.35 501.75 345.08 497.09C344.9 490.31 340.7 490.36 335.75 493.21C334.26 494.03 332.94 491.91 334.32 490.93L334.31 490.92Z" fill="#0E1016"/>
<path d="M497.03 182.91C489.52 187.07 481.24 189.2 473.29 192.22C449.52 201.24 429.57 215.49 416.36 237.5L414.02 235.46C417.91 232.41 421.87 229.54 425.97 226.8C432.13 222.72 438.67 218.73 445.87 216.54C451.99 214.62 462.39 212.86 464.4 221.19C465.77 228.1 460.66 234.48 457.54 239.38L449.63 251.65C444.41 259.87 439.17 268.09 434.09 276.37C431.6 280.58 428.74 284.9 425.34 288.52C420.28 294.07 414.11 298.48 407.7 302.31C401.65 305.81 395 309.81 389.68 314.29C382.28 320.35 375.93 327.64 370.91 335.79C365.95 343.91 361.58 352.74 358.81 361.8L355.92 360.4C358.58 356.87 361.37 353.56 364.34 350.33C370.4 343.86 377.17 337.68 385.9 334.88C389.37 333.87 393.96 333.32 396.98 336.39C399.63 339.69 398.38 344.05 397.6 347.47C395.84 353.82 393.24 359.83 390.95 365.93C383.53 385.29 376.16 404.71 370.38 424.61C368.35 429.74 364.5 433.71 361.02 437.79C355.18 444.28 349.01 450.37 342.8 456.38C336.66 462.19 330.78 468.4 326.72 475.83C326.27 476.67 325.22 476.98 324.39 476.52C323.57 476.07 323.26 475.05 323.69 474.23C325.74 470.26 328.28 466.59 331.05 463.16C339.46 453.3 349.4 444.74 357.82 434.96C360.93 431.21 364.58 427.48 366.36 422.96C369.69 411.37 373.67 399.94 377.86 388.67C380.88 380.56 384.02 372.5 387.2 364.47C388.78 360.45 390.43 356.44 391.93 352.46C393.24 348.78 394.92 344.53 394.83 340.73C394.75 336.85 389.75 337.49 386.9 338.28C372.67 343.02 362.3 357.57 353.44 369.06C358.8 347.19 369.96 326.08 387.42 311.56C394.73 305.19 403.88 300.68 411.81 295.22C419.76 289.8 426.05 282.71 430.71 274.32C435.77 265.96 440.92 257.68 446.15 249.44L454.05 237.11C456.42 233.44 459.57 229.23 460.31 224.95C462.2 214.6 445.31 220.26 440.33 222.58C429.35 227.74 419.13 235.51 409.48 242.88L413.63 235.91C422.45 221.33 434.43 208.58 449.05 199.78C459.81 193.02 471.52 187.92 483.49 183.76C487.24 182.27 491.05 180.61 494.49 178.57C496.06 177.68 495.51 179.49 496.05 180.75C496.48 181.76 498.35 182.07 497.03 182.91V182.91Z" fill="#0E1016"/>
<path d="M467.53 361.95C467.15 361.95 466.78 361.78 466.54 361.46C466.12 360.91 466.22 360.13 466.77 359.71C475.67 352.88 491.73 344.04 517.7 340.65C530.58 338.97 541.95 339.86 550.62 343.22C558.56 346.3 563.91 351.37 565.7 357.48C565.89 358.14 565.51 358.84 564.85 359.03C564.19 359.22 563.49 358.84 563.3 358.18C560.37 348.13 545.24 339.58 518.03 343.12C492.61 346.44 476.95 355.05 468.3 361.69C468.07 361.86 467.81 361.95 467.54 361.95H467.53Z" fill="#0E1016"/>
<path d="M455.46 446.85C455.08 446.85 454.71 446.68 454.47 446.36C454.05 445.81 454.16 445.03 454.7 444.61C454.91 444.45 476.47 428.15 505.18 421.86C533.01 415.76 555.16 420.33 559.03 432.97C559.23 433.63 558.86 434.33 558.2 434.53C557.54 434.73 556.84 434.36 556.64 433.7C553.71 424.13 535.27 417.82 505.72 424.3C477.55 430.47 456.43 446.44 456.22 446.6C455.99 446.77 455.73 446.86 455.46 446.86V446.85Z" fill="#0E1016"/>
<path d="M461.32 546.49C460.99 546.49 460.66 546.36 460.42 546.11C459.94 545.61 459.96 544.82 460.45 544.34C461.1 543.71 476.85 528.84 510.51 521.95C546.14 514.66 568.74 519.19 575.33 526.92C575.78 527.44 575.71 528.23 575.19 528.68C574.66 529.13 573.87 529.06 573.43 528.54C567.51 521.6 545.46 517.35 511.01 524.4C478.05 531.15 462.34 545.99 462.18 546.14C461.94 546.37 461.63 546.49 461.31 546.49H461.32Z" fill="#0E1016"/>
<path d="M491.16 665.06C490.64 665.06 490.15 664.73 489.97 664.2C489.78 663.63 490.04 663.14 490.13 662.98C491.69 660.07 507.85 643.09 546.93 632.56C589.6 621.06 611.66 621.97 617.97 624.33C618.62 624.57 618.95 625.29 618.7 625.94C618.46 626.59 617.73 626.92 617.09 626.67C611.47 624.57 589.72 623.61 547.58 634.97C509.65 645.2 493.77 661.6 492.37 664.08C492.28 664.49 491.98 664.84 491.55 664.99C491.42 665.03 491.29 665.05 491.16 665.05V665.06Z" fill="#0E1016"/>
<path d="M529.56 796.56C529.05 796.56 528.57 796.24 528.38 795.73C528.15 795.08 528.49 794.36 529.13 794.13C529.34 794.05 550.45 786.51 566.36 782.83C567.04 782.67 567.7 783.09 567.86 783.77C568.02 784.44 567.6 785.11 566.92 785.27C551.15 788.92 530.18 796.41 529.97 796.49C529.83 796.54 529.69 796.56 529.55 796.56H529.56Z" fill="#0E1016"/>
<path d="M701.99 887.03C707.21 911.57 710.87 936.49 711.32 961.61C711.35 986.74 708.48 1011.96 700.97 1035.98C700.8 1036.51 700.24 1036.8 699.71 1036.63C699.21 1036.47 698.93 1035.95 699.04 1035.45C700.39 1029.39 701.49 1023.29 702.44 1017.16C706.21 992.69 708.11 967.84 705.79 943.15C704.26 924.62 701.48 906.13 698.02 887.88C697.78 886.61 699.11 887.99 700.08 887.77C701.05 887.55 701.65 885.72 701.98 887.03H701.99Z" fill="#0E1016"/>
<path d="M598.21 954.87C597.65 954.87 597.14 954.49 597 953.93C596.83 953.26 597.23 952.58 597.9 952.41C598.57 952.24 664.69 935.45 705.99 935.45C706.52 935.45 707.04 935.45 707.56 935.45C708.25 935.45 708.8 936.02 708.8 936.71C708.8 937.4 708.23 937.95 707.55 937.95H707.54C707.03 937.95 706.51 937.95 705.99 937.95C665.01 937.95 599.2 954.66 598.53 954.83C598.43 954.86 598.32 954.87 598.22 954.87H598.21Z" fill="#0E1016"/>
<path d="M667.69 249.39C674.37 244.19 677.29 234.94 676.75 226.67C676.7 226.12 677.11 225.64 677.66 225.59C678.21 225.54 678.69 225.95 678.74 226.5C678.78 227.72 678.78 228.86 678.73 230.02C678.55 237.61 676.52 245.3 672.09 251.57C671.52 252.41 671.31 252.68 670.52 253.41C669.45 254.45 670.27 253.3 669.24 252.23C667.68 250.72 665.55 249.59 667.68 249.37L667.69 249.39Z" fill="#0E1016"/>
<path d="M671.67 236.19C671.67 236.19 668.92 241.33 666.61 244.87C665.75 246.2 664.68 246.3 664.36 247.08C663.83 248.39 664.79 248.83 664.79 248.83C664.79 248.83 667.49 252.78 668.64 251.83C671.85 249.18 672.56 244.63 672.59 242.17C672.63 238.97 671.66 236.19 671.66 236.19H671.67Z" fill="#0E1016"/>
<path d="M680.33 262.88C677.33 260.43 675.97 258.81 676.8 255.71C678.78 248.31 681.04 238.92 678.29 232.12L676.86 227.94C676.44 226.73 678.24 226.03 678.73 227.24L680.36 231.41C684.2 239.79 682 249.42 681.38 258.16C681.38 259.35 681.7 260.34 682.71 261.14C684.54 262.6 682.1 264.43 680.32 262.89L680.33 262.88Z" fill="#0E1016"/>
<path d="M277.7 520.62C271.91 553.39 267.14 586.36 264.03 619.5L261.22 618.8C269.87 598.75 278.88 578.87 288.9 559.46C292.28 553 295.74 546.57 299.64 540.35C300.83 538.51 303.66 540.24 302.48 542.15C287.42 566.86 275.66 593.54 263.91 619.96L260.37 628.12C263.09 591.88 267.56 555.76 273.42 519.91C273.94 517.12 278.11 517.76 277.69 520.64L277.7 520.62Z" fill="#0E1016"/>
<path d="M423.65 225.06C408.45 218.8 396.44 224.8 385.17 235.38C374.49 245.46 365.94 257.58 358.22 270.06C332.96 312.3 314.61 358.61 302.05 406.15C301.77 407.21 300.68 407.84 299.63 407.56C298.57 407.28 297.94 406.19 298.22 405.14C303.53 386.06 309.41 367.09 316.7 348.61C331.73 311.03 360.29 245.36 395.04 223.86C403.97 218.38 415.37 217.75 425.04 221.76C427.21 222.73 425.87 225.95 423.65 225.06V225.06Z" fill="#0E1016"/>
<path d="M447.79 249.51C442.87 241.24 436.94 233.09 428.81 227.86C426.82 226.55 428.73 223.53 430.77 224.79C435.1 227.74 438.75 231.47 441.91 235.47C445.05 239.47 447.73 243.77 450.15 248.17C450.51 248.83 450.27 249.65 449.62 250.01C448.97 250.37 448.16 250.14 447.79 249.51Z" fill="#0E1016"/>
<path d="M316.48 391.19C324.31 365.58 333.05 340.23 343.6 315.59C350.72 299.02 358.38 282.51 370.16 268.61C382.95 253.18 405.55 236.67 425.31 250.32C431.14 254.29 435.78 259.92 439.21 265.94C439.76 266.9 439.43 268.12 438.47 268.67C437.56 269.19 436.42 268.92 435.84 268.08C433.34 264.55 430.55 261.27 427.44 258.45C408.8 241.16 387.7 256.15 374.17 272C362.72 285.4 354.86 301.33 347.46 317.3C336.59 340.64 326.18 367.18 316.49 391.18L316.48 391.19Z" fill="#0E1016"/>
<path d="M708.68 958.41C728.49 955.79 747.63 946.21 760.45 930.71C762.74 927.83 764.89 924.75 766.76 921.6C767.45 920.33 768.35 921.15 769.59 921.92C770.88 922.71 771.91 923.22 770.95 924.39C768.97 926.81 767.11 929.3 765.01 931.6C758.88 938.56 751.86 944.86 743.94 949.73C733.39 956.27 721.55 960.49 709.36 962.72C706.47 963.17 705.75 958.89 708.68 958.42V958.41Z" fill="#0E1016"/>
<path d="M947.59 525.89C950.71 522.68 950.52 517.62 947.47 514.42C946.9 513.77 944.89 511.72 944.23 511.06C924.69 491.83 906.12 471.54 889.27 449.89C871.17 426.64 853.77 401.93 843.59 374.07C842.14 370.05 840.84 366.04 839.94 361.76H842.32C835.71 401.81 830.93 442.32 828.77 482.88C828.42 488.68 828.43 495.09 828.66 500.07C828.75 502.13 824.79 502.34 824.81 500.19C826.15 453.58 831.83 407.2 839.93 361.31L841.01 355.35C841.01 355.36 842.31 361.3 842.31 361.31C846.56 377.46 854.23 392.56 862.56 406.97C875.44 428.5 891.19 448.62 907.23 467.86C919.4 482.33 932.37 496.18 945.74 509.56L947.95 511.85L949.05 513.05C952.75 517 952.92 523.41 949 527.3C948.03 528.23 946.65 526.85 947.58 525.89H947.59Z" fill="#0E1016"/>
<path d="M590.49 817.26C587.92 833.69 585.16 851.23 582.14 867.62L582.08 867.96C582.06 868.07 582.09 867.93 582.08 867.89C582.06 867.71 581.92 867.56 581.74 867.54L581.9 867.56C583.07 867.68 593.49 868.88 594.92 869.04C609.18 870.71 688.57 879.74 704.54 881.56C705.22 881.64 705.71 882.25 705.63 882.93C705.55 883.61 704.94 884.1 704.26 884.02L616.55 874.1C609.26 873.29 590.7 871.21 583.66 870.41C583.15 870.35 581.94 870.22 581.42 870.15C580.16 870.03 579.22 868.68 579.53 867.47C582.81 851.01 586.76 833.6 590.5 817.26H590.49Z" fill="#0E1016"/>
<path d="M626.09 879.21C624.59 879.01 622.81 877.17 622.49 879.14C619.12 903.19 612.26 927.31 598.86 947.73C587.03 965.38 571.95 980.85 554.88 993.52C555.24 992.86 555.61 992.19 555.96 991.52C561.44 980.9 566.22 969.66 567.85 957.73C566.55 960.4 565.23 963.07 563.9 965.74C561.03 971.5 557.18 976.72 552.47 981.1C546.56 986.62 540.2 991.71 533.67 996.4C504.48 1017.6 468.84 1029.1 432.74 1029.47C432.22 1029.47 431.79 1029.88 431.75 1030.4C431.71 1030.95 432.13 1031.43 432.68 1031.47C444.9 1032.32 457.26 1031.79 469.47 1029.92C499.06 1025.03 528.27 1011.95 550.67 991.62C548.05 996.62 545.41 1001.6 542.75 1006.51C555.65 998.7 568.07 989.94 579.01 979.53C588.05 970.75 596.44 961.31 603.55 950.84C617.66 929.74 625.14 904.93 628.58 880C628.81 877.95 627.58 879.4 626.08 879.2L626.09 879.21Z" fill="#0E1016"/>
<path d="M683.19 613.34L695.73 618.04L693.12 631.17C690.9 638.76 682.94 643.12 675.35 640.9C667.76 638.68 663.4 630.72 665.62 623.13C667.82 615.61 675.65 611.25 683.2 613.35L683.19 613.34ZM696.76 612.86L684.82 608.38L684.72 608.35C674.34 605.39 663.54 611.41 660.58 621.79C657.62 632.17 663.64 642.97 674.02 645.93C684.39 648.88 695.2 642.87 698.16 632.5L698.19 632.4L700.68 619.9L712.62 624.38L712.72 624.41C723.1 627.37 733.9 621.35 736.86 610.97C739.82 600.59 733.8 589.79 723.42 586.83C713.05 583.88 702.24 589.89 699.28 600.26L699.25 600.36L696.76 612.86ZM701.7 614.71L704.31 601.58C706.53 593.99 714.49 589.63 722.08 591.85C729.67 594.07 734.03 602.03 731.81 609.62C729.61 617.14 721.78 621.5 714.23 619.4L701.7 614.7V614.71Z" fill="black"/>
<path d="M823.98 622.99L828.19 610.28L841.41 612.39C849.08 614.32 853.74 622.11 851.81 629.78C849.88 637.45 842.09 642.11 834.42 640.18C826.82 638.27 822.17 630.61 823.97 622.98L823.98 622.99ZM822.99 609.46L818.97 621.56L818.94 621.66C816.38 632.14 822.81 642.71 833.29 645.27C843.77 647.83 854.34 641.4 856.9 630.92C859.46 620.44 853.04 609.87 842.56 607.31L842.46 607.29L829.87 605.28L833.89 593.18L833.92 593.08C836.48 582.6 830.05 572.03 819.57 569.47C809.09 566.91 798.52 573.34 795.96 583.82C793.4 594.3 799.82 604.87 810.3 607.43L810.4 607.45L822.99 609.46ZM824.65 604.45L811.43 602.34C803.76 600.41 799.1 592.62 801.03 584.95C802.96 577.28 810.75 572.62 818.42 574.55C826.02 576.46 830.67 584.12 828.87 591.75L824.65 604.45V604.45Z" fill="black"/>
<path d="M650.09 767.54L663.41 766.19L666.89 779.12C668.27 786.91 663.06 794.34 655.27 795.72C647.48 797.1 640.05 791.89 638.67 784.1C637.31 776.38 642.4 769.01 650.09 767.54V767.54ZM662.04 761.1L649.36 762.38L649.25 762.4C638.64 764.35 631.62 774.54 633.57 785.15C635.52 795.76 645.71 802.78 656.32 800.83C666.93 798.88 673.95 788.7 672 778.09L671.98 777.99L668.67 765.68L681.35 764.4L681.46 764.38C692.07 762.43 699.09 752.24 697.14 741.63C695.19 731.02 685 724 674.39 725.95C663.78 727.9 656.76 738.08 658.71 748.69L658.73 748.79L662.04 761.1ZM667.29 760.57L663.81 747.65C662.43 739.86 667.64 732.43 675.43 731.05C683.22 729.67 690.65 734.88 692.03 742.67C693.39 750.39 688.3 757.76 680.61 759.23L667.29 760.57V760.57Z" fill="black"/>
<path d="M774.66 828.41L777.35 815.29L790.73 815.83C798.58 816.84 804.12 824.03 803.1 831.88C802.09 839.73 794.9 845.27 787.05 844.25C779.28 843.25 773.76 836.19 774.66 828.4V828.41ZM772.08 815.09L769.51 827.58L769.49 827.69C768.18 838.4 775.8 848.14 786.51 849.45C797.22 850.76 806.96 843.14 808.27 832.43C809.58 821.72 801.96 811.98 791.25 810.67H791.14L778.4 810.15L780.97 797.66L780.99 797.55C782.3 786.84 774.68 777.1 763.97 775.79C753.26 774.48 743.52 782.1 742.21 792.81C740.9 803.52 748.52 813.26 759.23 814.57H759.34L772.08 815.09ZM773.14 809.92L759.77 809.38C751.92 808.37 746.38 801.18 747.4 793.33C748.41 785.48 755.6 779.94 763.45 780.96C771.22 781.96 776.74 789.02 775.84 796.81L773.14 809.92V809.92Z" fill="black"/>
<path d="M787.92 711.27L784.58 698.3L796.84 692.92C804.34 690.39 812.47 694.41 815 701.91C817.53 709.41 813.51 717.54 806.01 720.07C798.59 722.58 790.53 718.66 787.92 711.27V711.27ZM779.75 700.43L782.92 712.78L782.95 712.88C786.48 723.08 797.6 728.49 807.8 724.96C818 721.43 823.41 710.31 819.88 700.11C816.35 689.91 805.23 684.51 795.04 688.03L794.94 688.07L783.27 693.2L780.1 680.85L780.07 680.75C776.54 670.55 765.42 665.14 755.22 668.67C745.02 672.2 739.61 683.32 743.14 693.52C746.67 703.72 757.79 709.12 767.98 705.6L768.08 705.56L779.75 700.43ZM778.44 695.32L766.19 700.7C758.69 703.23 750.56 699.21 748.03 691.71C745.5 684.21 749.52 676.08 757.02 673.55C764.44 671.04 772.5 674.96 775.11 682.35L778.44 695.31V695.32Z" fill="black"/>
<path d="M558.54 226.9C562.88 225.64 569.37 223.9 571.83 222.58C573.97 221.43 571.97 219.48 566.75 218.28C561.53 217.08 570.02 217.88 577.15 219.92C584.28 221.96 585.53 223.77 585.53 223.77L562.32 227.79L558.54 226.9V226.9Z" fill="#0E1016"/>
<path d="M624.42 238.21C624.42 238.21 628.55 244.98 630.13 262.5C630.91 271.15 627.12 256.56 625.63 251.91C624.14 247.26 620.04 237.59 620.04 237.59L624.41 238.21H624.42Z" fill="#0E1016"/>
<path d="M653.73 251C659.19 250.57 662.92 248.14 671.49 255.81C680.06 263.48 666.19 247.79 664.36 247.08C662.53 246.37 655.93 248.9 655.05 249.17C654.17 249.44 653.72 251 653.72 251H653.73Z" fill="#0E1016"/>
<path d="M462.61 534.24C463.12 536.37 465.48 539.98 469.78 537.46C474.08 534.94 467.37 540.41 465.46 541.76C463.55 543.11 461.62 544.4 461.62 544.4L462.61 534.24V534.24Z" fill="#0E1016"/>
<path d="M465.26 553.07C464.81 550.23 464.25 543.93 470.74 539.87C477.23 535.81 468.41 540.42 465.46 541.77C462.51 543.12 461.62 544.41 461.62 544.41L463.43 550.58L465.26 553.07V553.07Z" fill="#0E1016"/>
<path d="M458.38 451.18C458.41 448.94 459.16 444.22 464.27 441.26C469.38 438.3 462.75 441.55 460.32 442.59C457.89 443.63 456.52 444.75 456.52 444.75L457.16 450.95L458.38 451.19V451.18Z" fill="#0E1016"/>
<path d="M458.88 433.86C458.76 436.22 459.6 441.02 464.05 438.48C468.5 435.94 462.79 441.37 459.49 442.96C456.19 444.55 455.03 445.11 455.03 445.11L457.55 436.43L458.88 433.86Z" fill="#0E1016"/>
<path d="M469.45 366.82C470.21 363.93 471.59 358.71 477.34 355.77C483.09 352.83 469.52 359.16 469.09 359.48C468.66 359.8 468.5 364.4 468.5 364.4L469.45 366.82Z" fill="#0E1016"/>
<path d="M474.85 348.11C474.03 350.71 473.83 354.62 478.17 352.48C482.51 350.35 476.36 355.04 473.64 356.8C470.92 358.56 469.53 358.46 469.53 358.46L472.47 351.77L474.86 348.11H474.85Z" fill="#0E1016"/>
<path d="M490.49 653.53C491.04 655.23 493.71 658.29 498.56 654.81C503.42 651.33 491.76 662.01 491.55 662.22C491.34 662.43 490.49 653.53 490.49 653.53V653.53Z" fill="#0E1016"/>
<path d="M495.93 669.92C494.98 667.1 493.09 661.87 499.18 657.49C505.27 653.11 495.03 659.89 492.93 660.95C490.83 662.01 490.71 663.81 490.94 664.44C491.17 665.07 495.94 669.92 495.94 669.92H495.93Z" fill="#0E1016"/>
<path d="M530.6 787.37C531.01 789.29 532.79 792.81 537.64 791.22C542.49 789.63 532.42 794.22 530.85 794.72C529.28 795.23 529.89 791.04 529.89 791.04L530.6 787.36V787.37Z" fill="#0E1016"/>
<path d="M533.56 801.92C532.98 798.93 533.94 795.06 538.56 793.55C543.18 792.04 532.28 794.64 530.9 795.28C529.52 795.92 530.16 799.06 530.16 799.06L533.56 801.92Z" fill="#0E1016"/>
<path d="M462.77 230.77C467.46 222.37 470.13 218.32 468.48 214.66C466.56 210.41 456.16 213.52 445.87 216.54C435.58 219.56 452.63 215.94 456.84 216.87C461.05 217.8 463.27 219.17 463.27 222.98C463.27 226.79 462.78 230.76 462.78 230.76L462.77 230.77Z" fill="#0E1016"/>
<path d="M377.98 338.57C386.82 332.9 394.36 329.15 398.09 330.12C402.51 331.26 399.79 340.64 397.58 347.48C395.37 354.32 395.88 346.6 396.45 344.03C397.02 341.46 397.09 336.82 393.81 336.04C390.53 335.26 383.32 337.47 383.32 337.47L377.97 338.58L377.98 338.57Z" fill="#0E1016"/>
<path d="M332.23 490.25C334.67 486.11 340.65 478.95 345.39 479.53C350.59 480.17 350.59 486.67 348.85 503.23C347.16 519.25 346.95 503.94 346.87 501.09C346.79 498.24 346.71 491.11 343.31 490.08C339.91 489.05 336.03 490.79 336.03 490.79L332.24 490.25H332.23Z" fill="#0E1016"/>
<path d="M355.13 528.76C356.27 530.26 357.54 531.93 358.77 533.56C359.46 526.67 360.16 520.63 360.87 516.15C364.14 495.35 369.2 459.83 369.2 459.83C364.3 479.9 359.33 501.48 355.13 528.77V528.76Z" fill="#0E1016"/>
<path d="M316.69 805.25C312.98 813.82 310.38 819.27 310.38 819.27C310.38 819.27 313.02 814.18 316.69 805.25Z" fill="#0E1016"/>
<path d="M349.64 573.09C348.49 585.05 347.48 597.98 346.66 612.06C342.39 685.38 342.89 693.99 335.26 740.04C330.61 768.09 322.4 791.34 316.69 805.25C324.42 787.42 337.02 756.06 341.25 732.14C347.52 696.72 351.06 625.6 352.69 608.43C353.33 601.7 354.22 589.24 355.27 575.33L349.63 573.08L349.64 573.09Z" fill="#0E1016"/>
<path d="M629.15 238C623.94 240.21 617.4 240.51 610.49 238.46C603.58 236.41 592.48 230.33 583.43 225.39C574.39 220.45 572.36 217.19 577.93 220.15C583.5 223.11 598.47 228.58 605.69 231.93C612.91 235.27 617.27 236.41 621.74 236.87C626.21 237.33 629.14 238.01 629.14 238.01L629.15 238Z" fill="#0E1016"/>
<path d="M661.27 300.63C662.74 299.22 664.31 296.25 661.83 292.65C659.35 289.05 666.12 295.42 666.98 296.83C667.84 298.24 663.56 301.4 663.56 301.4L661.28 300.63H661.27Z" fill="#0E1016"/>
<path d="M662.35 288.2C664.24 290.11 666.92 292.73 669.7 293.53C672.48 294.33 674.51 294.43 676.44 294.48C678.37 294.53 671.41 297.3 669.16 297.59C666.91 297.88 663.92 292.03 663.92 292.03L662.35 288.2Z" fill="#0E1016"/>
<path d="M538.18 306.71C549.02 314.97 556.61 317.04 564.21 318.72C571.8 320.4 578.81 320.59 578.81 320.59L584.33 310.83C584.33 310.83 567.26 314.87 555.53 311.42C543.8 307.97 538.19 306.71 538.19 306.71H538.18Z" fill="#0E1016"/>
<path d="M489.73 186.33C491.84 185.45 493.42 185.49 494.86 186.33C496.3 187.17 497.26 188.4 495.5 190.7C493.74 192.99 499.13 189.49 500.57 187.05C502.01 184.6 500.07 182.59 500.07 182.59L494.11 181.93L489.73 186.33Z" fill="#0E1016"/>
<path d="M597.96 162.84C600.84 164.24 603.13 164.22 604.65 163.81C606.17 163.4 606.71 162.07 606.04 159.79C605.36 157.51 608.78 162.68 609.55 166.43C610.32 170.19 609.48 171.4 609.55 171.19C609.62 170.98 597.96 162.84 597.96 162.84V162.84Z" fill="#0E1016"/>
<path d="M198.66 292.78C207.5 284.7 216.43 279.9 225.64 277.76C234.85 275.62 242.29 277.26 247.71 284.45C253.13 291.64 257.08 299.75 260.29 314.33C263.49 328.91 258.18 290.76 256.04 281.05C253.9 271.34 251.88 265.41 245.19 263.39C238.5 261.37 236.99 260.74 226.14 267.8C215.29 274.86 211.76 277.01 206.34 282.18C200.92 287.35 198.66 292.79 198.66 292.79V292.78Z" fill="#0E1016"/>
<path d="M358.29 295.48C365.2 282.88 371.51 274.5 382.77 266.79C394.03 259.08 407.34 259.76 415.29 262.21C423.24 264.66 432.66 272.61 432.66 272.61L437.19 267.23C437.19 267.23 431.19 256.34 419.57 251.94C407.95 247.54 402.07 247.54 387.64 257.45C373.21 267.36 368.31 274.58 365.25 279.72C362.19 284.86 358.29 295.5 358.29 295.5V295.48Z" fill="#0E1016"/>
<path d="M277.18 523.58C277.18 523.58 284.25 532.15 288.84 536.33C293.43 540.51 298.59 544.93 298.59 544.93L300.72 542.14L277.32 522.78L277.18 523.57V523.58Z" fill="#0E1016"/>
<path d="M283.58 752.27C291.67 747.3 297.07 745.28 302.75 745.1C308.43 744.92 313.55 749.04 313.46 757.27C313.37 765.51 307.92 785.86 307.92 785.86L307.05 781.34C307.05 781.34 309.25 768.8 305.59 760.65C301.93 752.5 302.66 751.86 296.16 751.5C289.66 751.14 283.58 752.26 283.58 752.26V752.27Z" fill="#0E1016"/>
<path d="M138.58 988.32C151.33 980.28 158.34 976.67 167.23 978.29C178 980.25 177.51 996.2 168.68 1014.08C159.85 1031.96 166.95 1011.66 167.51 1006.4C168.08 1001.15 168.36 992.91 162.68 988.94C157 984.96 150.47 984.68 146.35 986.53C142.23 988.38 138.57 988.33 138.57 988.33L138.58 988.32Z" fill="#0E1016"/>
<path d="M541.13 996.16C541.13 996.16 535.65 1019.15 530.19 1031.87C524.73 1044.6 520.63 1036.67 520.63 1036.67C520.63 1036.67 528.89 1016.72 533.19 1002.31C535.75 993.72 541.14 996.16 541.14 996.16H541.13Z" fill="#0E1016"/>
<path d="M161.88 411.02C163.1 409.03 167.1 408.36 165.1 413.5C163.09 418.65 166.36 411.56 169.89 403.85C173.42 396.15 176.72 386.1 176.72 386.1L161.88 411.01V411.02Z" fill="#0E1016"/>
<path d="M216.07 383.78C217.18 380.12 221.2 377.18 221.02 383.78C220.84 390.38 222.84 376.66 223.01 368.8C223.18 360.95 223.01 356.37 223.01 356.37L216.06 383.78H216.07Z" fill="#0E1016"/>
<path d="M958.73 504.33C958.73 504.33 960.72 514.28 960.67 518.23C960.62 522.18 960.32 516.56 959 512.67C957.68 508.78 955.65 505.67 955.65 505.67L958.73 504.33Z" fill="#0E1016"/>
<path d="M380.03 704.74C399.67 716.2 417.35 732.33 426.95 753.29C434.12 768.64 438.08 785.47 439.99 802.29L434.58 794.34C429.59 787.06 424.45 779.79 418.89 772.93C415.32 768.5 411.4 764.36 406.27 762.01C398.1 758.07 390.72 762.34 393.49 771.83C394.93 777.34 398.45 782.14 401.4 787.03C404.55 792.1 407.55 797.37 410.17 802.78C416.44 815.33 420.75 828.99 421.03 843.13C417.32 838.16 413.36 832.88 409.48 828.07C403.97 821.59 398.54 814.17 390.28 811.24C387.33 810.24 383.91 809.83 381.04 811.24C380.07 811.72 379.23 812.48 378.54 813.31C380.63 808.88 386.51 808.18 390.84 809.25C399.99 811.46 406.33 819.05 412.2 825.81C415.1 829.25 417.81 832.79 420.36 836.52L417.25 837.6C416.71 831.93 415.24 826.12 413.47 820.65C409.76 809.56 404.18 799.09 397.9 789.19C393.89 782.66 388.91 776.01 388.7 767.95C388.57 764.43 390.15 760.49 393.18 758.44C403.69 751.51 416.09 762.08 422.29 770.1C428 777.08 433.2 784.36 438.32 791.77L434.22 793.31C433.66 788.99 432.79 784.54 431.81 780.2C423.98 745.09 408.77 725.65 380.04 704.69L380.03 704.74Z" fill="#0E1016"/>
<path d="M325.97 926.16C331.13 946.61 330.13 965.27 318.92 983.56C317.99 978.1 316.91 972.19 315.46 966.87C314.48 963.36 313.37 959.48 311.46 956.47C308.42 951.82 303.3 951.44 300.78 956.81C299.94 958.44 299.48 960.27 298.96 962.04C294.77 977 289.6 993.59 276.79 1003.55C284.27 994.29 288.7 983.53 292.13 972.33C293.58 967.66 294.77 962.88 296.2 958.14C296.53 957.23 296.93 955.96 297.41 955.11C299.53 950.55 304.37 947.51 309.45 949.79C313.76 951.64 315.95 956.04 317.4 960.06C319.44 965.78 320.72 971.63 321.74 977.6L318.18 976.99C326.54 960.8 327.68 944.05 325.96 926.17L325.97 926.16Z" fill="#0E1016"/>
<defs>
<linearGradient id="paint0_linear_121_754" x1="656.82" y1="655.74" x2="908.24" y2="717.18" gradientUnits="userSpaceOnUse">
<stop stop-color="#78E382"/>
<stop offset="1" stop-color="#3685DB"/>
</linearGradient>
<linearGradient id="paint1_linear_121_754" x1="884.03" y1="340.97" x2="946.58" y2="486.33" gradientUnits="userSpaceOnUse">
<stop stop-color="#78E382"/>
<stop offset="1" stop-color="#3685DB"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 101 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 122 KiB

@@ -0,0 +1,129 @@
<svg width="378" height="600" viewBox="0 0 378 600" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_118_2691)">
<path d="M52.51 256.81C52.51 256.81 45.39 236.73 45.92 222.75C46.45 208.77 47.9 204.03 47.9 204.03L41.62 196.12C41.62 196.12 43.11 195.5 44.7 196.12C46.29 196.74 47.26 197.14 47.26 197.14C47.26 197.14 48.2 188.87 50.89 181.48C53.58 174.09 56.94 166.32 56.94 166.32C56.94 166.32 31.36 134.25 29.44 127.61C27.52 120.97 32.24 107.9 32.44 88.67C32.64 69.44 24.52 46.3 19.6 36.25C14.68 26.2 8.47 10.37 8.47 10.37C8.47 10.37 31.4 26.51 49.98 53.59C68.56 80.67 76.51 104.73 78.01 112.64C79.51 120.55 80.36 129.13 80.36 129.13C80.36 129.13 86.49 122.44 89.65 119.25C92.81 116.06 95.75 112.27 95.75 112.27C95.75 112.27 93.02 119.98 91.98 125.61C90.94 131.24 90.47 138.92 90.47 138.92C90.47 138.92 93.96 135.92 98.65 134.78C103.34 133.64 107.2 133.01 107.2 133.01C107.2 133.01 118.91 115.02 128.26 103C137.61 90.98 149.09 78.96 149.09 78.96C149.09 78.96 145.35 97.39 142.55 107.54C139.75 117.69 138.01 132.13 138.14 131.32C138.27 130.51 144.61 131.91 146.56 131.98C148.51 132.05 150.83 133.13 150.83 133.13C150.83 133.13 156.58 125.51 166.72 122.13C176.86 118.75 183.15 119.04 183.15 119.04C183.15 119.04 178.13 123.87 175.61 128.51C173.09 133.15 169.23 137.98 169.23 137.98C169.23 137.98 175.99 139.72 181.12 142.23C186.25 144.74 192.68 149.28 192.68 149.28C192.68 149.28 195.75 128.67 194.94 117.87C194.13 107.07 196.2 105.02 204.7 99.8C213.2 94.58 241.29 69.28 252.49 57.33C263.69 45.38 275.77 27.16 279.69 20.35C279.69 20.35 282.11 42.2 269.42 78.26C256.73 114.32 246.34 131.77 233.08 148.45C219.82 165.13 215.3 167.94 215.3 167.94C215.3 167.94 232.52 189.29 241.54 205.71C250.56 222.13 277.83 281.26 287.88 312.34C297.93 343.42 325.15 440.06 327.84 463.85C330.53 487.64 341.96 544.7 344.13 558.2C344.13 558.2 326.64 478.31 311.18 439.78C295.72 401.25 274.22 330.09 249.81 282.67C225.4 235.25 215.01 221.3 215.01 221.3C215.01 221.3 213.39 229.6 210.77 236.81C208.15 244.02 205.45 248.19 205.45 248.19C205.45 248.19 212.37 246.63 214.01 245.65C215.65 244.67 213.57 251.78 210.77 255.93C207.97 260.08 202.92 265.49 202.92 265.49C202.92 265.49 202.92 283.01 187.01 293.81C171.1 304.61 164.96 305.77 162.12 309.72C159.28 313.67 157.58 317.93 159.05 336.27C160.52 354.61 167.2 382.9 167.2 382.9L156.41 383.08C156.41 383.08 154.86 390.33 148.66 396.81C142.46 403.29 137.62 406.57 137.62 406.57C137.62 406.57 139.99 413.05 143.18 417.88C146.37 422.71 146.83 423.9 146.83 423.9C146.83 423.9 138.28 422.08 132.52 417.15C126.76 412.22 125.58 410.58 125.58 410.58C125.58 410.58 115 412.77 106.88 408.94C98.76 405.11 89.55 396.81 89.55 396.81C89.55 396.81 89.09 403.83 85.9 406.57C82.71 409.31 77.23 411.95 77.23 411.95C77.23 411.95 77.23 417.88 79.33 422.17C81.43 426.46 84.17 429.1 84.17 429.1C84.17 429.1 75.23 425.27 71.49 420.71C67.75 416.15 65.56 412.77 65.56 412.77C65.56 412.77 49.69 410.31 44.58 398.81C39.47 387.31 39.84 378.28 39.84 378.28C39.84 378.28 30.72 366.15 35.28 356.57C39.84 346.99 49.79 345.71 53.62 346.99C57.45 348.27 63.84 351.28 63.84 351.28C63.84 351.28 69.59 344.62 75.61 331.12C81.63 317.62 84.7 305.49 83.44 302.25C82.18 299.01 75.96 289.63 71.08 287.37C66.2 285.11 58.52 281.89 51.34 271.18C44.16 260.47 43.04 251.11 43.04 251.11C43.04 251.11 46.42 255.46 48.7 256.13C50.98 256.8 52.54 256.77 52.54 256.77L52.51 256.81Z" fill="#ADD63D"/>
<path d="M167.2 382.94C167.2 382.94 160.59 349.3 159.05 336.31C157.51 323.32 157.81 309.31 165.49 305.87C173.17 302.43 192.85 293.32 197.51 285.21C202.17 277.1 202.91 265.52 202.91 265.52C202.91 265.52 208.03 261.43 210.76 255.96C213.49 250.49 214 245.68 214 245.68L205.44 248.22C205.44 248.22 210.36 238.57 212.34 231.98C214.32 225.39 214.99 221.32 214.99 221.32C214.99 221.32 238.33 257.8 250.6 284.29C262.87 310.78 286.79 367.25 301.95 411.75C317.11 456.25 332.34 507.48 338.22 532.85C344.1 558.22 346.81 568.69 348.34 579.62C349.87 590.55 350.82 596.9 350.82 596.9H146.4C146.4 596.9 148.45 585.23 149.26 577.29C150.07 569.35 150.86 558.22 150.86 558.22C150.86 558.22 242.6 557.01 248.61 557.01C254.62 557.01 260.4 556.22 260.66 544.93C260.92 533.64 260.6 396.04 260.6 392.23C260.6 388.42 258.38 382.99 249.48 382.95C240.58 382.91 167.19 382.95 167.19 382.95L167.2 382.94Z" fill="#E5F234"/>
<path d="M139.9 266.29C139.9 266.29 138.7 255.81 138.7 252.56V245.57C138.7 245.57 152.81 240.57 161.66 231.8C170.51 223.03 178.7 209.54 178.7 209.54C178.7 209.54 183.64 215.68 183.4 224.96C183.16 234.24 181.35 248.69 170.03 254.48C158.71 260.27 150.27 260.07 146.9 261.97C143.53 263.87 141.84 266.32 141.48 267.21C141.12 268.1 139.91 266.29 139.91 266.29H139.9Z" fill="white"/>
<path d="M55.84 221.33C55.84 221.33 54.71 235.46 59.38 247.24C64.05 259.02 69.99 261.56 76.21 263.4C82.43 265.24 86.53 265.95 87.95 268.07C89.37 270.19 90.5 273.44 90.5 273.44C90.5 273.44 91.63 262.41 91.35 251.38C91.07 240.35 90.5 239.22 90.5 239.22C90.5 239.22 81.31 247.2 70.98 240.17C60.65 233.14 60.23 227.34 59.38 225.93C58.53 224.52 55.84 221.34 55.84 221.34V221.33Z" fill="white"/>
<path d="M239.77 202.63C239.77 202.63 252.91 204.91 259.85 208.5C266.79 212.09 274.59 218.86 278.26 226.09C281.93 233.32 282.43 236.9 282.43 236.9C282.43 236.9 276.92 227.04 268.65 225.5C260.38 223.96 256.91 223.83 255.31 224.5C253.71 225.17 252.51 226.09 252.51 226.09L239.77 202.64V202.63Z" fill="#30993A"/>
<path d="M303.74 356.09C303.74 356.09 313.8 365.8 324.37 376.87C334.94 387.94 339.07 401.17 340.23 409.12C341.39 417.07 341.08 419.63 341.08 419.63C341.08 419.63 338.82 405.78 329.49 396.74C320.16 387.7 310.97 385.45 310.97 385.45L303.74 356.09V356.09Z" fill="#30993A"/>
<path d="M339.29 519.97C339.29 519.97 352.44 527.4 361.59 544.85C370.74 562.3 370.17 575.88 370.17 575.88C370.17 575.88 367.02 561.87 359.02 554.58C351.02 547.29 342.72 545.29 342.72 545.29L339.29 519.98V519.97Z" fill="#30993A"/>
<path d="M38.44 383.12H10.8C6.94001 383.12 3.82001 386.25 3.82001 390.1V548.14C3.82001 552.7 7.52001 556.4 12.08 556.4H250.98C255.57 556.4 259.31 552.7 259.35 548.1L260.63 390.73C260.66 386.54 257.27 383.12 253.08 383.12H156.42C156.42 383.12 155.19 391.65 146.41 399.13L137.63 406.61C137.63 406.61 140.2 415.72 143.52 419.83C146.84 423.94 148.03 426.34 148.03 426.34C148.03 426.34 137.22 421.39 132.54 417.19C127.86 412.99 125.6 410.62 125.6 410.62C125.6 410.62 109.78 414.06 100.47 406.14C91.16 398.22 89.57 396.85 89.57 396.85C89.57 396.85 87.97 408.97 82.61 410.48L77.25 411.99C77.25 411.99 77.09 417.58 79.35 422.21C81.61 426.84 84.19 429.14 84.19 429.14C84.19 429.14 77.44 428.68 71.51 420.75L65.58 412.81C65.58 412.81 50.74 411.15 45.9 401.8C41.06 392.45 38.48 383.12 38.48 383.12H38.44Z" fill="#E6FFFF"/>
<path d="M133.29 514.89C154.509 514.89 171.71 497.689 171.71 476.47C171.71 455.251 154.509 438.05 133.29 438.05C112.071 438.05 94.87 455.251 94.87 476.47C94.87 497.689 112.071 514.89 133.29 514.89Z" fill="#63ED4A"/>
<path d="M209.1 173.6C209.1 173.6 234.52 202.63 257.89 254.45C277.4 297.7 296.34 361.95 305.17 398.01C314 434.07 315.41 443.46 315.41 443.46L336.65 522.52C336.65 522.52 317.94 400.97 304.96 361.03C291.98 321.09 275.55 269.28 260.87 241.86C246.19 214.44 228.76 184.08 222.6 175.98C216.44 167.88 215.74 167.3 215.74 167.3L209.09 173.58L209.1 173.6Z" fill="#30993A"/>
<path d="M191.01 157.59C191.01 157.59 187.12 153.53 179.33 149.44C172.09 145.63 166.19 143.41 166.19 143.41L170.37 138.29C170.37 138.29 182.39 142.28 187.53 144.93C192.67 147.58 193.3 148.46 193.3 148.46L191.01 157.59V157.59Z" fill="#30993A"/>
<path d="M91.35 145.17C95.13 143.08 99.36 141.26 104.15 139.96C105.28 137.55 106.63 135 107.83 132.81C106.01 133.12 104.37 133.46 102.99 133.81C98.77 134.88 94.99 136.16 91.14 137.91L91.35 145.17V145.17Z" fill="#30993A"/>
<path d="M149.82 133.39C149.93 133.27 150.07 133.16 150.22 133.04C146.52 132.2 142.45 131.66 138.27 131.36C138.06 133.31 137.83 135.53 137.62 137.53C140.48 137.7 143.29 137.97 146.04 138.32C147.04 136.79 148.19 135.14 149.81 133.39H149.82Z" fill="#30993A"/>
<path d="M47.9 198.94C48.54 199.3 49.3 199.81 50.12 200.42C50.92 196.53 51.92 192.74 53.14 189.3C55.42 182.9 57.94 177.69 61.06 173.01C60.72 172.08 60.42 171.48 60.01 170.88L57.46 167.33C54.76 172.02 52.9 176.24 51.92 178.74C50.28 182.92 48.65 190.29 47.52 198.59L47.91 198.94H47.9Z" fill="#30993A"/>
<path d="M123.93 231.98C123.93 231.98 128.46 238.41 138.69 235.48C148.92 232.55 155.47 224.74 164.65 213.8C173.83 202.86 180.66 196.81 185.64 193.78C190.62 190.75 197.09 188.75 197.09 188.75C197.09 188.75 186.32 197.52 178.51 208.49C170.7 219.46 161.21 233.36 153.75 239.01C146.29 244.66 141.89 245.82 136.52 244.65C131.15 243.48 127.25 241.33 125.69 238.5C124.13 235.67 123.93 231.97 123.93 231.97V231.98Z" fill="#30993A"/>
<path d="M93.48 234.12C93.48 234.12 87.99 237.47 81.25 236.5C74.51 235.53 70.71 229.96 67.63 224.67C63.67 217.87 57.37 205.97 50.11 200.42C45.74 197.08 41.61 196.12 41.61 196.12C41.61 196.12 56.62 213.06 61.05 226.07C65.48 239.08 70.42 244.22 77.2 243.96C83.98 243.7 87.28 242.2 89.18 240.15C91.08 238.1 93.48 234.12 93.48 234.12V234.12Z" fill="#30993A"/>
<path d="M138.75 276.15C138.75 276.15 134.81 270.58 131.81 262.76C128.81 254.94 127.81 244.85 128.17 242.03C128.53 239.21 137.26 245.22 137.63 245.07C138 244.92 138.16 255.04 139.34 261.05C140.52 267.06 138.75 276.15 138.75 276.15V276.15Z" fill="#30993A"/>
<path d="M183.7 239.82C187.57 231.26 189.28 224.74 190.46 216.85C191.64 208.96 192.75 192.59 192.75 192.59C192.75 192.59 184.83 199.42 184.38 205.26C183.93 211.1 185.64 218.4 185.64 223.65C185.64 228.9 183.7 239.82 183.7 239.82V239.82Z" fill="#30993A"/>
<path d="M195.35 253.07C195.35 253.07 201.93 244.14 207.07 231.22C212.21 218.3 213.83 208.67 215.01 201.46C215.01 201.46 216.19 212.57 214.04 223.79C211.89 235.01 205.78 249.96 205.78 249.96L195.35 253.06V253.07Z" fill="#30993A"/>
<path d="M140.77 294.62C140.77 294.62 144.8 292.03 155.8 292.1C166.8 292.17 176.34 292.87 184.33 288.98C192.32 285.09 196.62 280.43 200.4 272.28C204.18 264.13 190.34 280.74 181.48 283.05C172.62 285.36 162.45 285.57 157.06 286.42C151.67 287.27 147.78 287.94 145.21 289.72C142.64 291.5 140.76 294.63 140.76 294.63L140.77 294.62Z" fill="#30993A"/>
<path d="M81.25 287.84C85.27 289.15 87.63 292.02 87.63 292.02L84.44 302.59C84.44 302.59 80.47 293.77 75.32 290.7C70.17 287.63 68.38 286.29 68.38 286.29C68.38 286.29 72.28 286.73 75.01 286.99C77.74 287.25 81.24 287.85 81.24 287.85L81.25 287.84Z" fill="#30993A"/>
<path d="M60.62 255.5C55.52 248.94 53.05 242.72 50.95 232.58C48.85 222.44 48.48 206.68 48.48 206.68L56.6 218.56C56.6 218.56 53.78 227.38 55.72 236.15C57.66 244.92 60.62 255.51 60.62 255.51V255.5Z" fill="#30993A"/>
<path d="M147.91 180.56C147.91 180.56 156.73 167.42 164.38 159.05C172.03 150.68 176.25 147.71 179.33 149.44C182.41 151.17 182.02 145.09 176.3 143.41C170.58 141.73 167.52 141.79 167.52 141.79C167.52 141.79 161.64 150.37 158.47 157.59C155.3 164.81 147.92 180.55 147.92 180.55L147.91 180.56Z" fill="#30993A"/>
<path d="M120.72 214.72C120.72 214.72 127.62 209.07 132.93 196.12C137.77 184.32 139.04 175.66 140.78 163.37C142.52 151.08 142.27 143.24 146.04 138.32C149.81 133.4 138.69 132.81 138.69 132.81C138.69 132.81 136.23 152.3 133.77 167.83C131.31 183.36 130.07 194.06 127.59 200.14C125.11 206.22 120.72 214.72 120.72 214.72V214.72Z" fill="#30993A"/>
<path d="M91.42 147.31C92.23 151.71 94.89 157.79 94.89 157.79L103.05 138.32L90.46 141.18L91.42 147.31V147.31Z" fill="#30993A"/>
<path d="M94.44 211.75C94.44 211.75 89.48 202.56 88.29 190.76C87.1 178.96 88.64 169.71 89.65 164.02C90.66 158.33 93.49 150.86 93.49 150.86L97.45 152.47C97.45 152.47 91.59 170.63 91.59 181.75C91.59 192.87 91.46 199.91 92.03 202.72C92.6 205.53 94.43 211.74 94.43 211.74L94.44 211.75Z" fill="#30993A"/>
<path d="M209.1 173.6C206.93 175.84 204.99 177.24 204.99 177.24C204.99 177.24 223.54 154.02 232.28 142.04C241.65 129.21 253.55 107.54 258.71 94.01C263.87 80.48 278.32 28.42 278.32 28.42C278.32 28.42 277.89 48.41 270.49 75.13C263.09 101.85 250.74 122.23 243.22 134.54C235.7 146.85 226.36 156.15 221.99 161.56C217.62 166.97 213.41 170.12 212.15 171.35C210.89 172.58 209.11 173.59 209.11 173.59L209.1 173.6Z" fill="#30993A"/>
<path d="M221.09 137.53C221.09 137.53 229.91 122.95 239.19 108.44C248.47 93.93 259.17 78.28 259.17 78.28C259.17 78.28 251.55 100.57 244.63 112.18C237.71 123.79 229.6 129.78 227.56 132.23C225.52 134.68 221.09 137.53 221.09 137.53V137.53Z" fill="#30993A"/>
<path d="M12.63 15.82C12.63 15.82 35.12 38.31 49.73 64.03C64.34 89.75 68.68 107.83 70.94 120.17C73.2 132.51 73.2 146.07 73.2 146.07L81.02 129.13C81.02 129.13 76.35 94.98 60.87 71.1C45.39 47.22 31.18 30.09 25.1 24.49C19.02 18.89 12.63 15.82 12.63 15.82Z" fill="#30993A"/>
<path d="M123.93 284.38C123.93 284.38 123.94 302.84 125.56 316.38C127.18 329.92 131.07 339.68 133.92 344.73C133.92 344.73 123.82 345.04 122.47 334.79C121.12 324.54 122.63 309.74 122.47 301.25C122.31 292.76 123.93 284.38 123.93 284.38Z" fill="#30993A"/>
<path d="M151.64 301.25C151.64 301.25 150.56 306.94 150.72 317.29C150.72 317.29 151.67 310.74 155.9 307.19C160.13 303.64 165.62 301.88 169.57 300.32C173.52 298.76 165.85 306.48 162.7 310.7C159.55 314.92 158.01 317.2 158.42 328.92C158.83 340.64 161.77 358.55 163.3 368.55C164.83 378.55 167.2 382.93 167.2 382.93L154.62 383.11C154.62 383.11 159.59 373.2 156.4 362.66C153.21 352.12 149.72 345.16 146.39 343.13C143.06 341.1 144.48 334.91 145.35 325.82C146.22 316.73 146.84 310.36 148.1 307.56C149.36 304.76 151.63 301.24 151.63 301.24L151.64 301.25Z" fill="#30993A"/>
<path d="M151.64 301.25C151.64 301.25 145.65 305.2 142.16 314.55C138.67 323.9 136.28 338.05 136.28 338.05C136.28 338.05 138.29 338.55 140.76 340.05C143.23 341.55 144.07 341.85 144.07 341.85C144.07 341.85 144.29 319.6 146.2 313.58C148.11 307.56 149.83 303.89 149.83 303.89L151.64 301.24V301.25Z" fill="#30993A"/>
<path d="M136.29 338.05C136.29 338.05 146.8 343.39 150.34 358.27C153.66 372.2 150.78 385.28 141.09 394.37C132.34 402.58 118.75 405.25 109.59 403.11C100.43 400.97 92.16 396.99 85.44 386.71C85.44 386.71 90.43 399.86 102.37 406.5C114.31 413.14 127.38 408.13 127.38 408.13C127.38 408.13 132.66 407.66 134.88 406.19C137.1 404.72 150.67 398.12 153.31 390.61C155.95 383.1 159.22 374.68 156.79 364.09C154.36 353.5 150.97 349.72 146.69 344.73C142.41 339.74 140.16 338.97 138.81 338.49C137.46 338.01 136.29 338.06 136.29 338.06V338.05Z" fill="#30993A"/>
<path d="M128.15 410.8C128.15 410.8 132.71 410.8 136.61 408.29C140.51 405.78 137.23 403.62 134.56 403.93C131.89 404.24 124.72 406.75 124.72 406.75L128.15 410.8Z" fill="#30993A"/>
<path d="M46.08 383.12C46.08 383.12 45.4 386.83 47.29 393.57C49.18 400.31 55.54 405.38 62.82 407.13C70.1 408.88 77.36 407.58 81.3 402.96C85.24 398.34 86.1 393.12 85.44 386.72C85.44 386.72 89.91 392.72 87.36 400.92C84.81 409.12 77.31 413.81 75.03 413.99C72.75 414.17 66.57 414.3 66.35 413.99C66.13 413.68 48.66 408.26 45.23 400.55C41.8 392.84 38.85 384.78 39.06 381.68C39.27 378.58 40.56 378.66 40.56 378.66L46.77 382.75" fill="#30993A"/>
<path d="M60.91 386.77C60.91 386.77 50.31 388.69 44.99 387.23C39.67 385.77 39.12 380.98 39.12 380.98L42.11 379.68C42.11 379.68 50.31 384.35 53.31 385.41C56.31 386.47 60.9 386.77 60.9 386.77H60.91Z" fill="#30993A"/>
<path d="M122.59 383.12C122.59 383.12 130.98 382.75 138.75 377.45C145.36 372.94 147.39 366.74 147.39 361.28C147.39 354.19 143.91 348.55 137.95 344.73C131.99 340.91 131.23 345.14 131.23 345.14C131.23 345.14 141.67 347.09 142.77 354.52C143.87 361.95 143.09 365.91 139.21 370.41C135.33 374.91 131.78 378.16 129.56 379.42C127.34 380.68 122.57 383.12 122.57 383.12H122.59Z" fill="#30993A"/>
<path d="M132.11 362.67C134.1 369.3 126.98 371.3 120.85 372.66C114.72 374.02 110.28 376.08 110.28 376.08C110.28 376.08 115.03 371.23 121.28 367.35C127.53 363.47 132.11 362.67 132.11 362.67Z" fill="#30993A"/>
<path d="M61.25 376.88C61.25 376.88 57.8 375.75 53.01 375.13C48.22 374.51 45.06 374.21 43.36 372.06C41.85 370.16 42.02 367.42 43.36 365.81C44.7 364.2 47.82 366.54 53.12 370.92C58.42 375.3 61.25 376.88 61.25 376.88Z" fill="#30993A"/>
<path d="M190.68 159.35C190.68 159.35 197.27 149.45 200.57 135.96C203.87 122.47 203.73 114.64 214.99 104.35C226.25 94.06 236 86.09 250.28 69.89C264.56 53.69 272.89 39 278.33 28.42C283.77 17.84 266.7 43.24 252.98 56.83C239.26 70.42 209.45 95.59 203.15 100.79C196.85 105.99 195.39 110.24 195.06 120.42C194.73 130.6 193.98 136.96 193.47 143.42C192.96 149.88 190.69 159.36 190.69 159.36L190.68 159.35Z" fill="#EBFFAB"/>
<path d="M94.44 211.75C94.44 211.75 94.38 180.96 104.45 155.22C114.52 129.48 146.83 83.26 146.83 83.26C146.83 83.26 128.29 97.73 115.02 121.34C101.75 144.95 92.5 163.02 92.04 175.67C91.58 188.32 92.78 198.15 92.65 201.47C92.52 204.79 94.43 211.75 94.43 211.75H94.44Z" fill="#EBFFAB"/>
<path d="M147.91 180.56C147.91 180.56 150.99 157.98 158.23 144.58C165.47 131.18 174.6 123.46 179.04 120.79C183.48 118.12 174.07 128.83 168.3 138.61C162.53 148.39 158.52 155.28 155.98 161.18C153.44 167.08 147.91 180.56 147.91 180.56Z" fill="#EBFFAB"/>
<path d="M91.94 118.93C91.94 118.93 85.2 128.78 79.57 146.98C73.94 165.18 75.08 178.72 75.56 183.87C76.04 189.02 77.98 198.6 77.98 198.6C77.98 198.6 69.99 181.45 70.59 166.01C71.19 150.57 73.71 141.41 79.32 132.8C84.93 124.19 91.94 116.36 91.94 116.36V118.92V118.93Z" fill="#EBFFAB"/>
<path d="M92.6 238.71C92.6 238.71 97.22 252.29 94.47 280.54C91.72 308.79 81.58 337.66 77.8 347.39C74.92 354.79 72.59 359.67 72.59 359.67C72.59 359.67 72.28 359.2 69.44 357.2C66.6 355.2 62.74 351.67 62.74 351.67C62.74 351.67 69.32 347.67 73.55 339.07C77.78 330.47 85.32 305.49 87.63 294.95C89.94 284.41 92.93 264.11 92.65 255.49C92.37 246.87 92.6 238.7 92.6 238.7V238.71Z" fill="#EBFFAB"/>
<path d="M103.71 359.12C99.5 362.43 95.34 366.85 95.34 366.85C95.34 366.85 109.25 356.96 119.87 352.43C129.08 348.5 136.82 347.78 139.94 352.26C142.73 356.26 143.1 351 139.94 348.51C136.78 346.02 130.02 341.66 121.63 345.56C113.24 349.46 108.05 354.08 106.42 356.1C104.79 358.12 103.71 359.13 103.71 359.13V359.12Z" fill="#EBFFAB"/>
<path d="M46.52 380.38C41.94 376.81 38.33 372.56 37.92 365.8C37.51 359.04 40.8 354.83 45.66 352.33C50.52 349.83 58.18 349.83 64.23 353.61C68.87 356.51 58.18 346.1 51.02 346.63C43.86 347.16 38.88 350.19 36.04 355.15C33.2 360.11 32.46 363.89 34.28 369.08C36.1 374.27 37.94 376.47 40.89 378.64C43.84 380.81 46.52 380.37 46.52 380.37V380.38Z" fill="#EBFFAB"/>
<path d="M45.15 255.5C45.15 255.5 48.12 263.94 57.64 271.67C67.16 279.4 75.62 280.92 81.26 281.61C81.26 281.61 68.65 284.92 60.63 279.34C52.61 273.76 48.39 267.23 46.61 262.71C44.83 258.19 45.16 255.5 45.16 255.5H45.15Z" fill="#EBFFAB"/>
<path d="M161.04 283.66C172.02 281.37 181.06 276.6 191.01 269.27C201.55 261.5 211.74 248.51 211.74 248.51C211.74 248.51 208.44 259.19 199.4 268.61C190.36 278.03 180.47 283.16 172.66 284.37C164.85 285.58 161.04 283.65 161.04 283.65V283.66Z" fill="#EBFFAB"/>
<path d="M132.71 221.33C135.69 214.65 141.01 206.72 148.75 200.75C156.49 194.78 163.2 192.17 163.2 192.17C163.2 192.17 152.17 201.37 146.82 206.75C141.47 212.13 132.7 221.33 132.7 221.33H132.71Z" fill="#EBFFAB"/>
<path d="M52.76 157.49C46.61 146.55 38.16 132.68 37.97 126.06C37.78 119.44 40.58 107.91 39.67 92.96C37.78 61.75 23.88 32.63 12.62 15.82C12.62 15.82 28.63 53.87 30.84 71.99C33.05 90.11 31.22 110.21 29.16 120.23C27.1 130.25 29.31 129.31 35.81 138.32C42.31 147.33 52.75 157.49 52.75 157.49H52.76Z" fill="#EBFFAB"/>
<path d="M115.47 396.75C112.39 396.75 104.91 396.73 99.08 392.58C92.55 387.92 89.89 382.72 88.14 378.19C88.14 378.19 94.01 385.08 101.51 390.17C107.69 394.36 115.47 396.75 115.47 396.75Z" fill="#EBFFAB"/>
<path d="M213.09 235.8C213.09 235.8 234.88 269.47 252.19 309.4C269.5 349.33 289.02 397.96 301.26 436.08C313.5 474.2 334.73 544.97 340.53 596.9H350.85C350.85 596.9 329.25 489.6 312.87 444.82C296.49 400.04 272.59 334.57 257.9 302.3C243.21 270.03 216.46 222.33 216.46 222.33C216.46 222.33 213.04 229.81 212.36 232C211.68 234.19 213.09 235.82 213.09 235.82V235.8Z" fill="#B1B511"/>
<path d="M159.81 320.37C159.81 320.37 169.7 321.92 193.18 321.3C216.66 320.68 243.87 314.47 248.01 313.13C252.15 311.79 252.16 309.25 250.41 305.36C248.66 301.47 253.19 304.77 255.67 310.71C258.15 316.65 257.96 319.73 237.88 323.26C217.8 326.79 188.91 327.27 177.64 327.82C166.37 328.37 159.05 326.27 159.05 326.27L159.81 320.37V320.37Z" fill="#B1B511"/>
<path d="M261.44 405.91C265.67 405.76 276.13 405.33 281.51 404.52C286.89 403.71 290.12 403.73 288.79 400.14C287.46 396.55 290.68 396.87 292.07 401.06C293.46 405.25 296.81 407.43 289.88 408.67C282.95 409.91 260.65 411.73 260.65 411.73L261.43 405.9L261.44 405.91Z" fill="#B1B511"/>
<path d="M260.78 508.93C260.78 508.93 272.96 508.5 291.41 506.82C309.86 505.14 316.92 503.72 318.52 503.4C320.12 503.08 320.82 501.9 319.93 498.67C319.04 495.44 323.37 499.92 323.89 503.8C324.41 507.68 323.94 509.27 316.4 510.36C308.86 511.45 260.11 515.24 260.11 515.24L260.78 509.57V508.93Z" fill="#B1B511"/>
<path d="M177.64 382.94C177.44 377.3 175.61 356.17 176.29 340.06C176.97 323.95 177.44 316.6 190.08 305.79C202.72 294.98 207.18 286.55 209.09 278.35C211 270.15 210.77 266.65 210.77 266.65C210.77 266.65 217.42 260.1 222.14 250.7C226.86 241.3 216.64 227.28 216.64 227.28L215.32 224.83L205.33 249.23L215.32 245.87C215.32 245.87 207.99 259.32 206.55 261.43C205.11 263.54 203.9 265.29 203.52 269.11C203.14 272.93 198.97 286.78 185.68 295.49C172.39 304.2 165.38 307.52 161.84 311.88C158.3 316.24 157.11 323.98 159.13 337.12C161.15 350.26 167.18 382.97 167.18 382.97H177.62L177.64 382.94Z" fill="#B1B511"/>
<path d="M150.28 565.92H257.19C263.95 565.92 269.42 560.44 269.42 553.69V401.39C269.42 396.59 265.53 392.7 260.73 392.7H260.6V549.45C260.61 553.28 257.5 556.37 253.67 556.35L151.3 555.73L150.26 565.92H150.28Z" fill="#B1B511"/>
<path d="M140.35 247.23C140.35 247.23 153.1 247.48 162.17 242.28C170.41 237.56 173.39 232.31 174.83 230.31C177 227.27 182.38 233.27 177.65 242.98C172.92 252.69 185.03 241.37 183.33 226.81C181.63 212.25 179.21 208.49 179.21 208.49C179.21 208.49 170.05 226.57 159.47 234.59C148.89 242.61 138.71 244.26 138.71 244.26L140.37 247.24L140.35 247.23Z" fill="#DBDBDB"/>
<path d="M63.05 253C61.76 250.78 60.35 246.57 60.62 243.22C60.89 239.87 62.62 238.15 63.82 237.84C65.02 237.53 55.94 224.81 55.94 224.81C55.94 224.81 53.7 231.54 56.95 241.4C60.2 251.26 63.32 256.16 63.18 254.58L63.05 253V253Z" fill="#DBDBDB"/>
<path d="M68.39 244.4C71.68 246.87 78.84 248.86 84.17 248.86C89.5 248.86 92.04 247.39 92.04 247.39L91.38 239.86C91.38 239.86 84.49 245.6 78.01 245.36C71.53 245.12 68.39 244.41 68.39 244.41V244.4Z" fill="#DBDBDB"/>
<path d="M171.36 471.24L254.54 394.37L169.56 463.75L171.36 471.24Z" fill="#B6D8D6"/>
<path d="M11.55 395.26L95.03 473.03L97.24 465.84L11.55 395.26Z" fill="#B6D8D6"/>
<path d="M5.95 550.56H251.05C252.98 550.56 254.54 549 254.54 547.07V384.54C254.54 384.54 260.49 383.66 260.61 392.71C260.73 401.76 260.62 549.46 260.62 549.46C260.62 549.46 260.49 556.48 246.84 557.43C233.19 558.38 9.93 556.4 9.93 556.4C9.93 556.4 5.95 556.76 5.95 553.66V550.56V550.56Z" fill="#B6D8D6"/>
<path d="M86.87 431.77C86.87 431.77 85.48 422.85 85.44 420.3L85.39 417.74C85.39 417.74 88.17 417.18 92.51 414.63C96.85 412.08 100.07 407.36 100.07 407.36L90.51 397.69C90.51 397.69 91.18 405.94 83.73 409.12C76.28 412.3 76.03 412.29 76.17 412.96C76.31 413.63 76.75 418.57 79.83 423.18C82.91 427.79 86.88 431.77 86.88 431.77H86.87Z" fill="#B6D8D6"/>
<path d="M96.67 408.63C96.67 408.63 100.73 413.29 113.11 415.68C124.14 417.81 131.5 415.97 131.5 415.97L126.27 411.33C126.27 411.33 112.77 412.87 107.17 409.95C101.57 407.03 96.67 405.1 96.67 405.1V408.63V408.63Z" fill="#B6D8D6"/>
<path d="M152.24 428.63C152.24 428.63 149.92 420.26 148.77 415.31L147.62 410.35C147.62 410.35 156.21 405.95 161.9 398.81C167.59 391.67 169.58 384.51 169.58 384.51H156.43C156.43 384.51 152.39 395.01 147.41 399.87C142.43 404.73 137.64 406.61 137.64 406.61C137.64 406.61 141.01 415.42 144.32 419.64C147.63 423.86 152.25 428.63 152.25 428.63H152.24Z" fill="#B6D8D6"/>
<path d="M11.55 539.42V416.1C11.55 415.02 12.4 414.13 13.48 414.08C14.63 414.03 15.59 414.95 15.59 416.1V539.42C15.59 540.54 14.69 541.44 13.57 541.44C12.45 541.44 11.55 540.54 11.55 539.42Z" fill="white"/>
<path d="M123.93 231.98C127.22 240.34 134.75 242.94 142.9 240.24C154.4 236.91 161.76 225.18 169.49 216.12C177.79 206.1 185.95 195.02 197.52 188.51C188.13 197.83 182.14 209.25 174.85 220.09C169.83 227.53 165.18 235.33 157.73 240.99C152.88 244.67 146.85 246.9 140.78 247.21C131.84 247.78 123.19 241.46 123.94 231.99L123.93 231.98Z" fill="#0E1016"/>
<path d="M140.5 244.41C139.88 252.6 140.98 261.04 142.12 269.17L138.16 268.34C139.94 265.98 141.69 263.78 143.95 261.83C144.78 261.12 145.89 260.42 146.99 260.2C150.78 259.37 154.13 259.16 157.75 258.19C171.83 255.13 181.28 242.36 181.39 228.24C181.34 221.76 180.13 214.85 176.47 209.44C175.14 207.09 178.29 207.99 179.97 206.88C181.75 205.71 182.15 202.65 183.72 205.08C186.96 212.44 187.48 220.7 186.71 228.6C185.67 238.21 180.89 247.25 173.71 253.65C169.38 257.38 163.98 259.79 158.53 261.19C154.98 262.23 150.86 262.57 147.48 263.64C143.52 266.37 141.19 272.12 138.77 276.15C137.28 265.51 135.64 254.71 136.6 243.95C136.96 241.33 140.78 241.8 140.52 244.41H140.5Z" fill="#0E1016"/>
<path d="M153.41 239.26C153.41 239.26 154.37 243.22 158.19 246.71C162.01 250.2 164.6 251.1 164.6 251.1C164.6 251.1 165.84 245.7 165.39 239.85C164.94 234 163.21 229.92 163.21 229.92C163.21 229.92 160.44 233.46 157.91 235.64C155.38 237.82 153.4 239.27 153.4 239.27L153.41 239.26Z" fill="#0E1016"/>
<path d="M140.77 294.62C144.36 285.43 159.13 284.22 167.6 282.43C190.27 278.44 204.78 263.89 214.21 243.56C214.21 243.51 216.98 245.8 216.99 245.79C210.33 249.47 203.07 252.88 195.35 253.08C198.89 251.63 202.19 250 205.46 248.24C210.09 245.72 214.92 242.65 219.38 239.84C213.39 261.52 197.4 281.72 174.32 285.77C172.35 286.15 170.15 286.43 168.15 286.58C159.95 287.43 145.65 287.08 140.77 294.63V294.62Z" fill="#0E1016"/>
<path d="M201.59 250.35C205.75 243.1 209.71 235.32 211.86 227.21C212.93 223.12 213.61 218.84 214.13 214.54C214.61 210.23 214.94 205.87 215.01 201.47C215.97 205.78 216.45 210.2 216.59 214.64C216.96 223.62 215.55 232.82 211.84 241.03C210.66 243.69 209.37 246.27 207.86 248.75C207.03 250.13 200.99 251.3 201.59 250.35V250.35Z" fill="#0E1016"/>
<path d="M206.56 264.2C206.28 273.72 202.08 282.8 196.03 290.03C189.86 297.04 180.9 301.27 172.41 304.17C168.79 305.62 164.34 306.26 162.12 309.76C160.08 313.3 160.21 317.65 160.09 321.81C160.09 324.74 160.08 327.72 160.23 330.58C161.43 348.08 164.88 365.64 169.69 382.48C170.08 383.78 169.03 383.58 167.72 383.96C166.38 384.36 165.28 385.12 164.96 383.76C163.55 377.94 162.29 372.1 161.21 366.24C158.57 351.58 156.11 336.72 156.44 321.76C156.66 308.69 157.93 304.5 170.77 299.88C181.65 295.59 191.46 290.51 197.03 279.88C199.71 275.05 201.7 269.63 202.04 264.12C202.21 261.24 206.48 261.27 206.56 264.2V264.2Z" fill="#0E1016"/>
<path d="M93.48 234.12C93.73 240.88 87.12 247.03 80.29 247.16C69.72 247.96 62.98 241.65 59.08 232.57C53.73 220 49.86 206.15 39.65 196.52L37.97 194.82C39.9 194.78 42.07 194.65 43.97 194.93C45.31 195.12 46.55 195.35 47.79 196.08L45.87 196.91C47.36 185.87 50.93 175.28 55.66 165.22C56.66 163.22 58.31 167.64 58.09 168.11C57.18 170.08 56.32 172.11 55.47 174.13C52.3 181.59 50.09 189.43 48.33 197.33L47.94 199.09L46.42 198.16C44.68 197.08 42.32 196.9 40.29 196.75L40.92 194.98C46.76 199.2 51.29 205.08 54.89 211.23C58.48 217.39 61.42 223.92 64.44 229.98C67.3 235.49 70.71 240.54 77.29 240.96C78.23 241.08 79.27 241.13 80.25 241.18C85.58 241.41 90.11 238.18 93.47 234.11L93.48 234.12Z" fill="#0E1016"/>
<path d="M94.44 211.75C86.88 193.62 89.17 173.07 95.11 154.82C103.77 127.42 128.74 96.38 148.78 75.82L153.34 71.11C149.83 84.55 146.56 98.2 143.76 111.79C138.7 134.88 137.12 158.17 133.92 181.61C133.04 187.5 131.61 193.46 129.46 199.07C127.38 204.7 124.78 210.21 120.73 214.73C122.24 208.89 124.08 203.42 125.67 197.85C130.18 180.84 130.83 163.34 133.31 145.87C136.22 122.37 141.72 99.26 148.36 76.55L151.28 78.21C142.86 87.6 134.89 97.44 127.32 107.52C118.9 118.9 110.94 130.65 104.76 143.31C100.1 153.14 96.96 163.73 94.79 174.35C92.28 186.77 93.25 199.16 94.44 211.76V211.75Z" fill="#0E1016"/>
<path d="M169.57 136.05C175.94 138.15 182.34 140.66 188.17 144.02C189.66 144.88 191.13 145.81 192.52 146.84C193.01 147.21 192.22 151.49 191.46 150.79C191.16 150.49 190.71 150.13 190.28 149.79C187.3 147.51 184.02 145.67 180.65 144.01C176.88 142.13 172.56 140.42 168.58 139C166.65 138.32 167.62 135.42 169.58 136.04L169.57 136.05Z" fill="#0E1016"/>
<path d="M138.05 129.14C142.39 129.59 146.69 130.3 150.93 131.26C151.74 131.46 151.14 131.96 151.02 132.58C150.9 133.2 150.4 133.8 150.4 133.8C146.23 133 142.02 132.44 137.8 132.15C135.79 131.97 136.04 128.97 138.06 129.14H138.05Z" fill="#0E1016"/>
<path d="M90.91 137.05C96.16 134.38 101.68 132.69 107.38 131.36C109.08 131 109.68 133.52 107.99 133.96C105.12 134.66 102.28 135.49 99.51 136.5C96.75 137.51 93.97 138.65 91.5 140.06C89.8 140.99 90.63 137.2 90.92 137.06L90.91 137.05Z" fill="#0E1016"/>
<path d="M146.15 138.19C151.47 128.09 161.6 120.6 172.75 118.15C177.9 116.88 183.57 116.64 188.88 116.36C183.17 121.2 178.46 127.27 174.25 133.42C167.5 143.28 161.53 153.73 155.97 164.34C153.18 169.69 150.47 175.08 147.92 180.55C149.5 174.69 151.52 168.96 153.82 163.34C158.44 152.07 163.98 141.17 170.87 131.08C174.44 126 178.23 121 183.07 117.02L184.26 120.11C167.82 121.79 157.81 126.3 146.16 138.19H146.15Z" fill="#0E1016"/>
<path d="M91.42 147.31C89.53 138.42 89.26 128.97 91.62 120.13C92.39 117.16 93.35 114.29 94.6 111.43L97.2 113.33C83.34 126.58 73.01 144.8 71.93 164.2C71.01 176.11 74.51 187.14 77.99 198.6C73.14 192.07 70.27 184.26 68.95 176.25C64.5 149.31 79.41 122.62 100.24 106.34C97.16 113.67 94.33 121.71 92.99 129.54C92.04 135.38 91.71 141.34 91.43 147.3L91.42 147.31Z" fill="#0E1016"/>
<path d="M190.68 159.35C191.11 151.76 191.51 144.32 191.76 136.95C192.09 129.32 190.85 121.99 191.95 114.14C192.49 110.07 194.27 105.68 197.32 102.47C199.9 99.78 203.28 97.12 205.94 94.7L222.94 79.99C239.56 65.22 256.8 50.77 269.53 32.39C273.68 26.38 277.65 20 280.85 13.48L286.62 0.899994C284.13 25.59 280.06 50.47 273.55 74.4C272.18 79.34 270.32 85.48 268.73 90.35C262.96 108.04 254.15 124.8 242.93 139.53C238.49 145.47 233.7 151.14 228.61 156.51C223.51 161.85 218.31 167.2 211.86 171.02C216.5 165.26 221.55 160.06 226.32 154.46C241.47 136.43 254.42 116.16 262.13 93.89C263.82 89.16 265.77 83.12 267.11 78.31C273.07 57.27 277.39 35.66 280.69 14.07L284.91 15.21C283.42 18.86 281.73 22.22 279.91 25.6C274.43 35.68 267.81 45.13 260.06 53.61C244.48 70.3 227.35 84.84 210.18 99.63C207.61 101.94 203.26 105.13 201.18 107.64C199.4 109.76 198.39 112.44 197.87 115.16C196.62 122.2 197.51 129.99 196.58 137.31C195.58 144.88 193.91 152.39 190.71 159.35H190.68Z" fill="#0E1016"/>
<path d="M221.09 137.53C229.36 128.97 236.98 120.09 243.03 109.92C249.06 99.77 253.99 88.98 259.16 78.28C254.61 101.76 242.21 125.15 221.09 137.53V137.53Z" fill="#0E1016"/>
<path d="M60.12 171.02C48.94 159.62 39.65 146.97 29.61 134.77C27.41 132.29 26.31 128.5 26.28 125.38C26.2 117.69 28.85 110.66 29.17 103.06C30.06 84.92 27.59 66.47 21.66 49.31C15.83 32.44 8.18 15.87 0 0C37.22 30.27 68.76 66.02 80.62 113.63C81.83 118.44 82.77 123.33 83.24 128.36C83.37 129.78 78.14 134.2 78.2 132.66C78.27 120.6 75 108.38 71.04 96.95C58.85 61.64 33.36 32.66 4.87 9.23L7.86 6.75C18.45 26.52 28.51 47.19 32.63 69.45C34.63 80.66 35.61 92.02 34.98 103.42C34.82 107.17 34.11 111.13 33.38 114.8C32.53 120.39 30.41 126.77 33.95 131.36C39.04 138 43.93 144.83 48.81 151.63C53.07 157.78 57.2 164.07 60.11 171.02H60.12Z" fill="#0E1016"/>
<path d="M49.3 204.17C45.85 221.94 47.62 240.92 54.15 257.81L55.12 260.29L52.63 259.57C48 258.16 44.14 255.02 41.63 250.95L43.89 250.14C46.65 265.15 55.4 277.72 68.88 285.01C74.25 288.2 80.17 291.47 83.47 297.23C84.49 298.94 85.23 300.79 85.76 302.67C86.13 303.99 82.19 308.59 82.01 307.33C81.6 301.55 79.07 296.58 74.39 293.07C69.69 289.4 64.08 286.54 59.43 282.6C49.65 274.58 43.65 262.82 41.46 250.49L40.6 245.07C40.6 245.07 43.73 249.68 43.72 249.68C45.37 252.31 47.72 254.49 50.48 255.91C51.37 256.38 52.4 256.81 53.26 257.04L51.74 258.8C49.96 254.42 48.6 249.95 47.48 245.4C45.2 236.31 44.23 226.86 44.55 217.5C44.72 212.79 45.08 208.17 46.01 203.41C46.19 202.49 47.08 201.89 48 202.07C48.96 202.26 49.56 203.23 49.32 204.17H49.3Z" fill="#0E1016"/>
<path d="M59.37 221.16C56.34 230.59 57.63 245.33 63.41 253.53C66.75 258.09 71.98 260.61 77.47 261.82C80.41 262.53 83.63 262.96 86.41 264.39C88.54 265.46 90.56 267.05 91.96 269.05C92.6 269.96 88.87 277.55 89.11 275.91C89.31 274.59 89.27 273.66 89.1 272.94C88.45 269.98 85.71 267.78 82.92 266.67C77.98 264.98 72.66 264.42 67.97 261.72C63.75 259.51 60.29 255.81 58.09 251.58C53.52 241.65 51.9 230.29 54.11 219.53C54.74 217.29 56.2 218.25 57.6 219.19C58.52 219.81 59.85 219.71 59.36 221.16H59.37Z" fill="#0E1016"/>
<path d="M92.51 238.14C95.67 258.03 92.93 278.23 88.23 297.64C85.85 307.31 83.05 316.88 79.62 326.26C76.97 333.28 74.25 340.58 69.74 346.72C68.15 348.86 66.56 350.76 64.53 352.59C63.75 353.28 63.44 352.45 62.75 351.67C62.03 350.87 61.26 350.39 62.09 349.71C63.74 348.22 65.36 346.32 66.7 344.48C70.98 338.59 73.53 331.77 76.16 324.97C82.82 306.54 88.03 287.37 90.18 267.85C91.1 258.18 91.29 248.2 89.95 238.61C89.63 236.96 92.16 236.36 92.52 238.13L92.51 238.14Z" fill="#0E1016"/>
<path d="M69.41 241.41C69.41 241.41 69.04 245.47 70.17 250C71.39 254.86 75.08 257.82 75.08 257.82C75.08 257.82 78.48 255.21 79.18 251.19C79.94 246.82 79.41 244.18 79.41 244.18C79.41 244.18 74.6 243.8 73.37 243.35C72.14 242.9 69.42 241.41 69.42 241.41H69.41Z" fill="#0E1016"/>
<path d="M69.44 357.21C63.58 353.07 58.04 348.96 50.8 348.76C42.32 348.18 35.19 355.85 35.56 364.14C36.73 376.89 49.58 383.25 60.91 386.77C45.81 388.16 27.03 375.05 32.5 358.18C38.6 339.99 61.79 340.81 69.44 357.2V357.21Z" fill="#0E1016"/>
<path d="M103.71 359.12C109.74 348.5 125.26 337.31 137.58 344.5C142.98 347.63 145.85 354.16 145.29 360.18C144.26 371.87 133.37 380.55 122.59 383.13C131.05 377.83 140.28 370.34 141.5 359.89C142.24 353.34 137.81 347.13 131.2 346.41C127.95 345.91 124.74 346.95 121.7 348.08C115.05 350.55 109.44 354.83 103.71 359.13V359.12Z" fill="#0E1016"/>
<path d="M110.28 376.08C110.28 376.08 117.23 367.29 122.45 363.71C127.67 360.13 130.84 360.74 131.86 362.18C132.88 363.62 132.19 366.86 127.95 367.97C124.04 368.99 120.79 370.16 116.75 372.34C113.05 374.34 110.28 376.09 110.28 376.09V376.08Z" fill="#0E1016"/>
<path d="M61.25 376.88C59.3 373.19 53.13 367.52 48.37 365.59C43.61 363.66 42.31 366.66 42.67 368.17C43.16 370.22 45.72 370.99 50.75 372.5C56.67 374.28 61.24 376.88 61.24 376.88H61.25Z" fill="#0E1016"/>
<path d="M123.93 284.38C123.56 293.38 122.92 306.01 122.98 314.85C123.11 324.86 124.33 334.97 125.54 344.9C125.7 345.99 124.93 347.01 123.84 347.15C122.79 347.29 121.83 346.57 121.64 345.54C120.71 340.44 120.08 335.36 119.73 330.22C118.49 314.86 120.85 299.39 123.93 284.37V284.38Z" fill="#0E1016"/>
<path d="M143.62 341.89C143.21 335.24 143.71 328.69 144.43 322.08C145.36 314.87 146.28 306.7 151.64 301.24C148.35 307.79 147.79 315.12 147.17 322.34C146.69 329.5 146.57 336.89 147.48 343.96C147.54 344.42 147.46 344.81 147.29 345.12C147.29 345.12 143.69 342.73 143.64 341.88L143.62 341.89Z" fill="#0E1016"/>
<path d="M157.18 387.4C161.37 376.69 160.41 364.27 155.43 354.04C152.76 348.62 148.5 343.82 143.07 341.11C142.3 340.74 141.53 340.39 140.77 340.05C141.72 340.9 142.68 341.67 143.63 342.49C149.72 348.33 153.5 356.35 154.61 364.69C155.54 370.84 155.26 377.45 153.47 383.38C150.17 392.77 143.3 400.95 134.89 406.19C136.57 410.07 138.32 413.86 140.55 417.45C141.49 418.94 142.45 420.38 143.42 421.8C138.38 419.3 133.81 415.8 130.06 411.62C129.03 410.49 128.17 409.32 127.39 408.13C116.93 410.53 105 408.18 96.8 401.07C92.3 397.48 88.58 391.49 85.45 386.71L86.68 394.39C86.86 396.15 86.78 398.14 86.5 399.93C85.78 405.13 81.68 409.66 76.49 410.38L74.43 410.67C74.78 412.79 75.17 415.01 75.78 417.04C76.59 419.95 77.93 422.68 79.44 425.31C79.25 425.19 79.05 425.08 78.86 424.97C73.93 422.03 70.84 416.98 68.43 411.94L67.97 410.93L66.76 410.84C49.57 409.02 40.2 394.65 41.74 378.51L40.33 376.55C40.47 374.54 37.54 374.28 37.32 376.29C36.59 387.35 40.11 398.93 48.33 406.58C52.5 410.36 57.65 413.08 63.18 414.18C63.97 414.33 64.71 414.45 65.52 414.53C70.66 424.27 76.07 428.49 86.87 431.76L84.7 428.14C81.93 423.6 79.39 418.87 78.4 413.66C82.36 413.01 85.7 410.61 87.95 407.76C89.51 405.79 90.43 403.89 90.66 401.42C94.32 406.01 98.47 408.14 103.99 410.42C110.82 413.19 118.53 414.3 125.77 412.58C128.46 415.87 131.65 418.54 135.08 421.02C140.16 424.71 146.27 426.81 152.25 428.63C149.36 424.46 146.16 419.94 143.52 415.64C142.02 413.11 140.67 410.4 139.49 407.68C147.49 403.24 153.63 395.79 157.19 387.41L157.18 387.4Z" fill="#0E1016"/>
<path d="M216.62 166.67C229.07 181.17 238.86 197.71 248.05 214.38C306.9 323.51 329.29 447.36 349.07 568.49C350.61 577.83 352.12 587.18 353.87 596.42C354.14 597.76 353.26 599.08 351.91 599.33C350.57 599.58 349.27 598.7 349.02 597.36C347.28 587.98 345.79 578.62 344.29 569.27C325.02 448.49 303.04 325.05 245.19 215.94C238.46 203.5 231.14 191.37 222.92 179.88C220.18 176.1 217.29 172.27 214.21 168.83C213.62 168.17 214.62 168.55 215.29 167.96C215.95 167.37 216.02 166.01 216.62 166.68V166.67Z" fill="#0E1016"/>
<path d="M144.83 596.56C146.93 583.43 148.14 570.11 148.6 556.81C148.71 555.07 150.03 555.65 151.32 555.73C152.61 555.81 153.84 555.39 153.73 557.14C152.58 570.19 150.87 584.27 148.94 597.23C148.47 599.98 144.41 599.34 144.84 596.56H144.83Z" fill="#0E1016"/>
<path d="M216.55 216.68C251.43 271.93 276.5 332.66 298.69 393.91C320.2 455.25 339.67 517.79 350.82 581.93C351 583 350.27 584.02 349.19 584.19C348.12 584.36 347.11 583.64 346.93 582.58C344.21 566.63 340.67 550.81 336.82 535.06C329.14 503.78 319.67 472.3 309.67 441.66C286.67 372.56 259.96 304.27 223.75 240.94C220.71 235.71 217.59 230.47 214.35 225.4C213.26 223.69 215.27 215.52 215.27 215.52C215.73 215.76 216.17 216.14 216.56 216.68H216.55Z" fill="#0E1016"/>
<path d="M251.13 223.4C257.61 221.28 264.73 221.6 271.03 224.16C277.13 226.99 281.46 232.01 284.84 237.64L281.91 238.71C281.36 236.46 280.5 234.1 279.55 231.89C274.67 220.15 265.75 212.05 253.63 208.35C250.07 207.17 246.41 206.87 242.68 206.06C241.08 205.65 236.7 204.18 238.47 201.74C239.08 201.05 240.15 201 240.82 201.63C242.11 202.84 244.57 203.12 247.05 203.54C249.55 203.93 252.13 204.47 254.61 205.25C270.54 210.14 280.82 222.1 285 238.02L287.88 249.76C285.01 244.81 281.1 235.95 276.8 232.12C270.93 225.89 261.02 224.38 253.54 228.36C251.78 229.51 252.59 228.06 251.87 226.81C251.07 225.43 248.94 224.14 251.16 223.38L251.13 223.4Z" fill="#0E1016"/>
<path d="M302.76 349.1C303.87 351.94 305.94 354.64 307.77 357.14C312.85 363.95 318.98 369.75 325.21 375.52C331.43 381.73 336.37 389.19 339.63 397.37C342.27 403.96 343.29 411 343.27 418.13C343.27 418.13 342.85 432.46 342.85 432.47L339.88 418.43C338.68 412.88 336.36 407.24 333.16 402.47C328.3 395.35 321.05 389.65 312.77 387.14C311.86 386.88 310.87 386.64 310.08 386.54C309.02 386.41 308.27 385.44 308.4 384.37C308.54 383.23 309.66 382.46 310.77 382.73C312.99 383.28 314.82 384.02 316.82 384.93C330.23 391.05 339.59 403.58 343.23 417.7L339.84 417.99C339.91 414.77 339.65 411.36 339.12 408.1C337.51 398.28 332.59 389.17 326.11 381.67C323.89 379.1 321.55 376.92 318.96 374.65C313.77 370.01 309 364.96 304.67 359.5C302.76 357.04 300.86 354.53 299.43 351.71C299.21 351.3 299.01 350.76 298.82 350.23C298.44 349.17 299 348 300.06 347.62C301.21 347.2 302.45 347.9 302.74 349.08L302.76 349.1Z" fill="#0E1016"/>
<path d="M339.73 517.08C354.7 529.44 366.38 546.33 369.99 565.6C371.31 570.8 371.51 580.67 371.89 586.1L368.2 574.43C366.1 568.03 363.03 561.56 358.72 556.35C354.45 551.43 348.53 547.29 342.15 545.77C341.02 545.6 340.25 544.53 340.44 543.41C340.63 542.19 341.91 541.41 343.09 541.8C345.04 542.44 346.57 543.21 348.31 544.14C360.2 550.53 366.55 561.19 371.12 573.49L368.13 574.03C367.53 564.87 364.8 555.58 360.54 547.4C356.14 539.19 350.13 531.87 343.25 525.6C341.27 523.81 339.17 522.05 337.09 520.55C334.78 518.79 337.37 515.32 339.73 517.09V517.08Z" fill="#0E1016"/>
<path d="M180.24 328.93C162.69 328.93 157.99 326.88 157.74 326.76C157.11 326.47 156.84 325.73 157.13 325.1C157.42 324.48 158.15 324.2 158.78 324.49C158.94 324.56 167.84 328.14 205.35 325.39C229.46 323.62 258.8 318.91 262.16 311.45C262.44 310.82 263.18 310.54 263.81 310.82C264.44 311.1 264.72 311.84 264.44 312.47C261.16 319.76 239.69 325.38 205.54 327.88C195.05 328.65 186.76 328.93 180.25 328.93H180.24Z" fill="#0E1016"/>
<path d="M260.59 412.59C259.93 412.59 259.38 412.08 259.34 411.42C259.3 410.73 259.82 410.14 260.51 410.09C266.14 409.74 271.67 409.35 276.5 408.95C294.77 407.44 298.49 404.54 298.52 404.51C299.03 404.05 299.82 404.09 300.29 404.61C300.75 405.12 300.71 405.92 300.19 406.38C299.79 406.73 295.77 409.88 276.71 411.45C271.86 411.85 266.32 412.24 260.67 412.59C260.64 412.59 260.62 412.59 260.59 412.59V412.59Z" fill="#0E1016"/>
<path d="M260.52 516.12C259.86 516.12 259.31 515.61 259.27 514.94C259.23 514.25 259.76 513.66 260.45 513.62L262.95 513.47C306.52 510.87 326.49 508.83 329.61 503.61C329.96 503.02 330.73 502.82 331.32 503.18C331.91 503.53 332.11 504.3 331.75 504.89C328.05 511.08 311.36 513.08 263.09 515.96L260.58 516.11C260.58 516.11 260.53 516.11 260.51 516.11L260.52 516.12Z" fill="#0E1016"/>
<path d="M156.09 380.98L247.28 381.31H250.13C255.91 380.75 261.5 384.32 262.26 390.39C262.52 404.5 262.22 427.75 262.29 441.91C262.23 476.43 262.56 512.85 263.01 547.35C263.44 554.44 258.01 559.91 250.89 559.47C250.12 559.49 247.45 559.45 246.62 559.47C177.04 559.32 88.35 559.12 18.65 558.88L12.95 558.86C12.52 558.86 11.9 558.84 11.33 558.79C6.11 558.3 1.84 553.45 2.03 548.21C2.29 510.67 2.34 434.23 2.39 397.18C2.42 393.18 1.81 388.6 4.72 385.34C6.73 382.79 10.29 381.47 13.42 381.7H16.27L39.07 381.68C41 381.71 40.94 384.57 39.07 384.57L16.27 384.55H13.42C12.4 384.55 11.69 384.55 10.89 384.72C7.76 385.37 5.28 388.31 5.24 391.53C4.95 429.96 5.59 506.38 5.57 545.36V548.21C5.46 552.18 8.98 555.51 12.93 555.21C84.84 554.94 174.77 554.79 246.6 554.61H249.45H250.87H251.58H251.93L252.19 554.59C255.4 554.51 258.17 551.63 258.15 548.42C258.18 548.1 258.15 544.99 258.17 544.51C258.81 503.42 258.94 460.25 258.84 419.13L258.81 396.33V393.48V392.06C258.81 391.8 258.8 391.63 258.79 391.44C258.73 389.74 257.95 388.1 256.75 386.9C254.89 385.11 252.7 384.8 250.11 384.94H247.26L156.07 385.27C153.25 385.23 153.22 381.05 156.07 380.99L156.09 380.98Z" fill="#0E1016"/>
<path d="M95.56 466.74C68.17 444 38.65 418.54 11.55 395.26C31.76 411.09 56.6 430.52 76.55 446.44C83.73 452.18 90.93 457.89 98.05 463.71C99.03 464.53 97.86 465.08 97.24 465.84C96.62 466.6 96.58 467.55 95.56 466.74V466.74Z" fill="#0E1016"/>
<path d="M254.54 394.37C234.23 412.87 213.13 430.45 192.02 448C184.98 453.84 177.9 459.64 170.77 465.38C170.02 465.99 169.46 465.54 168.86 464.79C168.26 464.05 167.83 463.29 168.55 462.68C175.56 456.8 182.63 450.99 189.74 445.23C211.08 427.95 232.44 410.68 254.54 394.38V394.37Z" fill="#0E1016"/>
<path d="M173.2 476.48C173.46 504.82 144.19 524.28 118.15 513.22C103.63 507.24 93.55 492.19 93.57 476.48C93.34 448.05 122.41 428.72 148.55 439.84C163.03 445.87 173.24 460.72 173.21 476.48H173.2ZM170.01 476.48C170.11 443.49 130.79 427.67 107.54 450.65C84.52 473.95 100.44 513.03 133.37 513.1C153.11 513.28 170.16 496.22 170.02 476.47L170.01 476.48Z" fill="#0E1016"/>
<path d="M128.71 493.09L107.69 473.05L112.52 467.98L128.44 483.16L171.93 436.98L177.03 441.78L128.71 493.09Z" fill="#0E1016"/>
<path d="M374.54 599.9H123.19C121.53 599.9 120.19 598.56 120.19 596.9C120.19 595.24 121.53 593.9 123.19 593.9H374.54C376.2 593.9 377.54 595.24 377.54 596.9C377.54 598.56 376.2 599.9 374.54 599.9V599.9Z" fill="#ADD63D"/>
<path d="M256.25 316.08C257.98 315.21 258.92 314.05 259.1 312.39C259.28 310.73 258.63 309.3 258.08 308.1C257.53 306.9 259.88 308.05 261.1 310.52C262.32 312.99 262.79 313.82 261.32 314.64C259.85 315.46 256.26 317.07 256.26 317.07V316.07L256.25 316.08Z" fill="#0E1016"/>
<path d="M257.89 317.99C259.39 317.19 260.74 317.3 261.85 318.04C262.77 318.66 263.07 319.21 263.67 320.59C264.27 321.97 263.21 315.68 262.59 314.9C261.97 314.12 261.86 314.24 260.34 314.9C258.82 315.56 257.88 317.98 257.88 317.98L257.89 317.99Z" fill="#0E1016"/>
<path d="M293.86 406.31C294.6 406.1 295.48 405.59 295.86 404.44C296.24 403.29 295.97 401.99 295.71 401.27C295.45 400.55 297.48 401.79 297.97 403.63C298.46 405.47 299.63 406.21 297.84 406.62C296.05 407.03 293.86 406.31 293.86 406.31Z" fill="#0E1016"/>
<path d="M294.19 408.8C295.48 408.44 296.75 408.5 297.5 409.07C298.25 409.64 298.91 410.26 299.34 411.48C299.77 412.7 299.09 409.43 298.72 408.04C298.35 406.65 297.83 406.63 297.83 406.63L293.38 407.91L294.18 408.81L294.19 408.8Z" fill="#0E1016"/>
<path d="M325.22 506.58C326.09 506.23 327.08 505.43 327.63 504.06C328.04 503.01 328.01 501.67 327.63 500.31C327.25 498.95 329.31 502.16 329.63 503.62C329.95 505.08 330.17 504.81 328.8 505.94C327.43 507.07 325.22 506.79 325.22 506.79V506.58V506.58Z" fill="#0E1016"/>
<path d="M325.22 509.22C326.34 508.81 327.83 508.64 328.87 509.22C329.91 509.8 330.44 510.44 330.87 512.04C331.3 513.64 331.5 511.84 330.87 509.24C330.24 506.64 329.86 505.93 329.86 505.93L325.5 507.47L325.21 509.22H325.22Z" fill="#0E1016"/>
<path d="M160.14 319.82C160.36 315.77 164.42 312.72 170.07 309.16C176.37 305.2 187.01 300.42 195.02 291.13C201.23 283.92 181.86 296.89 175.75 299.76C169.64 302.63 164.89 303.62 161.84 307.12C158.79 310.62 159.06 317.26 159.06 317.26L160.14 319.82Z" fill="#0E1016"/>
<path d="M187.37 200.83C185.8 203.13 186.02 210.41 186.48 214.85C186.94 219.29 181.41 215.65 181.41 210.48C181.41 205.31 186.16 200.33 186.28 200.15C186.4 199.97 187.38 200.82 187.38 200.82L187.37 200.83Z" fill="#0E1016"/>
<path d="M116.76 345.98C118.33 345.1 120.96 342.41 120.49 338.1C120.02 333.79 123.03 338.28 123.13 341.07C123.23 343.86 125.83 343.67 122.15 345.33C118.47 346.99 116.77 345.98 116.77 345.98H116.76Z" fill="#0E1016"/>
<path d="M124.29 334.25C124.47 335.91 124.89 338.7 125.97 340.06C127.05 341.42 128.18 342.04 130.04 342.24C131.9 342.44 124.66 344.93 123.95 343.42C123.24 341.91 123.03 335.78 123.03 335.78L124.29 334.26V334.25Z" fill="#0E1016"/>
<path d="M59.38 278.35C62.08 280.6 66.34 283.52 72.05 284.99C77.76 286.46 81.26 287.84 81.26 287.84C81.26 287.84 77.43 287.27 73.6 287.02C73.03 286.98 72.6 287.24 73.41 287.75C75.25 288.89 70.34 287.51 66.18 285.15C61.29 282.38 59.38 278.35 59.38 278.35V278.35Z" fill="#0E1016"/>
</g>
<defs>
<clipPath id="clip0_118_2691">
<rect width="377.54" height="599.9" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 46 KiB

+15
View File
@@ -0,0 +1,15 @@
<svg width="411" height="263" viewBox="0 0 411 263" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M48 63H352V223C352 225.209 350.209 227 348 227H52C49.7909 227 48 225.209 48 223V63Z" fill="#ABABAB"/>
<path d="M200.095 0.768747L200 0.729733L199.905 0.768747L48.3506 63.0187L48.4456 63.5H351.554L351.649 63.0187L200.095 0.768747Z" fill="#4E4E4E" stroke="#4E4E4E" stroke-width="0.5"/>
<path d="M200 126L48.4456 63.75L351.554 63.75L200 126Z" fill="#4E4E4E"/>
<path d="M200 126L48.4456 63.75L351.554 63.75L200 126Z" fill="#4E4E4E"/>
<path d="M200 126L48.4456 63.75L351.554 63.75L200 126Z" fill="#4E4E4E"/>
<rect x="83" y="17" width="243" height="109" rx="4" fill="#D9D9D9"/>
<path d="M298.761 216.282L156.266 145.147L349.202 65.2163L298.761 216.282Z" fill="#ABABAB"/>
<path d="M112.866 218.509L57.4252 69.206L252.915 142.671L112.866 218.509Z" fill="#ABABAB"/>
<rect x="97" y="32" width="74" height="8" fill="#7A7A7A"/>
<rect x="97" y="52" width="39" height="8" fill="#7A7A7A"/>
<circle cx="205" cy="78" r="31" fill="#F0FF1E"/>
<rect x="195.249" y="90.937" width="36.4759" height="5.76933" rx="2.88466" transform="rotate(-45.3223 195.249 90.937)" fill="#ABABAB"/>
<rect x="198.151" y="94.2307" width="18.5987" height="5.76933" rx="2.88466" transform="rotate(-135 198.151 94.2307)" fill="#ABABAB"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+9
View File
@@ -0,0 +1,9 @@
<svg width="91" height="43" viewBox="0 0 91 43" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.9734 0C24.8526 0 27.4793 0.412021 29.8535 1.23606C32.2528 2.06011 34.4123 3.13386 36.3318 4.45732C38.2766 5.75581 39.9814 7.10424 41.4463 8.50261C42.2545 9.25174 42.987 9.98839 43.6436 10.7125C44.3003 11.4367 44.9317 12.1609 45.5379 12.885C46.0935 12.1609 46.6618 11.4617 47.2427 10.7875C47.8489 10.0883 48.6066 9.32665 49.5158 8.50261C51.7132 6.38008 54.4409 4.43235 57.699 2.65941C60.9824 0.886471 64.7835 0 69.1024 0C73.1435 0 76.8183 0.97387 80.127 2.92161C83.4609 4.84437 86.1002 7.42886 88.045 10.6751C90.015 13.9213 91 17.5171 91 21.4625C91 24.4591 90.4317 27.2683 89.2952 29.8902C88.1586 32.4872 86.5927 34.7721 84.5974 36.7448C82.6021 38.6925 80.2785 40.2282 77.6266 41.3519C74.9746 42.4506 72.1332 43 69.1024 43C66.2231 43 63.5712 42.6005 61.1465 41.8014C58.7472 40.9774 56.5751 39.9286 54.6303 38.6551C52.7108 37.3815 51.0186 36.0706 49.5537 34.7221C48.7202 33.8981 47.9752 33.124 47.3185 32.3998C46.6871 31.6507 46.0935 30.9141 45.5379 30.1899C44.9065 30.9141 44.2624 31.6507 43.6057 32.3998C42.9491 33.149 42.2166 33.9231 41.4084 34.7221C39.9688 36.0706 38.2766 37.3815 36.3318 38.6551C34.4123 39.9036 32.2528 40.9399 29.8535 41.7639C27.4793 42.588 24.8526 43 21.9734 43C17.8818 43 14.1817 42.0386 10.873 40.1159C7.56439 38.1931 4.92506 35.6086 2.95504 32.3624C0.985013 29.0912 0 25.4579 0 21.4625C0 18.491 0.555648 15.7192 1.66694 13.1472C2.8035 10.5502 4.36941 8.26539 6.3647 6.29269C8.38523 4.31998 10.7215 2.78427 13.3734 1.68554C16.0507 0.561848 18.9173 0 21.9734 0ZM10.4563 21.4625C10.4563 23.5351 10.974 25.4204 12.0096 27.1185C13.0451 28.8165 14.4342 30.1649 16.1769 31.1638C17.9197 32.1626 19.8518 32.662 21.9734 32.662C24.3475 32.662 26.5322 32.1376 28.5275 31.0889C30.5228 30.0401 32.3791 28.7166 34.0966 27.1185C35.1826 26.0947 36.1171 25.1083 36.9001 24.1594C37.683 23.2105 38.365 22.3116 38.9459 21.4625C38.3397 20.6635 37.6199 19.777 36.7864 18.8031C35.9782 17.8043 35.0816 16.8554 34.0966 15.9564C32.4802 14.3833 30.649 13.0598 28.6032 11.9861C26.5575 10.8873 24.3475 10.338 21.9734 10.338C19.8518 10.338 17.9197 10.8499 16.1769 11.8737C14.4342 12.8725 13.0451 14.221 12.0096 15.919C10.974 17.592 10.4563 19.4399 10.4563 21.4625ZM80.4679 21.4625C80.4679 19.4399 79.9502 17.592 78.9146 15.919C77.9044 14.221 76.5405 12.8725 74.8231 11.8737C73.1056 10.8499 71.1987 10.338 69.1024 10.338C67.486 10.338 65.9453 10.5877 64.4804 11.0871C63.0155 11.5865 61.639 12.2607 60.351 13.1098C59.0881 13.9588 57.9263 14.9077 56.8655 15.9564C55.729 17.0052 54.7313 18.079 53.8726 19.1777C53.0139 20.2515 52.3951 21.0131 52.0162 21.4625C52.6477 22.3365 53.3549 23.248 54.1378 24.1969C54.9208 25.1208 55.83 26.0947 56.8655 27.1185C58.5577 28.7166 60.4015 30.0401 62.3968 31.0889C64.4173 32.1376 66.6525 32.662 69.1024 32.662C71.1987 32.662 73.1056 32.1626 74.8231 31.1638C76.5405 30.1649 77.9044 28.8165 78.9146 27.1185C79.9502 25.4204 80.4679 23.5351 80.4679 21.4625Z" fill="url(#paint0_linear_491_1466)"/>
<defs>
<linearGradient id="paint0_linear_491_1466" x1="46" y1="-1.40259e-06" x2="66" y2="80.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#D9E8FF"/>
<stop offset="1" stop-color="white" stop-opacity="0.36"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

+9
View File
@@ -0,0 +1,9 @@
<svg viewBox="0 0 67 67" xmlns="http://www.w3.org/2000/svg">
<path d="M22.21 67V44.6369H0V67H22.21Z" fill="#ffffff"/>
<path d="M0 44.6369L22.21 46.8285V44.6369H0Z" fill="#DDDDDD"/>
<path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="#ffffff"/>
<path d="M22.21 0H0V22.3184H22.21V0Z" fill="#ffffff"/>
<path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="#ffffff"/>
<path d="M66.6753 22.3185L44.5098 20.0822V22.3185H66.6753Z" fill="#DDDDDD"/>
<path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 576 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 120 60" width="120" height="60" style="enable-background:new 0 0 120 60;" xml:space="preserve">
<style type="text/css">
.st0{fill:#808285;}
.st1{fill:url(#SVGID_1_);}
.st2{fill:#005B99;}
.st3{enable-background:new ;}
.st4{fill:#77787B;}
</style>
<g>
<path class="st0" d="M34.1,42.6h9.4v-3.2C41.5,41.2,38.2,42.3,34.1,42.6L34.1,42.6z"/>
<path class="st0" d="M17.3,40.1v2.5h11.9c-2.4-0.2-5-0.6-7.6-1.2C20.1,41,18.7,40.6,17.3,40.1L17.3,40.1z"/>
<radialGradient id="SVGID_1_" cx="-183.5882" cy="811.1324" r="47.498" gradientTransform="matrix(1 0 0 1 241.0864 -776.3726)" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#0095DA"/>
<stop offset="0.21" style="stop-color:#0095DA"/>
<stop offset="0.33" style="stop-color:#00ACE4"/>
<stop offset="0.9045" style="stop-color:#005093"/>
<stop offset="0.9335" style="stop-color:#005396"/>
<stop offset="0.954" style="stop-color:#005C9F"/>
<stop offset="0.9718" style="stop-color:#006BAE"/>
<stop offset="0.988" style="stop-color:#0080C4"/>
<stop offset="1" style="stop-color:#0095DA"/>
</radialGradient>
<path class="st1" d="M43.5,18.5H29.2C23,17.1,15.4,17.1,10,18.2c2.4-0.3,5.1-0.3,7.8,0.1c0.7,0.1,1.3,0.2,2,0.3l0,0
c2.9,0.5,5.7,1.2,8.1,2.2l0,0c0.3,0.1,0.5,0.2,0.8,0.3h0c0.1,0,0.2,0.1,0.3,0.1h0c0.1,0,0.2,0.1,0.3,0.1h0c0.1,0,0.2,0.1,0.2,0.1
l0,0c0.1,0,0.1,0.1,0.2,0.1l0,0c0.1,0,0.1,0.1,0.2,0.1l0,0c0.1,0,0.2,0.1,0.2,0.1l0,0l0,0h0c0.1,0,0.1,0.1,0.2,0.1l0,0
c0.1,0,0.1,0.1,0.2,0.1l0,0c0.1,0,0.1,0.1,0.2,0.1l0,0c0.1,0,0.1,0.1,0.2,0.1l0,0c0.1,0,0.1,0.1,0.2,0.1l0,0c0.1,0,0.1,0.1,0.2,0.1
l0.1,0c0.1,0,0.1,0.1,0.2,0.1l0.1,0c0.1,0,0.1,0.1,0.2,0.1l0.1,0c0.1,0,0.1,0.1,0.2,0.1l0.1,0c0.1,0,0.1,0.1,0.1,0.1l0.1,0.1
c0.1,0,0.1,0.1,0.1,0.1l0.1,0.1c0.1,0,0.1,0.1,0.1,0.1l0.1,0.1c0,0,0.1,0.1,0.1,0.1c3.2,2.2,5.1,4.9,5.1,7.6c0,3.1-2.6,5.7-6.8,7.2
c2.9-1.5,4.6-3.6,4.6-6.1c0-3.1-2.7-6.3-7-8.7c-0.1,0-0.1-0.1-0.2-0.1c-0.1,0-0.1-0.1-0.2-0.1c-0.1,0-0.1-0.1-0.2-0.1
c-0.1,0-0.1-0.1-0.2-0.1c-0.1,0-0.1-0.1-0.2-0.1c-0.1,0-0.1-0.1-0.2-0.1c-0.1,0-0.1-0.1-0.2-0.1l-0.2-0.1c-0.1,0-0.2-0.1-0.2-0.1
l-0.1-0.1c-0.1,0-0.1-0.1-0.2-0.1c-0.1,0-0.1-0.1-0.2-0.1c-0.1,0-0.1-0.1-0.2-0.1c-0.1,0-0.1-0.1-0.2-0.1c-0.1,0-0.1-0.1-0.2-0.1
L26,22c-0.1,0-0.2-0.1-0.3-0.1h0l-0.2-0.1l0,0c-0.1,0-0.2-0.1-0.3-0.1l-0.1,0c-0.1-0.1-0.2-0.1-0.4-0.1h0c-0.1,0-0.2-0.1-0.3-0.1h0
c-0.1-0.1-0.3-0.1-0.5-0.2l-0.1,0c-0.1-0.1-0.3-0.1-0.5-0.1h0c-0.3-0.1-0.5-0.2-0.8-0.3l0,0c-0.3-0.1-0.6-0.2-0.8-0.3l0,0
c-0.2-0.1-0.3-0.1-0.5-0.1l0,0c-0.7-0.2-1.5-0.4-2.3-0.5l0,0c-0.6-0.1-1.2-0.2-1.8-0.3v19.2c0.2,0.1,0.4,0.1,0.6,0.1
C30.3,42,41,39.4,41.8,33.2c0.4-3.3-2-6.9-6.1-10c3.3,1.9,6,4.1,7.8,6.4c1,1.4,1.7,2.7,2,4.1c-0.1-1.9-0.9-4.8-2-6.9L43.5,18.5
L43.5,18.5z"/>
<g>
<path class="st2" d="M67.5,26.9c0,1-0.2,2-0.7,2.9c-0.4,0.9-1,1.7-1.8,2.3c-0.8,0.7-1.7,1.2-2.8,1.6c-1.1,0.4-2.2,0.6-3.5,0.6H49
v-9h4.5v5.3h5.2c0.6,0,1.2-0.1,1.7-0.3c0.5-0.2,1-0.4,1.4-0.7c0.4-0.3,0.7-0.7,0.9-1.1c0.2-0.4,0.3-0.9,0.3-1.4
c0-0.5-0.1-1-0.3-1.4c-0.2-0.4-0.5-0.8-0.9-1.1c-0.4-0.3-0.8-0.6-1.4-0.7c-0.5-0.2-1.1-0.3-1.7-0.3H49l2.9-3.8h6.8
c1.3,0,2.4,0.2,3.5,0.5c1.1,0.3,2,0.8,2.8,1.5s1.4,1.4,1.8,2.3C67.2,24.9,67.5,25.8,67.5,26.9z"/>
<g>
<path class="st2" d="M82.8,21.4v6.8l-8.9-8c-0.4-0.3-0.7-0.5-1-0.6c-0.3-0.1-0.6-0.1-0.8-0.1c-0.3,0-0.6,0.1-0.9,0.1
c-0.3,0.1-0.6,0.3-0.8,0.5c-0.2,0.2-0.4,0.5-0.5,0.8c-0.1,0.3-0.2,0.8-0.2,1.2v12h4.1v-8.5l8.9,8c0.3,0.3,0.7,0.5,0.9,0.6
c0.3,0.1,0.6,0.1,0.8,0.1c0.3,0,0.6-0.1,0.9-0.1c0.3-0.1,0.6-0.3,0.8-0.5c0.2-0.2,0.4-0.5,0.5-0.8c0.1-0.3,0.2-0.8,0.2-1.2V19.9
L82.8,21.4z"/>
</g>
<path class="st2" d="M103,25.5c1.8,0,3.1,0.3,4,1c0.9,0.7,1.4,1.6,1.4,3c0,0.7-0.1,1.4-0.3,2c-0.2,0.6-0.6,1.1-1.1,1.5
c-0.5,0.4-1.2,0.7-1.9,0.9c-0.8,0.2-1.7,0.3-2.8,0.3H88.7l2.9-3.7h11c0.5,0,0.9-0.1,1.2-0.3c0.3-0.2,0.4-0.4,0.4-0.8
s-0.1-0.7-0.4-0.8c-0.3-0.2-0.6-0.2-1.2-0.2h-7.9c-0.9,0-1.8-0.1-2.4-0.3c-0.7-0.2-1.2-0.5-1.7-0.8c-0.5-0.4-0.8-0.8-1-1.3
c-0.2-0.5-0.3-1.1-0.3-1.7c0-0.7,0.1-1.3,0.4-1.9c0.2-0.6,0.6-1,1.1-1.4c0.5-0.4,1.1-0.7,1.9-0.9c0.8-0.2,1.7-0.3,2.8-0.3h12.6
l-2.9,3.8H95.1c-0.5,0-0.9,0.1-1.2,0.2c-0.3,0.1-0.4,0.4-0.4,0.8c0,0.4,0.1,0.6,0.4,0.8c0.3,0.1,0.6,0.2,1.2,0.2L103,25.5
L103,25.5z"/>
</g>
<g class="st3">
<path class="st4" d="M57.5,36.6v5h-1.4v-2.7v-0.7l0.1-0.4v-0.4h-0.1l-0.2,0.3l-0.2,0.3l-0.4,0.7l-1.7,2.8h-1.3l-1.7-2.8l-0.4-0.7
l-0.2-0.3l-0.2-0.3h-0.1l0,0.4v0.4l0.1,0.7v2.7h-1.5v-5h2.4l1.4,2.3l0.4,0.7l0.2,0.3l0.2,0.3H53l0.2-0.3l0.2-0.3l0.4-0.7l1.4-2.3
L57.5,36.6L57.5,36.6z"/>
<path class="st4" d="M63.6,40.6h-3.3l-0.5,0.9h-1.6l2.6-5H63l2.6,5H64L63.6,40.6z M63.2,39.9l-1.3-2.6l-1.3,2.6H63.2z"/>
<path class="st4" d="M66.2,41.6v-5H70c1,0,1.8,0.2,2.2,0.5s0.7,0.8,0.7,1.6c0,0.7,0,1.2-0.1,1.5c0,0.3-0.2,0.6-0.5,0.9
c-0.3,0.3-1,0.5-2.3,0.5H66.2L66.2,41.6z M67.7,40.7h2.1c0.7,0,1.2-0.1,1.4-0.3s0.3-0.7,0.3-1.4c0-0.7-0.1-1.2-0.3-1.4
s-0.6-0.3-1.3-0.3h-2.2L67.7,40.7L67.7,40.7z"/>
<path class="st4" d="M75.3,37.4v1.3h3.6v0.7h-3.6v1.4h3.8v0.8h-5.3v-5h5.3v0.8L75.3,37.4L75.3,37.4z"/>
<path class="st4" d="M84.4,37.4v1.3H88v0.7h-3.6v1.4h3.8v0.8H83v-5h5.3v0.8L84.4,37.4L84.4,37.4z"/>
<path class="st4" d="M94,40.6h-3.3l-0.5,0.9h-1.6l2.6-5h2.2l2.6,5h-1.5L94,40.6z M93.7,39.9l-1.3-2.6L91,39.9H93.7z"/>
<path class="st4" d="M102.4,38.1H101v-0.1c0-0.3-0.1-0.4-0.3-0.5c-0.2-0.1-0.6-0.1-1.2-0.1c-0.7,0-1.2,0-1.4,0.1
c-0.2,0.1-0.3,0.3-0.3,0.5c0,0.3,0.1,0.5,0.3,0.6s0.8,0.1,1.7,0.1c1.1,0,1.9,0.1,2.2,0.3c0.3,0.2,0.5,0.5,0.5,1.1
c0,0.7-0.2,1.1-0.6,1.3s-1.2,0.3-2.5,0.3c-1.3,0-2.2-0.1-2.5-0.3c-0.4-0.2-0.6-0.6-0.6-1.2V40h1.4v0.1c0,0.3,0.1,0.5,0.3,0.6
c0.2,0.1,0.7,0.1,1.5,0.1c0.7,0,1.1,0,1.2-0.1s0.3-0.3,0.3-0.6c0-0.3-0.1-0.4-0.2-0.5c-0.1-0.1-0.4-0.1-0.9-0.1l-0.8,0
c-1.2,0-2-0.1-2.3-0.3c-0.4-0.2-0.5-0.6-0.5-1.1c0-0.6,0.2-1,0.6-1.2c0.4-0.2,1.2-0.3,2.5-0.3c1.1,0,1.9,0.1,2.3,0.3
c0.4,0.2,0.6,0.5,0.6,1L102.4,38.1L102.4,38.1z"/>
<path class="st4" d="M110,36.6l-2.9,3.1v1.9h-1.5v-1.9l-2.8-3.1h1.7l1.2,1.3l0.3,0.4l0.2,0.2l0.2,0.2l0.2-0.2l0.2-0.2l0.3-0.4
l1.2-1.3H110z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Some files were not shown because too many files have changed in this diff Show More