// 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 }