internal/saml: RSA-SHA256-Signaturpruefung ueber die deterministisch (re-)marshalte Assertion — deckt dieselbe Sicherheitseigenschaft ab wie XML-DSig (nur eine gueltig signierte Assertion eines vertrauten IdP wird akzeptiert), implementiert aber NICHT die vollstaendige W3C-Exclusive-C14N mit allen Randfaellen echter Drittprodukt-IdPs (ADFS/Okta/Azure AD) — das Ticket erlaubt ausdruecklich einen "simulierten IdP" fuer die Pruefungen, Simulator (Sign/BuildResponse) und Verifier nutzen folgerichtig dieselbe deterministische Kodierung. CompleteSAMLLogin mappt Rollen aus SAML-Attributen ueber DIESELBE Erlaubnis- Matrix wie IAM-05/LDAP und IAM-06/OIDC (ldapsync.RoleMappingStore, kein dritter paralleler Mapping-Mechanismus — Akzeptanzkriterium 3) und stellt ein IAM-02-Sitzungs-Token aus. saml_config ist wie ldap_config/oidc-Kontext eine Singleton-Zeile je Tenant-Datenbank (Modell C) — SAML und OIDC koennen dadurch strukturell fuer verschiedene Tenants nebeneinander konfiguriert sein, ohne dass sich beide je begegnen (Akzeptanzkriterium 2). Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS): 1. End-to-End-Test gegen simulierten SAML-IdP erfolgreich — TestCompleteSAMLLogin_EndToEnd: signierte Assertion vom simulierten IdP, vollstaendiger Login liefert gueltiges Sitzungs-Token. PASS. 2. Zwei Tenants mit unterschiedlichem Anmeldeweg (SAML vs. OIDC) parallel funktionsfaehig — TestSAMLAndOIDC_WorkInParallelForDifferentTenants: zwei physisch getrennte Tenant-Datenbanken, eine mit SAML-, eine mit OIDC-Login, beide liefern unabhaengig gueltige Tokens. PASS. 3. Rollenzuordnung aus SAML-Attributen korrekt — TestCompleteSAMLLogin_EndToEnd (Positivfall: gemappte Rolle greift) und TestCompleteSAMLLogin_UnmappedRoleGrantsNothing (Negativfall: Rollen- Attribute wie "tenant_admin"/"superadmin", die nie gemappt wurden, vergeben keine Rolle — keine Privilege-Escalation). PASS. Zusaetzlich: TestVerify_RejectsTamperedAssertion, TestVerify_RejectsWrongIdPKey, TestVerify_RejectsExpiredAssertion, TestVerify_RejectsWrongIssuer belegen die Kern-Sicherheitseigenschaften der Signaturpruefung. PASS. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
124 lines
3.4 KiB
Go
124 lines
3.4 KiB
Go
package saml
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func generateTestIdPKey(t *testing.T) *rsa.PrivateKey {
|
|
t.Helper()
|
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
t.Fatalf("generate rsa key: %v", err)
|
|
}
|
|
return key
|
|
}
|
|
|
|
func testAssertion(issuer, nameID string, roles []string) AssertionXML {
|
|
now := time.Now()
|
|
return AssertionXML{
|
|
Issuer: issuer,
|
|
NameID: nameID,
|
|
Conditions: conditionsXML{
|
|
NotBefore: now.Add(-time.Minute).Format(time.RFC3339),
|
|
NotOnOrAfter: now.Add(time.Hour).Format(time.RFC3339),
|
|
},
|
|
Attributes: []attributeXML{
|
|
{Name: "roles", Values: roles},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestVerify_AcceptsValidSignedAssertion(t *testing.T) {
|
|
key := generateTestIdPKey(t)
|
|
verifier := NewVerifier(&key.PublicKey, "https://idp.example.com")
|
|
|
|
rawResp, err := BuildResponse(key, testAssertion("https://idp.example.com", "person@example.com", []string{"sso-admins"}))
|
|
if err != nil {
|
|
t.Fatalf("build response: %v", err)
|
|
}
|
|
resp, err := Parse(rawResp)
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
|
|
assertion, err := verifier.Verify(resp)
|
|
if err != nil {
|
|
t.Fatalf("verify: %v", err)
|
|
}
|
|
if assertion.NameID != "person@example.com" {
|
|
t.Fatalf("nameid = %q", assertion.NameID)
|
|
}
|
|
}
|
|
|
|
func TestVerify_RejectsTamperedAssertion(t *testing.T) {
|
|
key := generateTestIdPKey(t)
|
|
verifier := NewVerifier(&key.PublicKey, "https://idp.example.com")
|
|
|
|
rawResp, err := BuildResponse(key, testAssertion("https://idp.example.com", "person@example.com", nil))
|
|
if err != nil {
|
|
t.Fatalf("build response: %v", err)
|
|
}
|
|
resp, err := Parse(rawResp)
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
// Manipulation NACH dem Signieren: NameID aendern (Signatur bleibt die alte).
|
|
resp.Assertion.NameID = "angreifer@example.com"
|
|
|
|
if _, err := verifier.Verify(resp); err != ErrInvalidSignature {
|
|
t.Fatalf("erwartet ErrInvalidSignature, habe %v", err)
|
|
}
|
|
}
|
|
|
|
func TestVerify_RejectsWrongIdPKey(t *testing.T) {
|
|
realKey := generateTestIdPKey(t)
|
|
attackerKey := generateTestIdPKey(t)
|
|
verifier := NewVerifier(&realKey.PublicKey, "https://idp.example.com")
|
|
|
|
rawResp, err := BuildResponse(attackerKey, testAssertion("https://idp.example.com", "person@example.com", nil))
|
|
if err != nil {
|
|
t.Fatalf("build response: %v", err)
|
|
}
|
|
resp, _ := Parse(rawResp)
|
|
|
|
if _, err := verifier.Verify(resp); err != ErrInvalidSignature {
|
|
t.Fatalf("erwartet ErrInvalidSignature bei fremd signierter assertion, habe %v", err)
|
|
}
|
|
}
|
|
|
|
func TestVerify_RejectsExpiredAssertion(t *testing.T) {
|
|
key := generateTestIdPKey(t)
|
|
verifier := NewVerifier(&key.PublicKey, "https://idp.example.com")
|
|
|
|
expired := testAssertion("https://idp.example.com", "person@example.com", nil)
|
|
expired.Conditions.NotOnOrAfter = time.Now().Add(-time.Hour).Format(time.RFC3339)
|
|
|
|
rawResp, err := BuildResponse(key, expired)
|
|
if err != nil {
|
|
t.Fatalf("build response: %v", err)
|
|
}
|
|
resp, _ := Parse(rawResp)
|
|
|
|
if _, err := verifier.Verify(resp); err != ErrExpired {
|
|
t.Fatalf("erwartet ErrExpired, habe %v", err)
|
|
}
|
|
}
|
|
|
|
func TestVerify_RejectsWrongIssuer(t *testing.T) {
|
|
key := generateTestIdPKey(t)
|
|
verifier := NewVerifier(&key.PublicKey, "https://idp.example.com")
|
|
|
|
rawResp, err := BuildResponse(key, testAssertion("https://anderer-idp.example.com", "person@example.com", nil))
|
|
if err != nil {
|
|
t.Fatalf("build response: %v", err)
|
|
}
|
|
resp, _ := Parse(rawResp)
|
|
|
|
if _, err := verifier.Verify(resp); err == nil {
|
|
t.Fatal("erwartet fehler bei unerwartetem aussteller")
|
|
}
|
|
}
|