package saml import ( "crypto" "crypto/rand" "crypto/rsa" "crypto/sha256" "encoding/base64" "encoding/xml" "errors" "fmt" "time" ) var ( ErrInvalidSignature = errors.New("saml: assertion-signatur ungueltig") ErrNotYetValid = errors.New("saml: assertion noch nicht gueltig") ErrExpired = errors.New("saml: assertion abgelaufen") ErrWrongIssuer = errors.New("saml: unerwarteter aussteller") ) // Verifier prueft SAML-Responses gegen den oeffentlichen Schluessel eines // konfigurierten IdP (Akzeptanzkriterium 1). type Verifier struct { idpPublicKey *rsa.PublicKey idpIssuer string } func NewVerifier(idpPublicKey *rsa.PublicKey, idpIssuer string) *Verifier { return &Verifier{idpPublicKey: idpPublicKey, idpIssuer: idpIssuer} } // Parse dekodiert eine base64-kodierte SAMLResponse (wie sie im // SAMLResponse-Formularfeld ankommt) in ihre XML-Struktur. func Parse(rawBase64 string) (ResponseXML, error) { data, err := base64.StdEncoding.DecodeString(rawBase64) if err != nil { return ResponseXML{}, fmt.Errorf("base64 dekodieren: %w", err) } var resp ResponseXML if err := xml.Unmarshal(data, &resp); err != nil { return ResponseXML{}, fmt.Errorf("saml-xml parsen: %w", err) } return resp, nil } // Verify prueft Signatur, Gueltigkeitszeitraum und Aussteller // (Akzeptanzkriterium 1). func (v *Verifier) Verify(resp ResponseXML) (AssertionXML, error) { sig, err := base64.StdEncoding.DecodeString(resp.SignatureValue) if err != nil { return AssertionXML{}, fmt.Errorf("signatur dekodieren: %w", err) } canonical, err := CanonicalAssertionBytes(resp.Assertion) if err != nil { return AssertionXML{}, err } hash := sha256.Sum256(canonical) if err := rsa.VerifyPKCS1v15(v.idpPublicKey, crypto.SHA256, hash[:], sig); err != nil { return AssertionXML{}, ErrInvalidSignature } if resp.Assertion.Issuer != v.idpIssuer { return AssertionXML{}, fmt.Errorf("%w: %q", ErrWrongIssuer, resp.Assertion.Issuer) } if err := resp.Assertion.WithinValidity(time.Now()); err != nil { return AssertionXML{}, err } return resp.Assertion, nil } // Sign signiert eine Assertion mit dem privaten IdP-Schluessel — wird vom // (simulierten) Test-IdP verwendet, um eine gueltige Response zu erzeugen. // Lebt hier statt nur in Tests, da ein echter Test-IdP-Modus fuer // Entwicklungszwecke (siehe Ticket: "sinnvolle Defaults ohne SAML- // Expertenwissen") denselben Signiervorgang braucht. func Sign(priv *rsa.PrivateKey, assertion AssertionXML) (string, error) { canonical, err := CanonicalAssertionBytes(assertion) if err != nil { return "", err } hash := sha256.Sum256(canonical) sig, err := rsa.SignPKCS1v15(rand.Reader, priv, crypto.SHA256, hash[:]) if err != nil { return "", fmt.Errorf("assertion signieren: %w", err) } return base64.StdEncoding.EncodeToString(sig), nil } // BuildResponse verpackt eine signierte Assertion in eine base64-kodierte // SAMLResponse, wie sie ein IdP im Browser-POST-Binding zurueckliefert. func BuildResponse(priv *rsa.PrivateKey, assertion AssertionXML) (string, error) { sig, err := Sign(priv, assertion) if err != nil { return "", err } resp := ResponseXML{Assertion: assertion, SignatureValue: sig} data, err := xml.Marshal(resp) if err != nil { return "", fmt.Errorf("response serialisieren: %w", err) } return base64.StdEncoding.EncodeToString(data), nil }