Commit e0709dc2 authored by Eric Chiang's avatar Eric Chiang

connector/saml: fix validation bug with multiple Assertion elements

When a SAML response provided multiple Assertion elements, only the
first one is checked for a valid signature. If the Assertion is
verified, the original Assertion is removed and the canonicalized
version is prepended to the Response. However, if there were
multiple assertions, the second assertion could end up first in the
list of Assertions, even if it was unsigned.

For example this:

    <Response>
      <!--
         Response unsigned. According to SAML spec must check
         assertion signature.
      -->
      <Assertion>
        <Signature>
          <!-- Correrctly signed assertion -->
        </Signature>
      </Assertion>

      <Assertion>
        <!-- Unsigned assertion inserted by attacker-->
      </Assertion>
    </Response>

could be verified then re-ordered to the following:

    <Response>
      <!--
         Response unsigned. According to SAML spec must check
         assertion signature.
      -->
      <Assertion>
        <!-- Unsigned assertion inserted by attacker-->
      </Assertion>

      <Assertion>
        <!-- Canonicalized, correrctly signed assertion -->
      </Assertion>
    </Response>

Fix this by removing all unverified child elements of the Response,
not just the original assertion.
parent f4865a35
......@@ -130,9 +130,7 @@ func (c *Config) Open(logger logrus.FieldLogger) (connector.Connector, error) {
return c.openConnector(logger)
}
func (c *Config) openConnector(logger logrus.FieldLogger) (interface {
connector.SAMLConnector
}, error) {
func (c *Config) openConnector(logger logrus.FieldLogger) (*provider, error) {
requiredFields := []struct {
name, val string
}{
......@@ -274,8 +272,11 @@ func (p *provider) HandlePOST(s connector.Scopes, samlResponse, inResponseTo str
if err != nil {
return ident, fmt.Errorf("decode response: %v", err)
}
rootElementSigned := true
if p.validator != nil {
if rawResp, err = verify(p.validator, rawResp); err != nil {
rawResp, rootElementSigned, err = verifyResponseSig(p.validator, rawResp)
if err != nil {
return ident, fmt.Errorf("verify signature: %v", err)
}
}
......@@ -285,6 +286,7 @@ func (p *provider) HandlePOST(s connector.Scopes, samlResponse, inResponseTo str
return ident, fmt.Errorf("unmarshal response: %v", err)
}
if rootElementSigned {
if p.issuer != "" && resp.Issuer != nil && resp.Issuer.Issuer != p.issuer {
return ident, fmt.Errorf("expected Issuer value %s, got %s", p.issuer, resp.Issuer.Issuer)
}
......@@ -304,6 +306,7 @@ func (p *provider) HandlePOST(s connector.Scopes, samlResponse, inResponseTo str
if err = p.validateStatus(&resp); err != nil {
return ident, err
}
}
assertion := resp.Assertion
if assertion == nil {
......@@ -481,41 +484,57 @@ func (p *provider) validateConditions(assertion *assertion) error {
return nil
}
// verify checks the signature info of a XML document and returns
// the signed elements.
// The Validate function of the goxmldsig library only looks for
// signatures on the root element level. But a saml Response is valid
// if the complete message is signed, or only the Assertion is signed,
// or but elements are signed. Therefore we first check a possible
// signature of the Response than of the Assertion. If one of these
// is successful the Response is considered as valid.
func verify(validator *dsig.ValidationContext, data []byte) (signed []byte, err error) {
// verifyResponseSig attempts to verify the signature of a SAML response or
// the assertion.
//
// If the root element is properly signed, this method returns it.
//
// The SAML spec requires supporting responses where the root element is
// unverified, but the sub <Assertion> elements are signed. In these cases,
// this method returns rootVerified=false to indicate that the <Assertion>
// elements should be trusted, but all other elements MUST be ignored.
//
// Note: we still don't support multiple <Assertion> tags. If there are
// multiple present this code will only process the first.
func verifyResponseSig(validator *dsig.ValidationContext, data []byte) (signed []byte, rootVerified bool, err error) {
doc := etree.NewDocument()
if err = doc.ReadFromBytes(data); err != nil {
return nil, fmt.Errorf("parse document: %v", err)
return nil, false, fmt.Errorf("parse document: %v", err)
}
verified := false
response := doc.Root()
transformedResponse, err := validator.Validate(response)
if err == nil {
verified = true
// Root element is verified, return it.
doc.SetRoot(transformedResponse)
signed, err = doc.WriteToBytes()
return signed, true, err
}
// Ensures xmlns are copied down to the assertion element when they are defined in the root
//
// TODO: Only select from child elements of the root.
assertion, err := etreeutils.NSSelectOne(response, "urn:oasis:names:tc:SAML:2.0:assertion", "Assertion")
if err != nil {
return nil, fmt.Errorf("response does not contain an Assertion element")
return nil, false, fmt.Errorf("response does not contain an Assertion element")
}
transformedAssertion, err := validator.Validate(assertion)
if err == nil {
verified = true
response.RemoveChild(assertion)
response.AddChild(transformedAssertion)
if err != nil {
return nil, false, fmt.Errorf("response does not contain a valid signature element: %v", err)
}
if verified != true {
return nil, fmt.Errorf("response does not contain a valid Signature element")
// Verified an assertion but not the response. Can't trust any child elements,
// except the assertion. Remove them all.
for _, el := range response.ChildElements() {
response.RemoveChild(el)
}
return doc.WriteToBytes()
// We still return the full <Response> element, even though it's unverified
// because the <Assertion> element is not a valid XML document on its own.
// It still requires the root element to define things like namespaces.
response.AddChild(transformedAssertion)
signed, err = doc.WriteToBytes()
return signed, false, err
}
func uuidv4() string {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment