mirror of
https://github.com/luxfi/aml.git
synced 2026-09-24 00:25:20 +00:00
11a44f6010f9bb7920eb6de0546d14e3b79e662e
The tenant key was the bare org, and an org name is unique within an issuer
rather than across issuers. `acme` on lux.id and `acme` on zoolabs.id are two
unrelated financial institutions, and every store keyed them as one — including
the tokenisation vault, where the org WAS the HKDF salt (pkg/token New). Two
brands therefore derived the same keys: one institution's customer names
tokenised to the other's pseudonyms, and its sealed records opened under the
other's tenant. That is a plaintext cross-brand disclosure of retained personal
data, against the purpose limitation retention exists under (Directive (EU)
2015/849 Art. 41(2)) and against HIP-0302. It is fixed now, before any record
exists, because doing it later re-keys every vault.
The tenant is now `<brand>/<org>`, minted in one place (api/tenant.go qualify)
and checked where it crosses into the store, the history column and the salt.
Both Identity implementations produce the same key: the brand comes from the
request's own Host in both, never from a header a client can write.
The auth path is now the caller that refuses rather than defaulting:
- brand.ForHost is gone, with brand.Default and brand.IssuerFor. While a
defaulting resolver existed the auth path used it, so hanzo.id
authenticated callers on every Host nobody claimed — a pod IP, a
*.svc.cluster.local name, localhost through a port-forward, an absent Host,
a misrouted vhost. brand.For reports ok, and there is no fallback to reach.
- `aud` must contain this deployment's IAM clientId (AML_CLIENT_ID, no
default, refuses to start without it). IAM stamps aud = the app's clientId,
so with no pin any first-party token from the same issuer was a credential
here, including one minted for a marketing site. RFC 9068 §4.
- `tokenType` must be "access-token". An id_token carries the same iss and
aud and is issued to a browser, not to an API caller.
- `owner` must be an org the caller is a member of, when the token carries a
membership set. IAM stamps the APP's org as `owner` (internal/oidc/jwt.go
Sign), so a shared or org-choice application would make every one of its
users one tenant. IAM refuses a cross-org login for a dedicated app
(internal/oidc/login.go), which is what makes the claim correct for the
shape this supports: one application per institution.
- a bearer that is not a compact JWS is refused before any keyset is
resolved, so an unauthenticated caller cannot spend a network round trip.
Keys, in api/jwks.go with the algorithms that accept them, because the two
lists have to agree:
- one lock per issuer, taken outside the map, so an issuer that does not
answer holds up only itself. One process-wide mutex held across the HTTP
fetch meant a single unreachable issuer — selected by a caller's Host —
serialised every request in the process behind it.
- a failed fetch is remembered briefly, so an unreachable issuer costs one
attempt per window instead of one per request.
- EC (ES256/384/512) and ML-DSA-65 verify, which is what IAM publishes
besides RSA (internal/oidc/jwks.go signingAlgs). A dropped key type is a
silent lockout of a whole brand, so a key that cannot be loaded now names
its kty and alg in the log.
Also here: caseOf returned refuse()'s value as an error, and refuse() returns
nil once it has answered the request — so an unauthenticated POST to a case
route dereferenced a nil case. It reports errNoCase and writes nothing.
Transaction.TenantID is deleted: a second tenant field nothing reads is an
invitation to scope data by a value the caller supplies.
Co-authored-by: Hanzo Dev <[email protected]>
Description
Real-time AML/CFT transaction monitoring, sanctions screening and case management
2.1 MiB
Languages
Go
88.1%
TypeScript
9.3%
CSS
1.2%
Python
0.7%
JavaScript
0.4%
Other
0.2%