From f618d18104d257d627db1b30371f3b906b800bec Mon Sep 17 00:00:00 2001 From: Oscar Sanderson Date: Mon, 2 Nov 2026 15:25:34 +0800 Subject: [PATCH 1/2] fix(federation): refuse an empty fragment in Entity Identifiers and source_endpoint isHTTPSURL checked url.URL.Fragment, which url.Parse leaves empty for a bare "#", so "https://ia.example.org#" passed as an authority hint, trust anchor hint or source_endpoint. It now refuses any "#", as the issuer, endpoint and redirect URI parsers do since v0.69.0. Co-Authored-By: Claude Opus 5.5 --- internal/federation/claims_validation.go | 5 +++-- internal/federation/claims_validation_test.go | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/internal/federation/claims_validation.go b/internal/federation/claims_validation.go index bdb80a95..4d225ea6 100644 --- a/internal/federation/claims_validation.go +++ b/internal/federation/claims_validation.go @@ -132,10 +132,11 @@ func parseSourceEndpoint(raw json.RawMessage) (string, error) { } // isHTTPSURL reports whether s is an absolute https URL with a host and -// no fragment. +// no fragment, not even an empty one: url.Parse drops a bare "#", so +// it's looked for in s itself. func isHTTPSURL(s string) bool { u, err := url.Parse(s) - return err == nil && u.Scheme == "https" && u.Host != "" && u.Fragment == "" && !strings.ContainsAny(s, " \t\r\n") + return err == nil && u.Scheme == "https" && u.Host != "" && !strings.ContainsAny(s, "# \t\r\n") } // maxJSONDepth bounds how deeply refuseDuplicateMembers descends; the diff --git a/internal/federation/claims_validation_test.go b/internal/federation/claims_validation_test.go index f74122f0..672c439c 100644 --- a/internal/federation/claims_validation_test.go +++ b/internal/federation/claims_validation_test.go @@ -32,6 +32,7 @@ func TestParseRefusesStatementsSection32Rejects(t *testing.T) { "§3.1.2 trust_anchor_hints in an EC": {ec, `"trust_anchor_hints":["https://ta.example.org"]`, false}, "§3.1.2 authority_hints empty": {ec, `"authority_hints":[]`, true}, "§3.1.2 authority_hints not an Entity ID": {ec, `"authority_hints":["not a url"]`, true}, + "§3.1.2 authority_hints empty fragment": {ec, `"authority_hints":["https://ia.example.org#"]`, true}, "§3.1.3 metadata_policy_crit empty": {ss, `"metadata_policy_crit":[]`, true}, "§3.1.3 metadata_policy_crit listed": {ss, `"metadata_policy_crit":["regexp"]`, false}, "§5 null entity-type metadata": {ec, `"metadata":{"openid_relying_party":null}`, true}, @@ -41,6 +42,7 @@ func TestParseRefusesStatementsSection32Rejects(t *testing.T) { "§5 empty entity-type metadata": {ec, `"metadata":{"openid_relying_party":{}}`, false}, "§3.2 source_endpoint not a URL": {ss, `"source_endpoint":"not a url"`, true}, "§3.2 source_endpoint not https": {ss, `"source_endpoint":"http://a.example.org/fetch"`, true}, + "§3.2 source_endpoint empty fragment": {ss, `"source_endpoint":"https://a.example.org/fetch#"`, true}, "§3.2 source_endpoint https": {ss, `"source_endpoint":"https://a.example.org/fetch"`, false}, "§6.1.2 duplicate metadata_policy parameter": {ss, `"metadata_policy":{"openid_relying_party":{"scope":{"value":"a"},"scope":{"value":"b"}}}`, true}, "§6.1.2 duplicate metadata_policy operator": {ss, `"metadata_policy":{"openid_relying_party":{"scope":{"value":"a","value":"b"}}}`, true}, From 73c60bebcae228507fe307827a5e2efb0c3e6e26 Mon Sep 17 00:00:00 2001 From: Oscar Sanderson Date: Mon, 2 Nov 2026 15:25:35 +0800 Subject: [PATCH 2/2] docs(upgrading): say v0.69.0 gates every call after a Trust Chain expires The v0.62.0 section said a flow already begun completes as before. Since v0.69.0 the client refuses every method that contacts or trusts the OP once the chain has expired, with a *client.Error. Co-Authored-By: Claude Opus 5.5 --- UPGRADING.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/UPGRADING.md b/UPGRADING.md index dd5b82ee..1a489d91 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1034,6 +1034,13 @@ have dropped since, until the process restarted. wrapping `client.ErrTrustChainExpired`. A flow already begun completes as before. +From v0.69.0 every method that contacts or trusts the OP refuses once +the chain has expired, including those that complete a flow already +begun (`HandleAuthorizationResponse`, `ExchangeCode`, +`PollBackchannelAuthentication`), and the error is a `*client.Error` +with code `client.ErrorTrustChainExpired`, still wrapping +`client.ErrTrustChainExpired`. + **What to change:** resolve the chain again and build a new Client before it expires, for example on a timer from the new `Client.TrustChainExpiresAt`: