toga-ai 1.0.775 → 1.0.776
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -6,7 +6,7 @@ project: SAML SSO Gateway
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-04
|
|
10
10
|
owners: ["rgirish", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- saml/index.php
|
|
@@ -49,7 +49,7 @@ Legacy `/?meta`, `/?acs`, `/?sls` query-style paths are rewritten automatically.
|
|
|
49
49
|
2. **Version + time check** — RelayState JSON: `v` (currently `1`), `time`, `domain` UUID, optional `urlParameters`. Validated against `MAX_TIME_TO_LIVE` (300s).
|
|
50
50
|
3. **Parse SAMLResponse** — base64-decoded, deserialized via LightSaml.
|
|
51
51
|
4. **Status check** — proceeds only on `STATUS_SUCCESS`.
|
|
52
|
-
5. **Assertion decryption
|
|
52
|
+
5. **Assertion decryption (dual-credential during cert overlap).** `/acs` builds two `X509Credential`s from the SP encryption keys in `_underscore/Assets/ssl/` (new + old) and calls `$reader->decryptMultiAssertion([$credentialNew, $credentialOld], $ctx)`. LightSaml's `decryptMulti()` tries each credential and only re-throws once all fail (fails closed), so an assertion encrypted to either the new or old SP key decrypts during a rotation overlap. See the SP cert-rotation workflow.
|
|
53
53
|
6. **Resolve client + environment** — `_Model_Core_Domain` by RelayState `domain` UUID → `_Model_Core_Client` + `_Model_Core_Environment`. Dev on real AWS instance downgrades to beta.
|
|
54
54
|
7. **Dynamic DB registration** — SQL joins `Clients`, `Databases`, `DatabaseHosts`, `Environments` for the environment slug. Calls `_Database::register()` + `connect()` for `DB_CLIENT`, `DB_CLIENT_LOGS`, `DB_CLIENT_ARCHIVE`.
|
|
55
55
|
8. **Identity mapping** — `_Model_<ClientIdentifier>_ClientAuthentication::getAuthenticatedSsoUser($assertion)`. Wrapped in try/catch; exceptions sent to Sentry.
|
|
@@ -59,6 +59,10 @@ Legacy `/?meta`, `/?acs`, `/?sls` query-style paths are rewritten automatically.
|
|
|
59
59
|
### Failure responses
|
|
60
60
|
`Invalid SAMLResponse (Code = 1–4)`, `SAML Authentication Failed`, `User Authentication Failed`.
|
|
61
61
|
|
|
62
|
+
## /meta SP credential (signing vs encryption)
|
|
63
|
+
|
|
64
|
+
`/meta` publishes TWO independent `KeyDescriptor`s: `use="signing"` and `use="encryption"`. They can carry **different** certs, which lets the SP encryption cert rotate without touching the signing cert. The private key that signs our **outbound** AuthnRequests lives in `_underscore` (`_Model_Core_ClientAuthentication::PATH_TO_PRIVATE_KEY`) and is **global — one key for all client IdPs**, so the signing cert cannot flip until every IdP has loaded the new metadata. Cert paths are set via class constants in `saml/Controller/Index.php` (`PATH_TO_SIGNING_CERTIFICATE`, `PATH_TO_ENCRYPTION_CERTIFICATE_NEW/OLD` + matching private-key constants — paths only, no key material). Full procedure: SP cert-rotation workflow.
|
|
65
|
+
|
|
62
66
|
## Per-client SSO extension pattern
|
|
63
67
|
|
|
64
68
|
Identity mapping lives in `_underscore/Model/<ClientIdentifier>/ClientAuthentication.php`.
|
|
@@ -90,6 +94,7 @@ To onboard a new SSO client, add the class in `_underscore` — not in this repo
|
|
|
90
94
|
- **`_Database::register()` auto-starts a lazy transaction (since Apr 2 2026).** Any code that registers DBs then writes must call `_Database::transactionCommit()` before exiting — otherwise MySQL silently rolls back all writes on connection close. The `/acs` handler calls `transactionCommit()` after `getAuthenticatedSsoUser()` succeeds and before the redirect. See `_underscore/Database.php:48`.
|
|
91
95
|
- **SAML signature verification is commented out — this is the repo's top security priority.** The IdP X509 cert verification block in the `/acs` flow is disabled, so the gateway accepts any well-formed, status-success `SAMLResponse` without proving it was signed by the expected IdP. The only remaining gate is the encrypted RelayState, which authenticates the *originating TOGa request* — not the *asserting IdP*. Threat model: an attacker who can craft or replay a `SAMLResponse` (or a malicious/compromised IdP) can forge an identity assertion for any user, since nothing binds the assertion to a trusted signing key. Re-enabling per-IdP X509 signature verification is the single highest-impact security fix for this repo.
|
|
92
96
|
- **Plaintext secrets in source — move to AWS SSM Parameter Store.** `saml/Config/production.ini` stores credentials in plaintext, and a Sentry DSN is hardcoded in `saml/Controller/Index.php`. Both are checked into the repo. Migrate these to AWS SSM Parameter Store (SecureString) and load at runtime; the source tree should reference parameter names only, never literal credential values.
|
|
97
|
+
- **`/meta` cert lines must be `trim()`-ed (CRLF).** The `.crt` files in `_underscore/Assets/ssl` are CRLF-encoded; the cert-format code does `explode("\n", ...)` and indents each line, so without a per-line `trim()` a stray `\r` ends up on every base64 line inside `<ds:X509Certificate>`. Trim each line in both the signing and encryption blocks.
|
|
93
98
|
- **SLS not implemented.** `/sls` falls through to the default banner.
|
|
94
99
|
- **`set_exception_handler(null)` at top of `saml()`.** Unhandled exceptions output raw PHP errors. All exceptions from `getAuthenticatedSsoUser()` are caught and sent to Sentry.
|
|
95
100
|
|
|
@@ -97,3 +102,4 @@ To onboard a new SSO client, add the class in `_underscore` — not in this repo
|
|
|
97
102
|
- 2026-06-11 — Added `transactionCommit()` before redirect; wrapped `getAuthenticatedSsoUser()` in try/catch with Sentry; echo+exit on auth failure (rgirish)
|
|
98
103
|
- 2026-06-25 — Sharpened signature-verification gotcha with explicit threat model and flagged it as the repo's top security priority; documented plaintext secrets in `production.ini` + hardcoded Sentry DSN in `Controller/Index.php` with SSM Parameter Store recommendation (jcardinal)
|
|
99
104
|
- 2026-08-27 — Documented non-prod shared-ALB self-registration postdeploy hook (`060_...sh` + `ebs/register_instance_to_shared_application_load_balancer.php`); skips `production`, requires `aws/aws-sdk-php` + two IAM actions; cross-linked worker2 ALB auto-registration feature (jcardinal)
|
|
105
|
+
- 2026-09-04 — `/acs` now dual-credential decrypt (`decryptMultiAssertion([new, old])`) for SP encryption-cert overlap; documented `/meta`'s two KeyDescriptors (signing vs encryption) and the global outbound-signing key; added the CRLF `/meta` cert-line `trim()` gotcha. (jcardinal)
|
|
@@ -6,7 +6,7 @@ project: SAML SSO Gateway
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-04
|
|
10
10
|
owners: ["jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Assets/ssl/togahub_com.crt
|
|
@@ -32,16 +32,58 @@ cert), see the SSL certificate trust-model strategy standard.
|
|
|
32
32
|
|
|
33
33
|
## How the SP credential is used
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
The SP cert/key is used in **four** places — three in `saml`, one in `_underscore`:
|
|
36
|
+
|
|
37
|
+
- **(a) `/meta` signing `KeyDescriptor`** — `saml/Controller/Index.php` publishes the cert IdPs
|
|
38
|
+
use to verify our outbound AuthnRequest signature. Read fresh via `file_get_contents` on each
|
|
39
|
+
request (no server-side cache).
|
|
40
|
+
- **(b) `/meta` encryption `KeyDescriptor`** — the cert IdPs use to encrypt assertions to us.
|
|
41
|
+
- **(c) `/acs` assertion decryption** — `saml/Controller/Index.php` decrypts the inbound
|
|
42
|
+
encrypted assertion with the matching private key.
|
|
43
|
+
- **(d) Outbound AuthnRequest signing (GLOBAL)** — `_Model_Core_ClientAuthentication`
|
|
44
|
+
(`_underscore/Model/Core/ClientAuthentication.php`, `PATH_TO_PRIVATE_KEY`, around lines
|
|
45
|
+
11 / 95 / 107) signs the redirect-binding AuthnRequest query string with XMLSecLibs
|
|
46
|
+
`RSA_SHA256`. **One key signs the login requests we send to ALL client IdPs — you cannot sign
|
|
47
|
+
per-client.** This is why signing and encryption rotate on different schedules (see
|
|
48
|
+
*Encryption-only rotation*).
|
|
49
|
+
|
|
42
50
|
- **IdPs cache metadata up to 7 days** (`cacheDuration PT604800S`), so a cert change is not
|
|
43
51
|
guaranteed to be picked up by every IdP immediately after deploy.
|
|
44
52
|
|
|
53
|
+
## Encryption-only rotation with an overlap window (2026-09-04)
|
|
54
|
+
|
|
55
|
+
Because a cert change is not picked up by every IdP at once, the safe rotation splits the two
|
|
56
|
+
uses and **rotates encryption first, signing later**:
|
|
57
|
+
|
|
58
|
+
- **Signing stays on the OLD cert.** If `/meta` advertised the new signing cert (or (d) signed
|
|
59
|
+
with the new key) before every IdP loaded the new metadata, any IdP that verifies our
|
|
60
|
+
AuthnRequest signature could **reject our login requests**. Signing flips only after all IdPs
|
|
61
|
+
are on the new metadata — a later, coordinated step.
|
|
62
|
+
- **Encryption rotates now, with both keys accepted.** `/meta` presents TWO independent
|
|
63
|
+
`KeyDescriptor`s: `use="signing"` = current/old cert, `use="encryption"` = new cert. Two
|
|
64
|
+
independent KeyDescriptors is valid SAML metadata, so encryption can rotate without touching
|
|
65
|
+
signing.
|
|
66
|
+
- **`/acs` accepts EITHER encryption key during overlap.** It builds two `X509Credential`s
|
|
67
|
+
(new + old) and calls `$reader->decryptMultiAssertion([$credentialNew, $credentialOld], $ctx)`.
|
|
68
|
+
In vendored LightSaml, `decryptMulti()` tries each credential and only re-throws once all fail
|
|
69
|
+
— so it fails closed. This lets an IdP that has re-encrypted to the new cert and one still on
|
|
70
|
+
the old cert both succeed.
|
|
71
|
+
- **Class constants (paths only, no key material)** in `saml/Controller/Index.php` replaced the
|
|
72
|
+
old single pair: `PATH_TO_SIGNING_CERTIFICATE`; `PATH_TO_ENCRYPTION_CERTIFICATE_NEW` +
|
|
73
|
+
`PATH_TO_ENCRYPTION_PRIVATE_KEY_NEW`; `PATH_TO_ENCRYPTION_CERTIFICATE_OLD` +
|
|
74
|
+
`PATH_TO_ENCRYPTION_PRIVATE_KEY_OLD`.
|
|
75
|
+
- **What forced it:** old cert `togahub_com.crt` (O=Agilant Solutions) expires **2026-09-16**;
|
|
76
|
+
new cert `togahub_com_2036-08-27.crt` (O=TOGA Technology, Inc.) is valid to 2036.
|
|
77
|
+
|
|
78
|
+
### Remaining steps / follow-ups (not yet done)
|
|
79
|
+
1. **Flip signing to the new cert** once every IdP has loaded the new metadata — advertise the
|
|
80
|
+
new signing cert in `/meta` and switch (d) to the new key. Coordinate the date.
|
|
81
|
+
2. **Log which SP key decrypted** (new vs old, identifier only — never assertion data) so you can
|
|
82
|
+
see which IdPs have cut over and when it is safe to drop the old key.
|
|
83
|
+
3. **Wrap the `/acs` decrypt in try/catch** and echo `Invalid SAMLResponse (Code = 5)` to match
|
|
84
|
+
the existing coded failure responses.
|
|
85
|
+
4. **Dedupe the two `/meta` cert-format blocks** into a shared helper (non-blocking).
|
|
86
|
+
|
|
45
87
|
## Current credential (as of 2026-08-27, staged — cutover pending)
|
|
46
88
|
|
|
47
89
|
A dedicated **self-signed** replacement was generated to decouple the SP credential from the
|
|
@@ -88,6 +130,14 @@ short-lived DigiCert/Thawte TLS wildcard that had been reused as the SP cert (se
|
|
|
88
130
|
6. **Deploy the cert+key to the other bilateral services** that use the same SP credential —
|
|
89
131
|
AS2 (AWS Transfer Family) and any other pinned endpoint — importing both cert and key.
|
|
90
132
|
|
|
133
|
+
### Gotcha — trim each cert line (CRLF)
|
|
134
|
+
|
|
135
|
+
The `.crt` files in `_underscore/Assets/ssl` are **CRLF-encoded**. The `/meta` cert-format code
|
|
136
|
+
does `explode("\n", ...)` then indents each line. Without a per-line `trim()`, a stray carriage
|
|
137
|
+
return (`\r`) is embedded on every base64 line inside `<ds:X509Certificate>` in the metadata XML.
|
|
138
|
+
Fix: `trim()` each line in the cert-format code — applied to both the signing and encryption
|
|
139
|
+
blocks. (Latent bug that pre-dated this change in the single original block.)
|
|
140
|
+
|
|
91
141
|
## Background — why the change
|
|
92
142
|
|
|
93
143
|
The old `_underscore/Assets/ssl/togahub_com.crt` was a **reused DigiCert/Thawte multi-domain
|
|
@@ -105,5 +155,12 @@ every pinning partner to reinstall on each renewal. The dedicated self-signed 10
|
|
|
105
155
|
`_underscore`-then-`saml` cutover ordering. Discovered the prior cert was a reused short-lived
|
|
106
156
|
DigiCert wildcard TLS cert, which is the reason for the switch. Cutover itself still pending.
|
|
107
157
|
(jcardinal)
|
|
108
|
-
|
|
109
|
-
|
|
158
|
+
- 2026-09-04 — Documented the **encryption-only rotation** actually built in
|
|
159
|
+
`saml/Controller/Index.php`: `/meta` now presents two `KeyDescriptor`s (signing=old,
|
|
160
|
+
encryption=new) and `/acs` uses `decryptMultiAssertion([new, old])` so both encryption keys
|
|
161
|
+
work during overlap; new path-only class constants replaced the single cert/key pair.
|
|
162
|
+
Recorded that outbound AuthnRequest signing uses one GLOBAL key for all IdPs, which is why
|
|
163
|
+
signing stays on the old cert until every IdP loads the new metadata. Added the CRLF
|
|
164
|
+
per-line-`trim()` gotcha for `/meta` cert formatting, and the open follow-ups. Change is
|
|
165
|
+
working-tree only (deploys via `saml` on `_production`); Rate is testing against
|
|
166
|
+
`saml-production`. (jcardinal)
|
package/package.json
CHANGED