diff --git a/Gopkg.lock b/Gopkg.lock index fb9a8a1b7df04..671a533d99727 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -625,6 +625,14 @@ pruneopts = "NUT" revision = "16b258d86efc53ac5cf3e550604f566c32624fab" +[[projects]] + branch = "master" + digest = "1:314f0a78b6c1e696ab04602e2ccd682521beff84de6254e2de067df9647da360" + name = "github.com/mattermost/xml-roundtrip-validator" + packages = ["."] + pruneopts = "NUT" + revision = "bcd7e1b9601e23e68c7a801c1e70c75b01573241" + [[projects]] digest = "1:70ef8268170621826f8c111ac1674afe75136f526f449453b4a6631c6dba1946" name = "github.com/mattn/go-runewidth" @@ -772,14 +780,15 @@ version = "v0.0.4" [[projects]] - digest = "1:ebd11dba223a1ad42de8d1dd567c44016257aa642af4552c64d7db98cbb2d989" + digest = "1:7023f0ea0bbcf29335a25ed48c5fc3c7a59997739a5af42f25524b70d86e221f" name = "github.com/russellhaering/gosaml2" packages = [ ".", "types", + "uuid", ] pruneopts = "NUT" - revision = "8908227c114abe0b63b1f0606abae72d11bf632a" + revision = "v0.6.0" [[projects]] branch = "master" @@ -801,13 +810,6 @@ pruneopts = "NUT" revision = "2dfbae5fcf46374f166f8969cb07e167f1be6273" -[[projects]] - digest = "1:54f275b550b8a8a20bc91e2067ed740600b69dae9dcf9cdfe905bac1b9cfd1d0" - name = "github.com/satori/go.uuid" - packages = ["."] - pruneopts = "NUT" - revision = "5bf94b69c6b68ee1b541973bb8e1144db23a194b" - [[projects]] branch = "master" digest = "1:c7ce4b0ec2e461e9e71c883b7f1c9f4e963396b1ddab6f5cd49d6f622fa46a67" diff --git a/Gopkg.toml b/Gopkg.toml index 6836fefeeb71a..f18e1506d0d7c 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -171,7 +171,7 @@ ignored = ["github.com/Sirupsen/logrus", "github.com/gravitational/license/gener [[constraint]] name = "github.com/russellhaering/gosaml2" - revision = "8908227c114abe0b63b1f0606abae72d11bf632a" + revision = "v0.6.0" [[constraint]] name = "github.com/gravitational/reporting" diff --git a/lib/services/saml.go b/lib/services/saml.go index 52a0954d6893f..c5c74a1c3967b 100644 --- a/lib/services/saml.go +++ b/lib/services/saml.go @@ -574,7 +574,9 @@ func (o *SAMLConnectorV2) GetServiceProvider(clock clockwork.Clock) (*saml2.SAML } } o.Spec.Issuer = metadata.EntityID - o.Spec.SSO = metadata.IDPSSODescriptor.SingleSignOnService.Location + if len(metadata.IDPSSODescriptor.SingleSignOnServices) > 0 { + o.Spec.SSO = metadata.IDPSSODescriptor.SingleSignOnServices[0].Location + } } if o.Spec.Issuer == "" { return nil, trace.BadParameter("no issuer or entityID set, either set issuer as a parameter or via entity_descriptor spec") diff --git a/vendor/github.com/mattermost/xml-roundtrip-validator/LICENSE.txt b/vendor/github.com/mattermost/xml-roundtrip-validator/LICENSE.txt new file mode 100644 index 0000000000000..261eeb9e9f8b2 --- /dev/null +++ b/vendor/github.com/mattermost/xml-roundtrip-validator/LICENSE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/mattermost/xml-roundtrip-validator/validator.go b/vendor/github.com/mattermost/xml-roundtrip-validator/validator.go new file mode 100644 index 0000000000000..6931b938e76d4 --- /dev/null +++ b/vendor/github.com/mattermost/xml-roundtrip-validator/validator.go @@ -0,0 +1,281 @@ +package validator + +import ( + "bytes" + "encoding/xml" + "errors" + "fmt" + "io" +) + +// XMLRoundtripError is returned when a round-trip token doesn't match the original +type XMLRoundtripError struct { + Expected, Observed xml.Token + Overflow []byte +} + +func (err XMLRoundtripError) Error() string { + if len(err.Overflow) == 0 { + return fmt.Sprintf("roundtrip error: expected %v, observed %v", err.Expected, err.Observed) + } + return fmt.Sprintf("roundtrip error: unexpected overflow after token: %s", err.Overflow) +} + +// XMLValidationError is returned when validating an XML document fails +type XMLValidationError struct { + Start, End, Line, Column int64 + err error +} + +func (err XMLValidationError) Error() string { + return fmt.Sprintf("validator: in token starting at %d:%d: %s", err.Line, err.Column, err.err.Error()) +} + +func (err XMLValidationError) Unwrap() error { + return err.err +} + +// Validate makes sure the given XML bytes survive round trips through encoding/xml without mutations +func Validate(xmlReader io.Reader) error { + xmlBuffer := &bytes.Buffer{} + xmlReader = &byteReader{io.TeeReader(xmlReader, xmlBuffer)} + decoder := xml.NewDecoder(xmlReader) + decoder.Strict = false + decoder.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) { return input, nil } + offset := int64(0) + for { + token, err := decoder.RawToken() + if err == io.EOF { + return nil + } else if err != nil { + return err + } + if err := CheckToken(token); err != nil { + xmlBytes := xmlBuffer.Bytes() + line := bytes.Count(xmlBytes[0:offset], []byte{'\n'}) + 1 + lineStart := int64(bytes.LastIndexByte(xmlBytes[0:offset], '\n')) + 1 + column := offset - lineStart + 1 + return XMLValidationError{ + Start: offset, + End: decoder.InputOffset(), + Line: int64(line), + Column: column, + err: err, + } + } + offset = decoder.InputOffset() + } +} + +// ValidateAll is like Validate, but instead of returning after the first error, +// it accumulates errors and validates the entire document +func ValidateAll(xmlReader io.Reader) []error { + xmlBuffer := &bytes.Buffer{} + xmlReader = io.TeeReader(xmlReader, xmlBuffer) + errs := []error{} + offset := int64(0) + line := int64(1) + column := int64(1) + for { + err := Validate(xmlReader) + if err == nil { + // reached the end with no additional errors + break + } + validationError := XMLValidationError{} + if errors.As(err, &validationError) { + // validation errors contain line numbers and offsets, but + // these offsets are based on the offset where Validate + // was called, so they need to be adjusted to accordingly + validationError.Start += offset + validationError.End += offset + if validationError.Line == 1 { + validationError.Column += column - 1 + } + validationError.Line += line - 1 + errs = append(errs, validationError) + xmlBytes := xmlBuffer.Bytes() + offset += int64(len(xmlBytes)) + newLines := int64(bytes.Count(xmlBytes, []byte("\n"))) + line += newLines + if newLines > 0 { + column = int64(len(xmlBytes) - bytes.LastIndex(xmlBytes, []byte("\n"))) + } else { + column += int64(len(xmlBytes)) + } + xmlBuffer.Reset() + } else { + // this was not a validation error, but likely + // completely unparseable XML instead; no point + // in trying to continue + errs = append(errs, err) + break + } + } + return errs +} + +// bufio implements a ByteReader but we explicitly don't want any buffering +type byteReader struct { + r io.Reader +} + +func (r *byteReader) ReadByte() (byte, error) { + p := make([]byte, 1) + _, err := r.r.Read(p) + return p[0], err +} + +func (r *byteReader) Read(p []byte) (int, error) { + return r.r.Read(p) +} + +// CheckToken computes a round trip for a given xml.Token and returns an +// error if the newly calculated token differs from the original +func CheckToken(before xml.Token) error { + buffer := &bytes.Buffer{} + encoder := xml.NewEncoder(buffer) + + switch t := before.(type) { + case xml.EndElement: + // xml.Encoder expects matching StartElements for all EndElements + if err := encoder.EncodeToken(xml.StartElement{Name: t.Name}); err != nil { + return err + } + } + + if err := encoder.EncodeToken(before); err != nil { + return err + } + if err := encoder.Flush(); err != nil { + return err + } + encoded := buffer.Bytes() + decoder := xml.NewDecoder(bytes.NewReader(encoded)) + decoder.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) { return input, nil } + + switch before.(type) { + case xml.EndElement: + // throw away the StartElement we added above + if _, err := decoder.RawToken(); err != nil { + return err + } + } + + after, err := decoder.RawToken() + if err != nil { + return err + } + + if !tokenEquals(before, after) { + return XMLRoundtripError{before, after, nil} + } + offset := decoder.InputOffset() + if offset != int64(len(encoded)) { + // this is likely unreachable, but just in case + return XMLRoundtripError{before, after, encoded[offset:]} + } + return nil +} + +func tokenEquals(before, after xml.Token) bool { + switch t1 := before.(type) { + + case xml.CharData: + t2, ok := after.(xml.CharData) + if !ok { + return false + } + return bytes.Equal(t1, t2) + + case xml.Comment: + t2, ok := after.(xml.Comment) + if !ok { + return false + } + return bytes.Equal(t1, t2) + + case xml.Directive: + t2, ok := after.(xml.Directive) + if !ok { + return false + } + return bytes.Equal(t1, t2) + + case xml.EndElement: + t2, ok := after.(xml.EndElement) + if !ok { + return false + } + // local name should equal; namespace prefixes get erased + return t1.Name.Local == t2.Name.Local && t2.Name.Space == "" + + case xml.ProcInst: + t2, ok := after.(xml.ProcInst) + if !ok { + return false + } + return t1.Target == t2.Target && bytes.Equal(t1.Inst, t2.Inst) + + case xml.StartElement: + t2, ok := after.(xml.StartElement) + if !ok { + return false + } + // encoding/xml messes up namespace prefixes on both tag and attribute names; + // they need adjusting to make the comparison possible + fixNamespacePrefixes(&t1, &t2) + if t1.Name != t2.Name { + return false + } + if len(t1.Attr) != len(t2.Attr) { + return false + } + // after the call to fixNamespacePrefixes, all attributes should match; + // ordering is preserved + for i, attr := range t1.Attr { + if attr != t2.Attr[i] { + return false + } + } + return true + } + return false +} + +func fixNamespacePrefixes(before, after *xml.StartElement) { + // if the after token has more attributes than the before token, + // the round trip likely introduced new xmlns attributes + if len(after.Attr) > len(before.Attr) { + + // handle erased tag prefixes; the corresponding xmlns attribute is always the first one + if (before.Name.Space != "" && after.Name.Space == "" && after.Attr[0].Name == xml.Name{Local: "xmlns"}) { + after.Name.Space = after.Attr[0].Value + after.Attr = after.Attr[1:] + } + + // handle attribute prefixes; the xmlns attribute always comes immediately before the prefixed attribute + for len(after.Attr) > len(before.Attr) && len(after.Attr) > 1 { + var xmlns *xml.Attr + i := 1 + for ; i < len(after.Attr); i++ { + if after.Attr[i-1].Name.Space == "xmlns" && after.Attr[i-1].Name.Local == after.Attr[i].Name.Space { + xmlns = &after.Attr[i-1] + break + } + } + if xmlns == nil { + break + } + prefix := xmlns.Name.Local + space := xmlns.Value + copy(after.Attr[i-1:], after.Attr[i:]) + after.Attr = after.Attr[:len(after.Attr)-1] + for j := range after.Attr { + if after.Attr[j].Name.Space == prefix { + after.Attr[j].Name.Space = space + } + } + } + } +} diff --git a/vendor/github.com/russellhaering/gosaml2/attribute.go b/vendor/github.com/russellhaering/gosaml2/attribute.go index 0109cf94da223..6b63cd509d10e 100644 --- a/vendor/github.com/russellhaering/gosaml2/attribute.go +++ b/vendor/github.com/russellhaering/gosaml2/attribute.go @@ -1,3 +1,16 @@ +// Copyright 2016 Russell Haering et al. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package saml2 import "github.com/russellhaering/gosaml2/types" @@ -17,3 +30,36 @@ func (vals Values) Get(k string) string { } return "" } + +//GetSize returns the number of values for an attribute at a key. +//Returns '0' in case of error or if key is not found. +func (vals Values) GetSize(k string) int { + if vals == nil { + return 0 + } + + v, ok := vals[k] + if ok { + return len(v.Values) + } + + return 0 +} + +//GetAll returns all the values for an attribute at a key. +//Returns an empty slice in case of error of if key is not found. +func (vals Values) GetAll(k string) []string { + var av []string + + if vals == nil { + return av + } + + if v, ok := vals[k]; ok && len(v.Values) > 0 { + for i := 0; i < len(v.Values); i++ { + av = append(av, string(v.Values[i].Value)) + } + } + + return av +} diff --git a/vendor/github.com/russellhaering/gosaml2/authn_request.go b/vendor/github.com/russellhaering/gosaml2/authn_request.go index 9f8360adc7ac3..3c3da162d3027 100644 --- a/vendor/github.com/russellhaering/gosaml2/authn_request.go +++ b/vendor/github.com/russellhaering/gosaml2/authn_request.go @@ -1,3 +1,16 @@ +// Copyright 2016 Russell Haering et al. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package saml2 import "time" diff --git a/vendor/github.com/russellhaering/gosaml2/build_logout_response.go b/vendor/github.com/russellhaering/gosaml2/build_logout_response.go new file mode 100644 index 0000000000000..a7f488be90a82 --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/build_logout_response.go @@ -0,0 +1,157 @@ +// Copyright 2016 Russell Haering et al. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package saml2 + +import ( + "bytes" + "encoding/base64" + "html/template" + + "github.com/beevik/etree" + "github.com/russellhaering/gosaml2/uuid" +) + +func (sp *SAMLServiceProvider) buildLogoutResponse(statusCodeValue string, reqID string, includeSig bool) (*etree.Document, error) { + logoutResponse := &etree.Element{ + Space: "samlp", + Tag: "LogoutResponse", + } + + logoutResponse.CreateAttr("xmlns:samlp", "urn:oasis:names:tc:SAML:2.0:protocol") + logoutResponse.CreateAttr("xmlns:saml", "urn:oasis:names:tc:SAML:2.0:assertion") + + arId := uuid.NewV4() + + logoutResponse.CreateAttr("ID", "_"+arId.String()) + logoutResponse.CreateAttr("Version", "2.0") + logoutResponse.CreateAttr("IssueInstant", sp.Clock.Now().UTC().Format(issueInstantFormat)) + logoutResponse.CreateAttr("Destination", sp.IdentityProviderSLOURL) + logoutResponse.CreateAttr("InResponseTo", reqID) + + // NOTE(russell_h): In earlier versions we mistakenly sent the IdentityProviderIssuer + // in the AuthnRequest. For backwards compatibility we will fall back to that + // behavior when ServiceProviderIssuer isn't set. + if sp.ServiceProviderIssuer != "" { + logoutResponse.CreateElement("saml:Issuer").SetText(sp.ServiceProviderIssuer) + } else { + logoutResponse.CreateElement("saml:Issuer").SetText(sp.IdentityProviderIssuer) + } + + status := logoutResponse.CreateElement("samlp:Status") + statusCode := status.CreateElement("samlp:StatusCode") + statusCode.CreateAttr("Value", statusCodeValue) + + doc := etree.NewDocument() + + // Only POST binding includes in (includeSig) + if includeSig { + signed, err := sp.SignLogoutResponse(logoutResponse) + if err != nil { + return nil, err + } + + doc.SetRoot(signed) + } else { + doc.SetRoot(logoutResponse) + } + return doc, nil +} +func (sp *SAMLServiceProvider) BuildLogoutResponseDocument(status string, reqID string) (*etree.Document, error) { + return sp.buildLogoutResponse(status, reqID, true) +} + +func (sp *SAMLServiceProvider) BuildLogoutResponseDocumentNoSig(status string, reqID string) (*etree.Document, error) { + return sp.buildLogoutResponse(status, reqID, false) +} + +func (sp *SAMLServiceProvider) SignLogoutResponse(el *etree.Element) (*etree.Element, error) { + ctx := sp.SigningContext() + + sig, err := ctx.ConstructSignature(el, true) + if err != nil { + return nil, err + } + + ret := el.Copy() + + var children []etree.Token + children = append(children, ret.Child[0]) // issuer is always first + children = append(children, sig) // next is the signature + children = append(children, ret.Child[1:]...) // then all other children + ret.Child = children + + return ret, nil +} + +func (sp *SAMLServiceProvider) buildLogoutResponseBodyPostFromDocument(relayState string, doc *etree.Document) ([]byte, error) { + respBuf, err := doc.WriteToBytes() + if err != nil { + return nil, err + } + + encodedRespBuf := base64.StdEncoding.EncodeToString(respBuf) + + var tmpl *template.Template + var rv bytes.Buffer + + if relayState != "" { + tmpl = template.Must(template.New("saml-post-form").Parse(`` + + `
` + + `` + + `` + + `` + + `
` + + `` + + `` + + ``)) + data := struct { + URL string + SAMLResponse string + RelayState string + }{ + URL: sp.IdentityProviderSLOURL, + SAMLResponse: encodedRespBuf, + RelayState: relayState, + } + if err = tmpl.Execute(&rv, data); err != nil { + return nil, err + } + } else { + tmpl = template.Must(template.New("saml-post-form").Parse(`` + + `
` + + `` + + `` + + `
` + + `` + + `` + + ``)) + data := struct { + URL string + SAMLResponse string + }{ + URL: sp.IdentityProviderSLOURL, + SAMLResponse: encodedRespBuf, + } + + if err = tmpl.Execute(&rv, data); err != nil { + return nil, err + } + } + + return rv.Bytes(), nil +} + +func (sp *SAMLServiceProvider) BuildLogoutResponseBodyPostFromDocument(relayState string, doc *etree.Document) ([]byte, error) { + return sp.buildLogoutResponseBodyPostFromDocument(relayState, doc) +} diff --git a/vendor/github.com/russellhaering/gosaml2/build_request.go b/vendor/github.com/russellhaering/gosaml2/build_request.go index 4ccfc8c03dd08..f07868f831335 100644 --- a/vendor/github.com/russellhaering/gosaml2/build_request.go +++ b/vendor/github.com/russellhaering/gosaml2/build_request.go @@ -1,3 +1,16 @@ +// Copyright 2016 Russell Haering et al. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package saml2 import ( @@ -5,16 +18,17 @@ import ( "compress/flate" "encoding/base64" "fmt" + "html/template" "net/http" "net/url" "github.com/beevik/etree" - "github.com/satori/go.uuid" + "github.com/russellhaering/gosaml2/uuid" ) const issueInstantFormat = "2006-01-02T15:04:05Z" -func (sp *SAMLServiceProvider) BuildAuthRequestDocument() (*etree.Document, error) { +func (sp *SAMLServiceProvider) buildAuthnRequest(includeSig bool) (*etree.Document, error) { authnRequest := &etree.Element{ Space: "samlp", Tag: "AuthnRequest", @@ -23,7 +37,9 @@ func (sp *SAMLServiceProvider) BuildAuthRequestDocument() (*etree.Document, erro authnRequest.CreateAttr("xmlns:samlp", "urn:oasis:names:tc:SAML:2.0:protocol") authnRequest.CreateAttr("xmlns:saml", "urn:oasis:names:tc:SAML:2.0:assertion") - authnRequest.CreateAttr("ID", "_"+uuid.NewV4().String()) + arId := uuid.NewV4() + + authnRequest.CreateAttr("ID", "_"+arId.String()) authnRequest.CreateAttr("Version", "2.0") authnRequest.CreateAttr("ProtocolBinding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST") authnRequest.CreateAttr("AssertionConsumerServiceURL", sp.AssertionConsumerServiceURL) @@ -55,7 +71,8 @@ func (sp *SAMLServiceProvider) BuildAuthRequestDocument() (*etree.Document, erro doc := etree.NewDocument() - if sp.SignAuthnRequests { + // Only POST binding includes in (includeSig) + if sp.SignAuthnRequests && includeSig { signed, err := sp.SignAuthnRequest(authnRequest) if err != nil { return nil, err @@ -68,6 +85,14 @@ func (sp *SAMLServiceProvider) BuildAuthRequestDocument() (*etree.Document, erro return doc, nil } +func (sp *SAMLServiceProvider) BuildAuthRequestDocument() (*etree.Document, error) { + return sp.buildAuthnRequest(true) +} + +func (sp *SAMLServiceProvider) BuildAuthRequestDocumentNoSig() (*etree.Document, error) { + return sp.buildAuthnRequest(false) +} + // SignAuthnRequest takes a document, builds a signature, creates another document // and inserts the signature in it. According to the schema, the position of the // signature is right after the Issuer [1] then all other children. @@ -101,7 +126,7 @@ func (sp *SAMLServiceProvider) BuildAuthRequest() (string, error) { return doc.WriteToString() } -func (sp *SAMLServiceProvider) BuildAuthURLFromDocument(relayState string, doc *etree.Document) (string, error) { +func (sp *SAMLServiceProvider) buildAuthURLFromDocument(relayState, binding string, doc *etree.Document) (string, error) { parsedUrl, err := url.Parse(sp.IdentityProviderSSOURL) if err != nil { return "", err @@ -137,10 +162,113 @@ func (sp *SAMLServiceProvider) BuildAuthURLFromDocument(relayState string, doc * qs.Add("RelayState", relayState) } + if sp.SignAuthnRequests && binding == BindingHttpRedirect { + // Sign URL encoded query (see Section 3.4.4.1 DEFLATE Encoding of saml-bindings-2.0-os.pdf) + ctx := sp.SigningContext() + qs.Add("SigAlg", ctx.GetSignatureMethodIdentifier()) + var rawSignature []byte + if rawSignature, err = ctx.SignString(signatureInputString(qs.Get("SAMLRequest"), qs.Get("RelayState"), qs.Get("SigAlg"))); err != nil { + return "", fmt.Errorf("unable to sign query string of redirect URL: %v", err) + } + + // Now add base64 encoded Signature + qs.Add("Signature", base64.StdEncoding.EncodeToString(rawSignature)) + } + + //Here the parameters may appear in any order. parsedUrl.RawQuery = qs.Encode() return parsedUrl.String(), nil } +func (sp *SAMLServiceProvider) BuildAuthURLFromDocument(relayState string, doc *etree.Document) (string, error) { + return sp.buildAuthURLFromDocument(relayState, BindingHttpPost, doc) +} + +func (sp *SAMLServiceProvider) BuildAuthURLRedirect(relayState string, doc *etree.Document) (string, error) { + return sp.buildAuthURLFromDocument(relayState, BindingHttpRedirect, doc) +} + +func (sp *SAMLServiceProvider) buildAuthBodyPostFromDocument(relayState string, doc *etree.Document) ([]byte, error) { + reqBuf, err := doc.WriteToBytes() + if err != nil { + return nil, err + } + + encodedReqBuf := base64.StdEncoding.EncodeToString(reqBuf) + + var tmpl *template.Template + var rv bytes.Buffer + + if relayState != "" { + tmpl = template.Must(template.New("saml-post-form").Parse(`` + + `
` + + `` + + `` + + `` + + `
` + + ``)) + + data := struct { + URL string + SAMLRequest string + RelayState string + }{ + URL: sp.IdentityProviderSSOURL, + SAMLRequest: encodedReqBuf, + RelayState: relayState, + } + if err = tmpl.Execute(&rv, data); err != nil { + return nil, err + } + } else { + tmpl = template.Must(template.New("saml-post-form").Parse(`` + + `
` + + `` + + `` + + `
` + + ``)) + + data := struct { + URL string + SAMLRequest string + }{ + URL: sp.IdentityProviderSSOURL, + SAMLRequest: encodedReqBuf, + } + if err = tmpl.Execute(&rv, data); err != nil { + return nil, err + } + } + + return rv.Bytes(), nil +} + +//BuildAuthBodyPost builds the POST body to be sent to IDP. +func (sp *SAMLServiceProvider) BuildAuthBodyPost(relayState string) ([]byte, error) { + var doc *etree.Document + var err error + + if sp.SignAuthnRequests { + doc, err = sp.BuildAuthRequestDocument() + } else { + doc, err = sp.BuildAuthRequestDocumentNoSig() + } + + if err != nil { + return nil, err + } + + return sp.buildAuthBodyPostFromDocument(relayState, doc) +} + +//BuildAuthBodyPostFromDocument builds the POST body to be sent to IDP. +//It takes the AuthnRequest xml as input. +func (sp *SAMLServiceProvider) BuildAuthBodyPostFromDocument(relayState string, doc *etree.Document) ([]byte, error) { + return sp.buildAuthBodyPostFromDocument(relayState, doc) +} + // BuildAuthURL builds redirect URL to be sent to principal func (sp *SAMLServiceProvider) BuildAuthURL(relayState string) (string, error) { doc, err := sp.BuildAuthRequestDocument() @@ -162,3 +290,259 @@ func (sp *SAMLServiceProvider) AuthRedirect(w http.ResponseWriter, r *http.Reque http.Redirect(w, r, url, http.StatusFound) return nil } + +func (sp *SAMLServiceProvider) buildLogoutRequest(includeSig bool, nameID string, sessionIndex string) (*etree.Document, error) { + logoutRequest := &etree.Element{ + Space: "samlp", + Tag: "LogoutRequest", + } + + logoutRequest.CreateAttr("xmlns:samlp", "urn:oasis:names:tc:SAML:2.0:protocol") + logoutRequest.CreateAttr("xmlns:saml", "urn:oasis:names:tc:SAML:2.0:assertion") + + arId := uuid.NewV4() + + logoutRequest.CreateAttr("ID", "_"+arId.String()) + logoutRequest.CreateAttr("Version", "2.0") + logoutRequest.CreateAttr("IssueInstant", sp.Clock.Now().UTC().Format(issueInstantFormat)) + logoutRequest.CreateAttr("Destination", sp.IdentityProviderSLOURL) + + // NOTE(russell_h): In earlier versions we mistakenly sent the IdentityProviderIssuer + // in the AuthnRequest. For backwards compatibility we will fall back to that + // behavior when ServiceProviderIssuer isn't set. + // TODO: Throw error in case Issuer is empty. + if sp.ServiceProviderIssuer != "" { + logoutRequest.CreateElement("saml:Issuer").SetText(sp.ServiceProviderIssuer) + } else { + logoutRequest.CreateElement("saml:Issuer").SetText(sp.IdentityProviderIssuer) + } + + nameId := logoutRequest.CreateElement("saml:NameID") + nameId.SetText(nameID) + nameId.CreateAttr("Format", sp.NameIdFormat) + + //Section 3.7.1 - http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf says + //SessionIndex is optional. If the IDP supports SLO then it must send SessionIndex as per + //Section 4.1.4.2 of https://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf. + //As per section 4.4.3.1 of //docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf, + //a LogoutRequest issued by Session Participant to Identity Provider, must contain + //at least one SessionIndex element needs to be included. + nameId = logoutRequest.CreateElement("samlp:SessionIndex") + nameId.SetText(sessionIndex) + + doc := etree.NewDocument() + + if includeSig { + signed, err := sp.SignLogoutRequest(logoutRequest) + if err != nil { + return nil, err + } + + doc.SetRoot(signed) + } else { + doc.SetRoot(logoutRequest) + } + + return doc, nil +} + +func (sp *SAMLServiceProvider) SignLogoutRequest(el *etree.Element) (*etree.Element, error) { + ctx := sp.SigningContext() + + sig, err := ctx.ConstructSignature(el, true) + if err != nil { + return nil, err + } + + ret := el.Copy() + + var children []etree.Token + children = append(children, ret.Child[0]) // issuer is always first + children = append(children, sig) // next is the signature + children = append(children, ret.Child[1:]...) // then all other children + ret.Child = children + + return ret, nil +} + +func (sp *SAMLServiceProvider) BuildLogoutRequestDocumentNoSig(nameID string, sessionIndex string) (*etree.Document, error) { + return sp.buildLogoutRequest(false, nameID, sessionIndex) +} + +func (sp *SAMLServiceProvider) BuildLogoutRequestDocument(nameID string, sessionIndex string) (*etree.Document, error) { + return sp.buildLogoutRequest(true, nameID, sessionIndex) +} + +//BuildLogoutBodyPostFromDocument builds the POST body to be sent to IDP. +//It takes the LogoutRequest xml as input. +func (sp *SAMLServiceProvider) BuildLogoutBodyPostFromDocument(relayState string, doc *etree.Document) ([]byte, error) { + return sp.buildLogoutBodyPostFromDocument(relayState, doc) +} + +func (sp *SAMLServiceProvider) buildLogoutBodyPostFromDocument(relayState string, doc *etree.Document) ([]byte, error) { + reqBuf, err := doc.WriteToBytes() + if err != nil { + return nil, err + } + + encodedReqBuf := base64.StdEncoding.EncodeToString(reqBuf) + var tmpl *template.Template + var rv bytes.Buffer + + if relayState != "" { + tmpl = template.Must(template.New("saml-post-form").Parse(`` + + `
` + + `` + + `` + + `` + + `
` + + ``)) + + data := struct { + URL string + SAMLRequest string + RelayState string + }{ + URL: sp.IdentityProviderSLOURL, + SAMLRequest: encodedReqBuf, + RelayState: relayState, + } + if err = tmpl.Execute(&rv, data); err != nil { + return nil, err + } + } else { + tmpl = template.Must(template.New("saml-post-form").Parse(`` + + `
` + + `` + + `` + + `
` + + ``)) + + data := struct { + URL string + SAMLRequest string + }{ + URL: sp.IdentityProviderSLOURL, + SAMLRequest: encodedReqBuf, + } + if err = tmpl.Execute(&rv, data); err != nil { + return nil, err + } + } + + return rv.Bytes(), nil +} + +func (sp *SAMLServiceProvider) BuildLogoutURLRedirect(relayState string, doc *etree.Document) (string, error) { + return sp.buildLogoutURLFromDocument(relayState, BindingHttpRedirect, doc) +} + +func (sp *SAMLServiceProvider) buildLogoutURLFromDocument(relayState, binding string, doc *etree.Document) (string, error) { + parsedUrl, err := url.Parse(sp.IdentityProviderSLOURL) + if err != nil { + return "", err + } + + logoutRequest, err := doc.WriteToString() + if err != nil { + return "", err + } + + buf := &bytes.Buffer{} + + fw, err := flate.NewWriter(buf, flate.DefaultCompression) + if err != nil { + return "", fmt.Errorf("flate NewWriter error: %v", err) + } + + _, err = fw.Write([]byte(logoutRequest)) + if err != nil { + return "", fmt.Errorf("flate.Writer Write error: %v", err) + } + + err = fw.Close() + if err != nil { + return "", fmt.Errorf("flate.Writer Close error: %v", err) + } + + qs := parsedUrl.Query() + + qs.Add("SAMLRequest", base64.StdEncoding.EncodeToString(buf.Bytes())) + + if relayState != "" { + qs.Add("RelayState", relayState) + } + + if binding == BindingHttpRedirect { + // Sign URL encoded query (see Section 3.4.4.1 DEFLATE Encoding of saml-bindings-2.0-os.pdf) + ctx := sp.SigningContext() + qs.Add("SigAlg", ctx.GetSignatureMethodIdentifier()) + var rawSignature []byte + //qs.Encode() sorts the keys (See https://golang.org/pkg/net/url/#Values.Encode). + //If RelayState parameter is present then RelayState parameter + //will be put first by Encode(). Hence encode them separately and concatenate. + //Signature string has to have parameters in the order - SAMLRequest=value&RelayState=value&SigAlg=value. + //(See Section 3.4.4.1 saml-bindings-2.0-os.pdf). + var orderedParams = []string{"SAMLRequest", "RelayState", "SigAlg"} + + var paramValueMap = make(map[string]string) + paramValueMap["SAMLRequest"] = base64.StdEncoding.EncodeToString(buf.Bytes()) + if relayState != "" { + paramValueMap["RelayState"] = relayState + } + paramValueMap["SigAlg"] = ctx.GetSignatureMethodIdentifier() + + ss := "" + + for _, k := range orderedParams { + v, ok := paramValueMap[k] + if ok { + //Add the value after URL encoding. + u := url.Values{} + u.Add(k, v) + e := u.Encode() + if ss != "" { + ss += "&" + e + } else { + ss = e + } + } + } + + //Now generate the signature on the string of ordered parameters. + if rawSignature, err = ctx.SignString(ss); err != nil { + return "", fmt.Errorf("unable to sign query string of redirect URL: %v", err) + } + + // Now add base64 encoded Signature + qs.Add("Signature", base64.StdEncoding.EncodeToString(rawSignature)) + } + + //Here the parameters may appear in any order. + parsedUrl.RawQuery = qs.Encode() + return parsedUrl.String(), nil +} + +// signatureInputString constructs the string to be fed into the signature algorithm, as described +// in section 3.4.4.1 of +// https://www.oasis-open.org/committees/download.php/56779/sstc-saml-bindings-errata-2.0-wd-06.pdf +func signatureInputString(samlRequest, relayState, sigAlg string) string { + var params [][2]string + if relayState == "" { + params = [][2]string{{"SAMLRequest", samlRequest}, {"SigAlg", sigAlg}} + } else { + params = [][2]string{{"SAMLRequest", samlRequest}, {"RelayState", relayState}, {"SigAlg", sigAlg}} + } + + var buf bytes.Buffer + for _, kv := range params { + k, v := kv[0], kv[1] + if buf.Len() > 0 { + buf.WriteByte('&') + } + buf.WriteString(url.QueryEscape(k) + "=" + url.QueryEscape(v)) + } + return buf.String() +} diff --git a/vendor/github.com/russellhaering/gosaml2/decode_logout_request.go b/vendor/github.com/russellhaering/gosaml2/decode_logout_request.go new file mode 100644 index 0000000000000..c7545d0664a41 --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/decode_logout_request.go @@ -0,0 +1,84 @@ +// Copyright 2016 Russell Haering et al. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package saml2 + +import ( + "encoding/base64" + "fmt" + + dsig "github.com/russellhaering/goxmldsig" +) + +func (sp *SAMLServiceProvider) validateLogoutRequestAttributes(request *LogoutRequest) error { + if request.Destination != "" && request.Destination != sp.ServiceProviderSLOURL { + return ErrInvalidValue{ + Key: DestinationAttr, + Expected: sp.ServiceProviderSLOURL, + Actual: request.Destination, + } + } + + if request.Version != "2.0" { + return ErrInvalidValue{ + Reason: ReasonUnsupported, + Key: "SAML version", + Expected: "2.0", + Actual: request.Version, + } + } + + return nil +} + +func (sp *SAMLServiceProvider) ValidateEncodedLogoutRequestPOST(encodedRequest string) (*LogoutRequest, error) { + raw, err := base64.StdEncoding.DecodeString(encodedRequest) + if err != nil { + return nil, err + } + + // Parse the raw request - parseResponse is generic + doc, el, err := parseResponse(raw) + if err != nil { + return nil, err + } + + var requestSignatureValidated bool + if !sp.SkipSignatureValidation { + el, err = sp.validateElementSignature(el) + if err == dsig.ErrMissingSignature { + // Unfortunately we just blew away our Response + el = doc.Root() + } else if err != nil { + return nil, err + } else if el == nil { + return nil, fmt.Errorf("missing transformed logout request") + } else { + requestSignatureValidated = true + } + } + + decodedRequest := &LogoutRequest{} + err = xmlUnmarshalElement(el, decodedRequest) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal logout request: %v", err) + } + decodedRequest.SignatureValidated = requestSignatureValidated + + err = sp.ValidateDecodedLogoutRequest(decodedRequest) + if err != nil { + return nil, err + } + + return decodedRequest, nil +} diff --git a/vendor/github.com/russellhaering/gosaml2/decode_response.go b/vendor/github.com/russellhaering/gosaml2/decode_response.go index 06e5d9799b4df..f34259e9130e0 100644 --- a/vendor/github.com/russellhaering/gosaml2/decode_response.go +++ b/vendor/github.com/russellhaering/gosaml2/decode_response.go @@ -1,9 +1,23 @@ +// Copyright 2016 Russell Haering et al. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package saml2 import ( "bytes" "compress/flate" "crypto/tls" + "crypto/x509" "encoding/base64" "fmt" "io/ioutil" @@ -14,6 +28,7 @@ import ( "github.com/russellhaering/gosaml2/types" dsig "github.com/russellhaering/goxmldsig" "github.com/russellhaering/goxmldsig/etreeutils" + rtvalidator "github.com/mattermost/xml-roundtrip-validator" ) func (sp *SAMLServiceProvider) validationContext() *dsig.ValidationContext { @@ -25,7 +40,7 @@ func (sp *SAMLServiceProvider) validationContext() *dsig.ValidationContext { // validateResponseAttributes validates a SAML Response's tag and attributes. It does // not inspect child elements of the Response at all. func (sp *SAMLServiceProvider) validateResponseAttributes(response *types.Response) error { - if response.Destination != sp.AssertionConsumerServiceURL { + if response.Destination != "" && response.Destination != sp.AssertionConsumerServiceURL { return ErrInvalidValue{ Key: DestinationAttr, Expected: sp.AssertionConsumerServiceURL, @@ -45,22 +60,42 @@ func (sp *SAMLServiceProvider) validateResponseAttributes(response *types.Respon return nil } -func (sp *SAMLServiceProvider) unmarshalResponse(el *etree.Element) (*types.Response, error) { - response := &types.Response{} +// validateLogoutResponseAttributes validates a SAML Response's tag and attributes. It does +// not inspect child elements of the Response at all. +func (sp *SAMLServiceProvider) validateLogoutResponseAttributes(response *types.LogoutResponse) error { + if response.Destination != "" && response.Destination != sp.ServiceProviderSLOURL { + return ErrInvalidValue{ + Key: DestinationAttr, + Expected: sp.ServiceProviderSLOURL, + Actual: response.Destination, + } + } + if response.Version != "2.0" { + return ErrInvalidValue{ + Reason: ReasonUnsupported, + Key: "SAML version", + Expected: "2.0", + Actual: response.Version, + } + } + + return nil +} + +func xmlUnmarshalElement(el *etree.Element, obj interface{}) error { doc := etree.NewDocument() doc.SetRoot(el) data, err := doc.WriteToBytes() if err != nil { - return nil, err + return err } - err = xml.Unmarshal(data, response) + err = xml.Unmarshal(data, obj) if err != nil { - return nil, err + return err } - - return response, nil + return nil } func (sp *SAMLServiceProvider) getDecryptCert() (*tls.Certificate, error) { @@ -90,25 +125,74 @@ func (sp *SAMLServiceProvider) getDecryptCert() (*tls.Certificate, error) { } } + if sp.ValidateEncryptionCert { + // Check Validity period of certificate + if len(decryptCert.Certificate) < 1 || len(decryptCert.Certificate[0]) < 1 { + return nil, fmt.Errorf("empty decryption cert") + } else if cert, err := x509.ParseCertificate(decryptCert.Certificate[0]); err != nil { + return nil, fmt.Errorf("invalid x509 decryption cert: %v", err) + } else { + now := sp.Clock.Now() + if now.Before(cert.NotBefore) || now.After(cert.NotAfter) { + return nil, fmt.Errorf("decryption cert is not valid at this time") + } + } + } + return &decryptCert, nil } -func (sp *SAMLServiceProvider) decryptAssertions(response *types.Response) error { - for _, ea := range response.EncryptedAssertions { - decryptCert, err := sp.getDecryptCert() +func (sp *SAMLServiceProvider) decryptAssertions(el *etree.Element) error { + var decryptCert *tls.Certificate + + decryptAssertion := func(ctx etreeutils.NSContext, encryptedElement *etree.Element) error { + if encryptedElement.Parent() != el { + return fmt.Errorf("found encrypted assertion with unexpected parent element: %s", encryptedElement.Parent().Tag) + } + + detached, err := etreeutils.NSDetatch(ctx, encryptedElement) // make a detached copy if err != nil { - return err + return fmt.Errorf("unable to detach encrypted assertion: %v", err) } - assertion, err := ea.Decrypt(decryptCert) + encryptedAssertion := &types.EncryptedAssertion{} + err = xmlUnmarshalElement(detached, encryptedAssertion) if err != nil { - return err + return fmt.Errorf("unable to unmarshal encrypted assertion: %v", err) + } + + if decryptCert == nil { + decryptCert, err = sp.getDecryptCert() + if err != nil { + return fmt.Errorf("unable to get decryption certificate: %v", err) + } + } + + raw, derr := encryptedAssertion.DecryptBytes(decryptCert) + if derr != nil { + return fmt.Errorf("unable to decrypt encrypted assertion: %v", derr) + } + + doc, _, err := parseResponse(raw) + if err != nil { + return fmt.Errorf("unable to create element from decrypted assertion bytes: %v", derr) + } + + // Replace the original encrypted assertion with the decrypted one. + if el.RemoveChild(encryptedElement) == nil { + // Out of an abundance of caution, make sure removed worked + panic("unable to remove encrypted assertion") } - response.Assertions = append(response.Assertions, *assertion) + el.AddChild(doc.Root()) + return nil } - return nil + if err := etreeutils.NSFindIterate(el, SAMLAssertionNamespace, EncryptedAssertionTag, decryptAssertion); err != nil { + return err + } else { + return nil + } } func (sp *SAMLServiceProvider) validateElementSignature(el *etree.Element) (*etree.Element, error) { @@ -116,24 +200,28 @@ func (sp *SAMLServiceProvider) validateElementSignature(el *etree.Element) (*etr } func (sp *SAMLServiceProvider) validateAssertionSignatures(el *etree.Element) error { + signedAssertions := 0 + unsignedAssertions := 0 validateAssertion := func(ctx etreeutils.NSContext, unverifiedAssertion *etree.Element) error { if unverifiedAssertion.Parent() != el { return fmt.Errorf("found assertion with unexpected parent element: %s", unverifiedAssertion.Parent().Tag) } - detatched, err := etreeutils.NSDetatch(ctx, unverifiedAssertion) + detached, err := etreeutils.NSDetatch(ctx, unverifiedAssertion) // make a detached copy if err != nil { - return err + return fmt.Errorf("unable to detach unverified assertion: %v", err) } - assertion, err := sp.validationContext().Validate(detatched) - if err != nil { + assertion, err := sp.validationContext().Validate(detached) + if err == dsig.ErrMissingSignature { + unsignedAssertions++ + return nil + } else if err != nil { return err } // Replace the original unverified Assertion with the verified one. Note that - // at this point only the Assertion (and not the parent Response) can be trusted - // as having been signed by the IdP. + // if the Response is not signed, only signed Assertions (and not the parent Response) can be trusted. if el.RemoveChild(unverifiedAssertion) == nil { // Out of an abundance of caution, check to make sure an Assertion was actually // removed. If it wasn't a programming error has occurred. @@ -141,11 +229,20 @@ func (sp *SAMLServiceProvider) validateAssertionSignatures(el *etree.Element) er } el.AddChild(assertion) + signedAssertions++ return nil } - return etreeutils.NSFindIterate(el, SAMLAssertionNamespace, AssertionTag, validateAssertion) + if err := etreeutils.NSFindIterate(el, SAMLAssertionNamespace, AssertionTag, validateAssertion); err != nil { + return err + } else if signedAssertions > 0 && unsignedAssertions > 0 { + return fmt.Errorf("invalid to have both signed and unsigned assertions") + } else if signedAssertions < 1 { + return dsig.ErrMissingSignature + } else { + return nil + } } //ValidateEncodedResponse both decodes and validates, based on SP @@ -157,60 +254,191 @@ func (sp *SAMLServiceProvider) ValidateEncodedResponse(encodedResponse string) ( return nil, err } - doc := etree.NewDocument() - err = doc.ReadFromBytes(raw) + // Parse the raw response + doc, el, err := parseResponse(raw) if err != nil { - // Attempt to inflate the response in case it happens to be compressed (as with one case at saml.oktadev.com) - buf, err := ioutil.ReadAll(flate.NewReader(bytes.NewReader(raw))) - if err != nil { + return nil, err + } + + var responseSignatureValidated bool + if !sp.SkipSignatureValidation { + el, err = sp.validateElementSignature(el) + if err == dsig.ErrMissingSignature { + // Unfortunately we just blew away our Response + el = doc.Root() + } else if err != nil { return nil, err + } else if el == nil { + return nil, fmt.Errorf("missing transformed response") + } else { + responseSignatureValidated = true } + } - doc = etree.NewDocument() - err = doc.ReadFromBytes(buf) - if err != nil { + err = sp.decryptAssertions(el) + if err != nil { + return nil, err + } + + var assertionSignaturesValidated bool + if !sp.SkipSignatureValidation { + err = sp.validateAssertionSignatures(el) + if err == dsig.ErrMissingSignature { + if !responseSignatureValidated { + return nil, fmt.Errorf("response and/or assertions must be signed") + } + } else if err != nil { return nil, err + } else { + assertionSignaturesValidated = true + } + } + + decodedResponse := &types.Response{} + err = xmlUnmarshalElement(el, decodedResponse) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal response: %v", err) + } + decodedResponse.SignatureValidated = responseSignatureValidated + if assertionSignaturesValidated { + for idx := 0; idx < len(decodedResponse.Assertions); idx++ { + decodedResponse.Assertions[idx].SignatureValidated = true } } - if doc.Root() == nil { - return nil, fmt.Errorf("unable to parse response") + err = sp.Validate(decodedResponse) + if err != nil { + return nil, err + } + + return decodedResponse, nil +} + +// DecodeUnverifiedBaseResponse decodes several attributes from a SAML response for the purpose +// of determining how to validate the response. This is useful for Service Providers which +// expose a single Assertion Consumer Service URL but consume Responses from many IdPs. +func DecodeUnverifiedBaseResponse(encodedResponse string) (*types.UnverifiedBaseResponse, error) { + raw, err := base64.StdEncoding.DecodeString(encodedResponse) + if err != nil { + return nil, err + } + + var response *types.UnverifiedBaseResponse + + err = maybeDeflate(raw, func(maybeXML []byte) error { + response = &types.UnverifiedBaseResponse{} + return xml.Unmarshal(maybeXML, response) + }) + if err != nil { + return nil, err + } + + return response, nil +} + +// maybeDeflate invokes the passed decoder over the passed data. If an error is +// returned, it then attempts to deflate the passed data before re-invoking +// the decoder over the deflated data. +func maybeDeflate(data []byte, decoder func([]byte) error) error { + err := decoder(data) + if err == nil { + return nil + } + + deflated, err := ioutil.ReadAll(flate.NewReader(bytes.NewReader(data))) + if err != nil { + return err + } + + return decoder(deflated) +} + +// parseResponse is a helper function that was refactored out so that the XML parsing behavior can be isolated and unit tested +func parseResponse(xml []byte) (*etree.Document, *etree.Element, error) { + var doc *etree.Document + var rawXML []byte + + err := maybeDeflate(xml, func(xml []byte) error { + doc = etree.NewDocument() + rawXML = xml + return doc.ReadFromBytes(xml) + }) + if err != nil { + return nil, nil, err } el := doc.Root() + if el == nil { + return nil, nil, fmt.Errorf("unable to parse response") + } - if !sp.SkipSignatureValidation { - el, err = sp.validateElementSignature(el) - if err == dsig.ErrMissingSignature { - // The Response wasn't signed. It is possible that the Assertion inside of - // the Response was signed. + // Examine the response for attempts to exploit weaknesses in Go's encoding/xml + err = rtvalidator.Validate(bytes.NewReader(rawXML)) + if err != nil { + return nil, nil, err + } - // Unfortunately we just blew away our Response - el = doc.Root() + return doc, el, nil +} - err = sp.validateAssertionSignatures(el) - if err != nil { - return nil, err - } - } else if err != nil || el == nil { - return nil, err - } +// DecodeUnverifiedLogoutResponse decodes several attributes from a SAML Logout response, without doing any verifications. +func DecodeUnverifiedLogoutResponse(encodedResponse string) (*types.LogoutResponse, error) { + raw, err := base64.StdEncoding.DecodeString(encodedResponse) + if err != nil { + return nil, err } - decodedResponse, err := sp.unmarshalResponse(el) + var response *types.LogoutResponse + + err = maybeDeflate(raw, func(maybeXML []byte) error { + response = &types.LogoutResponse{} + return xml.Unmarshal(maybeXML, response) + }) if err != nil { return nil, err } - err = sp.decryptAssertions(decodedResponse) + return response, nil +} + +func (sp *SAMLServiceProvider) ValidateEncodedLogoutResponsePOST(encodedResponse string) (*types.LogoutResponse, error) { + raw, err := base64.StdEncoding.DecodeString(encodedResponse) if err != nil { return nil, err } - err = sp.Validate(decodedResponse) + // Parse the raw response + doc, el, err := parseResponse(raw) + if err != nil { + return nil, err + } + + var responseSignatureValidated bool + if !sp.SkipSignatureValidation { + el, err = sp.validateElementSignature(el) + if err == dsig.ErrMissingSignature { + // Unfortunately we just blew away our Response + el = doc.Root() + } else if err != nil { + return nil, err + } else if el == nil { + return nil, fmt.Errorf("missing transformed logout response") + } else { + responseSignatureValidated = true + } + } + + decodedResponse := &types.LogoutResponse{} + err = xmlUnmarshalElement(el, decodedResponse) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal logout response: %v", err) + } + decodedResponse.SignatureValidated = responseSignatureValidated + + err = sp.ValidateDecodedLogoutResponse(decodedResponse) if err != nil { return nil, err } return decodedResponse, nil -} +} \ No newline at end of file diff --git a/vendor/github.com/russellhaering/gosaml2/logout_request.go b/vendor/github.com/russellhaering/gosaml2/logout_request.go new file mode 100644 index 0000000000000..3a1cee9713dd1 --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/logout_request.go @@ -0,0 +1,36 @@ +// Copyright 2016 Russell Haering et al. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package saml2 + +import ( + "encoding/xml" + "github.com/russellhaering/gosaml2/types" + "time" +) + +// LogoutRequest is the go struct representation of a logout request +type LogoutRequest struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol LogoutRequest"` + ID string `xml:"ID,attr"` + Version string `xml:"Version,attr"` + //ProtocolBinding string `xml:",attr"` + + IssueInstant time.Time `xml:"IssueInstant,attr"` + + Destination string `xml:"Destination,attr"` + Issuer *types.Issuer `xml:"Issuer"` + + NameID *types.NameID `xml:"NameID"` + SignatureValidated bool `xml:"-"` // not read, not dumped +} diff --git a/vendor/github.com/russellhaering/gosaml2/providertests/testdata/adfs_sp_encryption_cert.pem b/vendor/github.com/russellhaering/gosaml2/providertests/testdata/adfs_sp_encryption_cert.pem new file mode 120000 index 0000000000000..7a678d3297818 --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/providertests/testdata/adfs_sp_encryption_cert.pem @@ -0,0 +1 @@ +./oktaenc_sp_encryption_cert.pem \ No newline at end of file diff --git a/vendor/github.com/russellhaering/gosaml2/providertests/testdata/adfs_sp_encryption_key.pem b/vendor/github.com/russellhaering/gosaml2/providertests/testdata/adfs_sp_encryption_key.pem new file mode 120000 index 0000000000000..87fa6131e9881 --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/providertests/testdata/adfs_sp_encryption_key.pem @@ -0,0 +1 @@ +./oktaenc_sp_encryption_key.pem \ No newline at end of file diff --git a/vendor/github.com/russellhaering/gosaml2/providertests/testdata/adfs_sp_signing_cert.pem b/vendor/github.com/russellhaering/gosaml2/providertests/testdata/adfs_sp_signing_cert.pem new file mode 120000 index 0000000000000..3da3924f96540 --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/providertests/testdata/adfs_sp_signing_cert.pem @@ -0,0 +1 @@ +./oktaenc_sp_signing_cert.pem \ No newline at end of file diff --git a/vendor/github.com/russellhaering/gosaml2/providertests/testdata/adfs_sp_signing_key.pem b/vendor/github.com/russellhaering/gosaml2/providertests/testdata/adfs_sp_signing_key.pem new file mode 120000 index 0000000000000..0b3061dc71a46 --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/providertests/testdata/adfs_sp_signing_key.pem @@ -0,0 +1 @@ +./oktaenc_sp_signing_key.pem \ No newline at end of file diff --git a/vendor/github.com/russellhaering/gosaml2/retrieve_assertion.go b/vendor/github.com/russellhaering/gosaml2/retrieve_assertion.go index 58815212478b5..2acb1169235ce 100644 --- a/vendor/github.com/russellhaering/gosaml2/retrieve_assertion.go +++ b/vendor/github.com/russellhaering/gosaml2/retrieve_assertion.go @@ -1,3 +1,16 @@ +// Copyright 2016 Russell Haering et al. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package saml2 import "fmt" @@ -48,6 +61,8 @@ func (sp *SAMLServiceProvider) RetrieveAssertionInfo(encodedResponse string) (*A } assertion := response.Assertions[0] + assertionInfo.Assertions = response.Assertions + assertionInfo.ResponseSignatureValidated = response.SignatureValidated warningInfo, err := sp.VerifyAssertionConditions(&assertion) if err != nil { @@ -86,6 +101,8 @@ func (sp *SAMLServiceProvider) RetrieveAssertionInfo(encodedResponse string) (*A if assertion.AuthnStatement.SessionNotOnOrAfter != nil { assertionInfo.SessionNotOnOrAfter = assertion.AuthnStatement.SessionNotOnOrAfter } + + assertionInfo.SessionIndex = assertion.AuthnStatement.SessionIndex } assertionInfo.WarningInfo = warningInfo diff --git a/vendor/github.com/russellhaering/gosaml2/saml.go b/vendor/github.com/russellhaering/gosaml2/saml.go index 91ee9a59f5a22..516fb149c9f2e 100644 --- a/vendor/github.com/russellhaering/gosaml2/saml.go +++ b/vendor/github.com/russellhaering/gosaml2/saml.go @@ -1,17 +1,49 @@ +// Copyright 2016 Russell Haering et al. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package saml2 import ( + "encoding/base64" "sync" "time" + "github.com/russellhaering/gosaml2/types" dsig "github.com/russellhaering/goxmldsig" + dsigtypes "github.com/russellhaering/goxmldsig/types" ) +type ErrSaml struct { + Message string + System error +} + +func (serr ErrSaml) Error() string { + if serr.Message != "" { + return serr.Message + } + return "SAML error" +} + type SAMLServiceProvider struct { - IdentityProviderSSOURL string - IdentityProviderIssuer string + IdentityProviderSSOURL string + IdentityProviderSSOBinding string + IdentityProviderSLOURL string + IdentityProviderSLOBinding string + IdentityProviderIssuer string AssertionConsumerServiceURL string + ServiceProviderSLOURL string ServiceProviderIssuer string SignAuthnRequests bool @@ -25,8 +57,10 @@ type SAMLServiceProvider struct { RequestedAuthnContext *RequestedAuthnContext AudienceURI string IDPCertificateStore dsig.X509CertificateStore - SPKeyStore dsig.X509KeyStore + SPKeyStore dsig.X509KeyStore // Required encryption key, default signing key + SPSigningKeyStore dsig.X509KeyStore // Optional signing key NameIdFormat string + ValidateEncryptionCert bool SkipSignatureValidation bool AllowMissingAttributes bool Clock *dsig.Clock @@ -50,6 +84,151 @@ type RequestedAuthnContext struct { Contexts []string } +func (sp *SAMLServiceProvider) Metadata() (*types.EntityDescriptor, error) { + signingCertBytes, err := sp.GetSigningCertBytes() + if err != nil { + return nil, err + } + encryptionCertBytes, err := sp.GetEncryptionCertBytes() + if err != nil { + return nil, err + } + return &types.EntityDescriptor{ + ValidUntil: time.Now().UTC().Add(time.Hour * 24 * 7), // 7 days + EntityID: sp.ServiceProviderIssuer, + SPSSODescriptor: &types.SPSSODescriptor{ + AuthnRequestsSigned: sp.SignAuthnRequests, + WantAssertionsSigned: !sp.SkipSignatureValidation, + ProtocolSupportEnumeration: SAMLProtocolNamespace, + KeyDescriptors: []types.KeyDescriptor{ + { + Use: "signing", + KeyInfo: dsigtypes.KeyInfo{ + X509Data: dsigtypes.X509Data{ + X509Certificates: []dsigtypes.X509Certificate{dsigtypes.X509Certificate{ + Data: base64.StdEncoding.EncodeToString(signingCertBytes), + }}, + }, + }, + }, + { + Use: "encryption", + KeyInfo: dsigtypes.KeyInfo{ + X509Data: dsigtypes.X509Data{ + X509Certificates: []dsigtypes.X509Certificate{dsigtypes.X509Certificate{ + Data: base64.StdEncoding.EncodeToString(encryptionCertBytes), + }}, + }, + }, + EncryptionMethods: []types.EncryptionMethod{ + {Algorithm: types.MethodAES128GCM, DigestMethod: nil}, + {Algorithm: types.MethodAES128CBC, DigestMethod: nil}, + {Algorithm: types.MethodAES256CBC, DigestMethod: nil}, + }, + }, + }, + AssertionConsumerServices: []types.IndexedEndpoint{{ + Binding: BindingHttpPost, + Location: sp.AssertionConsumerServiceURL, + Index: 1, + }}, + }, + }, nil +} + +func (sp *SAMLServiceProvider) MetadataWithSLO(validityHours int64) (*types.EntityDescriptor, error) { + signingCertBytes, err := sp.GetSigningCertBytes() + if err != nil { + return nil, err + } + encryptionCertBytes, err := sp.GetEncryptionCertBytes() + if err != nil { + return nil, err + } + + if validityHours <= 0 { + //By default let's keep it to 7 days. + validityHours = int64(time.Hour * 24 * 7) + } + + return &types.EntityDescriptor{ + ValidUntil: time.Now().UTC().Add(time.Duration(validityHours)), // default 7 days + EntityID: sp.ServiceProviderIssuer, + SPSSODescriptor: &types.SPSSODescriptor{ + AuthnRequestsSigned: sp.SignAuthnRequests, + WantAssertionsSigned: !sp.SkipSignatureValidation, + ProtocolSupportEnumeration: SAMLProtocolNamespace, + KeyDescriptors: []types.KeyDescriptor{ + { + Use: "signing", + KeyInfo: dsigtypes.KeyInfo{ + X509Data: dsigtypes.X509Data{ + X509Certificates: []dsigtypes.X509Certificate{dsigtypes.X509Certificate{ + Data: base64.StdEncoding.EncodeToString(signingCertBytes), + }}, + }, + }, + }, + { + Use: "encryption", + KeyInfo: dsigtypes.KeyInfo{ + X509Data: dsigtypes.X509Data{ + X509Certificates: []dsigtypes.X509Certificate{dsigtypes.X509Certificate{ + Data: base64.StdEncoding.EncodeToString(encryptionCertBytes), + }}, + }, + }, + EncryptionMethods: []types.EncryptionMethod{ + {Algorithm: types.MethodAES128GCM, DigestMethod: nil}, + {Algorithm: types.MethodAES128CBC, DigestMethod: nil}, + {Algorithm: types.MethodAES256CBC, DigestMethod: nil}, + }, + }, + }, + AssertionConsumerServices: []types.IndexedEndpoint{{ + Binding: BindingHttpPost, + Location: sp.AssertionConsumerServiceURL, + Index: 1, + }}, + SingleLogoutServices: []types.Endpoint{{ + Binding: BindingHttpPost, + Location: sp.ServiceProviderSLOURL, + }}, + }, + }, nil +} + +func (sp *SAMLServiceProvider) GetEncryptionKey() dsig.X509KeyStore { + return sp.SPKeyStore +} + +func (sp *SAMLServiceProvider) GetSigningKey() dsig.X509KeyStore { + if sp.SPSigningKeyStore == nil { + return sp.GetEncryptionKey() // Default is signing key is same as encryption key + } + return sp.SPSigningKeyStore +} + +func (sp *SAMLServiceProvider) GetEncryptionCertBytes() ([]byte, error) { + if _, encryptionCert, err := sp.GetEncryptionKey().GetKeyPair(); err != nil { + return nil, ErrSaml{Message: "no SP encryption certificate", System: err} + } else if len(encryptionCert) < 1 { + return nil, ErrSaml{Message: "empty SP encryption certificate"} + } else { + return encryptionCert, nil + } +} + +func (sp *SAMLServiceProvider) GetSigningCertBytes() ([]byte, error) { + if _, signingCert, err := sp.GetSigningKey().GetKeyPair(); err != nil { + return nil, ErrSaml{Message: "no SP signing certificate", System: err} + } else if len(signingCert) < 1 { + return nil, ErrSaml{Message: "empty SP signing certificate"} + } else { + return signingCert, nil + } +} + func (sp *SAMLServiceProvider) SigningContext() *dsig.SigningContext { sp.signingContextMu.RLock() signingContext := sp.signingContext @@ -62,7 +241,7 @@ func (sp *SAMLServiceProvider) SigningContext() *dsig.SigningContext { sp.signingContextMu.Lock() defer sp.signingContextMu.Unlock() - sp.signingContext = dsig.NewDefaultSigningContext(sp.SPKeyStore) + sp.signingContext = dsig.NewDefaultSigningContext(sp.GetSigningKey()) sp.signingContext.SetSignatureMethod(sp.SignAuthnRequestsAlgorithm) if sp.SignAuthnRequestsCanonicalizer != nil { sp.signingContext.Canonicalizer = sp.SignAuthnRequestsCanonicalizer @@ -84,9 +263,12 @@ type WarningInfo struct { } type AssertionInfo struct { - NameID string - Values Values - WarningInfo *WarningInfo - AuthnInstant *time.Time - SessionNotOnOrAfter *time.Time + NameID string + Values Values + WarningInfo *WarningInfo + SessionIndex string + AuthnInstant *time.Time + SessionNotOnOrAfter *time.Time + Assertions []types.Assertion + ResponseSignatureValidated bool } diff --git a/vendor/github.com/russellhaering/gosaml2/test_constants.go b/vendor/github.com/russellhaering/gosaml2/test_constants.go index a35c5cde446fa..470727550b510 100644 --- a/vendor/github.com/russellhaering/gosaml2/test_constants.go +++ b/vendor/github.com/russellhaering/gosaml2/test_constants.go @@ -1,3 +1,16 @@ +// Copyright 2016 Russell Haering et al. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package saml2 var idpCertificate = ` @@ -374,3 +387,32 @@ qRnqQ+TccSu/B6uONFsDEngGcXSKfB+a const exampleBase64 = `PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cDovL2xvY2FsaG9zdDo4MDgwL3YxL19zYW1sX2NhbGxiYWNrIiBJRD0iaWQxMDM1MzI4MDQ2NDc3ODc5NzUzODEzMjUiIEluUmVzcG9uc2VUbz0iXzg2OTljNjU1LWM0ODItNDUxYS05YjdmLTYxNjY4ZjE0MGI0NyIgSXNzdWVJbnN0YW50PSIyMDE2LTAzLTE2VDAxOjAyOjU3LjY4MloiIFZlcnNpb249IjIuMCI+PHNhbWwyOklzc3VlciB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDplbnRpdHkiPmh0dHA6Ly93d3cub2t0YS5jb20vZXhrNXp0MHIxMkVkaTRyRDIwaDc8L3NhbWwyOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8+PGRzOlJlZmVyZW5jZSBVUkk9IiNpZDEwMzUzMjgwNDY0Nzc4Nzk3NTM4MTMyNSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjc2hhMjU2Ii8+PGRzOkRpZ2VzdFZhbHVlPm5wVEFsNmtyYWtzQmxDUmx1bmJ5RDZuSUNUY2ZzRGFIalBYVnhvRFBydzA9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPlNiQjAzZkkxVFZzdEo3cTFCNlh4OFlSR2tEcE5ROGFyNHpGM3AzYWlra2NxOFRUUzBlUjI4Rm9RdU4xSFg3MlBuMnJjY0U0T05pellOUzYvcnZybHlWL1NsWFhtQzltaFRMUlBlSno1bXJ4anFPNVFZRDFZM0l6bW5rZlE2S3V0dWtrY0dPSkVwYTN2WWVzZjVKS1JTKzBXR1J0ek9TNHdKRjE4b0dJWitiYThQNmd4bU1yeUE4eEIvZUpneHBmcm1VYkJqUEhMU2ZsamViaDg4RWlOSUQwODhYdVNHeWQrM0RtcFc1QjUyRFFCOGNBeXlPQlJrUlJjcUxGSWd4aWJtdnRJaWVxdVUwYTJuY29qcHUwKzRvamwrNHdEQ1dkR09FeXF0Sm9UUVhDNHNLUmFVNzlGSzVJRmZFaVlNcXZpRkQwb2F1NHNQajBnbkZDRUY1Rmw0dz09PC9kczpTaWduYXR1cmVWYWx1ZT48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlEcERDQ0FveWdBd0lCQWdJR0FWTElCaEF3TUEwR0NTcUdTSWIzRFFFQkJRVUFNSUdTTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHCkExVUVDQXdLUTJGc2FXWnZjbTVwWVRFV01CUUdBMVVFQnd3TlUyRnVJRVp5WVc1amFYTmpiekVOTUFzR0ExVUVDZ3dFVDJ0MFlURVUKTUJJR0ExVUVDd3dMVTFOUFVISnZkbWxrWlhJeEV6QVJCZ05WQkFNTUNtUmxkaTB4TVRZNE1EY3hIREFhQmdrcWhraUc5dzBCQ1FFVwpEV2x1Wm05QWIydDBZUzVqYjIwd0hoY05NVFl3TWpBNU1qRTFNakEyV2hjTk1qWXdNakE1TWpFMU16QTJXakNCa2pFTE1Ba0dBMVVFCkJoTUNWVk14RXpBUkJnTlZCQWdNQ2tOaGJHbG1iM0p1YVdFeEZqQVVCZ05WQkFjTURWTmhiaUJHY21GdVkybHpZMjh4RFRBTEJnTlYKQkFvTUJFOXJkR0V4RkRBU0JnTlZCQXNNQzFOVFQxQnliM1pwWkdWeU1STXdFUVlEVlFRRERBcGtaWFl0TVRFMk9EQTNNUnd3R2dZSgpLb1pJaHZjTkFRa0JGZzFwYm1adlFHOXJkR0V1WTI5dE1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBCm10akJPWjhNbWhVeWk4Y0drNGRVWTZGajFNRkR0L3EzRkZpYVFwTHp1My9xNWxSVlVOVUJiQXRxUVd3WTEwZHpmWmd1SE91dkE1cDUKUXlpVkR2VWhlK1hrVndOMlIyV2ZBclFKUlRQbkljT2FIcnhxUWYzbzVjQ0lHMjFadHlzRkhKU284Y2xQU09lKzBWc29SZ2NKMWFGNAoyck9Ed2dxUlJaZE85V2gzNTAyWGxKNzk5REpRMjNJQzdYYXNLRXNHS3pKcWhsUnJmZC9GeUl1WlQwc0ZIREtSejVzblNKaG05Z3BOCnVRbENtazdPTloxc1hxdHQrbkJJZldJcWVvWVF1YlBXN3BUNUdUYzd3b3VXcTRUQ2pISmlLOWsySGl5TnhXMEUzSlgwOHN3RVppMisKTFZEamdMek5jNGx3alNZSWozQU90UFpzOHM2MDZvQmRJQm5pNHdJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0SUJBUUJNeFNrSgpUeGtYeHNvS05XMGF3Sk5wV1JiVTgxUXBoZU1GZkVOSXpMYW00SXRjLzVrU1pBYVN5LzllMlFLZm80akJvL01NYkNxMnZNOVR5ZUpRCkRKcFJhaW9VVGQybEdoNFRMVXhBeEN4dFVrL3Bhc2NMKzNObjkzNkxGbVVDTHhheG5iZUd6UE9YQWhzY0N0VTFIMG5Gc1hSbkt4NWEKY1BYWVNLRlpaWmt0aWVTa3d3Mk9pOGRnMkRZYVFoR1FNU0ZNVnFnVmZ3RXU0YnZDUkJ2ZFNpTlhkV0dDWlFtRlZ6QlpaLzlyT0x6UApwdlRGVFBucGthdkptODFGTGxVaGlFL29GZ0tsQ0RMV0RrblNwWEFJMHVaR0VSY3dQY2E2eHZJTWg4NkxqUUtqYlZjaTlGWURTdFhDCnFSbnFRK1RjY1N1L0I2dU9ORnNERW5nR2NYU0tmQithPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWwycDpTdGF0dXMgeG1sbnM6c2FtbDJwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiPjxzYW1sMnA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1sMnA6U3RhdHVzPjxzYW1sMjpBc3NlcnRpb24geG1sbnM6c2FtbDI9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJpZDEwMzUzMjgwNDY1MjY1ODg5MDAwODk0MjQiIElzc3VlSW5zdGFudD0iMjAxNi0wMy0xNlQwMTowMjo1Ny42ODJaIiBWZXJzaW9uPSIyLjAiPjxzYW1sMjpJc3N1ZXIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDplbnRpdHkiIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj5odHRwOi8vd3d3Lm9rdGEuY29tL2V4azV6dDByMTJFZGk0ckQyMGg3PC9zYW1sMjpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+PGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxkc2lnLW1vcmUjcnNhLXNoYTI1NiIvPjxkczpSZWZlcmVuY2UgVVJJPSIjaWQxMDM1MzI4MDQ2NTI2NTg4OTAwMDg5NDI0Ij48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNzaGEyNTYiLz48ZHM6RGlnZXN0VmFsdWU+Tm8xVnlRbGs4WGlmNEZpSitoYVZpd0VReVNJekJhMTRsR3kwY29DbjBjOD08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+VlNWOFZ3NDdxN24vWFp3YVFPUFdRZUtJNVpBNjlmbkdaeUVGaGV4NHh1YUlmQytMT1luZmQ4cThxY1pzbTFNNmt2NDdIL2RSNllYUklNalBLWFpleVgvTUtjbUdQQ2FkcVdGVDdFV0Z2enVPL3V5L0FCL0NMNVpDUWlZOUgvYU9oRHlzTzhnbHNlMVMrWTJLMEN3dnNvUndNZkZpTzJYT1loVk9zbmdVU2tDQmRMSUI2T3E0Zitac0swcncvRTc5bjlRVWQ4b3dEcTNkVkMxOFNGWVlkY0lWRGhRcHBnbHl1QkVaZnUydEcwNmdEOWpsczdaRTh2amNNZkhtaHVIdHhsSDNvdk5MQjM1TkZPL1ZyQ05kRnFtRDc2R25FQTk4Zm9pSnhDWDh2ek5IRjRyUFVGWEFFZGlTNE9kUUF4YjdqTk5Wb0tWWXVhZHVuTHlneXNaR1NnPT08L2RzOlNpZ25hdHVyZVZhbHVlPjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSURwRENDQW95Z0F3SUJBZ0lHQVZMSUJoQXdNQTBHQ1NxR1NJYjNEUUVCQlFVQU1JR1NNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUcKQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVXTUJRR0ExVUVCd3dOVTJGdUlFWnlZVzVqYVhOamJ6RU5NQXNHQTFVRUNnd0VUMnQwWVRFVQpNQklHQTFVRUN3d0xVMU5QVUhKdmRtbGtaWEl4RXpBUkJnTlZCQU1NQ21SbGRpMHhNVFk0TURjeEhEQWFCZ2txaGtpRzl3MEJDUUVXCkRXbHVabTlBYjJ0MFlTNWpiMjB3SGhjTk1UWXdNakE1TWpFMU1qQTJXaGNOTWpZd01qQTVNakUxTXpBMldqQ0JrakVMTUFrR0ExVUUKQmhNQ1ZWTXhFekFSQmdOVkJBZ01Da05oYkdsbWIzSnVhV0V4RmpBVUJnTlZCQWNNRFZOaGJpQkdjbUZ1WTJselkyOHhEVEFMQmdOVgpCQW9NQkU5cmRHRXhGREFTQmdOVkJBc01DMU5UVDFCeWIzWnBaR1Z5TVJNd0VRWURWUVFEREFwa1pYWXRNVEUyT0RBM01Sd3dHZ1lKCktvWklodmNOQVFrQkZnMXBibVp2UUc5cmRHRXVZMjl0TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUEKbXRqQk9aOE1taFV5aThjR2s0ZFVZNkZqMU1GRHQvcTNGRmlhUXBMenUzL3E1bFJWVU5VQmJBdHFRV3dZMTBkemZaZ3VIT3V2QTVwNQpReWlWRHZVaGUrWGtWd04yUjJXZkFyUUpSVFBuSWNPYUhyeHFRZjNvNWNDSUcyMVp0eXNGSEpTbzhjbFBTT2UrMFZzb1JnY0oxYUY0CjJyT0R3Z3FSUlpkTzlXaDM1MDJYbEo3OTlESlEyM0lDN1hhc0tFc0dLekpxaGxScmZkL0Z5SXVaVDBzRkhES1J6NXNuU0pobTlncE4KdVFsQ21rN09OWjFzWHF0dCtuQklmV0lxZW9ZUXViUFc3cFQ1R1RjN3dvdVdxNFRDakhKaUs5azJIaXlOeFcwRTNKWDA4c3dFWmkyKwpMVkRqZ0x6TmM0bHdqU1lJajNBT3RQWnM4czYwNm9CZElCbmk0d0lEQVFBQk1BMEdDU3FHU0liM0RRRUJCUVVBQTRJQkFRQk14U2tKClR4a1h4c29LTlcwYXdKTnBXUmJVODFRcGhlTUZmRU5JekxhbTRJdGMvNWtTWkFhU3kvOWUyUUtmbzRqQm8vTU1iQ3Eydk05VHllSlEKREpwUmFpb1VUZDJsR2g0VExVeEF4Q3h0VWsvcGFzY0wrM05uOTM2TEZtVUNMeGF4bmJlR3pQT1hBaHNjQ3RVMUgwbkZzWFJuS3g1YQpjUFhZU0tGWlpaa3RpZVNrd3cyT2k4ZGcyRFlhUWhHUU1TRk1WcWdWZndFdTRidkNSQnZkU2lOWGRXR0NaUW1GVnpCWlovOXJPTHpQCnB2VEZUUG5wa2F2Sm04MUZMbFVoaUUvb0ZnS2xDRExXRGtuU3BYQUkwdVpHRVJjd1BjYTZ4dklNaDg2TGpRS2piVmNpOUZZRFN0WEMKcVJucVErVGNjU3UvQjZ1T05Gc0RFbmdHY1hTS2ZCK2E8L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT48c2FtbDI6U3ViamVjdCB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI+PHNhbWwyOk5hbWVJRCBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OnVuc3BlY2lmaWVkIj5ydXNzZWxsLmhhZXJpbmdAc2NhbGVmdC5jb208L3NhbWwyOk5hbWVJRD48c2FtbDI6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sMjpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBJblJlc3BvbnNlVG89Il84Njk5YzY1NS1jNDgyLTQ1MWEtOWI3Zi02MTY2OGYxNDBiNDciIE5vdE9uT3JBZnRlcj0iMjAxNi0wMy0xNlQwMTowNzo1Ny42ODJaIiBSZWNpcGllbnQ9Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC92MS9fc2FtbF9jYWxsYmFjayIvPjwvc2FtbDI6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWwyOlN1YmplY3Q+PHNhbWwyOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE2LTAzLTE2VDAwOjU3OjU3LjY4MloiIE5vdE9uT3JBZnRlcj0iMjAxNi0wMy0xNlQwMTowNzo1Ny42ODJaIiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI+PHNhbWwyOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWwyOkF1ZGllbmNlPjEyMzwvc2FtbDI6QXVkaWVuY2U+PC9zYW1sMjpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDI6Q29uZGl0aW9ucz48c2FtbDI6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE2LTAzLTE2VDAxOjAyOjU3LjY4MloiIFNlc3Npb25JbmRleD0iXzg2OTljNjU1LWM0ODItNDUxYS05YjdmLTYxNjY4ZjE0MGI0NyIgeG1sbnM6c2FtbDI9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPjxzYW1sMjpBdXRobkNvbnRleHQ+PHNhbWwyOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkUHJvdGVjdGVkVHJhbnNwb3J0PC9zYW1sMjpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWwyOkF1dGhuQ29udGV4dD48L3NhbWwyOkF1dGhuU3RhdGVtZW50Pjwvc2FtbDI6QXNzZXJ0aW9uPjwvc2FtbDJwOlJlc3BvbnNlPg==` const exampleBase64_2 = `PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cDovL2xvY2FsaG9zdDo4MDgwL3YxL19zYW1sX2NhbGxiYWNrIiBJRD0iaWQyMTI4MjQ4OTI5NTEwNjcwODM0NTU5MTg1IiBJblJlc3BvbnNlVG89Il9kYTIxM2RmOC1lZjk1LTQxZDAtYjliZi03MWQyNzE3MzVjZDciIElzc3VlSW5zdGFudD0iMjAxNi0wMy0yOFQxNjozODoxOC41NjVaIiBWZXJzaW9uPSIyLjAiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSI+PHNhbWwyOklzc3VlciB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDplbnRpdHkiPmh0dHA6Ly93d3cub2t0YS5jb20vZXhrNXp0MHIxMkVkaTRyRDIwaDc8L3NhbWwyOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8+PGRzOlJlZmVyZW5jZSBVUkk9IiNpZDIxMjgyNDg5Mjk1MTA2NzA4MzQ1NTkxODUiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiPjxlYzpJbmNsdXNpdmVOYW1lc3BhY2VzIHhtbG5zOmVjPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiIFByZWZpeExpc3Q9InhzIi8+PC9kczpUcmFuc2Zvcm0+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNzaGEyNTYiLz48ZHM6RGlnZXN0VmFsdWU+V3ZnVy9KZlA0bWpVKy8xd3R5WDA2RTlFR3hZTnNvQ1UrcmJTWm5Bdmoycz08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+R0ExVVJvTU9FNUVGZmtIWWltR1htN0VjcGgvbTBzMTM1VnlGOVd1dDZOU3B1WmRRMmNyTTFJc2x2S0NSamtFMDlyWmdhZ1FRTUFUaFVjT0Z1WDM1ZFpQejlKNElocHQxanVoZkd2MUFWOEk4amlPS0ZFVGo2NU1pUGFiREVpOCtQNllXZjRxTnVqQUpYSEtKSWEvTUZYQnFvS1IvaW1MUVQ4ZXUxbmhWQlFHWXFXd1plUGRkZlhPMkpZazJjZTdtdG55TVQwZFVWYitvK3RsRURZYTdyaTlmajRKTC96MVhYN3lyYlZaeG4ybWRLUEp0U1NQOHVITk9XU002ajF2cDRvSytLU0R2aUJmaVZMbFZBNThub3o1R3lGdHA2NDJoK0xWMnF1S2JuY01GZm5mQjFrZkhMSy94YXo5VWFEQnkrYkhLNG9HelNwVmhacWNPenpsaUtBPT08L2RzOlNpZ25hdHVyZVZhbHVlPjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSURwRENDQW95Z0F3SUJBZ0lHQVZMSUJoQXdNQTBHQ1NxR1NJYjNEUUVCQlFVQU1JR1NNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUcKQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVXTUJRR0ExVUVCd3dOVTJGdUlFWnlZVzVqYVhOamJ6RU5NQXNHQTFVRUNnd0VUMnQwWVRFVQpNQklHQTFVRUN3d0xVMU5QVUhKdmRtbGtaWEl4RXpBUkJnTlZCQU1NQ21SbGRpMHhNVFk0TURjeEhEQWFCZ2txaGtpRzl3MEJDUUVXCkRXbHVabTlBYjJ0MFlTNWpiMjB3SGhjTk1UWXdNakE1TWpFMU1qQTJXaGNOTWpZd01qQTVNakUxTXpBMldqQ0JrakVMTUFrR0ExVUUKQmhNQ1ZWTXhFekFSQmdOVkJBZ01Da05oYkdsbWIzSnVhV0V4RmpBVUJnTlZCQWNNRFZOaGJpQkdjbUZ1WTJselkyOHhEVEFMQmdOVgpCQW9NQkU5cmRHRXhGREFTQmdOVkJBc01DMU5UVDFCeWIzWnBaR1Z5TVJNd0VRWURWUVFEREFwa1pYWXRNVEUyT0RBM01Sd3dHZ1lKCktvWklodmNOQVFrQkZnMXBibVp2UUc5cmRHRXVZMjl0TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUEKbXRqQk9aOE1taFV5aThjR2s0ZFVZNkZqMU1GRHQvcTNGRmlhUXBMenUzL3E1bFJWVU5VQmJBdHFRV3dZMTBkemZaZ3VIT3V2QTVwNQpReWlWRHZVaGUrWGtWd04yUjJXZkFyUUpSVFBuSWNPYUhyeHFRZjNvNWNDSUcyMVp0eXNGSEpTbzhjbFBTT2UrMFZzb1JnY0oxYUY0CjJyT0R3Z3FSUlpkTzlXaDM1MDJYbEo3OTlESlEyM0lDN1hhc0tFc0dLekpxaGxScmZkL0Z5SXVaVDBzRkhES1J6NXNuU0pobTlncE4KdVFsQ21rN09OWjFzWHF0dCtuQklmV0lxZW9ZUXViUFc3cFQ1R1RjN3dvdVdxNFRDakhKaUs5azJIaXlOeFcwRTNKWDA4c3dFWmkyKwpMVkRqZ0x6TmM0bHdqU1lJajNBT3RQWnM4czYwNm9CZElCbmk0d0lEQVFBQk1BMEdDU3FHU0liM0RRRUJCUVVBQTRJQkFRQk14U2tKClR4a1h4c29LTlcwYXdKTnBXUmJVODFRcGhlTUZmRU5JekxhbTRJdGMvNWtTWkFhU3kvOWUyUUtmbzRqQm8vTU1iQ3Eydk05VHllSlEKREpwUmFpb1VUZDJsR2g0VExVeEF4Q3h0VWsvcGFzY0wrM05uOTM2TEZtVUNMeGF4bmJlR3pQT1hBaHNjQ3RVMUgwbkZzWFJuS3g1YQpjUFhZU0tGWlpaa3RpZVNrd3cyT2k4ZGcyRFlhUWhHUU1TRk1WcWdWZndFdTRidkNSQnZkU2lOWGRXR0NaUW1GVnpCWlovOXJPTHpQCnB2VEZUUG5wa2F2Sm04MUZMbFVoaUUvb0ZnS2xDRExXRGtuU3BYQUkwdVpHRVJjd1BjYTZ4dklNaDg2TGpRS2piVmNpOUZZRFN0WEMKcVJucVErVGNjU3UvQjZ1T05Gc0RFbmdHY1hTS2ZCK2E8L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT48c2FtbDJwOlN0YXR1cyB4bWxuczpzYW1sMnA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCI+PHNhbWwycDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWwycDpTdGF0dXM+PHNhbWwyOkFzc2VydGlvbiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9ImlkMjEyODI0ODkyOTU3NzY3ODIxMjY0NjgzMTkiIElzc3VlSW5zdGFudD0iMjAxNi0wMy0yOFQxNjozODoxOC41NjVaIiBWZXJzaW9uPSIyLjAiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSI+PHNhbWwyOklzc3VlciBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OmVudGl0eSIgeG1sbnM6c2FtbDI9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPmh0dHA6Ly93d3cub2t0YS5jb20vZXhrNXp0MHIxMkVkaTRyRDIwaDc8L3NhbWwyOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8+PGRzOlJlZmVyZW5jZSBVUkk9IiNpZDIxMjgyNDg5Mjk1Nzc2NzgyMTI2NDY4MzE5Ij48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIj48ZWM6SW5jbHVzaXZlTmFtZXNwYWNlcyB4bWxuczplYz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIiBQcmVmaXhMaXN0PSJ4cyIvPjwvZHM6VHJhbnNmb3JtPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjc2hhMjU2Ii8+PGRzOkRpZ2VzdFZhbHVlPkZzV0dDQkMrdC9MYVZrVUtVdlJRcHp5WlRtbHhVenc0UjlGT3pYUFBKUnc9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPmhTNTBXZ1lzL2NuM3V4bWhyemEvMC8wUVczSDdid2RqUFoyaFFtRzdJZVNkN2F3VE9naEJxZHJqdmFQZlE3dFJXK1VLNmV3TWdJQlZLRzZqVjNxWUFXZVcyVTcwaE1iN2hFOXFKcUJLeVl5aW1taFZXVUx4MUhCMlltbFUxd21pc3B5d29QbFhRNmdqMGlXYUwyUkZJODN2VXA3WDUwZVo2ZEVMcW9KVlpwelFJMDY1VHQwVEc3VXVLVVcxZmxZc2JpUzlOYVhudXcrbWNyQlcyNVpBOUY1Q0xlUEhraTAxWnpVdytYdE5tS3RoRWI3U1IzMG16UG9qMDhEamkyMmRhWXZHdTgySVIwMXdJWlBvUUpQQ0dNVDZ5MnhDL3BRUHFHbGpBZy92VWErZ2FZZ2FNYUFWWXhoay9oZmdNVUJsT2VLQUNCYUdUbXlnYWIxTno1S3ZQZz09PC9kczpTaWduYXR1cmVWYWx1ZT48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlEcERDQ0FveWdBd0lCQWdJR0FWTElCaEF3TUEwR0NTcUdTSWIzRFFFQkJRVUFNSUdTTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHCkExVUVDQXdLUTJGc2FXWnZjbTVwWVRFV01CUUdBMVVFQnd3TlUyRnVJRVp5WVc1amFYTmpiekVOTUFzR0ExVUVDZ3dFVDJ0MFlURVUKTUJJR0ExVUVDd3dMVTFOUFVISnZkbWxrWlhJeEV6QVJCZ05WQkFNTUNtUmxkaTB4TVRZNE1EY3hIREFhQmdrcWhraUc5dzBCQ1FFVwpEV2x1Wm05QWIydDBZUzVqYjIwd0hoY05NVFl3TWpBNU1qRTFNakEyV2hjTk1qWXdNakE1TWpFMU16QTJXakNCa2pFTE1Ba0dBMVVFCkJoTUNWVk14RXpBUkJnTlZCQWdNQ2tOaGJHbG1iM0p1YVdFeEZqQVVCZ05WQkFjTURWTmhiaUJHY21GdVkybHpZMjh4RFRBTEJnTlYKQkFvTUJFOXJkR0V4RkRBU0JnTlZCQXNNQzFOVFQxQnliM1pwWkdWeU1STXdFUVlEVlFRRERBcGtaWFl0TVRFMk9EQTNNUnd3R2dZSgpLb1pJaHZjTkFRa0JGZzFwYm1adlFHOXJkR0V1WTI5dE1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBCm10akJPWjhNbWhVeWk4Y0drNGRVWTZGajFNRkR0L3EzRkZpYVFwTHp1My9xNWxSVlVOVUJiQXRxUVd3WTEwZHpmWmd1SE91dkE1cDUKUXlpVkR2VWhlK1hrVndOMlIyV2ZBclFKUlRQbkljT2FIcnhxUWYzbzVjQ0lHMjFadHlzRkhKU284Y2xQU09lKzBWc29SZ2NKMWFGNAoyck9Ed2dxUlJaZE85V2gzNTAyWGxKNzk5REpRMjNJQzdYYXNLRXNHS3pKcWhsUnJmZC9GeUl1WlQwc0ZIREtSejVzblNKaG05Z3BOCnVRbENtazdPTloxc1hxdHQrbkJJZldJcWVvWVF1YlBXN3BUNUdUYzd3b3VXcTRUQ2pISmlLOWsySGl5TnhXMEUzSlgwOHN3RVppMisKTFZEamdMek5jNGx3alNZSWozQU90UFpzOHM2MDZvQmRJQm5pNHdJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0SUJBUUJNeFNrSgpUeGtYeHNvS05XMGF3Sk5wV1JiVTgxUXBoZU1GZkVOSXpMYW00SXRjLzVrU1pBYVN5LzllMlFLZm80akJvL01NYkNxMnZNOVR5ZUpRCkRKcFJhaW9VVGQybEdoNFRMVXhBeEN4dFVrL3Bhc2NMKzNObjkzNkxGbVVDTHhheG5iZUd6UE9YQWhzY0N0VTFIMG5Gc1hSbkt4NWEKY1BYWVNLRlpaWmt0aWVTa3d3Mk9pOGRnMkRZYVFoR1FNU0ZNVnFnVmZ3RXU0YnZDUkJ2ZFNpTlhkV0dDWlFtRlZ6QlpaLzlyT0x6UApwdlRGVFBucGthdkptODFGTGxVaGlFL29GZ0tsQ0RMV0RrblNwWEFJMHVaR0VSY3dQY2E2eHZJTWg4NkxqUUtqYlZjaTlGWURTdFhDCnFSbnFRK1RjY1N1L0I2dU9ORnNERW5nR2NYU0tmQithPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWwyOlN1YmplY3QgeG1sbnM6c2FtbDI9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPjxzYW1sMjpOYW1lSUQgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPnBob2ViZS5zaW1vbkBzY2FsZWZ0LmNvbTwvc2FtbDI6TmFtZUlEPjxzYW1sMjpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+PHNhbWwyOlN1YmplY3RDb25maXJtYXRpb25EYXRhIEluUmVzcG9uc2VUbz0iX2RhMjEzZGY4LWVmOTUtNDFkMC1iOWJmLTcxZDI3MTczNWNkNyIgTm90T25PckFmdGVyPSIyMDE2LTAzLTI4VDE2OjQzOjE4LjU2NVoiIFJlY2lwaWVudD0iaHR0cDovL2xvY2FsaG9zdDo4MDgwL3YxL19zYW1sX2NhbGxiYWNrIi8+PC9zYW1sMjpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDI6U3ViamVjdD48c2FtbDI6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTYtMDMtMjhUMTY6MzM6MTguNTY1WiIgTm90T25PckFmdGVyPSIyMDE2LTAzLTI4VDE2OjQzOjE4LjU2NVoiIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj48c2FtbDI6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDI6QXVkaWVuY2U+MTIzPC9zYW1sMjpBdWRpZW5jZT48L3NhbWwyOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sMjpDb25kaXRpb25zPjxzYW1sMjpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTYtMDMtMjhUMTY6Mzg6MTguNTY1WiIgU2Vzc2lvbkluZGV4PSJfZGEyMTNkZjgtZWY5NS00MWQwLWI5YmYtNzFkMjcxNzM1Y2Q3IiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI+PHNhbWwyOkF1dGhuQ29udGV4dD48c2FtbDI6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmRQcm90ZWN0ZWRUcmFuc3BvcnQ8L3NhbWwyOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDI6QXV0aG5Db250ZXh0Pjwvc2FtbDI6QXV0aG5TdGF0ZW1lbnQ+PHNhbWwyOkF0dHJpYnV0ZVN0YXRlbWVudCB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI+PHNhbWwyOkF0dHJpYnV0ZSBOYW1lPSJGaXJzdE5hbWUiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dW5zcGVjaWZpZWQiPjxzYW1sMjpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTp0eXBlPSJ4czpzdHJpbmciPlBob2ViZTwvc2FtbDI6QXR0cmlidXRlVmFsdWU+PC9zYW1sMjpBdHRyaWJ1dGU+PHNhbWwyOkF0dHJpYnV0ZSBOYW1lPSJMYXN0TmFtZSIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDp1bnNwZWNpZmllZCI+PHNhbWwyOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+U2ltb248L3NhbWwyOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDI6QXR0cmlidXRlPjxzYW1sMjpBdHRyaWJ1dGUgTmFtZT0iRW1haWwiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dW5zcGVjaWZpZWQiPjxzYW1sMjpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTp0eXBlPSJ4czpzdHJpbmciPnBob2ViZS5zaW1vbkBzY2FsZWZ0LmNvbTwvc2FtbDI6QXR0cmlidXRlVmFsdWU+PC9zYW1sMjpBdHRyaWJ1dGU+PHNhbWwyOkF0dHJpYnV0ZSBOYW1lPSJMb2dpbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDp1bnNwZWNpZmllZCI+PHNhbWwyOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+cGhvZWJlLnNpbW9uQHNjYWxlZnQuY29tPC9zYW1sMjpBdHRyaWJ1dGVWYWx1ZT48L3NhbWwyOkF0dHJpYnV0ZT48L3NhbWwyOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWwyOkFzc2VydGlvbj48L3NhbWwycDpSZXNwb25zZT4=` + +const commentInjectionAttackResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7http://www.okta.com/exk5zt0r12Edi4rD20h7FsWGCBC+t/LaVkUKUvRQpzyZTmlxUzw4R9FOzXPPJRw=hS50WgYs/cn3uxmhrza/0/0QW3H7bwdjPZ2hQmG7IeSd7awTOghBqdrjvaPfQ7tRW+UK6ewMgIBVKG6jV3qYAWeW2U70hMb7hE9qJqBKyYyimmhVWULx1HB2YmlU1wmispywoPlXQ6gj0iWaL2RFI83vUp7X50eZ6dELqoJVZpzQI065Tt0TG7UuKUW1flYsbiS9NaXnuw+mcrBW25ZA9F5CLePHki01ZzUw+XtNmKthEb7SR30mzPoj08Dji22daYvGu82IR01wIZPoQJPCGMT6y2xC/pQPqGljAg/vUa+gaYgaMaAVYxhk/hfgMUBlOeKACBaGTmygab1Nz5KvPg==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com.evil.com123urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.comphoebesimon` + + +const doubleColonAssertionInjectionAttackResponse = ` + +https://app.onelogin.com/saml/metadata/634027 + +<::Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" Version="2.0" ID="x" IssueInstant="2017-03-08T07:53:39Z">https://app.onelogin.com/saml/metadata/634027gd5V090n/m4JRrtpo5WgrwPyyy0=what@launchdarkly.com{audience}urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport +https://app.onelogin.com/saml/metadata/634027gd5V090n/m4JRrtpo5WgrwPyyy0=SLzvdNM+1R1+3XsXpC+/RIvb5L4Lhy7Eb7caPG2CLMPYhzbKLAwIiT7/0fEMO/xL7rdIgEShbcU9iu5PX4hGYBhirsFIZvdHytns5+JKHnlVBmHm4TsSU1z+dGMXBa//L0KFSrvdgBUpsr5vs50SuYnnVp61VN+zCLMqO221CQfP95QyMcSQ+fiyq4GOmWLwQy1m1+NV3U8zlapp6FIH5stW/dp4OqpRdafV96rVwmmR4yeUw7VAzbJuMrPgkXO9nUbHeMUTgQxkQ4ThzG5jt6fT+Ro1NOYS4zpVtzqlQwGzqWxQVRLEqXIf500/Qi0NuFQOW42ZAUiXDgdLENTVGA==MIIEJjCCAw6gAwIBAgIUOHrykO4ce1TbjvGgXXVVnR4NsqMwDQYJKoZIhvcNAQEFBQAwXTELMAkGA1UEBhMCVVMxFTATBgNVBAoMDExhdW5jaERhcmtseTEVMBMGA1UECwwMT25lTG9naW4gSWRQMSAwHgYDVQQDDBdPbmVMb2dpbiBBY2NvdW50IDEwMjEyNzAeFw0xNzAzMDYwMjQ2NTNaFw0yMjAzMDcwMjQ2NTNaMF0xCzAJBgNVBAYTAlVTMRUwEwYDVQQKDAxMYXVuY2hEYXJrbHkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEgMB4GA1UEAwwXT25lTG9naW4gQWNjb3VudCAxMDIxMjcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCaJ02AnJe5vq+zzkmrIHhRy8V/UxJogbJGEJW6nqrEmO7Q4sXO7dLIKxGccCEz0KAavGKWzSX9uhVvKpazpD4bW80wPQIgFxN3CjiA3qlYIfhhh4emSZo2AnaTuG4BPVGFNPx0jxXGAhh/3xkpIsqARJFPB6njT2+MwFctm3fockx3Yp4e1xoUD8qQR0f/8oq1LjrYd2Vlckmmw7qrzSqS8POHW/I1jx9Y/vAjTPWDKXmbmLcTe3188PDrthSyoBuaAGBRVTP9WTuYMh4kGvmfX6sNvIDGejUcUCq6IObRr4xLSZiGy5uoyqsQc9agAhQm+26Gpq0R3NSvN91JdbZHAgMBAAGjgd0wgdowDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUnbxBsHgNVq3OSXEuG5EkR0Jd1UswgZoGA1UdIwSBkjCBj4AUnbxBsHgNVq3OSXEuG5EkR0Jd1UuhYaRfMF0xCzAJBgNVBAYTAlVTMRUwEwYDVQQKDAxMYXVuY2hEYXJrbHkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEgMB4GA1UEAwwXT25lTG9naW4gQWNjb3VudCAxMDIxMjeCFDh68pDuHHtU247xoF11VZ0eDbKjMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQUFAAOCAQEAL/6j2qpMCrnolwKT7mfPEpA6btbtl0R0t6zSwYUVU9T3PK0/P3LKXvbjSySov0E4R9d5qlOcyj5CbYiuqAO2aON3xy82s0dN3FHRiO6kcjoRPwVIIF0S8x7tpzcPKa42zSPfBqMRw4ezUEzTijFriepkSWST1Btr3QeK2Cxhr0fC1xmw/YK82BV0/oVRslGL27ro+v3/dNY0A0r32Xe2+THomrY/YaZaDCPCjHo8dlxrX3D/mPfoiiKSkm2mGagQXT0giTHVo3oIq+u+KdrBcQn65EBcjfFKDIeFCdiVmO0xPl9mmWskVRLy2/wpuDIp6hnAphl9lj5DY48eBsrEXQ==arun@launchdarkly.com{audience}urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport + +` \ No newline at end of file diff --git a/vendor/github.com/russellhaering/gosaml2/types/encrypted_assertion.go b/vendor/github.com/russellhaering/gosaml2/types/encrypted_assertion.go index 62da1bb9d33f0..734d90f18cd0f 100644 --- a/vendor/github.com/russellhaering/gosaml2/types/encrypted_assertion.go +++ b/vendor/github.com/russellhaering/gosaml2/types/encrypted_assertion.go @@ -1,3 +1,16 @@ +// Copyright 2016 Russell Haering et al. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package types import ( @@ -13,16 +26,24 @@ type EncryptedAssertion struct { XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion EncryptedAssertion"` EncryptionMethod EncryptionMethod `xml:"EncryptedData>EncryptionMethod"` EncryptedKey EncryptedKey `xml:"EncryptedData>KeyInfo>EncryptedKey"` + DetEncryptedKey EncryptedKey `xml:"EncryptedKey"` // detached EncryptedKey element CipherValue string `xml:"EncryptedData>CipherData>CipherValue"` } -func (ea *EncryptedAssertion) decrypt(cert *tls.Certificate) ([]byte, error) { +func (ea *EncryptedAssertion) DecryptBytes(cert *tls.Certificate) ([]byte, error) { data, err := base64.StdEncoding.DecodeString(ea.CipherValue) if err != nil { return nil, err } - k, err := ea.EncryptedKey.DecryptSymmetricKey(cert) + // EncryptedKey must include CipherValue. EncryptedKey may be part of EncryptedData. + ek := &ea.EncryptedKey + if ek.CipherValue == "" { + // Use detached EncryptedKey element (sibling of EncryptedData). See: + // https://www.w3.org/TR/2002/REC-xmlenc-core-20021210/Overview.html#sec-Extensions-to-KeyInfo + ek = &ea.DetEncryptedKey + } + k, err := ek.DecryptSymmetricKey(cert) if err != nil { return nil, fmt.Errorf("cannot decrypt, error retrieving private key: %s", err) } @@ -40,7 +61,7 @@ func (ea *EncryptedAssertion) decrypt(cert *tls.Certificate) ([]byte, error) { return nil, fmt.Errorf("cannot open AES-GCM: %s", err) } return plainText, nil - case MethodAES128CBC: + case MethodAES128CBC, MethodAES256CBC, MethodTripleDESCBC: nonce, data := data[:k.BlockSize()], data[k.BlockSize():] c := cipher.NewCBCDecrypter(k, nonce) c.CryptBlocks(data, data) @@ -59,13 +80,16 @@ func (ea *EncryptedAssertion) decrypt(cert *tls.Certificate) ([]byte, error) { // Decrypt decrypts and unmarshals the EncryptedAssertion. func (ea *EncryptedAssertion) Decrypt(cert *tls.Certificate) (*Assertion, error) { - plaintext, err := ea.decrypt(cert) + plaintext, err := ea.DecryptBytes(cert) + if err != nil { + return nil, fmt.Errorf("Error decrypting assertion: %v", err) + } assertion := &Assertion{} err = xml.Unmarshal(plaintext, assertion) if err != nil { - return nil, fmt.Errorf("Error decrypting assertion: %v", err) + return nil, fmt.Errorf("Error unmarshaling assertion: %v", err) } return assertion, nil diff --git a/vendor/github.com/russellhaering/gosaml2/types/encrypted_key.go b/vendor/github.com/russellhaering/gosaml2/types/encrypted_key.go index 40405f715108d..f74f8c8642fb9 100644 --- a/vendor/github.com/russellhaering/gosaml2/types/encrypted_key.go +++ b/vendor/github.com/russellhaering/gosaml2/types/encrypted_key.go @@ -1,8 +1,22 @@ +// Copyright 2016 Russell Haering et al. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package types import ( "bytes" "crypto/aes" + "crypto/des" "crypto/cipher" "crypto/rand" "crypto/rsa" @@ -11,8 +25,10 @@ import ( "crypto/sha512" "crypto/tls" "encoding/base64" + "encoding/hex" "fmt" "hash" + "strings" ) //EncryptedKey contains the decryption key data from the saml2 core and xmlenc @@ -26,25 +42,33 @@ type EncryptedKey struct { //EncryptionMethod specifies the type of encryption that was used. type EncryptionMethod struct { - Algorithm string `xml:",attr"` - DigestMethod DigestMethod + Algorithm string `xml:",attr,omitempty"` + //Digest method is present for algorithms like RSA-OAEP. + //See https://www.w3.org/TR/xmlenc-core1/. + //To convey the digest methods an entity supports, + //DigestMethod in extensions element is used. + //See http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-metadata-algsupport.html. + DigestMethod *DigestMethod `xml:",omitempty"` } //DigestMethod is a digest type specification type DigestMethod struct { - Algorithm string `xml:",attr"` + Algorithm string `xml:",attr,omitempty"` } //Well-known public-key encryption methods const ( MethodRSAOAEP = "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p" MethodRSAOAEP2 = "http://www.w3.org/2009/xmlenc11#rsa-oaep" + MethodRSAv1_5 = "http://www.w3.org/2001/04/xmlenc#rsa-1_5" ) //Well-known private key encryption methods const ( MethodAES128GCM = "http://www.w3.org/2009/xmlenc11#aes128-gcm" MethodAES128CBC = "http://www.w3.org/2001/04/xmlenc#aes128-cbc" + MethodAES256CBC = "http://www.w3.org/2001/04/xmlenc#aes256-cbc" + MethodTripleDESCBC = "http://www.w3.org/2001/04/xmlenc#tripledes-cbc" ) //Well-known hash methods @@ -54,20 +78,43 @@ const ( MethodSHA512 = "http://www.w3.org/2000/09/xmldsig#sha512" ) -//DecryptSymmetricKey returns the private key contained in the EncryptedKey document -func (ek *EncryptedKey) DecryptSymmetricKey(cert *tls.Certificate) (cipher.Block, error) { - encCert, err := base64.StdEncoding.DecodeString(ek.X509Data) - if err != nil { - return nil, fmt.Errorf("error getting certificate from encryptedkey: %v", err) +//SHA-1 is commonly used for certificate fingerprints (openssl -fingerprint and ADFS thumbprint). +//SHA-1 is sufficient for our purposes here (error message). +func debugKeyFp(keyBytes []byte) string { + if len(keyBytes) < 1 { + return "" + } + hashFunc := sha1.New() + hashFunc.Write(keyBytes) + sum := strings.ToLower(hex.EncodeToString(hashFunc.Sum(nil))) + var ret string + for idx := 0; idx+1 < len(sum); idx += 2 { + if idx == 0 { + ret += sum[idx : idx+2] + } else { + ret += ":" + sum[idx:idx+2] + } } + return ret +} +//DecryptSymmetricKey returns the private key contained in the EncryptedKey document +func (ek *EncryptedKey) DecryptSymmetricKey(cert *tls.Certificate) (cipher.Block, error) { if len(cert.Certificate) < 1 { return nil, fmt.Errorf("decryption tls.Certificate has no public certs attached") } - if !bytes.Equal(cert.Certificate[0], encCert) { - return nil, fmt.Errorf("key decryption attempted with mismatched cert: %#v != %#v", - string(cert.Certificate[0]), string(encCert)) + // The EncryptedKey may or may not include X509Data (certificate). + // If included, the EncryptedKey certificate: + // - is FYI only (fail if it does not match the SP certificate) + // - is NOT used to decrypt CipherData + if ek.X509Data != "" { + if encCert, err := base64.StdEncoding.DecodeString(ek.X509Data); err != nil { + return nil, fmt.Errorf("error decoding EncryptedKey certificate: %v", err) + } else if !bytes.Equal(cert.Certificate[0], encCert) { + return nil, fmt.Errorf("key decryption attempted with mismatched cert, SP cert(%.11s), assertion cert(%.11s)", + debugKeyFp(cert.Certificate[0]), debugKeyFp(encCert)) + } } cipherText, err := base64.StdEncoding.DecodeString(ek.CipherValue) @@ -79,16 +126,27 @@ func (ek *EncryptedKey) DecryptSymmetricKey(cert *tls.Certificate) (cipher.Block case *rsa.PrivateKey: var h hash.Hash - switch ek.EncryptionMethod.DigestMethod.Algorithm { - case MethodSHA1: - h = sha1.New() - case MethodSHA256: - h = sha256.New() - case MethodSHA512: - h = sha512.New() - } + if ek.EncryptionMethod.DigestMethod == nil { + //if digest method is not present lets set default method to SHA1. + //Digest method is used by methods like RSA-OAEP. + h = sha1.New() + } else { + switch ek.EncryptionMethod.DigestMethod.Algorithm { + case "", MethodSHA1: + h = sha1.New() // default + case MethodSHA256: + h = sha256.New() + case MethodSHA512: + h = sha512.New() + default: + return nil, fmt.Errorf("unsupported digest algorithm: %v", + ek.EncryptionMethod.DigestMethod.Algorithm) + } + } switch ek.EncryptionMethod.Algorithm { + case "": + return nil, fmt.Errorf("missing encryption algorithm") case MethodRSAOAEP, MethodRSAOAEP2: pt, err := rsa.DecryptOAEP(h, rand.Reader, pk, cipherText, nil) if err != nil { @@ -101,6 +159,26 @@ func (ek *EncryptedKey) DecryptSymmetricKey(cert *tls.Certificate) (cipher.Block } return b, nil + case MethodRSAv1_5: + pt, err := rsa.DecryptPKCS1v15(rand.Reader, pk, cipherText) + if err != nil { + return nil, fmt.Errorf("rsa internal error: %v", err) + } + + //From https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf the xml encryption + //methods to be supported are from http://www.w3.org/2001/04/xmlenc#Element. + //https://www.w3.org/TR/2002/REC-xmlenc-core-20021210/Overview.html#Element. + //https://www.w3.org/TR/2002/REC-xmlenc-core-20021210/#sec-Algorithms + //Sec 5.4 Key Transport: + //The RSA v1.5 Key Transport algorithm given below are those used in conjunction with TRIPLEDES + //Please also see https://www.w3.org/TR/xmlenc-core/#sec-Algorithms and + //https://www.w3.org/TR/xmlenc-core/#rsav15note. + b, err := des.NewTripleDESCipher(pt) + if err != nil { + return nil, err + } + + return b, nil default: return nil, fmt.Errorf("unsupported encryption algorithm: %s", ek.EncryptionMethod.Algorithm) } diff --git a/vendor/github.com/russellhaering/gosaml2/types/metadata.go b/vendor/github.com/russellhaering/gosaml2/types/metadata.go index 6c8f4f972f640..03b2c73cb3e05 100644 --- a/vendor/github.com/russellhaering/gosaml2/types/metadata.go +++ b/vendor/github.com/russellhaering/gosaml2/types/metadata.go @@ -1,30 +1,75 @@ +// Copyright 2016 Russell Haering et al. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package types import ( "encoding/xml" + "time" dsigtypes "github.com/russellhaering/goxmldsig/types" ) type EntityDescriptor struct { - XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata EntityDescriptor"` + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata EntityDescriptor"` + ValidUntil time.Time `xml:"validUntil,attr"` // SAML 2.0 8.3.6 Entity Identifier could be used to represent issuer - EntityID string `xml:"entityID,attr"` - IDPSSODescriptor IDPSSODescriptor `xml:"IDPSSODescriptor"` + EntityID string `xml:"entityID,attr"` + SPSSODescriptor *SPSSODescriptor `xml:"SPSSODescriptor,omitempty"` + IDPSSODescriptor *IDPSSODescriptor `xml:"IDPSSODescriptor,omitempty"` + Extensions *Extensions `xml:"Extensions,omitempty"` +} + +type Endpoint struct { + Binding string `xml:"Binding,attr"` + Location string `xml:"Location,attr"` + ResponseLocation string `xml:"ResponseLocation,attr,omitempty"` +} + +type IndexedEndpoint struct { + Binding string `xml:"Binding,attr"` + Location string `xml:"Location,attr"` + Index int `xml:"index,attr"` +} + +type SPSSODescriptor struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata SPSSODescriptor"` + AuthnRequestsSigned bool `xml:"AuthnRequestsSigned,attr"` + WantAssertionsSigned bool `xml:"WantAssertionsSigned,attr"` + ProtocolSupportEnumeration string `xml:"protocolSupportEnumeration,attr"` + KeyDescriptors []KeyDescriptor `xml:"KeyDescriptor"` + SingleLogoutServices []Endpoint `xml:"SingleLogoutService"` + NameIDFormats []string `xml:"NameIDFormat"` + AssertionConsumerServices []IndexedEndpoint `xml:"AssertionConsumerService"` + Extensions *Extensions `xml:"Extensions,omitempty"` } type IDPSSODescriptor struct { - XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata IDPSSODescriptor"` - KeyDescriptors []KeyDescriptor `xml:"KeyDescriptor"` - NameIDFormats []NameIDFormat `xml:"NameIDFormat"` - SingleSignOnService SingleSignOnService `xml:"SingleSignOnService"` - Attributes []Attribute `xml:"Attribute"` + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata IDPSSODescriptor"` + WantAuthnRequestsSigned bool `xml:"WantAuthnRequestsSigned,attr"` + KeyDescriptors []KeyDescriptor `xml:"KeyDescriptor"` + NameIDFormats []NameIDFormat `xml:"NameIDFormat"` + SingleSignOnServices []SingleSignOnService `xml:"SingleSignOnService"` + SingleLogoutServices []SingleLogoutService `xml:"SingleLogoutService"` + Attributes []Attribute `xml:"Attribute"` + Extensions *Extensions `xml:"Extensions,omitempty"` } type KeyDescriptor struct { - XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata KeyDescriptor"` - Use string `xml:"use,attr"` - KeyInfo dsigtypes.KeyInfo `xml:"KeyInfo"` + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata KeyDescriptor"` + Use string `xml:"use,attr"` + KeyInfo dsigtypes.KeyInfo `xml:"KeyInfo"` + EncryptionMethods []EncryptionMethod `xml:"EncryptionMethod"` } type NameIDFormat struct { @@ -37,3 +82,20 @@ type SingleSignOnService struct { Binding string `xml:"Binding,attr"` Location string `xml:"Location,attr"` } + +type SingleLogoutService struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata SingleLogoutService"` + Binding string `xml:"Binding,attr"` + Location string `xml:"Location,attr"` +} + +type SigningMethod struct { + Algorithm string `xml:",attr"` + MinKeySize string `xml:"MinKeySize,attr,omitempty"` + MaxKeySize string `xml:"MaxKeySize,attr,omitempty"` +} + +type Extensions struct { + DigestMethod *DigestMethod `xml:",omitempty"` + SigningMethod *SigningMethod `xml:",omitempty"` +} diff --git a/vendor/github.com/russellhaering/gosaml2/types/response.go b/vendor/github.com/russellhaering/gosaml2/types/response.go index 8b5cac74e2f1a..2a54c466e7b15 100644 --- a/vendor/github.com/russellhaering/gosaml2/types/response.go +++ b/vendor/github.com/russellhaering/gosaml2/types/response.go @@ -1,3 +1,16 @@ +// Copyright 2016 Russell Haering et al. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package types import ( @@ -5,14 +18,44 @@ import ( "time" ) +// UnverifiedBaseResponse extracts several basic attributes of a SAML Response +// which may be useful in deciding how to validate the Response. An UnverifiedBaseResponse +// is parsed by this library prior to any validation of the Response, so the +// values it contains may have been supplied by an attacker and should not be +// trusted as authoritative from the IdP. +type UnverifiedBaseResponse struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol Response"` + ID string `xml:"ID,attr"` + InResponseTo string `xml:"InResponseTo,attr"` + Destination string `xml:"Destination,attr"` + Version string `xml:"Version,attr"` + Issuer *Issuer `xml:"Issuer"` +} + type Response struct { XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol Response"` + ID string `xml:"ID,attr"` + InResponseTo string `xml:"InResponseTo,attr"` Destination string `xml:"Destination,attr"` Version string `xml:"Version,attr"` + IssueInstant time.Time `xml:"IssueInstant,attr"` Status *Status `xml:"Status"` Issuer *Issuer `xml:"Issuer"` Assertions []Assertion `xml:"Assertion"` EncryptedAssertions []EncryptedAssertion `xml:"EncryptedAssertion"` + SignatureValidated bool `xml:"-"` // not read, not dumped +} + +type LogoutResponse struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol LogoutResponse"` + ID string `xml:"ID,attr"` + InResponseTo string `xml:"InResponseTo,attr"` + Destination string `xml:"Destination,attr"` + Version string `xml:"Version,attr"` + IssueInstant time.Time `xml:"IssueInstant,attr"` + Status *Status `xml:"Status"` + Issuer *Issuer `xml:"Issuer"` + SignatureValidated bool `xml:"-"` // not read, not dumped } type Status struct { @@ -30,13 +73,22 @@ type Issuer struct { Value string `xml:",chardata"` } +type Signature struct { + SignatureDocument []byte `xml:",innerxml"` +} + type Assertion struct { XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Assertion"` + Version string `xml:"Version,attr"` + ID string `xml:"ID,attr"` + IssueInstant time.Time `xml:"IssueInstant,attr"` Issuer *Issuer `xml:"Issuer"` + Signature *Signature `xml:"Signature"` Subject *Subject `xml:"Subject"` Conditions *Conditions `xml:"Conditions"` AttributeStatement *AttributeStatement `xml:"AttributeStatement"` AuthnStatement *AuthnStatement `xml:"AuthnStatement"` + SignatureValidated bool `xml:"-"` // not read, not dumped } type Subject struct { @@ -45,6 +97,16 @@ type Subject struct { SubjectConfirmation *SubjectConfirmation `xml:"SubjectConfirmation"` } +type AuthnContext struct { + XMLName xml.Name `xml:urn:oasis:names:tc:SAML:2.0:assertion AuthnContext"` + AuthnContextClassRef *AuthnContextClassRef `xml:"AuthnContextClassRef"` +} + +type AuthnContextClassRef struct { + XMLName xml.Name `xml:urn:oasis:names:tc:SAML:2.0:assertion AuthnContextClassRef"` + Value string `xml:",chardata"` +} + type NameID struct { XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion NameID"` Value string `xml:",chardata"` @@ -112,7 +174,13 @@ type AttributeValue struct { } type AuthnStatement struct { - XMLName xml.Name `xml:"AuthnStatement"` - AuthnInstant *time.Time `xml:"AuthnInstant,attr,omitempty"` - SessionNotOnOrAfter *time.Time `xml:"SessionNotOnOrAfter,attr,omitempty"` + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion AuthnStatement"` + //Section 4.1.4.2 - https://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf + //If the identity provider supports the Single Logout profile, defined in Section 4.4 + //, any such authentication statements MUST include a SessionIndex attribute to enable + //per-session logout requests by the service provider. + SessionIndex string `xml:"SessionIndex,attr,omitempty"` + AuthnInstant *time.Time `xml:"AuthnInstant,attr,omitempty"` + SessionNotOnOrAfter *time.Time `xml:"SessionNotOnOrAfter,attr,omitempty"` + AuthnContext *AuthnContext `xml:"AuthnContext"` } diff --git a/vendor/github.com/russellhaering/gosaml2/uuid/uuid.go b/vendor/github.com/russellhaering/gosaml2/uuid/uuid.go new file mode 100644 index 0000000000000..a17f2eb70f510 --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/uuid/uuid.go @@ -0,0 +1,40 @@ +// Copyright 2016 Russell Haering et al. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package uuid + +// relevant bits from https://github.com/abneptis/GoUUID/blob/master/uuid.go + +import ( + "crypto/rand" + "fmt" +) + +type UUID [16]byte + +// NewV4 returns random generated UUID. +func NewV4() *UUID { + u := &UUID{} + _, err := rand.Read(u[:16]) + if err != nil { + panic(err) + } + + u[8] = (u[8] | 0x80) & 0xBf + u[6] = (u[6] | 0x40) & 0x4f + return u +} + +func (u *UUID) String() string { + return fmt.Sprintf("%x-%x-%x-%x-%x", u[:4], u[4:6], u[6:8], u[8:10], u[10:]) +} diff --git a/vendor/github.com/russellhaering/gosaml2/validate.go b/vendor/github.com/russellhaering/gosaml2/validate.go index 53182406333bc..537d0cef35cb1 100644 --- a/vendor/github.com/russellhaering/gosaml2/validate.go +++ b/vendor/github.com/russellhaering/gosaml2/validate.go @@ -1,3 +1,16 @@ +// Copyright 2016 Russell Haering et al. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package saml2 import ( @@ -131,6 +144,7 @@ func (sp *SAMLServiceProvider) Validate(response *types.Response) error { issuer := response.Issuer if issuer == nil { + // FIXME?: SAML Core 2.0 Section 3.2.2 has Response.Issuer as [Optional] return ErrMissingElement{Tag: IssuerTag} } @@ -161,6 +175,18 @@ func (sp *SAMLServiceProvider) Validate(response *types.Response) error { } for _, assertion := range response.Assertions { + issuer = assertion.Issuer + if issuer == nil { + return ErrMissingElement{Tag: IssuerTag} + } + if sp.IdentityProviderIssuer != "" && assertion.Issuer.Value != sp.IdentityProviderIssuer { + return ErrInvalidValue{ + Key: IssuerTag, + Expected: sp.IdentityProviderIssuer, + Actual: issuer.Value, + } + } + subject := assertion.Subject if subject == nil { return ErrMissingElement{Tag: SubjectTag} @@ -216,3 +242,67 @@ func (sp *SAMLServiceProvider) Validate(response *types.Response) error { return nil } + +func (sp *SAMLServiceProvider) ValidateDecodedLogoutResponse(response *types.LogoutResponse) error { + err := sp.validateLogoutResponseAttributes(response) + if err != nil { + return err + } + + issuer := response.Issuer + if issuer == nil { + // FIXME?: SAML Core 2.0 Section 3.2.2 has Response.Issuer as [Optional] + return ErrMissingElement{Tag: IssuerTag} + } + + if sp.IdentityProviderIssuer != "" && response.Issuer.Value != sp.IdentityProviderIssuer { + return ErrInvalidValue{ + Key: IssuerTag, + Expected: sp.IdentityProviderIssuer, + Actual: response.Issuer.Value, + } + } + + status := response.Status + if status == nil { + return ErrMissingElement{Tag: StatusTag} + } + + statusCode := status.StatusCode + if statusCode == nil { + return ErrMissingElement{Tag: StatusCodeTag} + } + + if statusCode.Value != StatusCodeSuccess { + return ErrInvalidValue{ + Key: StatusCodeTag, + Expected: StatusCodeSuccess, + Actual: statusCode.Value, + } + } + + return nil +} + +func (sp *SAMLServiceProvider) ValidateDecodedLogoutRequest(request *LogoutRequest) error { + err := sp.validateLogoutRequestAttributes(request) + if err != nil { + return err + } + + issuer := request.Issuer + if issuer == nil { + // FIXME?: SAML Core 2.0 Section 3.2.2 has Response.Issuer as [Optional] + return ErrMissingElement{Tag: IssuerTag} + } + + if sp.IdentityProviderIssuer != "" && request.Issuer.Value != sp.IdentityProviderIssuer { + return ErrInvalidValue{ + Key: IssuerTag, + Expected: sp.IdentityProviderIssuer, + Actual: request.Issuer.Value, + } + } + + return nil +} diff --git a/vendor/github.com/russellhaering/gosaml2/xml_constants.go b/vendor/github.com/russellhaering/gosaml2/xml_constants.go index 64bfc3b9173be..db2f351829eb1 100644 --- a/vendor/github.com/russellhaering/gosaml2/xml_constants.go +++ b/vendor/github.com/russellhaering/gosaml2/xml_constants.go @@ -1,8 +1,22 @@ +// Copyright 2016 Russell Haering et al. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package saml2 const ( ResponseTag = "Response" AssertionTag = "Assertion" + EncryptedAssertionTag = "EncryptedAssertion" SubjectTag = "Subject" NameIdTag = "NameID" SubjectConfirmationTag = "SubjectConfirmation" @@ -45,7 +59,12 @@ const ( AuthnPolicyMatchMaximum = "maximum" AuthnPolicyMatchBetter = "better" - StatusCodeSuccess = "urn:oasis:names:tc:SAML:2.0:status:Success" + StatusCodeSuccess = "urn:oasis:names:tc:SAML:2.0:status:Success" + StatusCodePartialLogout = "urn:oasis:names:tc:SAML:2.0:status:PartialLogout" + StatusCodeUnknownPrincipal = "urn:oasis:names:tc:SAML:2.0:status:UnknownPrincipal" + + BindingHttpPost = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + BindingHttpRedirect = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" ) const ( diff --git a/vendor/github.com/satori/go.uuid/LICENSE b/vendor/github.com/satori/go.uuid/LICENSE deleted file mode 100644 index 488357b8af1fe..0000000000000 --- a/vendor/github.com/satori/go.uuid/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (C) 2013-2016 by Maxim Bublis - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/satori/go.uuid/uuid.go b/vendor/github.com/satori/go.uuid/uuid.go deleted file mode 100644 index 295f3fc2c57fa..0000000000000 --- a/vendor/github.com/satori/go.uuid/uuid.go +++ /dev/null @@ -1,481 +0,0 @@ -// Copyright (C) 2013-2015 by Maxim Bublis -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -// Package uuid provides implementation of Universally Unique Identifier (UUID). -// Supported versions are 1, 3, 4 and 5 (as specified in RFC 4122) and -// version 2 (as specified in DCE 1.1). -package uuid - -import ( - "bytes" - "crypto/md5" - "crypto/rand" - "crypto/sha1" - "database/sql/driver" - "encoding/binary" - "encoding/hex" - "fmt" - "hash" - "net" - "os" - "sync" - "time" -) - -// UUID layout variants. -const ( - VariantNCS = iota - VariantRFC4122 - VariantMicrosoft - VariantFuture -) - -// UUID DCE domains. -const ( - DomainPerson = iota - DomainGroup - DomainOrg -) - -// Difference in 100-nanosecond intervals between -// UUID epoch (October 15, 1582) and Unix epoch (January 1, 1970). -const epochStart = 122192928000000000 - -// Used in string method conversion -const dash byte = '-' - -// UUID v1/v2 storage. -var ( - storageMutex sync.Mutex - storageOnce sync.Once - epochFunc = unixTimeFunc - clockSequence uint16 - lastTime uint64 - hardwareAddr [6]byte - posixUID = uint32(os.Getuid()) - posixGID = uint32(os.Getgid()) -) - -// String parse helpers. -var ( - urnPrefix = []byte("urn:uuid:") - byteGroups = []int{8, 4, 4, 4, 12} -) - -func initClockSequence() { - buf := make([]byte, 2) - safeRandom(buf) - clockSequence = binary.BigEndian.Uint16(buf) -} - -func initHardwareAddr() { - interfaces, err := net.Interfaces() - if err == nil { - for _, iface := range interfaces { - if len(iface.HardwareAddr) >= 6 { - copy(hardwareAddr[:], iface.HardwareAddr) - return - } - } - } - - // Initialize hardwareAddr randomly in case - // of real network interfaces absence - safeRandom(hardwareAddr[:]) - - // Set multicast bit as recommended in RFC 4122 - hardwareAddr[0] |= 0x01 -} - -func initStorage() { - initClockSequence() - initHardwareAddr() -} - -func safeRandom(dest []byte) { - if _, err := rand.Read(dest); err != nil { - panic(err) - } -} - -// Returns difference in 100-nanosecond intervals between -// UUID epoch (October 15, 1582) and current time. -// This is default epoch calculation function. -func unixTimeFunc() uint64 { - return epochStart + uint64(time.Now().UnixNano()/100) -} - -// UUID representation compliant with specification -// described in RFC 4122. -type UUID [16]byte - -// NullUUID can be used with the standard sql package to represent a -// UUID value that can be NULL in the database -type NullUUID struct { - UUID UUID - Valid bool -} - -// The nil UUID is special form of UUID that is specified to have all -// 128 bits set to zero. -var Nil = UUID{} - -// Predefined namespace UUIDs. -var ( - NamespaceDNS, _ = FromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8") - NamespaceURL, _ = FromString("6ba7b811-9dad-11d1-80b4-00c04fd430c8") - NamespaceOID, _ = FromString("6ba7b812-9dad-11d1-80b4-00c04fd430c8") - NamespaceX500, _ = FromString("6ba7b814-9dad-11d1-80b4-00c04fd430c8") -) - -// And returns result of binary AND of two UUIDs. -func And(u1 UUID, u2 UUID) UUID { - u := UUID{} - for i := 0; i < 16; i++ { - u[i] = u1[i] & u2[i] - } - return u -} - -// Or returns result of binary OR of two UUIDs. -func Or(u1 UUID, u2 UUID) UUID { - u := UUID{} - for i := 0; i < 16; i++ { - u[i] = u1[i] | u2[i] - } - return u -} - -// Equal returns true if u1 and u2 equals, otherwise returns false. -func Equal(u1 UUID, u2 UUID) bool { - return bytes.Equal(u1[:], u2[:]) -} - -// Version returns algorithm version used to generate UUID. -func (u UUID) Version() uint { - return uint(u[6] >> 4) -} - -// Variant returns UUID layout variant. -func (u UUID) Variant() uint { - switch { - case (u[8] & 0x80) == 0x00: - return VariantNCS - case (u[8]&0xc0)|0x80 == 0x80: - return VariantRFC4122 - case (u[8]&0xe0)|0xc0 == 0xc0: - return VariantMicrosoft - } - return VariantFuture -} - -// Bytes returns bytes slice representation of UUID. -func (u UUID) Bytes() []byte { - return u[:] -} - -// Returns canonical string representation of UUID: -// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. -func (u UUID) String() string { - buf := make([]byte, 36) - - hex.Encode(buf[0:8], u[0:4]) - buf[8] = dash - hex.Encode(buf[9:13], u[4:6]) - buf[13] = dash - hex.Encode(buf[14:18], u[6:8]) - buf[18] = dash - hex.Encode(buf[19:23], u[8:10]) - buf[23] = dash - hex.Encode(buf[24:], u[10:]) - - return string(buf) -} - -// SetVersion sets version bits. -func (u *UUID) SetVersion(v byte) { - u[6] = (u[6] & 0x0f) | (v << 4) -} - -// SetVariant sets variant bits as described in RFC 4122. -func (u *UUID) SetVariant() { - u[8] = (u[8] & 0xbf) | 0x80 -} - -// MarshalText implements the encoding.TextMarshaler interface. -// The encoding is the same as returned by String. -func (u UUID) MarshalText() (text []byte, err error) { - text = []byte(u.String()) - return -} - -// UnmarshalText implements the encoding.TextUnmarshaler interface. -// Following formats are supported: -// "6ba7b810-9dad-11d1-80b4-00c04fd430c8", -// "{6ba7b810-9dad-11d1-80b4-00c04fd430c8}", -// "urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8" -func (u *UUID) UnmarshalText(text []byte) (err error) { - if len(text) < 32 { - err = fmt.Errorf("uuid: UUID string too short: %s", text) - return - } - - t := text[:] - braced := false - - if bytes.Equal(t[:9], urnPrefix) { - t = t[9:] - } else if t[0] == '{' { - braced = true - t = t[1:] - } - - b := u[:] - - for i, byteGroup := range byteGroups { - if i > 0 { - if t[0] != '-' { - err = fmt.Errorf("uuid: invalid string format") - return - } - t = t[1:] - } - - if len(t) < byteGroup { - err = fmt.Errorf("uuid: UUID string too short: %s", text) - return - } - - if i == 4 && len(t) > byteGroup && - ((braced && t[byteGroup] != '}') || len(t[byteGroup:]) > 1 || !braced) { - err = fmt.Errorf("uuid: UUID string too long: %s", text) - return - } - - _, err = hex.Decode(b[:byteGroup/2], t[:byteGroup]) - if err != nil { - return - } - - t = t[byteGroup:] - b = b[byteGroup/2:] - } - - return -} - -// MarshalBinary implements the encoding.BinaryMarshaler interface. -func (u UUID) MarshalBinary() (data []byte, err error) { - data = u.Bytes() - return -} - -// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. -// It will return error if the slice isn't 16 bytes long. -func (u *UUID) UnmarshalBinary(data []byte) (err error) { - if len(data) != 16 { - err = fmt.Errorf("uuid: UUID must be exactly 16 bytes long, got %d bytes", len(data)) - return - } - copy(u[:], data) - - return -} - -// Value implements the driver.Valuer interface. -func (u UUID) Value() (driver.Value, error) { - return u.String(), nil -} - -// Scan implements the sql.Scanner interface. -// A 16-byte slice is handled by UnmarshalBinary, while -// a longer byte slice or a string is handled by UnmarshalText. -func (u *UUID) Scan(src interface{}) error { - switch src := src.(type) { - case []byte: - if len(src) == 16 { - return u.UnmarshalBinary(src) - } - return u.UnmarshalText(src) - - case string: - return u.UnmarshalText([]byte(src)) - } - - return fmt.Errorf("uuid: cannot convert %T to UUID", src) -} - -// Value implements the driver.Valuer interface. -func (u NullUUID) Value() (driver.Value, error) { - if !u.Valid { - return nil, nil - } - // Delegate to UUID Value function - return u.UUID.Value() -} - -// Scan implements the sql.Scanner interface. -func (u *NullUUID) Scan(src interface{}) error { - if src == nil { - u.UUID, u.Valid = Nil, false - return nil - } - - // Delegate to UUID Scan function - u.Valid = true - return u.UUID.Scan(src) -} - -// FromBytes returns UUID converted from raw byte slice input. -// It will return error if the slice isn't 16 bytes long. -func FromBytes(input []byte) (u UUID, err error) { - err = u.UnmarshalBinary(input) - return -} - -// FromBytesOrNil returns UUID converted from raw byte slice input. -// Same behavior as FromBytes, but returns a Nil UUID on error. -func FromBytesOrNil(input []byte) UUID { - uuid, err := FromBytes(input) - if err != nil { - return Nil - } - return uuid -} - -// FromString returns UUID parsed from string input. -// Input is expected in a form accepted by UnmarshalText. -func FromString(input string) (u UUID, err error) { - err = u.UnmarshalText([]byte(input)) - return -} - -// FromStringOrNil returns UUID parsed from string input. -// Same behavior as FromString, but returns a Nil UUID on error. -func FromStringOrNil(input string) UUID { - uuid, err := FromString(input) - if err != nil { - return Nil - } - return uuid -} - -// Returns UUID v1/v2 storage state. -// Returns epoch timestamp, clock sequence, and hardware address. -func getStorage() (uint64, uint16, []byte) { - storageOnce.Do(initStorage) - - storageMutex.Lock() - defer storageMutex.Unlock() - - timeNow := epochFunc() - // Clock changed backwards since last UUID generation. - // Should increase clock sequence. - if timeNow <= lastTime { - clockSequence++ - } - lastTime = timeNow - - return timeNow, clockSequence, hardwareAddr[:] -} - -// NewV1 returns UUID based on current timestamp and MAC address. -func NewV1() UUID { - u := UUID{} - - timeNow, clockSeq, hardwareAddr := getStorage() - - binary.BigEndian.PutUint32(u[0:], uint32(timeNow)) - binary.BigEndian.PutUint16(u[4:], uint16(timeNow>>32)) - binary.BigEndian.PutUint16(u[6:], uint16(timeNow>>48)) - binary.BigEndian.PutUint16(u[8:], clockSeq) - - copy(u[10:], hardwareAddr) - - u.SetVersion(1) - u.SetVariant() - - return u -} - -// NewV2 returns DCE Security UUID based on POSIX UID/GID. -func NewV2(domain byte) UUID { - u := UUID{} - - timeNow, clockSeq, hardwareAddr := getStorage() - - switch domain { - case DomainPerson: - binary.BigEndian.PutUint32(u[0:], posixUID) - case DomainGroup: - binary.BigEndian.PutUint32(u[0:], posixGID) - } - - binary.BigEndian.PutUint16(u[4:], uint16(timeNow>>32)) - binary.BigEndian.PutUint16(u[6:], uint16(timeNow>>48)) - binary.BigEndian.PutUint16(u[8:], clockSeq) - u[9] = domain - - copy(u[10:], hardwareAddr) - - u.SetVersion(2) - u.SetVariant() - - return u -} - -// NewV3 returns UUID based on MD5 hash of namespace UUID and name. -func NewV3(ns UUID, name string) UUID { - u := newFromHash(md5.New(), ns, name) - u.SetVersion(3) - u.SetVariant() - - return u -} - -// NewV4 returns random generated UUID. -func NewV4() UUID { - u := UUID{} - safeRandom(u[:]) - u.SetVersion(4) - u.SetVariant() - - return u -} - -// NewV5 returns UUID based on SHA-1 hash of namespace UUID and name. -func NewV5(ns UUID, name string) UUID { - u := newFromHash(sha1.New(), ns, name) - u.SetVersion(5) - u.SetVariant() - - return u -} - -// Returns UUID based on hashing of namespace UUID and name. -func newFromHash(h hash.Hash, ns UUID, name string) UUID { - u := UUID{} - h.Write(ns[:]) - h.Write([]byte(name)) - copy(u[:], h.Sum(nil)) - - return u -}