mirror of
https://github.com/luxfi/aml.git
synced 2026-09-24 00:25:20 +00:00
brand: prove the bearer scheme is compared, and name what the pin does not settle
A negative row presented the token as "Basic <jwt>", six characters where Bearer is seven, so a verifier that never compared the scheme would still have refused it — on a token it had read one character into. Digest is exactly as long, so the row now fails for the reason it claims to test. Killing that mutant is what showed the test was passing on the wrong evidence. The issuer pin stops another brand's token; it does not make org names unique across issuers, and the tenant here IS the org name. Stated where the decision lives: one record plane serving several brands has to scope by brand as well as by org, one plane per brand does not. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -14,7 +14,8 @@ Real-time AML/CFT transaction monitoring engine. Pure Go, single binary, embedde
|
||||
- **Scoring**: Weight-of-evidence (pure Go math)
|
||||
- **Sanctions**: Jaro-Winkler + token-based fuzzy name matching
|
||||
- **HTTP**: Hanzo Base router (net/http)
|
||||
- **Auth**: X-Org-Id header (set by gateway from JWT owner claim)
|
||||
- **Auth**: IAM access token — the `owner` claim of a bearer JWT the request's own brand issued (`api.IAMIdentity`); or, behind an authenticating gateway, the header it writes from that same claim (`api.TrustedProxyHeader`)
|
||||
- **White-label**: brand per request Host (`pkg/brand`, mirroring HIP-0111) — one `amld` serves lux.network, hanzo.ai, zoo.ngo, pars.network
|
||||
|
||||
## Build & Run
|
||||
|
||||
@@ -92,6 +93,12 @@ pkg/
|
||||
api/routes.go -- /v1/aml/* HTTP routes + SanctionsStore on Base
|
||||
api/records.go -- The one place retention, token and replay are joined
|
||||
api/anomaly.go -- Model state for governance + candidate scoring
|
||||
api/iam.go -- Identity from an IAM token: brand of the Host pins the
|
||||
issuer, `owner` claim is the tenant, JWKS with a
|
||||
bounded cache
|
||||
api/brand.go -- GET /v1/aml/config: the brand identity of this Host
|
||||
brand/brand.go -- Brand id and request Host -> issuer + domains
|
||||
(HIP-0111; the same registry as hanzoai/cloud brand)
|
||||
```
|
||||
|
||||
## Behavioural detection
|
||||
@@ -171,12 +178,33 @@ scores, learns and reports what it *would* have alerted on, changing nothing.
|
||||
| POST | /v1/aml/relationships/search | Art. 78 five-year lookback by party |
|
||||
| POST | /v1/aml/sanctions/search | Search sanctions lists by name |
|
||||
| GET | /v1/aml/health | Health check, 503 when records cannot be kept |
|
||||
| GET | /v1/aml/config | Brand identity of the request's Host: brand, display name, issuer, domain |
|
||||
|
||||
Every route resolves its tenant through `Handler.Identity`. The deployment wires
|
||||
`api.TrustedProxyHeader("X-Org-Id")`, which is sound only because the gateway
|
||||
authenticates the caller, sets the header from the verified JWT owner claim,
|
||||
Every route resolves its tenant through `Handler.Identity`, and `/v1/aml/config`
|
||||
is the one exception: it names the issuer a caller needs in order to obtain a
|
||||
token, so requiring one would be a lock whose key is behind it.
|
||||
|
||||
Two identities, one seam. `api.IAMIdentity(api.JWKS(ttl, stale))` takes the tenant
|
||||
from the `owner` claim of a bearer token, verified against the JWKS of the issuer
|
||||
belonging to the request's own Host — so a Lux token does not authenticate on a
|
||||
Zoo host even where one in-cluster IAM publishes both brands' signing certificates.
|
||||
`api.TrustedProxyHeader("X-Org-Id")` takes it from the header a gateway writes from
|
||||
that same claim, which is sound only where the gateway authenticates the caller,
|
||||
strips any client-supplied copy, and is the only route to this service.
|
||||
|
||||
## White-label
|
||||
|
||||
`pkg/brand` maps a brand id, and a request Host, to that brand's public identity:
|
||||
its OIDC issuer and the domains it serves on. It is a copy of the fleet's canonical
|
||||
registry (HIP-0111, `hanzoai/cloud` `brand/brand.go`) kept as a leaf, so one
|
||||
`amld` answers as Lux on lux.network, as Zoo on zoo.ngo and zoo.cloud, as Hanzo on
|
||||
hanzo.ai, and as Pars on pars.network — brand, console identity and trusted issuer
|
||||
all from the Host. An unrecognised Host falls back to Hanzo.
|
||||
|
||||
Org names are unique within an issuer, not across issuers. One deployment serving
|
||||
more than one brand from one record plane therefore needs its store scoped by
|
||||
brand as well as by org; a deployment per brand needs nothing further.
|
||||
|
||||
## Records, tokenisation, and the sandbox
|
||||
|
||||
Three obligations, three packages, one join in `pkg/api/records.go`.
|
||||
|
||||
@@ -21,6 +21,13 @@ package api
|
||||
// This is stricter than the fleet edge (hanzoai/cloud auth_identity.go), which
|
||||
// trusts the union of brand issuers because one binary serves every brand's apps;
|
||||
// an AML record plane answers for one brand at a time, so it pins.
|
||||
//
|
||||
// What the pin does not settle: an org name is unique within an issuer, not across
|
||||
// issuers, and the tenant returned here is the org name. A deployment serving more
|
||||
// than one brand out of ONE record plane therefore has to scope that plane by
|
||||
// brand as well as by org — otherwise two brands' orgs of the same name are one
|
||||
// tenant. A deployment per brand needs nothing further, and that is the shape this
|
||||
// runs in today.
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
|
||||
@@ -283,6 +283,15 @@ func TestIAMIdentityRefusesEveryDoubt(t *testing.T) {
|
||||
r.Header.Set("Authorization", "Basic "+valid)
|
||||
return r
|
||||
}(),
|
||||
// Digest is exactly as long as Bearer, so this row fails only if the scheme
|
||||
// is actually compared. With "Basic" alone a verifier that skipped the
|
||||
// compare would still refuse — it would read one character into the token
|
||||
// and reject a mangled one — and the test would pass for the wrong reason.
|
||||
func() *http.Request {
|
||||
r := bearing("api.hanzo.ai", "")
|
||||
r.Header.Set("Authorization", "Digest "+valid)
|
||||
return r
|
||||
}(),
|
||||
} {
|
||||
if org, err := id(r); err == nil {
|
||||
t.Errorf("a token outside the bearer header authenticated as %q", org)
|
||||
|
||||
Reference in New Issue
Block a user