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>
91 lines
2.9 KiB
Go
91 lines
2.9 KiB
Go
// Package saml implementiert Core IAM-11: SAML 2.0 als zusaetzlicher
|
|
// Anmeldeweg neben OIDC (IAM-06).
|
|
//
|
|
// WICHTIGER HINWEIS: Die Signaturpruefung hier verifiziert eine RSA-SHA256-
|
|
// Signatur ueber die deterministisch (re-)marshalte Assertion — das deckt
|
|
// dieselbe Sicherheitseigenschaft ab wie XML-DSig (nur ein gueltig
|
|
// signiertes Assertion eines vertrauten IdP wird akzeptiert), implementiert
|
|
// aber NICHT die vollstaendige W3C-Exclusive-XML-Canonicalization (C14N) mit
|
|
// all ihren Randfaellen (Namespace-Normalisierung, Attribut-Reihenfolge
|
|
// etc.), die produktive SAML-Bibliotheken brauchen, um mit echten
|
|
// Enterprise-IdPs (ADFS, Okta, Azure AD) byteidentisch zu sein. Das Ticket
|
|
// erlaubt ausdruecklich einen "simulierten IdP" fuer die Pruefungen — dieser
|
|
// Simulator und der Verifier hier verwenden folgerichtig dieselbe
|
|
// deterministische Kodierung, echte Interop mit einem Drittprodukt-IdP ist
|
|
// nicht Teil dieser Kachel.
|
|
package saml
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
type attributeXML struct {
|
|
Name string `xml:"Name,attr"`
|
|
Values []string `xml:"AttributeValue"`
|
|
}
|
|
|
|
type conditionsXML struct {
|
|
NotBefore string `xml:"NotBefore,attr"`
|
|
NotOnOrAfter string `xml:"NotOnOrAfter,attr"`
|
|
}
|
|
|
|
// AssertionXML ist die (vereinfachte) SAML-Assertion.
|
|
type AssertionXML struct {
|
|
XMLName xml.Name `xml:"Assertion"`
|
|
Issuer string `xml:"Issuer"`
|
|
NameID string `xml:"Subject>NameID"`
|
|
Conditions conditionsXML `xml:"Conditions"`
|
|
Attributes []attributeXML `xml:"AttributeStatement>Attribute"`
|
|
}
|
|
|
|
// ResponseXML umhuellt die Assertion mit der Signatur ueber deren kanonische
|
|
// Bytes (siehe Paket-Dokumentation).
|
|
type ResponseXML struct {
|
|
XMLName xml.Name `xml:"Response"`
|
|
Assertion AssertionXML `xml:"Assertion"`
|
|
SignatureValue string `xml:"SignatureValue"` // base64
|
|
}
|
|
|
|
// CanonicalAssertionBytes liefert die deterministischen Bytes, ueber die
|
|
// signiert/verifiziert wird.
|
|
func CanonicalAssertionBytes(a AssertionXML) ([]byte, error) {
|
|
b, err := xml.Marshal(a)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("assertion serialisieren: %w", err)
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
// Attribute liefert die Werte eines benannten SAML-Attributs (z.B. "roles"),
|
|
// oder nil wenn nicht vorhanden.
|
|
func (a AssertionXML) Attribute(name string) []string {
|
|
for _, attr := range a.Attributes {
|
|
if attr.Name == name {
|
|
return attr.Values
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WithinValidity prueft die Conditions/NotBefore/NotOnOrAfter der Assertion
|
|
// gegen die aktuelle Zeit.
|
|
func (a AssertionXML) WithinValidity(now time.Time) error {
|
|
notBefore, err := time.Parse(time.RFC3339, a.Conditions.NotBefore)
|
|
if err != nil {
|
|
return fmt.Errorf("notbefore parsen: %w", err)
|
|
}
|
|
notOnOrAfter, err := time.Parse(time.RFC3339, a.Conditions.NotOnOrAfter)
|
|
if err != nil {
|
|
return fmt.Errorf("notonorafter parsen: %w", err)
|
|
}
|
|
if now.Before(notBefore) {
|
|
return ErrNotYetValid
|
|
}
|
|
if !now.Before(notOnOrAfter) {
|
|
return ErrExpired
|
|
}
|
|
return nil
|
|
}
|