From 31dfb54b6fa771a4cd821efeb53df9269dac0e5b Mon Sep 17 00:00:00 2001 From: Eric Chiang Date: Tue, 20 Dec 2016 17:24:17 -0800 Subject: [PATCH 1/4] connector: add a SAML connector --- connector/connector.go | 17 ++ connector/saml/saml.go | 387 ++++++++++++++++++++++++++ connector/saml/saml_test.go | 42 +++ connector/saml/testdata/okta-ca.pem | 19 ++ connector/saml/testdata/okta-resp.xml | 33 +++ connector/saml/types.go | 177 ++++++++++++ glide.yaml | 8 + 7 files changed, 683 insertions(+) create mode 100644 connector/saml/saml.go create mode 100644 connector/saml/saml_test.go create mode 100644 connector/saml/testdata/okta-ca.pem create mode 100644 connector/saml/testdata/okta-resp.xml create mode 100644 connector/saml/types.go diff --git a/connector/connector.go b/connector/connector.go index 95a7ec13..c92d7589 100644 --- a/connector/connector.go +++ b/connector/connector.go @@ -66,6 +66,23 @@ type CallbackConnector interface { HandleCallback(s Scopes, r *http.Request) (identity Identity, err error) } +// SAMLConnector represents SAML connectors which implement the HTTP POST binding. +// +// RelayState is handled by the server. +type SAMLConnector interface { + // POSTData returns an encoded SAML request and SSO URL for the server to + // render a POST form with. + POSTData(s Scopes) (sooURL, samlRequest string, err error) + + // TODO(ericchiang): Provide expected "InResponseTo" ID value. + // + // See: https://www.oasis-open.org/committees/download.php/35711/sstc-saml-core-errata-2.0-wd-06-diff.pdf + // "3.2.2 Complex Type StatusResponseType" + + // HandlePOST decodes, verifies, and maps attributes from the SAML response. + HandlePOST(s Scopes, samlResponse string) (identity Identity, err error) +} + // RefreshConnector is a connector that can update the client claims. type RefreshConnector interface { // Refresh is called when a client attempts to claim a refresh token. The diff --git a/connector/saml/saml.go b/connector/saml/saml.go new file mode 100644 index 00000000..0c5b806b --- /dev/null +++ b/connector/saml/saml.go @@ -0,0 +1,387 @@ +// Package saml contains login methods for SAML. +package saml + +import ( + "bytes" + "compress/flate" + "crypto/rand" + "crypto/x509" + "encoding/base64" + "encoding/hex" + "encoding/pem" + "encoding/xml" + "errors" + "fmt" + "io/ioutil" + "strings" + "time" + + "github.com/Sirupsen/logrus" + "github.com/beevik/etree" + dsig "github.com/russellhaering/goxmldsig" + + "github.com/coreos/dex/connector" +) + +const ( + bindingRedirect = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + bindingPOST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + + nameIDFormatEmailAddress = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + nameIDFormatUnspecified = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" + nameIDFormatX509Subject = "urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName" + nameIDFormatWindowsDN = "urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName" + nameIDFormatEncrypted = "urn:oasis:names:tc:SAML:2.0:nameid-format:encrypted" + nameIDFormatEntity = "urn:oasis:names:tc:SAML:2.0:nameid-format:entity" + nameIDFormatKerberos = "urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos" + nameIDFormatPersistent = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" + nameIDformatTransient = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" +) + +var ( + nameIDFormats = []string{ + nameIDFormatEmailAddress, + nameIDFormatUnspecified, + nameIDFormatX509Subject, + nameIDFormatWindowsDN, + nameIDFormatEncrypted, + nameIDFormatEntity, + nameIDFormatKerberos, + nameIDFormatPersistent, + nameIDformatTransient, + } + nameIDFormatLookup = make(map[string]string) +) + +func init() { + suffix := func(s, sep string) string { + if i := strings.LastIndex(s, sep); i > 0 { + return s[i+1:] + } + return s + } + for _, format := range nameIDFormats { + nameIDFormatLookup[suffix(format, ":")] = format + nameIDFormatLookup[format] = format + } +} + +// Config represents configuration options for the SAML provider. +type Config struct { + // TODO(ericchiang): A bunch of these fields could be auto-filled if + // we supported SAML metadata discovery. + // + // https://www.oasis-open.org/committees/download.php/35391/sstc-saml-metadata-errata-2.0-wd-04-diff.pdf + + Issuer string `json:"issuer"` + SSOURL string `json:"ssoURL"` + + // X509 CA file or raw data to verify XML signatures. + CA string `json:"ca"` + CAData []byte `json:"caData"` + + InsecureSkipSignatureValidation bool `json:"insecureSkipSignatureValidation"` + + // Assertion attribute names to lookup various claims with. + UsernameAttr string `json:"usernameAttr"` + EmailAttr string `json:"emailAttr"` + GroupsAttr string `json:"groupsAttr"` + // If GroupsDelim is supplied the connector assumes groups are returned as a + // single string instead of multiple attribute values. This delimiter will be + // used split the groups string. + GroupsDelim string `json:"groupsDelim"` + + RedirectURI string `json:"redirectURI"` + + // Requested format of the NameID. The NameID value is is mapped to the ID Token + // 'sub' claim. + // + // This can be an abbreviated form of the full URI with just the last component. For + // example, if this value is set to "emailAddress" the format will resolve to: + // + // urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress + // + // If no value is specified, this value defaults to: + // + // urn:oasis:names:tc:SAML:2.0:nameid-format:persistent + // + NameIDPolicyFormat string `json:"nameIDPolicyFormat"` +} + +type certStore struct { + certs []*x509.Certificate +} + +func (c certStore) Certificates() (roots []*x509.Certificate, err error) { + return c.certs, nil +} + +// Open validates the config and returns a connector. It does not actually +// validate connectivity with the provider. +func (c *Config) Open(logger logrus.FieldLogger) (connector.Connector, error) { + return c.openConnector(logger) +} + +func (c *Config) openConnector(logger logrus.FieldLogger) (interface { + connector.SAMLConnector +}, error) { + requiredFields := []struct { + name, val string + }{ + {"issuer", c.Issuer}, + {"ssoURL", c.SSOURL}, + {"usernameAttr", c.UsernameAttr}, + {"emailAttr", c.EmailAttr}, + {"redirectURI", c.RedirectURI}, + } + var missing []string + for _, f := range requiredFields { + if f.val == "" { + missing = append(missing, f.name) + } + } + switch len(missing) { + case 0: + case 1: + return nil, fmt.Errorf("missing required field %q", missing[0]) + default: + return nil, fmt.Errorf("missing required fields %q", missing) + } + + p := &provider{ + issuer: c.Issuer, + ssoURL: c.SSOURL, + now: time.Now, + usernameAttr: c.UsernameAttr, + emailAttr: c.EmailAttr, + groupsAttr: c.GroupsAttr, + groupsDelim: c.GroupsDelim, + redirectURI: c.RedirectURI, + logger: logger, + + nameIDPolicyFormat: c.NameIDPolicyFormat, + } + + if p.nameIDPolicyFormat == "" { + p.nameIDPolicyFormat = nameIDFormatPersistent + } else { + if format, ok := nameIDFormatLookup[p.nameIDPolicyFormat]; ok { + p.nameIDPolicyFormat = format + } else { + return nil, fmt.Errorf("invalid nameIDPolicyFormat: %q", p.nameIDPolicyFormat) + } + } + + if !c.InsecureSkipSignatureValidation { + if (c.CA == "") == (c.CAData == nil) { + return nil, errors.New("must provide either 'ca' or 'caData'") + } + + var caData []byte + if c.CA != "" { + data, err := ioutil.ReadFile(c.CA) + if err != nil { + return nil, fmt.Errorf("read ca file: %v", err) + } + caData = data + } else { + caData = c.CAData + } + + var ( + certs []*x509.Certificate + block *pem.Block + ) + for { + block, caData = pem.Decode(caData) + if block == nil { + break + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("parse cert: %v", err) + } + certs = append(certs, cert) + } + if len(certs) == 0 { + return nil, errors.New("no certificates found in ca data") + } + p.validator = dsig.NewDefaultValidationContext(certStore{certs}) + } + return p, nil +} + +type provider struct { + issuer string + ssoURL string + + now func() time.Time + + // If nil, don't do signature validation. + validator *dsig.ValidationContext + + // Attribute mappings + usernameAttr string + emailAttr string + groupsAttr string + groupsDelim string + + redirectURI string + + nameIDPolicyFormat string + + logger logrus.FieldLogger +} + +func (p *provider) POSTData(s connector.Scopes) (action, value string, err error) { + + // NOTE(ericchiang): If we can't follow up with the identity provider, can we + // support refresh tokens? + if s.OfflineAccess { + return "", "", fmt.Errorf("SAML does not support offline access") + } + + r := &authnRequest{ + ProtocolBinding: bindingPOST, + ID: "_" + uuidv4(), + IssueInstant: xmlTime(p.now()), + Destination: p.ssoURL, + Issuer: &issuer{ + Issuer: p.issuer, + }, + NameIDPolicy: &nameIDPolicy{ + AllowCreate: true, + Format: p.nameIDPolicyFormat, + }, + } + + data, err := xml.MarshalIndent(r, "", " ") + if err != nil { + return "", "", fmt.Errorf("marshal authn request: %v", err) + } + + buff := new(bytes.Buffer) + fw, err := flate.NewWriter(buff, flate.DefaultCompression) + if err != nil { + return "", "", fmt.Errorf("new flate writer: %v", err) + } + if _, err := fw.Write(data); err != nil { + return "", "", fmt.Errorf("compress message: %v", err) + } + if err := fw.Close(); err != nil { + return "", "", fmt.Errorf("flush message: %v", err) + } + + return p.ssoURL, base64.StdEncoding.EncodeToString(buff.Bytes()), nil +} + +func (p *provider) HandlePOST(s connector.Scopes, samlResponse string) (ident connector.Identity, err error) { + rawResp, err := base64.StdEncoding.DecodeString(samlResponse) + if err != nil { + return ident, fmt.Errorf("decode response: %v", err) + } + if p.validator != nil { + if rawResp, err = verify(p.validator, rawResp); err != nil { + return ident, fmt.Errorf("verify signature: %v", err) + } + } + + var resp response + if err := xml.Unmarshal(rawResp, &resp); err != nil { + return ident, fmt.Errorf("unmarshal response: %v", err) + } + + if resp.Destination != "" && resp.Destination != p.redirectURI { + return ident, fmt.Errorf("expected destination %q got %q", p.redirectURI, resp.Destination) + + } + + assertion := resp.Assertion + if assertion == nil { + return ident, fmt.Errorf("response did not contain an assertion") + } + subject := assertion.Subject + if subject == nil { + return ident, fmt.Errorf("response did not contain a subject") + } + + switch { + case subject.NameID != nil: + if ident.UserID = subject.NameID.Value; ident.UserID == "" { + return ident, fmt.Errorf("NameID element does not contain a value") + } + default: + return ident, fmt.Errorf("subject does not contain an NameID element") + } + + attributes := assertion.AttributeStatement + if attributes == nil { + return ident, fmt.Errorf("response did not contain a AttributeStatement") + } + + if ident.Email, _ = attributes.get(p.emailAttr); ident.Email == "" { + return ident, fmt.Errorf("no attribute with name %q", p.emailAttr) + } + ident.EmailVerified = true + + if ident.Username, _ = attributes.get(p.usernameAttr); ident.Username == "" { + return ident, fmt.Errorf("no attribute with name %q", p.usernameAttr) + } + + if s.Groups && p.groupsAttr != "" { + if p.groupsDelim != "" { + groupsStr, ok := attributes.get(p.groupsAttr) + if !ok { + return ident, fmt.Errorf("no attribute with name %q", p.groupsAttr) + } + // TOOD(ericchiang): Do we need to further trim whitespace? + ident.Groups = strings.Split(groupsStr, p.groupsDelim) + } else { + groups, ok := attributes.all(p.groupsAttr) + if !ok { + return ident, fmt.Errorf("no attribute with name %q", p.groupsAttr) + } + ident.Groups = groups + } + } + + return ident, nil +} + +// verify checks the signature info of a XML document and returns +// the signed elements. +func verify(validator *dsig.ValidationContext, data []byte) (signed []byte, err error) { + doc := etree.NewDocument() + if err := doc.ReadFromBytes(data); err != nil { + return nil, fmt.Errorf("parse document: %v", err) + } + + result, err := validator.Validate(doc.Root()) + if err != nil { + return nil, err + } + doc.SetRoot(result) + return doc.WriteToBytes() +} + +func uuidv4() string { + u := make([]byte, 16) + if _, err := rand.Read(u); err != nil { + panic(err) + } + u[6] = (u[6] | 0x40) & 0x4F + u[8] = (u[8] | 0x80) & 0xBF + + r := make([]byte, 36) + r[8] = '-' + r[13] = '-' + r[18] = '-' + r[23] = '-' + hex.Encode(r, u[0:4]) + hex.Encode(r[9:], u[4:6]) + hex.Encode(r[14:], u[6:8]) + hex.Encode(r[19:], u[8:10]) + hex.Encode(r[24:], u[10:]) + + return string(r) +} diff --git a/connector/saml/saml_test.go b/connector/saml/saml_test.go new file mode 100644 index 00000000..4e455688 --- /dev/null +++ b/connector/saml/saml_test.go @@ -0,0 +1,42 @@ +package saml + +import ( + "crypto/x509" + "encoding/pem" + "errors" + "io/ioutil" + "testing" + + sdig "github.com/russellhaering/goxmldsig" +) + +func loadCert(ca string) (*x509.Certificate, error) { + data, err := ioutil.ReadFile(ca) + if err != nil { + return nil, err + } + block, _ := pem.Decode(data) + if block == nil { + return nil, errors.New("ca file didn't contain any PEM data") + } + return x509.ParseCertificate(block.Bytes) +} + +func TestVerify(t *testing.T) { + cert, err := loadCert("testdata/okta-ca.pem") + if err != nil { + t.Fatal(err) + } + s := certStore{[]*x509.Certificate{cert}} + + validator := sdig.NewDefaultValidationContext(s) + + data, err := ioutil.ReadFile("testdata/okta-resp.xml") + if err != nil { + t.Fatal(err) + } + + if _, err := verify(validator, data); err != nil { + t.Fatal(err) + } +} diff --git a/connector/saml/testdata/okta-ca.pem b/connector/saml/testdata/okta-ca.pem new file mode 100644 index 00000000..de7f1c88 --- /dev/null +++ b/connector/saml/testdata/okta-ca.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDpDCCAoygAwIBAgIGAVjgvNroMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi05NjkyNDQxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYxMjA4MjMxOTIzWhcNMjYxMjA4MjMyMDIzWjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtOTY5MjQ0MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +4dW2YlcXjZwnTmLnV7IOBq8hhrdbqlNwdjCHRyx1BizOk3RbVP56grgPdyWScTCPpJ6vZZ8rtrY0 +m1rwr+cxifNuQGKTlE33g2hReo/N9f3LFUMITlnnNH80Yium3SYuEqGeHLYerelXOnEKx6x+X5qD +eg2DRW6I9/v/mfN2KAQEDqF9aSNlNFWZWmb52kukMv3tLWw0puaevicIZ/nZrW+D3CLDVVfWHeVt +46EF2bkLdgbIJOU3GzLoolgBOCkydX9x6xTw6knwQaqYsRGflacw6571IzWEwjmd17uJXkarnhM1 +51pqwIoksTzycbjinIg6B1rNpGFDN7Ah+9EnVQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQDQOtlj +7Yk1j4GV/135zLOlsaonq8zfsu05VfY5XydNPFEkyIVcegLa8RiqALyFf+FjY/wVyhXtARw7NBUo +u/Jh63cVya/VEoP1SVa0XQLf/ial+XuwdBBL1yc+7o2XHOfluDaXw/v2FWYpZtICf3a39giGaNWp +eCT4g2TDWM4Jf/X7/mRbLX9tQO7XRural1CXx8VIMcmbKNbUtQiO8yEtVQ+FJKOsl7KOSzkgqNiL +rJy+Y0D9biLZVKp07KWAY2FPCEtCkBrvo8BhvWbxWMA8CVQNAiTylM27Pc6kbc64pNr7C1Jx1wuE +mVy9Fgb4PA2N3hPeD7mBmGGp7CfDbGcy +-----END CERTIFICATE----- diff --git a/connector/saml/testdata/okta-resp.xml b/connector/saml/testdata/okta-resp.xml new file mode 100644 index 00000000..9dff8e10 --- /dev/null +++ b/connector/saml/testdata/okta-resp.xml @@ -0,0 +1,33 @@ +http://www.okta.com/exk91cb99lKkKSYoy0h7Phu93l0D97JSMIYDZBdVeNLN0pwBVHhzUDWxbh4sc6g=M2gMHOmnMAFgh2apq/2jHwDYmisUkYMUqxrWkQJf3RHFotl4EeDlcqq/FzOboJc3NcbKBqQY3CWsWhWh5cNWHDgNneaahW4czww+9DCM0R/zz5c6GuMYFEh5df2sDn/dWk/jbKMiAMgPdKJ2x/+5Xk9q4axC52TdQrrbZtzAAAn4CgrT6Kf11qfMl5wpDarg3qPw7ANxWn2DKzCsvCkOIwM2+AXh+sEXmTvvZIQ0vpv098FH/ZTGt4sCwb1bmRZ3UZLhBcxVc/sjuEW/sQ6pbQHkjrXIR5bxXzGNUxYpcGjrp9HGF+In0BAc+Ds/A0H142e1rgtcX8LH2pbG8URJSQ==MIIDpDCCAoygAwIBAgIGAVjgvNroMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi05NjkyNDQxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYxMjA4MjMxOTIzWhcNMjYxMjA4MjMyMDIzWjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtOTY5MjQ0MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +4dW2YlcXjZwnTmLnV7IOBq8hhrdbqlNwdjCHRyx1BizOk3RbVP56grgPdyWScTCPpJ6vZZ8rtrY0 +m1rwr+cxifNuQGKTlE33g2hReo/N9f3LFUMITlnnNH80Yium3SYuEqGeHLYerelXOnEKx6x+X5qD +eg2DRW6I9/v/mfN2KAQEDqF9aSNlNFWZWmb52kukMv3tLWw0puaevicIZ/nZrW+D3CLDVVfWHeVt +46EF2bkLdgbIJOU3GzLoolgBOCkydX9x6xTw6knwQaqYsRGflacw6571IzWEwjmd17uJXkarnhM1 +51pqwIoksTzycbjinIg6B1rNpGFDN7Ah+9EnVQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQDQOtlj +7Yk1j4GV/135zLOlsaonq8zfsu05VfY5XydNPFEkyIVcegLa8RiqALyFf+FjY/wVyhXtARw7NBUo +u/Jh63cVya/VEoP1SVa0XQLf/ial+XuwdBBL1yc+7o2XHOfluDaXw/v2FWYpZtICf3a39giGaNWp +eCT4g2TDWM4Jf/X7/mRbLX9tQO7XRural1CXx8VIMcmbKNbUtQiO8yEtVQ+FJKOsl7KOSzkgqNiL +rJy+Y0D9biLZVKp07KWAY2FPCEtCkBrvo8BhvWbxWMA8CVQNAiTylM27Pc6kbc64pNr7C1Jx1wuE +mVy9Fgb4PA2N3hPeD7mBmGGp7CfDbGcyhttp://www.okta.com/exk91cb99lKkKSYoy0h7ufwWUjecX6I/aQb4WW9P9ZMLG3C8hN6LaZyyb/EATIs=jKtNBzxAL67ssuzWkkbf0yzqRyZ51y2JjBQ9C6bW8io/JOYQB2v7Bix7Eu/RjJslO7OBqD+3tPrK7ZBOy2+LFuAh3cDNa3U5NhO0raLrn/2YoJXfjj3XX3hyQv6GVxo0EY1KJNXOzWxjp9RVDpHslPTIL1yDC/oy0Mlzxu6pXBEerz9J2/Caenq66Skb5/DAT8FvrJ2s1bxuMagShs3APhC1hD8mvktZ+ZcN8ujs2SebteGK4IoOCx+e8+v2CyycBv1l5l+v5I+D2HnbAw4LfvHnW4rZOJT2AvoI47p1YBK1qDsJutG3jUPKy4Yx5YF73Xi1oytr+rrHyx/lfFPd2A==MIIDpDCCAoygAwIBAgIGAVjgvNroMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi05NjkyNDQxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYxMjA4MjMxOTIzWhcNMjYxMjA4MjMyMDIzWjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtOTY5MjQ0MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +4dW2YlcXjZwnTmLnV7IOBq8hhrdbqlNwdjCHRyx1BizOk3RbVP56grgPdyWScTCPpJ6vZZ8rtrY0 +m1rwr+cxifNuQGKTlE33g2hReo/N9f3LFUMITlnnNH80Yium3SYuEqGeHLYerelXOnEKx6x+X5qD +eg2DRW6I9/v/mfN2KAQEDqF9aSNlNFWZWmb52kukMv3tLWw0puaevicIZ/nZrW+D3CLDVVfWHeVt +46EF2bkLdgbIJOU3GzLoolgBOCkydX9x6xTw6knwQaqYsRGflacw6571IzWEwjmd17uJXkarnhM1 +51pqwIoksTzycbjinIg6B1rNpGFDN7Ah+9EnVQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQDQOtlj +7Yk1j4GV/135zLOlsaonq8zfsu05VfY5XydNPFEkyIVcegLa8RiqALyFf+FjY/wVyhXtARw7NBUo +u/Jh63cVya/VEoP1SVa0XQLf/ial+XuwdBBL1yc+7o2XHOfluDaXw/v2FWYpZtICf3a39giGaNWp +eCT4g2TDWM4Jf/X7/mRbLX9tQO7XRural1CXx8VIMcmbKNbUtQiO8yEtVQ+FJKOsl7KOSzkgqNiL +rJy+Y0D9biLZVKp07KWAY2FPCEtCkBrvo8BhvWbxWMA8CVQNAiTylM27Pc6kbc64pNr7C1Jx1wuE +mVy9Fgb4PA2N3hPeD7mBmGGp7CfDbGcyeric.chiang+okta@coreos.comhttp://localhost:5556/dex/callbackurn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport diff --git a/connector/saml/types.go b/connector/saml/types.go new file mode 100644 index 00000000..7c1d89be --- /dev/null +++ b/connector/saml/types.go @@ -0,0 +1,177 @@ +package saml + +import ( + "encoding/xml" + "fmt" + "time" +) + +const timeFormat = "2006-01-02T15:04:05Z" + +type xmlTime time.Time + +func (t xmlTime) MarshalXMLAttr(name xml.Name) (xml.Attr, error) { + return xml.Attr{ + Name: name, + Value: time.Time(t).UTC().Format(timeFormat), + }, nil +} + +func (t *xmlTime) UnmarshalXMLAttr(attr xml.Attr) error { + got, err := time.Parse(timeFormat, attr.Value) + if err != nil { + return err + } + *t = xmlTime(got) + return nil +} + +type samlVersion struct{} + +func (s samlVersion) MarshalXMLAttr(name xml.Name) (xml.Attr, error) { + return xml.Attr{ + Name: name, + Value: "2.0", + }, nil +} + +func (s *samlVersion) UnmarshalXMLAttr(attr xml.Attr) error { + if attr.Value != "2.0" { + return fmt.Errorf(`saml version expected "2.0" got %q`, attr.Value) + } + return nil +} + +type authnRequest struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol AuthnRequest"` + + ID string `xml:"ID,attr"` + Version samlVersion `xml:"Version,attr"` + + ProviderName string `xml:"ProviderName,attr,omitempty"` + IssueInstant xmlTime `xml:"IssueInstant,attr,omitempty"` + Consent bool `xml:"Consent,attr,omitempty"` + Destination string `xml:"Destination,attr,omitempty"` + + ForceAuthn bool `xml:"ForceAuthn,attr,omitempty"` + IsPassive bool `xml:"IsPassive,attr,omitempty"` + ProtocolBinding string `xml:"ProtocolBinding,attr,omitempty"` + + Subject *subject `xml:"Subject,omitempty"` + Issuer *issuer `xml:"Issuer,omitempty"` + NameIDPolicy *nameIDPolicy `xml:"NameIDPolicy,omitempty"` + + // TODO(ericchiang): Make this configurable and determine appropriate default values. + RequestAuthnContext *requestAuthnContext `xml:"RequestAuthnContext,omitempty"` +} + +type subject struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Subject"` + + NameID *nameID `xml:"NameID,omitempty"` + + // TODO(ericchiang): Do we need to deal with baseID? +} + +type nameID struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion NameID"` + + Format string `xml:"Format,omitempty"` + Value string `xml:",chardata"` +} + +type issuer struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Issuer"` + Issuer string `xml:",chardata"` +} + +type nameIDPolicy struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol NameIDPolicy"` + AllowCreate bool `xml:"AllowCreate,attr,omitempty"` + Format string `xml:"Format,attr,omitempty"` +} + +type requestAuthnContext struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol RequestAuthnContext"` + + AuthnContextClassRefs []authnContextClassRef +} + +type authnContextClassRef struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol AuthnContextClassRef"` + Value string `xml:",chardata"` +} + +type response struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol Response"` + + ID string `xml:"ID,attr"` + Version samlVersion `xml:"Version,attr"` + + Destination string `xml:"Destination,attr,omitempty"` + + Issuer *issuer `xml:"Issuer,omitempty"` + + // TODO(ericchiang): How do deal with multiple assertions? + Assertion *assertion `xml:"Assertion,omitempty"` +} + +type assertion struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Assertion"` + + Version samlVersion `xml:"Version,attr"` + ID string `xml:"ID,attr"` + IssueInstance xmlTime `xml:"IssueInstance,attr"` + + Issuer issuer `xml:"Issuer"` + + Subject *subject `xml:"Subject,omitempty"` + + AttributeStatement *attributeStatement `xml:"AttributeStatement,omitempty"` +} + +type attributeStatement struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion AttributeStatement"` + + Attributes []attribute `xml:"Attribute"` +} + +func (a *attributeStatement) get(name string) (s string, ok bool) { + for _, attr := range a.Attributes { + if attr.Name == name { + ok = true + if len(attr.AttributeValues) > 0 { + return attr.AttributeValues[0].Value, true + } + } + } + return +} + +func (a *attributeStatement) all(name string) (s []string, ok bool) { + for _, attr := range a.Attributes { + if attr.Name == name { + ok = true + for _, val := range attr.AttributeValues { + s = append(s, val.Value) + } + } + } + return +} + +type attribute struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Attribute"` + + Name string `xml:"Name,attr"` + + NameFormat string `xml:"NameFormat,attr,omitempty"` + FriendlyName string `xml:"FriendlyName,attr,omitempty"` + + AttributeValues []attributeValue `xml:"AttributeValue,omitempty"` +} + +type attributeValue struct { + XMLName xml.Name `xml:"AttributeValue"` + Value string `xml:",chardata"` +} diff --git a/glide.yaml b/glide.yaml index 05d6ec42..3e7d13e6 100644 --- a/glide.yaml +++ b/glide.yaml @@ -131,3 +131,11 @@ import: version: v0.11.0 - package: golang.org/x/sys/unix version: 833a04a10549a95dc34458c195cbad61bbb6cb4d + +# XML signature validation for SAML connector +- package: github.com/russellhaering/goxmldsig + version: d9f653eb27ee8b145f7d5a45172e81a93def0860 +- package: github.com/beevik/etree + version: 4cd0dd976db869f817248477718071a28e978df0 +- package: github.com/jonboulle/clockwork + version: bcac9884e7502bb2b474c0339d889cb981a2f27f From 0f4a1f69c5c3e70fa325f92bd6c125f7248977f6 Mon Sep 17 00:00:00 2001 From: Eric Chiang Date: Tue, 20 Dec 2016 17:24:32 -0800 Subject: [PATCH 2/4] *: wire up SAML POST binding --- Documentation/saml-connector.md | 72 +++++++++++++++++++++++++++++++++ README.md | 1 + cmd/dex/config.go | 12 +++--- server/handlers.go | 72 ++++++++++++++++++++++++++------- 4 files changed, 138 insertions(+), 19 deletions(-) create mode 100644 Documentation/saml-connector.md diff --git a/Documentation/saml-connector.md b/Documentation/saml-connector.md new file mode 100644 index 00000000..d78a6f2b --- /dev/null +++ b/Documentation/saml-connector.md @@ -0,0 +1,72 @@ +# Authentication through SAML 2.0 + +## Overview + +The experimental SAML provider allows authentication through the SAML 2.0 HTTP POST binding. + +The connector uses the value of the `NameID` element as the user's unique identifier which dex assumes is both unique and never changes. Use the `nameIDPolicyFormat` to ensure this is set to a value which satisfies these requirements. + +## Caveats + +There are known issues with the XML signature validation for this connector. In addition work is still being done to ensure this connector implements best security practices for SAML 2.0. + +The connector doesn't support signed AuthnRequests or encrypted attributes. + +The connector doesn't support refresh tokens since the SAML 2.0 protocol doesn't provide a way to requery a provider without interaction. + +## Configuration + +```yaml +connectors: +- type: samlExperimental # will be changed to "saml" later without support for the "samlExperimental" value + id: saml + config: + # Issuer used for validating the SAML response. + issuer: https://saml.example.com + # SSO URL used for POST value. + ssoURL: https://saml.example.com/sso + + # CA to use when validating the SAML response. + ca: /path/to/ca.pem + + # CA's can also be provided inline as a base64'd blob. + # + # catData: ( RAW base64'd PEM encoded CA ) + + # To skip signature validation, uncomment the following field. This should + # only be used during testing and may be removed in the future. + # + # insucreSkipSignatureValidation: true + + # Dex's callback URL. Must match the "Destination" attribute of all responses + # exactly. + redirectURI: https://dex.example.com/callback + + # Name of attributes in the returned assertions to map to ID token claims. + usernameAttr: name + emailAttr: email + groupsAttr: groups # optional + + # By default, multiple groups are assumed to be represented as multiple + # attributes with the same name. + # + # If "groupsDelim" is provided groups are assumed to be represented as a + # single attribute and the delimiter is used to split the attribute's value + # into multiple groups. + # + # groupsDelim: ", " + + + # Requested format of the NameID. The NameID value is is mapped to the ID Token + # 'sub' claim. This can be an abbreviated form of the full URI with just the last + # component. For example, if this value is set to "emailAddress" the format will + # resolve to: + # + # urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress + # + # If no value is specified, this value defaults to: + # + # urn:oasis:names:tc:SAML:2.0:nameid-format:persistent + # + nameIDPolicyFormat: persistent +``` diff --git a/README.md b/README.md index 2812b2ac..ca3e08d7 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ More docs for running dex as a Kubernetes authenticator can be found [here](Docu * Identity provider logins * [LDAP](Documentation/ldap-connector.md) * [GitHub](Documentation/github-connector.md) + * [SAML 2.0 (experimental)](Documentation/saml-connector.md) * Client libraries * [Go][go-oidc] diff --git a/cmd/dex/config.go b/cmd/dex/config.go index 40a4fe61..19a87fc3 100644 --- a/cmd/dex/config.go +++ b/cmd/dex/config.go @@ -14,6 +14,7 @@ import ( "github.com/coreos/dex/connector/ldap" "github.com/coreos/dex/connector/mock" "github.com/coreos/dex/connector/oidc" + "github.com/coreos/dex/connector/saml" "github.com/coreos/dex/server" "github.com/coreos/dex/storage" "github.com/coreos/dex/storage/kubernetes" @@ -177,11 +178,12 @@ type ConnectorConfig interface { } var connectors = map[string]func() ConnectorConfig{ - "mockCallback": func() ConnectorConfig { return new(mock.CallbackConfig) }, - "mockPassword": func() ConnectorConfig { return new(mock.PasswordConfig) }, - "ldap": func() ConnectorConfig { return new(ldap.Config) }, - "github": func() ConnectorConfig { return new(github.Config) }, - "oidc": func() ConnectorConfig { return new(oidc.Config) }, + "mockCallback": func() ConnectorConfig { return new(mock.CallbackConfig) }, + "mockPassword": func() ConnectorConfig { return new(mock.PasswordConfig) }, + "ldap": func() ConnectorConfig { return new(ldap.Config) }, + "github": func() ConnectorConfig { return new(github.Config) }, + "oidc": func() ConnectorConfig { return new(oidc.Config) }, + "samlExperimental": func() ConnectorConfig { return new(saml.Config) }, } // UnmarshalJSON allows Connector to implement the unmarshaler interface to diff --git a/server/handlers.go b/server/handlers.go index c962265f..5a0b9b34 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -227,6 +227,31 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) { if err := s.templates.password(w, authReqID, r.URL.String(), "", false); err != nil { s.logger.Errorf("Server template error: %v", err) } + case connector.SAMLConnector: + action, value, err := conn.POSTData(scopes) + if err != nil { + s.logger.Errorf("Creating SAML data: %v", err) + s.renderError(w, http.StatusInternalServerError, "Connector Login Error") + return + } + + // TODO(ericchiang): Don't inline this. + fmt.Fprintf(w, ` + + + + SAML login + + +
+ + +
+ + + `, action, value, authReqID) default: s.renderError(w, http.StatusBadRequest, "Requested resource does not exist.") } @@ -266,20 +291,24 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) { } func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request) { - // SAML redirect bindings use the "RelayState" URL query field. When we support - // SAML, we'll have to check that field too and possibly let callback connectors - // indicate which field is used to determine the state. - // - // See: - // https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf - // Section: "3.4.3 RelayState" - state := r.URL.Query().Get("state") - if state == "" { - s.renderError(w, http.StatusBadRequest, "User session error.") + var authID string + switch r.Method { + case "GET": // OAuth2 callback + if authID = r.URL.Query().Get("state"); authID == "" { + s.renderError(w, http.StatusBadRequest, "User session error.") + return + } + case "POST": // SAML POST binding + if authID = r.PostFormValue("RelayState"); authID == "" { + s.renderError(w, http.StatusBadRequest, "User session error.") + return + } + default: + s.renderError(w, http.StatusBadRequest, "Method not supported") return } - authReq, err := s.storage.GetAuthRequest(state) + authReq, err := s.storage.GetAuthRequest(authID) if err != nil { if err == storage.ErrNotFound { s.logger.Errorf("Invalid 'state' parameter provided: %v", err) @@ -296,13 +325,28 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request) s.renderError(w, http.StatusInternalServerError, "Requested resource does not exist.") return } - callbackConnector, ok := conn.Connector.(connector.CallbackConnector) - if !ok { + + var identity connector.Identity + switch conn := conn.Connector.(type) { + case connector.CallbackConnector: + if r.Method != "GET" { + s.logger.Errorf("SAML request mapped to OAuth2 connector") + s.renderError(w, http.StatusBadRequest, "Invalid request") + return + } + identity, err = conn.HandleCallback(parseScopes(authReq.Scopes), r) + case connector.SAMLConnector: + if r.Method != "POST" { + s.logger.Errorf("OAuth2 request mapped to SAML connector") + s.renderError(w, http.StatusBadRequest, "Invalid request") + return + } + identity, err = conn.HandlePOST(parseScopes(authReq.Scopes), r.PostFormValue("SAMLResponse")) + default: s.renderError(w, http.StatusInternalServerError, "Requested resource does not exist.") return } - identity, err := callbackConnector.HandleCallback(parseScopes(authReq.Scopes), r) if err != nil { s.logger.Errorf("Failed to authenticate: %v", err) s.renderError(w, http.StatusInternalServerError, "Failed to return user's identity.") From 78665074edc3a7f3b7bbbe404c5d0f73e308052d Mon Sep 17 00:00:00 2001 From: Eric Chiang Date: Mon, 9 Jan 2017 14:50:17 -0800 Subject: [PATCH 3/4] cmd/example-app: add option to not request a refresh token --- cmd/example-app/main.go | 5 ++++- cmd/example-app/templates.go | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cmd/example-app/main.go b/cmd/example-app/main.go index a188dccc..ffa21c29 100644 --- a/cmd/example-app/main.go +++ b/cmd/example-app/main.go @@ -241,12 +241,15 @@ func (a *app) handleLogin(w http.ResponseWriter, r *http.Request) { authCodeURL := "" scopes = append(scopes, "openid", "profile", "email") - if a.offlineAsScope { + if r.FormValue("offline_acecss") != "yes" { + authCodeURL = a.oauth2Config(scopes).AuthCodeURL(exampleAppState) + } else if a.offlineAsScope { scopes = append(scopes, "offline_access") authCodeURL = a.oauth2Config(scopes).AuthCodeURL(exampleAppState) } else { authCodeURL = a.oauth2Config(scopes).AuthCodeURL(exampleAppState, oauth2.AccessTypeOffline) } + http.Redirect(w, r, authCodeURL, http.StatusSeeOther) } diff --git a/cmd/example-app/templates.go b/cmd/example-app/templates.go index 66142540..c0f9dfbd 100644 --- a/cmd/example-app/templates.go +++ b/cmd/example-app/templates.go @@ -14,6 +14,9 @@ var indexTmpl = template.Must(template.New("index.html").Parse(`

Extra scopes: +

+

+ Request offline access:

From 7ea2d24011edd19c7133141c18d17b3f9b4bde06 Mon Sep 17 00:00:00 2001 From: Eric Chiang Date: Mon, 9 Jan 2017 14:51:47 -0800 Subject: [PATCH 4/4] vendor: revendor --- glide.lock | 10 +- vendor/github.com/beevik/etree/LICENSE | 24 + vendor/github.com/beevik/etree/etree.go | 938 ++++++++++++++++++ vendor/github.com/beevik/etree/helpers.go | 188 ++++ vendor/github.com/beevik/etree/path.go | 470 +++++++++ vendor/github.com/jonboulle/clockwork/LICENSE | 201 ++++ .../jonboulle/clockwork/clockwork.go | 179 ++++ .../russellhaering/goxmldsig/LICENSE | 175 ++++ .../russellhaering/goxmldsig/canonicalize.go | 251 +++++ .../russellhaering/goxmldsig/clock.go | 55 + .../russellhaering/goxmldsig/keystore.go | 63 ++ .../russellhaering/goxmldsig/sign.go | 186 ++++ .../russellhaering/goxmldsig/tls_keystore.go | 34 + .../russellhaering/goxmldsig/validate.go | 397 ++++++++ .../russellhaering/goxmldsig/xml_constants.go | 78 ++ 15 files changed, 3247 insertions(+), 2 deletions(-) create mode 100644 vendor/github.com/beevik/etree/LICENSE create mode 100644 vendor/github.com/beevik/etree/etree.go create mode 100644 vendor/github.com/beevik/etree/helpers.go create mode 100644 vendor/github.com/beevik/etree/path.go create mode 100644 vendor/github.com/jonboulle/clockwork/LICENSE create mode 100644 vendor/github.com/jonboulle/clockwork/clockwork.go create mode 100644 vendor/github.com/russellhaering/goxmldsig/LICENSE create mode 100644 vendor/github.com/russellhaering/goxmldsig/canonicalize.go create mode 100644 vendor/github.com/russellhaering/goxmldsig/clock.go create mode 100644 vendor/github.com/russellhaering/goxmldsig/keystore.go create mode 100644 vendor/github.com/russellhaering/goxmldsig/sign.go create mode 100644 vendor/github.com/russellhaering/goxmldsig/tls_keystore.go create mode 100644 vendor/github.com/russellhaering/goxmldsig/validate.go create mode 100644 vendor/github.com/russellhaering/goxmldsig/xml_constants.go diff --git a/glide.lock b/glide.lock index 81ab6445..0ae0472a 100644 --- a/glide.lock +++ b/glide.lock @@ -1,6 +1,8 @@ -hash: 4d7d84f09a330d27458fb821ae7ada243cfa825808dc7ab116db28a08f9166a2 -updated: 2017-01-08T19:23:40.352046548+01:00 +hash: 2f68b742168a81ebbe604be42801d37e9da71dff5aeb6b8f8e91ed81ff0edec0 +updated: 2017-01-09T14:51:09.514065012-08:00 imports: +- name: github.com/beevik/etree + version: 4cd0dd976db869f817248477718071a28e978df0 - name: github.com/cockroachdb/cockroach-go version: 31611c0501c812f437d4861d87d117053967c955 subpackages: @@ -26,6 +28,8 @@ imports: version: e7e23673cac3f529f49e22f94e4af6d12bb49dba - name: github.com/inconshreveable/mousetrap version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 +- name: github.com/jonboulle/clockwork + version: bcac9884e7502bb2b474c0339d889cb981a2f27f - name: github.com/kylelemons/godebug version: eadb3ce320cbab8393bea5ca17bebac3f78a021b subpackages: @@ -41,6 +45,8 @@ imports: version: c97913dcbd76de40b051a9b4cd827f7eaeb7a868 subpackages: - cacheobject +- name: github.com/russellhaering/goxmldsig + version: d9f653eb27ee8b145f7d5a45172e81a93def0860 - name: github.com/Sirupsen/logrus version: d26492970760ca5d33129d2d799e34be5c4782eb - name: github.com/spf13/cobra diff --git a/vendor/github.com/beevik/etree/LICENSE b/vendor/github.com/beevik/etree/LICENSE new file mode 100644 index 00000000..e14ad682 --- /dev/null +++ b/vendor/github.com/beevik/etree/LICENSE @@ -0,0 +1,24 @@ +Copyright 2015 Brett Vickers. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER ``AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/beevik/etree/etree.go b/vendor/github.com/beevik/etree/etree.go new file mode 100644 index 00000000..21f83522 --- /dev/null +++ b/vendor/github.com/beevik/etree/etree.go @@ -0,0 +1,938 @@ +// Copyright 2015 Brett Vickers. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package etree provides XML services through an Element Tree +// abstraction. +package etree + +import ( + "bufio" + "bytes" + "encoding/xml" + "errors" + "io" + "os" + "strings" +) + +const ( + // NoIndent is used with Indent to disable all indenting. + NoIndent = -1 +) + +// ErrXML is returned when XML parsing fails due to incorrect formatting. +var ErrXML = errors.New("etree: invalid XML format") + +// ReadSettings allow for changing the default behavior of the ReadFrom* +// methods. +type ReadSettings struct { + // CharsetReader to be passed to standard xml.Decoder. Default: nil. + CharsetReader func(charset string, input io.Reader) (io.Reader, error) +} + +// newReadSettings creates a default ReadSettings record. +func newReadSettings() ReadSettings { + return ReadSettings{} +} + +// WriteSettings allow for changing the serialization behavior of the WriteTo* +// methods. +type WriteSettings struct { + // CanonicalEndTags forces the production of XML end tags, even for + // elements that have no child elements. Default: false. + CanonicalEndTags bool + + // CanonicalText forces the production of XML character references for + // text data characters &, <, and >. If false, XML character references + // are also produced for " and '. Default: false. + CanonicalText bool + + // CanonicalAttrVal forces the production of XML character references for + // attribute value characters &, < and ". If false, XML character + // references are also produced for > and '. Default: false. + CanonicalAttrVal bool +} + +// newWriteSettings creates a default WriteSettings record. +func newWriteSettings() WriteSettings { + return WriteSettings{ + CanonicalEndTags: false, + CanonicalText: false, + CanonicalAttrVal: false, + } +} + +// A Token is an empty interface that represents an Element, CharData, +// Comment, Directive, or ProcInst. +type Token interface { + Parent() *Element + dup(parent *Element) Token + setParent(parent *Element) + writeTo(w *bufio.Writer, s *WriteSettings) +} + +// A Document is a container holding a complete XML hierarchy. Its embedded +// element contains zero or more children, one of which is usually the root +// element. The embedded element may include other children such as +// processing instructions or BOM CharData tokens. +type Document struct { + Element + ReadSettings ReadSettings + WriteSettings WriteSettings +} + +// An Element represents an XML element, its attributes, and its child tokens. +type Element struct { + Space, Tag string // namespace and tag + Attr []Attr // key-value attribute pairs + Child []Token // child tokens (elements, comments, etc.) + parent *Element // parent element +} + +// An Attr represents a key-value attribute of an XML element. +type Attr struct { + Space, Key string // The attribute's namespace and key + Value string // The attribute value string +} + +// CharData represents character data within XML. +type CharData struct { + Data string + parent *Element + whitespace bool +} + +// A Comment represents an XML comment. +type Comment struct { + Data string + parent *Element +} + +// A Directive represents an XML directive. +type Directive struct { + Data string + parent *Element +} + +// A ProcInst represents an XML processing instruction. +type ProcInst struct { + Target string + Inst string + parent *Element +} + +// NewDocument creates an XML document without a root element. +func NewDocument() *Document { + return &Document{ + Element{Child: make([]Token, 0)}, + newReadSettings(), + newWriteSettings(), + } +} + +// Copy returns a recursive, deep copy of the document. +func (d *Document) Copy() *Document { + return &Document{*(d.dup(nil).(*Element)), d.ReadSettings, d.WriteSettings} +} + +// Root returns the root element of the document, or nil if there is no root +// element. +func (d *Document) Root() *Element { + for _, t := range d.Child { + if c, ok := t.(*Element); ok { + return c + } + } + return nil +} + +// SetRoot replaces the document's root element with e. If the document +// already has a root when this function is called, then the document's +// original root is unbound first. If the element e is bound to another +// document (or to another element within a document), then it is unbound +// first. +func (d *Document) SetRoot(e *Element) { + if e.parent != nil { + e.parent.RemoveChild(e) + } + e.setParent(&d.Element) + + for i, t := range d.Child { + if _, ok := t.(*Element); ok { + t.setParent(nil) + d.Child[i] = e + return + } + } + d.Child = append(d.Child, e) +} + +// ReadFrom reads XML from the reader r into the document d. It returns the +// number of bytes read and any error encountered. +func (d *Document) ReadFrom(r io.Reader) (n int64, err error) { + return d.Element.readFrom(r, d.ReadSettings.CharsetReader) +} + +// ReadFromFile reads XML from the string s into the document d. +func (d *Document) ReadFromFile(filename string) error { + f, err := os.Open(filename) + if err != nil { + return err + } + defer f.Close() + _, err = d.ReadFrom(f) + return err +} + +// ReadFromBytes reads XML from the byte slice b into the document d. +func (d *Document) ReadFromBytes(b []byte) error { + _, err := d.ReadFrom(bytes.NewReader(b)) + return err +} + +// ReadFromString reads XML from the string s into the document d. +func (d *Document) ReadFromString(s string) error { + _, err := d.ReadFrom(strings.NewReader(s)) + return err +} + +// WriteTo serializes an XML document into the writer w. It +// returns the number of bytes written and any error encountered. +func (d *Document) WriteTo(w io.Writer) (n int64, err error) { + cw := newCountWriter(w) + b := bufio.NewWriter(cw) + for _, c := range d.Child { + c.writeTo(b, &d.WriteSettings) + } + err, n = b.Flush(), cw.bytes + return +} + +// WriteToFile serializes an XML document into the file named +// filename. +func (d *Document) WriteToFile(filename string) error { + f, err := os.Create(filename) + if err != nil { + return err + } + defer f.Close() + _, err = d.WriteTo(f) + return err +} + +// WriteToBytes serializes the XML document into a slice of +// bytes. +func (d *Document) WriteToBytes() (b []byte, err error) { + var buf bytes.Buffer + if _, err = d.WriteTo(&buf); err != nil { + return + } + return buf.Bytes(), nil +} + +// WriteToString serializes the XML document into a string. +func (d *Document) WriteToString() (s string, err error) { + var b []byte + if b, err = d.WriteToBytes(); err != nil { + return + } + return string(b), nil +} + +type indentFunc func(depth int) string + +// Indent modifies the document's element tree by inserting CharData entities +// containing carriage returns and indentation. The amount of indentation per +// depth level is given as spaces. Pass etree.NoIndent for spaces if you want +// no indentation at all. +func (d *Document) Indent(spaces int) { + var indent indentFunc + switch { + case spaces < 0: + indent = func(depth int) string { return "" } + default: + indent = func(depth int) string { return crIndent(depth*spaces, crsp) } + } + d.Element.indent(0, indent) +} + +// IndentTabs modifies the document's element tree by inserting CharData +// entities containing carriage returns and tabs for indentation. One tab is +// used per indentation level. +func (d *Document) IndentTabs() { + indent := func(depth int) string { return crIndent(depth, crtab) } + d.Element.indent(0, indent) +} + +// NewElement creates an unparented element with the specified tag. The tag +// may be prefixed by a namespace and a colon. +func NewElement(tag string) *Element { + space, stag := spaceDecompose(tag) + return newElement(space, stag, nil) +} + +// newElement is a helper function that creates an element and binds it to +// a parent element if possible. +func newElement(space, tag string, parent *Element) *Element { + e := &Element{ + Space: space, + Tag: tag, + Attr: make([]Attr, 0), + Child: make([]Token, 0), + parent: parent, + } + if parent != nil { + parent.addChild(e) + } + return e +} + +// Copy creates a recursive, deep copy of the element and all its attributes +// and children. The returned element has no parent but can be parented to a +// another element using AddElement, or to a document using SetRoot. +func (e *Element) Copy() *Element { + var parent *Element + return e.dup(parent).(*Element) +} + +// Text returns the characters immediately following the element's +// opening tag. +func (e *Element) Text() string { + if len(e.Child) == 0 { + return "" + } + if cd, ok := e.Child[0].(*CharData); ok { + return cd.Data + } + return "" +} + +// SetText replaces an element's subsidiary CharData text with a new string. +func (e *Element) SetText(text string) { + if len(e.Child) > 0 { + if cd, ok := e.Child[0].(*CharData); ok { + cd.Data = text + return + } + } + cd := newCharData(text, false, e) + copy(e.Child[1:], e.Child[0:]) + e.Child[0] = cd +} + +// CreateElement creates an element with the specified tag and adds it as the +// last child element of the element e. The tag may be prefixed by a namespace +// and a colon. +func (e *Element) CreateElement(tag string) *Element { + space, stag := spaceDecompose(tag) + return newElement(space, stag, e) +} + +// AddChild adds the token t as the last child of element e. If token t was +// already the child of another element, it is first removed from its current +// parent element. +func (e *Element) AddChild(t Token) { + if t.Parent() != nil { + t.Parent().RemoveChild(t) + } + t.setParent(e) + e.addChild(t) +} + +// InsertChild inserts the token t before e's existing child token ex. If ex +// is nil (or if ex is not a child of e), then t is added to the end of e's +// child token list. If token t was already the child of another element, it +// is first removed from its current parent element. +func (e *Element) InsertChild(ex Token, t Token) { + if t.Parent() != nil { + t.Parent().RemoveChild(t) + } + t.setParent(e) + + for i, c := range e.Child { + if c == ex { + e.Child = append(e.Child, nil) + copy(e.Child[i+1:], e.Child[i:]) + e.Child[i] = t + return + } + } + e.addChild(t) +} + +// RemoveChild attempts to remove the token t from element e's list of +// children. If the token t is a child of e, then it is returned. Otherwise, +// nil is returned. +func (e *Element) RemoveChild(t Token) Token { + for i, c := range e.Child { + if c == t { + e.Child = append(e.Child[:i], e.Child[i+1:]...) + c.setParent(nil) + return t + } + } + return nil +} + +// ReadFrom reads XML from the reader r and stores the result as a new child +// of element e. +func (e *Element) readFrom(ri io.Reader, charsetReader func(charset string, input io.Reader) (io.Reader, error)) (n int64, err error) { + r := newCountReader(ri) + dec := xml.NewDecoder(r) + dec.CharsetReader = charsetReader + var stack stack + stack.push(e) + for { + t, err := dec.RawToken() + switch { + case err == io.EOF: + return r.bytes, nil + case err != nil: + return r.bytes, err + case stack.empty(): + return r.bytes, ErrXML + } + + top := stack.peek().(*Element) + + switch t := t.(type) { + case xml.StartElement: + e := newElement(t.Name.Space, t.Name.Local, top) + for _, a := range t.Attr { + e.createAttr(a.Name.Space, a.Name.Local, a.Value) + } + stack.push(e) + case xml.EndElement: + stack.pop() + case xml.CharData: + data := string(t) + newCharData(data, isWhitespace(data), top) + case xml.Comment: + newComment(string(t), top) + case xml.Directive: + newDirective(string(t), top) + case xml.ProcInst: + newProcInst(t.Target, string(t.Inst), top) + } + } +} + +// SelectAttr finds an element attribute matching the requested key and +// returns it if found. The key may be prefixed by a namespace and a colon. +func (e *Element) SelectAttr(key string) *Attr { + space, skey := spaceDecompose(key) + for i, a := range e.Attr { + if spaceMatch(space, a.Space) && skey == a.Key { + return &e.Attr[i] + } + } + return nil +} + +// SelectAttrValue finds an element attribute matching the requested key and +// returns its value if found. The key may be prefixed by a namespace and a +// colon. If the key is not found, the dflt value is returned instead. +func (e *Element) SelectAttrValue(key, dflt string) string { + space, skey := spaceDecompose(key) + for _, a := range e.Attr { + if spaceMatch(space, a.Space) && skey == a.Key { + return a.Value + } + } + return dflt +} + +// ChildElements returns all elements that are children of element e. +func (e *Element) ChildElements() []*Element { + var elements []*Element + for _, t := range e.Child { + if c, ok := t.(*Element); ok { + elements = append(elements, c) + } + } + return elements +} + +// SelectElement returns the first child element with the given tag. The tag +// may be prefixed by a namespace and a colon. +func (e *Element) SelectElement(tag string) *Element { + space, stag := spaceDecompose(tag) + for _, t := range e.Child { + if c, ok := t.(*Element); ok && spaceMatch(space, c.Space) && stag == c.Tag { + return c + } + } + return nil +} + +// SelectElements returns a slice of all child elements with the given tag. +// The tag may be prefixed by a namespace and a colon. +func (e *Element) SelectElements(tag string) []*Element { + space, stag := spaceDecompose(tag) + var elements []*Element + for _, t := range e.Child { + if c, ok := t.(*Element); ok && spaceMatch(space, c.Space) && stag == c.Tag { + elements = append(elements, c) + } + } + return elements +} + +// FindElement returns the first element matched by the XPath-like path +// string. Panics if an invalid path string is supplied. +func (e *Element) FindElement(path string) *Element { + return e.FindElementPath(MustCompilePath(path)) +} + +// FindElementPath returns the first element matched by the XPath-like path +// string. +func (e *Element) FindElementPath(path Path) *Element { + p := newPather() + elements := p.traverse(e, path) + switch { + case len(elements) > 0: + return elements[0] + default: + return nil + } +} + +// FindElements returns a slice of elements matched by the XPath-like path +// string. Panics if an invalid path string is supplied. +func (e *Element) FindElements(path string) []*Element { + return e.FindElementsPath(MustCompilePath(path)) +} + +// FindElementsPath returns a slice of elements matched by the Path object. +func (e *Element) FindElementsPath(path Path) []*Element { + p := newPather() + return p.traverse(e, path) +} + +// indent recursively inserts proper indentation between an +// XML element's child tokens. +func (e *Element) indent(depth int, indent indentFunc) { + e.stripIndent() + n := len(e.Child) + if n == 0 { + return + } + + oldChild := e.Child + e.Child = make([]Token, 0, n*2+1) + isCharData, firstNonCharData := false, true + for _, c := range oldChild { + + // Insert CR+indent before child if it's not character data. + // Exceptions: when it's the first non-character-data child, or when + // the child is at root depth. + _, isCharData = c.(*CharData) + if !isCharData { + if !firstNonCharData || depth > 0 { + newCharData(indent(depth), true, e) + } + firstNonCharData = false + } + + e.addChild(c) + + // Recursively process child elements. + if ce, ok := c.(*Element); ok { + ce.indent(depth+1, indent) + } + } + + // Insert CR+indent before the last child. + if !isCharData { + if !firstNonCharData || depth > 0 { + newCharData(indent(depth-1), true, e) + } + } +} + +// stripIndent removes any previously inserted indentation. +func (e *Element) stripIndent() { + // Count the number of non-indent child tokens + n := len(e.Child) + for _, c := range e.Child { + if cd, ok := c.(*CharData); ok && cd.whitespace { + n-- + } + } + if n == len(e.Child) { + return + } + + // Strip out indent CharData + newChild := make([]Token, n) + j := 0 + for _, c := range e.Child { + if cd, ok := c.(*CharData); ok && cd.whitespace { + continue + } + newChild[j] = c + j++ + } + e.Child = newChild +} + +// dup duplicates the element. +func (e *Element) dup(parent *Element) Token { + ne := &Element{ + Space: e.Space, + Tag: e.Tag, + Attr: make([]Attr, len(e.Attr)), + Child: make([]Token, len(e.Child)), + parent: parent, + } + for i, t := range e.Child { + ne.Child[i] = t.dup(ne) + } + for i, a := range e.Attr { + ne.Attr[i] = a + } + return ne +} + +// Parent returns the element token's parent element, or nil if it has no +// parent. +func (e *Element) Parent() *Element { + return e.parent +} + +// setParent replaces the element token's parent. +func (e *Element) setParent(parent *Element) { + e.parent = parent +} + +// writeTo serializes the element to the writer w. +func (e *Element) writeTo(w *bufio.Writer, s *WriteSettings) { + w.WriteByte('<') + if e.Space != "" { + w.WriteString(e.Space) + w.WriteByte(':') + } + w.WriteString(e.Tag) + for _, a := range e.Attr { + w.WriteByte(' ') + a.writeTo(w, s) + } + if len(e.Child) > 0 { + w.WriteString(">") + for _, c := range e.Child { + c.writeTo(w, s) + } + w.Write([]byte{'<', '/'}) + if e.Space != "" { + w.WriteString(e.Space) + w.WriteByte(':') + } + w.WriteString(e.Tag) + w.WriteByte('>') + } else { + if s.CanonicalEndTags { + w.Write([]byte{'>', '<', '/'}) + if e.Space != "" { + w.WriteString(e.Space) + w.WriteByte(':') + } + w.WriteString(e.Tag) + w.WriteByte('>') + } else { + w.Write([]byte{'/', '>'}) + } + } +} + +// addChild adds a child token to the element e. +func (e *Element) addChild(t Token) { + e.Child = append(e.Child, t) +} + +// CreateAttr creates an attribute and adds it to element e. The key may be +// prefixed by a namespace and a colon. If an attribute with the key already +// exists, its value is replaced. +func (e *Element) CreateAttr(key, value string) *Attr { + space, skey := spaceDecompose(key) + return e.createAttr(space, skey, value) +} + +// createAttr is a helper function that creates attributes. +func (e *Element) createAttr(space, key, value string) *Attr { + for i, a := range e.Attr { + if space == a.Space && key == a.Key { + e.Attr[i].Value = value + return &e.Attr[i] + } + } + a := Attr{space, key, value} + e.Attr = append(e.Attr, a) + return &e.Attr[len(e.Attr)-1] +} + +// RemoveAttr removes and returns the first attribute of the element whose key +// matches the given key. The key may be prefixed by a namespace and a colon. +// If an equal attribute does not exist, nil is returned. +func (e *Element) RemoveAttr(key string) *Attr { + space, skey := spaceDecompose(key) + for i, a := range e.Attr { + if space == a.Space && skey == a.Key { + e.Attr = append(e.Attr[0:i], e.Attr[i+1:]...) + return &a + } + } + return nil +} + +var xmlReplacerNormal = strings.NewReplacer( + "&", "&", + "<", "<", + ">", ">", + "'", "'", + `"`, """, +) + +var xmlReplacerCanonicalText = strings.NewReplacer( + "&", "&", + "<", "<", + ">", ">", + "\r", " ", +) + +var xmlReplacerCanonicalAttrVal = strings.NewReplacer( + "&", "&", + "<", "<", + `"`, """, + "\t", " ", + "\n", " ", + "\r", " ", +) + +// writeTo serializes the attribute to the writer. +func (a *Attr) writeTo(w *bufio.Writer, s *WriteSettings) { + if a.Space != "" { + w.WriteString(a.Space) + w.WriteByte(':') + } + w.WriteString(a.Key) + w.WriteString(`="`) + var r *strings.Replacer + if s.CanonicalAttrVal { + r = xmlReplacerCanonicalAttrVal + } else { + r = xmlReplacerNormal + } + w.WriteString(r.Replace(a.Value)) + w.WriteByte('"') +} + +// NewCharData creates a parentless XML character data entity. +func NewCharData(data string) *CharData { + return newCharData(data, false, nil) +} + +// newCharData creates an XML character data entity and binds it to a parent +// element. If parent is nil, the CharData token remains unbound. +func newCharData(data string, whitespace bool, parent *Element) *CharData { + c := &CharData{ + Data: data, + whitespace: whitespace, + parent: parent, + } + if parent != nil { + parent.addChild(c) + } + return c +} + +// CreateCharData creates an XML character data entity and adds it as a child +// of element e. +func (e *Element) CreateCharData(data string) *CharData { + return newCharData(data, false, e) +} + +// dup duplicates the character data. +func (c *CharData) dup(parent *Element) Token { + return &CharData{ + Data: c.Data, + whitespace: c.whitespace, + parent: parent, + } +} + +// Parent returns the character data token's parent element, or nil if it has +// no parent. +func (c *CharData) Parent() *Element { + return c.parent +} + +// setParent replaces the character data token's parent. +func (c *CharData) setParent(parent *Element) { + c.parent = parent +} + +// writeTo serializes the character data entity to the writer. +func (c *CharData) writeTo(w *bufio.Writer, s *WriteSettings) { + var r *strings.Replacer + if s.CanonicalText { + r = xmlReplacerCanonicalText + } else { + r = xmlReplacerNormal + } + w.WriteString(r.Replace(c.Data)) +} + +// NewComment creates a parentless XML comment. +func NewComment(comment string) *Comment { + return newComment(comment, nil) +} + +// NewComment creates an XML comment and binds it to a parent element. If +// parent is nil, the Comment remains unbound. +func newComment(comment string, parent *Element) *Comment { + c := &Comment{ + Data: comment, + parent: parent, + } + if parent != nil { + parent.addChild(c) + } + return c +} + +// CreateComment creates an XML comment and adds it as a child of element e. +func (e *Element) CreateComment(comment string) *Comment { + return newComment(comment, e) +} + +// dup duplicates the comment. +func (c *Comment) dup(parent *Element) Token { + return &Comment{ + Data: c.Data, + parent: parent, + } +} + +// Parent returns comment token's parent element, or nil if it has no parent. +func (c *Comment) Parent() *Element { + return c.parent +} + +// setParent replaces the comment token's parent. +func (c *Comment) setParent(parent *Element) { + c.parent = parent +} + +// writeTo serialies the comment to the writer. +func (c *Comment) writeTo(w *bufio.Writer, s *WriteSettings) { + w.WriteString("") +} + +// NewDirective creates a parentless XML directive. +func NewDirective(data string) *Directive { + return newDirective(data, nil) +} + +// newDirective creates an XML directive and binds it to a parent element. If +// parent is nil, the Directive remains unbound. +func newDirective(data string, parent *Element) *Directive { + d := &Directive{ + Data: data, + parent: parent, + } + if parent != nil { + parent.addChild(d) + } + return d +} + +// CreateDirective creates an XML directive and adds it as the last child of +// element e. +func (e *Element) CreateDirective(data string) *Directive { + return newDirective(data, e) +} + +// dup duplicates the directive. +func (d *Directive) dup(parent *Element) Token { + return &Directive{ + Data: d.Data, + parent: parent, + } +} + +// Parent returns directive token's parent element, or nil if it has no +// parent. +func (d *Directive) Parent() *Element { + return d.parent +} + +// setParent replaces the directive token's parent. +func (d *Directive) setParent(parent *Element) { + d.parent = parent +} + +// writeTo serializes the XML directive to the writer. +func (d *Directive) writeTo(w *bufio.Writer, s *WriteSettings) { + w.WriteString("") +} + +// NewProcInst creates a parentless XML processing instruction. +func NewProcInst(target, inst string) *ProcInst { + return newProcInst(target, inst, nil) +} + +// newProcInst creates an XML processing instruction and binds it to a parent +// element. If parent is nil, the ProcInst remains unbound. +func newProcInst(target, inst string, parent *Element) *ProcInst { + p := &ProcInst{ + Target: target, + Inst: inst, + parent: parent, + } + if parent != nil { + parent.addChild(p) + } + return p +} + +// CreateProcInst creates a processing instruction and adds it as a child of +// element e. +func (e *Element) CreateProcInst(target, inst string) *ProcInst { + return newProcInst(target, inst, e) +} + +// dup duplicates the procinst. +func (p *ProcInst) dup(parent *Element) Token { + return &ProcInst{ + Target: p.Target, + Inst: p.Inst, + parent: parent, + } +} + +// Parent returns processing instruction token's parent element, or nil if it +// has no parent. +func (p *ProcInst) Parent() *Element { + return p.parent +} + +// setParent replaces the processing instruction token's parent. +func (p *ProcInst) setParent(parent *Element) { + p.parent = parent +} + +// writeTo serializes the processing instruction to the writer. +func (p *ProcInst) writeTo(w *bufio.Writer, s *WriteSettings) { + w.WriteString("") +} diff --git a/vendor/github.com/beevik/etree/helpers.go b/vendor/github.com/beevik/etree/helpers.go new file mode 100644 index 00000000..4f8350e7 --- /dev/null +++ b/vendor/github.com/beevik/etree/helpers.go @@ -0,0 +1,188 @@ +// Copyright 2015 Brett Vickers. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package etree + +import ( + "io" + "strings" +) + +// A simple stack +type stack struct { + data []interface{} +} + +func (s *stack) empty() bool { + return len(s.data) == 0 +} + +func (s *stack) push(value interface{}) { + s.data = append(s.data, value) +} + +func (s *stack) pop() interface{} { + value := s.data[len(s.data)-1] + s.data[len(s.data)-1] = nil + s.data = s.data[:len(s.data)-1] + return value +} + +func (s *stack) peek() interface{} { + return s.data[len(s.data)-1] +} + +// A fifo is a simple first-in-first-out queue. +type fifo struct { + data []interface{} + head, tail int +} + +func (f *fifo) add(value interface{}) { + if f.len()+1 >= len(f.data) { + f.grow() + } + f.data[f.tail] = value + if f.tail++; f.tail == len(f.data) { + f.tail = 0 + } +} + +func (f *fifo) remove() interface{} { + value := f.data[f.head] + f.data[f.head] = nil + if f.head++; f.head == len(f.data) { + f.head = 0 + } + return value +} + +func (f *fifo) len() int { + if f.tail >= f.head { + return f.tail - f.head + } + return len(f.data) - f.head + f.tail +} + +func (f *fifo) grow() { + c := len(f.data) * 2 + if c == 0 { + c = 4 + } + buf, count := make([]interface{}, c), f.len() + if f.tail >= f.head { + copy(buf[0:count], f.data[f.head:f.tail]) + } else { + hindex := len(f.data) - f.head + copy(buf[0:hindex], f.data[f.head:]) + copy(buf[hindex:count], f.data[:f.tail]) + } + f.data, f.head, f.tail = buf, 0, count +} + +// countReader implements a proxy reader that counts the number of +// bytes read from its encapsulated reader. +type countReader struct { + r io.Reader + bytes int64 +} + +func newCountReader(r io.Reader) *countReader { + return &countReader{r: r} +} + +func (cr *countReader) Read(p []byte) (n int, err error) { + b, err := cr.r.Read(p) + cr.bytes += int64(b) + return b, err +} + +// countWriter implements a proxy writer that counts the number of +// bytes written by its encapsulated writer. +type countWriter struct { + w io.Writer + bytes int64 +} + +func newCountWriter(w io.Writer) *countWriter { + return &countWriter{w: w} +} + +func (cw *countWriter) Write(p []byte) (n int, err error) { + b, err := cw.w.Write(p) + cw.bytes += int64(b) + return b, err +} + +// isWhitespace returns true if the byte slice contains only +// whitespace characters. +func isWhitespace(s string) bool { + for i := 0; i < len(s); i++ { + if c := s[i]; c != ' ' && c != '\t' && c != '\n' && c != '\r' { + return false + } + } + return true +} + +// spaceMatch returns true if namespace a is the empty string +// or if namespace a equals namespace b. +func spaceMatch(a, b string) bool { + switch { + case a == "": + return true + default: + return a == b + } +} + +// spaceDecompose breaks a namespace:tag identifier at the ':' +// and returns the two parts. +func spaceDecompose(str string) (space, key string) { + colon := strings.IndexByte(str, ':') + if colon == -1 { + return "", str + } + return str[:colon], str[colon+1:] +} + +// Strings used by crIndent +const ( + crsp = "\n " + crtab = "\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t" +) + +// crIndent returns a carriage return followed by n copies of the +// first non-CR character in the source string. +func crIndent(n int, source string) string { + switch { + case n < 0: + return source[:1] + case n < len(source): + return source[:n+1] + default: + return source + strings.Repeat(source[1:2], n-len(source)+1) + } +} + +// nextIndex returns the index of the next occurrence of sep in s, +// starting from offset. It returns -1 if the sep string is not found. +func nextIndex(s, sep string, offset int) int { + switch i := strings.Index(s[offset:], sep); i { + case -1: + return -1 + default: + return offset + i + } +} + +// isInteger returns true if the string s contains an integer. +func isInteger(s string) bool { + for i := 0; i < len(s); i++ { + if (s[i] < '0' || s[i] > '9') && !(i == 0 && s[i] == '-') { + return false + } + } + return true +} diff --git a/vendor/github.com/beevik/etree/path.go b/vendor/github.com/beevik/etree/path.go new file mode 100644 index 00000000..126eb154 --- /dev/null +++ b/vendor/github.com/beevik/etree/path.go @@ -0,0 +1,470 @@ +// Copyright 2015 Brett Vickers. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package etree + +import ( + "strconv" + "strings" +) + +/* +A Path is an object that represents an optimized version of an +XPath-like search string. Although path strings are XPath-like, +only the following limited syntax is supported: + + . Selects the current element + .. Selects the parent of the current element + * Selects all child elements + // Selects all descendants of the current element + tag Selects all child elements with the given tag + [#] Selects the element of the given index (1-based, + negative starts from the end) + [@attrib] Selects all elements with the given attribute + [@attrib='val'] Selects all elements with the given attribute set to val + [tag] Selects all elements with a child element named tag + [tag='val'] Selects all elements with a child element named tag + and text equal to val + +Examples: + +Select the title elements of all descendant book elements having a +'category' attribute of 'WEB': + //book[@category='WEB']/title + +Select the first book element with a title child containing the text +'Great Expectations': + .//book[title='Great Expectations'][1] + +Starting from the current element, select all children of book elements +with an attribute 'language' set to 'english': + ./book/*[@language='english'] + +Select all descendant book elements whose title element has an attribute +'language' set to 'french': + //book/title[@language='french']/.. +*/ +type Path struct { + segments []segment +} + +// ErrPath is returned by path functions when an invalid etree path is provided. +type ErrPath string + +// Error returns the string describing a path error. +func (err ErrPath) Error() string { + return "etree: " + string(err) +} + +// CompilePath creates an optimized version of an XPath-like string that +// can be used to query elements in an element tree. +func CompilePath(path string) (Path, error) { + var comp compiler + segments := comp.parsePath(path) + if comp.err != ErrPath("") { + return Path{nil}, comp.err + } + return Path{segments}, nil +} + +// MustCompilePath creates an optimized version of an XPath-like string that +// can be used to query elements in an element tree. Panics if an error +// occurs. Use this function to create Paths when you know the path is +// valid (i.e., if it's hard-coded). +func MustCompilePath(path string) Path { + p, err := CompilePath(path) + if err != nil { + panic(err) + } + return p +} + +// A segment is a portion of a path between "/" characters. +// It contains one selector and zero or more [filters]. +type segment struct { + sel selector + filters []filter +} + +func (seg *segment) apply(e *Element, p *pather) { + seg.sel.apply(e, p) + for _, f := range seg.filters { + f.apply(p) + } +} + +// A selector selects XML elements for consideration by the +// path traversal. +type selector interface { + apply(e *Element, p *pather) +} + +// A filter pares down a list of candidate XML elements based +// on a path filter in [brackets]. +type filter interface { + apply(p *pather) +} + +// A pather is helper object that traverses an element tree using +// a Path object. It collects and deduplicates all elements matching +// the path query. +type pather struct { + queue fifo + results []*Element + inResults map[*Element]bool + candidates []*Element + scratch []*Element // used by filters +} + +// A node represents an element and the remaining path segments that +// should be applied against it by the pather. +type node struct { + e *Element + segments []segment +} + +func newPather() *pather { + return &pather{ + results: make([]*Element, 0), + inResults: make(map[*Element]bool), + candidates: make([]*Element, 0), + scratch: make([]*Element, 0), + } +} + +// traverse follows the path from the element e, collecting +// and then returning all elements that match the path's selectors +// and filters. +func (p *pather) traverse(e *Element, path Path) []*Element { + for p.queue.add(node{e, path.segments}); p.queue.len() > 0; { + p.eval(p.queue.remove().(node)) + } + return p.results +} + +// eval evalutes the current path node by applying the remaining +// path's selector rules against the node's element. +func (p *pather) eval(n node) { + p.candidates = p.candidates[0:0] + seg, remain := n.segments[0], n.segments[1:] + seg.apply(n.e, p) + + if len(remain) == 0 { + for _, c := range p.candidates { + if in := p.inResults[c]; !in { + p.inResults[c] = true + p.results = append(p.results, c) + } + } + } else { + for _, c := range p.candidates { + p.queue.add(node{c, remain}) + } + } +} + +// A compiler generates a compiled path from a path string. +type compiler struct { + err ErrPath +} + +// parsePath parses an XPath-like string describing a path +// through an element tree and returns a slice of segment +// descriptors. +func (c *compiler) parsePath(path string) []segment { + // If path starts or ends with //, fix it + if strings.HasPrefix(path, "//") { + path = "." + path + } + if strings.HasSuffix(path, "//") { + path = path + "*" + } + + // Paths cannot be absolute + if strings.HasPrefix(path, "/") { + c.err = ErrPath("paths cannot be absolute.") + return nil + } + + // Split path into segment objects + var segments []segment + for _, s := range splitPath(path) { + segments = append(segments, c.parseSegment(s)) + if c.err != ErrPath("") { + break + } + } + return segments +} + +func splitPath(path string) []string { + pieces := make([]string, 0) + start := 0 + inquote := false + for i := 0; i+1 <= len(path); i++ { + if path[i] == '\'' { + inquote = !inquote + } else if path[i] == '/' && !inquote { + pieces = append(pieces, path[start:i]) + start = i + 1 + } + } + return append(pieces, path[start:]) +} + +// parseSegment parses a path segment between / characters. +func (c *compiler) parseSegment(path string) segment { + pieces := strings.Split(path, "[") + seg := segment{ + sel: c.parseSelector(pieces[0]), + filters: make([]filter, 0), + } + for i := 1; i < len(pieces); i++ { + fpath := pieces[i] + if fpath[len(fpath)-1] != ']' { + c.err = ErrPath("path has invalid filter [brackets].") + break + } + seg.filters = append(seg.filters, c.parseFilter(fpath[:len(fpath)-1])) + } + return seg +} + +// parseSelector parses a selector at the start of a path segment. +func (c *compiler) parseSelector(path string) selector { + switch path { + case ".": + return new(selectSelf) + case "..": + return new(selectParent) + case "*": + return new(selectChildren) + case "": + return new(selectDescendants) + default: + return newSelectChildrenByTag(path) + } +} + +// parseFilter parses a path filter contained within [brackets]. +func (c *compiler) parseFilter(path string) filter { + if len(path) == 0 { + c.err = ErrPath("path contains an empty filter expression.") + return nil + } + + // Filter contains [@attr='val'] or [tag='val']? + eqindex := strings.Index(path, "='") + if eqindex >= 0 { + rindex := nextIndex(path, "'", eqindex+2) + if rindex != len(path)-1 { + c.err = ErrPath("path has mismatched filter quotes.") + return nil + } + switch { + case path[0] == '@': + return newFilterAttrVal(path[1:eqindex], path[eqindex+2:rindex]) + default: + return newFilterChildText(path[:eqindex], path[eqindex+2:rindex]) + } + } + + // Filter contains [@attr], [N] or [tag] + switch { + case path[0] == '@': + return newFilterAttr(path[1:]) + case isInteger(path): + pos, _ := strconv.Atoi(path) + switch { + case pos > 0: + return newFilterPos(pos - 1) + default: + return newFilterPos(pos) + } + default: + return newFilterChild(path) + } +} + +// selectSelf selects the current element into the candidate list. +type selectSelf struct{} + +func (s *selectSelf) apply(e *Element, p *pather) { + p.candidates = append(p.candidates, e) +} + +// selectParent selects the element's parent into the candidate list. +type selectParent struct{} + +func (s *selectParent) apply(e *Element, p *pather) { + if e.parent != nil { + p.candidates = append(p.candidates, e.parent) + } +} + +// selectChildren selects the element's child elements into the +// candidate list. +type selectChildren struct{} + +func (s *selectChildren) apply(e *Element, p *pather) { + for _, c := range e.Child { + if c, ok := c.(*Element); ok { + p.candidates = append(p.candidates, c) + } + } +} + +// selectDescendants selects all descendant child elements +// of the element into the candidate list. +type selectDescendants struct{} + +func (s *selectDescendants) apply(e *Element, p *pather) { + var queue fifo + for queue.add(e); queue.len() > 0; { + e := queue.remove().(*Element) + p.candidates = append(p.candidates, e) + for _, c := range e.Child { + if c, ok := c.(*Element); ok { + queue.add(c) + } + } + } +} + +// selectChildrenByTag selects into the candidate list all child +// elements of the element having the specified tag. +type selectChildrenByTag struct { + space, tag string +} + +func newSelectChildrenByTag(path string) *selectChildrenByTag { + s, l := spaceDecompose(path) + return &selectChildrenByTag{s, l} +} + +func (s *selectChildrenByTag) apply(e *Element, p *pather) { + for _, c := range e.Child { + if c, ok := c.(*Element); ok && spaceMatch(s.space, c.Space) && s.tag == c.Tag { + p.candidates = append(p.candidates, c) + } + } +} + +// filterPos filters the candidate list, keeping only the +// candidate at the specified index. +type filterPos struct { + index int +} + +func newFilterPos(pos int) *filterPos { + return &filterPos{pos} +} + +func (f *filterPos) apply(p *pather) { + if f.index >= 0 { + if f.index < len(p.candidates) { + p.scratch = append(p.scratch, p.candidates[f.index]) + } + } else { + if -f.index <= len(p.candidates) { + p.scratch = append(p.scratch, p.candidates[len(p.candidates)+f.index]) + } + } + p.candidates, p.scratch = p.scratch, p.candidates[0:0] +} + +// filterAttr filters the candidate list for elements having +// the specified attribute. +type filterAttr struct { + space, key string +} + +func newFilterAttr(str string) *filterAttr { + s, l := spaceDecompose(str) + return &filterAttr{s, l} +} + +func (f *filterAttr) apply(p *pather) { + for _, c := range p.candidates { + for _, a := range c.Attr { + if spaceMatch(f.space, a.Space) && f.key == a.Key { + p.scratch = append(p.scratch, c) + break + } + } + } + p.candidates, p.scratch = p.scratch, p.candidates[0:0] +} + +// filterAttrVal filters the candidate list for elements having +// the specified attribute with the specified value. +type filterAttrVal struct { + space, key, val string +} + +func newFilterAttrVal(str, value string) *filterAttrVal { + s, l := spaceDecompose(str) + return &filterAttrVal{s, l, value} +} + +func (f *filterAttrVal) apply(p *pather) { + for _, c := range p.candidates { + for _, a := range c.Attr { + if spaceMatch(f.space, a.Space) && f.key == a.Key && f.val == a.Value { + p.scratch = append(p.scratch, c) + break + } + } + } + p.candidates, p.scratch = p.scratch, p.candidates[0:0] +} + +// filterChild filters the candidate list for elements having +// a child element with the specified tag. +type filterChild struct { + space, tag string +} + +func newFilterChild(str string) *filterChild { + s, l := spaceDecompose(str) + return &filterChild{s, l} +} + +func (f *filterChild) apply(p *pather) { + for _, c := range p.candidates { + for _, cc := range c.Child { + if cc, ok := cc.(*Element); ok && + spaceMatch(f.space, cc.Space) && + f.tag == cc.Tag { + p.scratch = append(p.scratch, c) + } + } + } + p.candidates, p.scratch = p.scratch, p.candidates[0:0] +} + +// filterChildText filters the candidate list for elements having +// a child element with the specified tag and text. +type filterChildText struct { + space, tag, text string +} + +func newFilterChildText(str, text string) *filterChildText { + s, l := spaceDecompose(str) + return &filterChildText{s, l, text} +} + +func (f *filterChildText) apply(p *pather) { + for _, c := range p.candidates { + for _, cc := range c.Child { + if cc, ok := cc.(*Element); ok && + spaceMatch(f.space, cc.Space) && + f.tag == cc.Tag && + f.text == cc.Text() { + p.scratch = append(p.scratch, c) + } + } + } + p.candidates, p.scratch = p.scratch, p.candidates[0:0] +} diff --git a/vendor/github.com/jonboulle/clockwork/LICENSE b/vendor/github.com/jonboulle/clockwork/LICENSE new file mode 100644 index 00000000..5c304d1a --- /dev/null +++ b/vendor/github.com/jonboulle/clockwork/LICENSE @@ -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/jonboulle/clockwork/clockwork.go b/vendor/github.com/jonboulle/clockwork/clockwork.go new file mode 100644 index 00000000..999fddd5 --- /dev/null +++ b/vendor/github.com/jonboulle/clockwork/clockwork.go @@ -0,0 +1,179 @@ +package clockwork + +import ( + "sync" + "time" +) + +// Clock provides an interface that packages can use instead of directly +// using the time module, so that chronology-related behavior can be tested +type Clock interface { + After(d time.Duration) <-chan time.Time + Sleep(d time.Duration) + Now() time.Time + Since(t time.Time) time.Duration +} + +// FakeClock provides an interface for a clock which can be +// manually advanced through time +type FakeClock interface { + Clock + // Advance advances the FakeClock to a new point in time, ensuring any existing + // sleepers are notified appropriately before returning + Advance(d time.Duration) + // BlockUntil will block until the FakeClock has the given number of + // sleepers (callers of Sleep or After) + BlockUntil(n int) +} + +// NewRealClock returns a Clock which simply delegates calls to the actual time +// package; it should be used by packages in production. +func NewRealClock() Clock { + return &realClock{} +} + +// NewFakeClock returns a FakeClock implementation which can be +// manually advanced through time for testing. The initial time of the +// FakeClock will be an arbitrary non-zero time. +func NewFakeClock() FakeClock { + // use a fixture that does not fulfill Time.IsZero() + return NewFakeClockAt(time.Date(1984, time.April, 4, 0, 0, 0, 0, time.UTC)) +} + +// NewFakeClockAt returns a FakeClock initialised at the given time.Time. +func NewFakeClockAt(t time.Time) FakeClock { + return &fakeClock{ + time: t, + } +} + +type realClock struct{} + +func (rc *realClock) After(d time.Duration) <-chan time.Time { + return time.After(d) +} + +func (rc *realClock) Sleep(d time.Duration) { + time.Sleep(d) +} + +func (rc *realClock) Now() time.Time { + return time.Now() +} + +func (rc *realClock) Since(t time.Time) time.Duration { + return rc.Now().Sub(t) +} + +type fakeClock struct { + sleepers []*sleeper + blockers []*blocker + time time.Time + + l sync.RWMutex +} + +// sleeper represents a caller of After or Sleep +type sleeper struct { + until time.Time + done chan time.Time +} + +// blocker represents a caller of BlockUntil +type blocker struct { + count int + ch chan struct{} +} + +// After mimics time.After; it waits for the given duration to elapse on the +// fakeClock, then sends the current time on the returned channel. +func (fc *fakeClock) After(d time.Duration) <-chan time.Time { + fc.l.Lock() + defer fc.l.Unlock() + now := fc.time + done := make(chan time.Time, 1) + if d.Nanoseconds() == 0 { + // special case - trigger immediately + done <- now + } else { + // otherwise, add to the set of sleepers + s := &sleeper{ + until: now.Add(d), + done: done, + } + fc.sleepers = append(fc.sleepers, s) + // and notify any blockers + fc.blockers = notifyBlockers(fc.blockers, len(fc.sleepers)) + } + return done +} + +// notifyBlockers notifies all the blockers waiting until the +// given number of sleepers are waiting on the fakeClock. It +// returns an updated slice of blockers (i.e. those still waiting) +func notifyBlockers(blockers []*blocker, count int) (newBlockers []*blocker) { + for _, b := range blockers { + if b.count == count { + close(b.ch) + } else { + newBlockers = append(newBlockers, b) + } + } + return +} + +// Sleep blocks until the given duration has passed on the fakeClock +func (fc *fakeClock) Sleep(d time.Duration) { + <-fc.After(d) +} + +// Time returns the current time of the fakeClock +func (fc *fakeClock) Now() time.Time { + fc.l.RLock() + t := fc.time + fc.l.RUnlock() + return t +} + +// Since returns the duration that has passed since the given time on the fakeClock +func (fc *fakeClock) Since(t time.Time) time.Duration { + return fc.Now().Sub(t) +} + +// Advance advances fakeClock to a new point in time, ensuring channels from any +// previous invocations of After are notified appropriately before returning +func (fc *fakeClock) Advance(d time.Duration) { + fc.l.Lock() + defer fc.l.Unlock() + end := fc.time.Add(d) + var newSleepers []*sleeper + for _, s := range fc.sleepers { + if end.Sub(s.until) >= 0 { + s.done <- end + } else { + newSleepers = append(newSleepers, s) + } + } + fc.sleepers = newSleepers + fc.blockers = notifyBlockers(fc.blockers, len(fc.sleepers)) + fc.time = end +} + +// BlockUntil will block until the fakeClock has the given number of sleepers +// (callers of Sleep or After) +func (fc *fakeClock) BlockUntil(n int) { + fc.l.Lock() + // Fast path: current number of sleepers is what we're looking for + if len(fc.sleepers) == n { + fc.l.Unlock() + return + } + // Otherwise, set up a new blocker + b := &blocker{ + count: n, + ch: make(chan struct{}), + } + fc.blockers = append(fc.blockers, b) + fc.l.Unlock() + <-b.ch +} diff --git a/vendor/github.com/russellhaering/goxmldsig/LICENSE b/vendor/github.com/russellhaering/goxmldsig/LICENSE new file mode 100644 index 00000000..67db8588 --- /dev/null +++ b/vendor/github.com/russellhaering/goxmldsig/LICENSE @@ -0,0 +1,175 @@ + + 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. diff --git a/vendor/github.com/russellhaering/goxmldsig/canonicalize.go b/vendor/github.com/russellhaering/goxmldsig/canonicalize.go new file mode 100644 index 00000000..7488ef5a --- /dev/null +++ b/vendor/github.com/russellhaering/goxmldsig/canonicalize.go @@ -0,0 +1,251 @@ +package dsig + +import ( + "sort" + "strings" + + "github.com/beevik/etree" +) + +// Canonicalizer is an implementation of a canonicalization algorithm. +type Canonicalizer interface { + Canonicalize(el *etree.Element) ([]byte, error) + Algorithm() AlgorithmID +} + +type c14N10ExclusiveCanonicalizer struct { + InclusiveNamespaces map[string]struct{} +} + +// MakeC14N10ExclusiveCanonicalizerWithPrefixList constructs an exclusive Canonicalizer +// from a PrefixList in NMTOKENS format (a white space separated list). +func MakeC14N10ExclusiveCanonicalizerWithPrefixList(prefixList string) Canonicalizer { + prefixes := strings.Fields(prefixList) + prefixSet := make(map[string]struct{}, len(prefixes)) + + for _, prefix := range prefixes { + prefixSet[prefix] = struct{}{} + } + + return &c14N10ExclusiveCanonicalizer{ + InclusiveNamespaces: prefixSet, + } +} + +// Canonicalize transforms the input Element into a serialized XML document in canonical form. +func (c *c14N10ExclusiveCanonicalizer) Canonicalize(el *etree.Element) ([]byte, error) { + scope := make(map[string]c14nSpace) + return canonicalSerialize(excCanonicalPrep(el, scope, c.InclusiveNamespaces)) +} + +func (c *c14N10ExclusiveCanonicalizer) Algorithm() AlgorithmID { + return CanonicalXML10ExclusiveAlgorithmId +} + +type c14N11Canonicalizer struct{} + +// MakeC14N11Canonicalizer constructs an inclusive canonicalizer. +func MakeC14N11Canonicalizer() Canonicalizer { + return &c14N11Canonicalizer{} +} + +// Canonicalize transforms the input Element into a serialized XML document in canonical form. +func (c *c14N11Canonicalizer) Canonicalize(el *etree.Element) ([]byte, error) { + scope := make(map[string]struct{}) + return canonicalSerialize(canonicalPrep(el, scope)) +} + +func (c *c14N11Canonicalizer) Algorithm() AlgorithmID { + return CanonicalXML11AlgorithmId +} + +func composeAttr(space, key string) string { + if space != "" { + return space + ":" + key + } + + return key +} + +type attrsByKey []etree.Attr + +func (a attrsByKey) Len() int { + return len(a) +} + +func (a attrsByKey) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} + +func (a attrsByKey) Less(i, j int) bool { + // As I understand it: any "xmlns" attribute should come first, followed by any + // any "xmlns:prefix" attributes, presumably ordered by prefix. Lastly any other + // attributes in lexicographical order. + if a[i].Space == "" && a[i].Key == "xmlns" { + return true + } + + if a[i].Space == "xmlns" { + if a[j].Space == "xmlns" { + return a[i].Key < a[j].Key + } + return true + } + + if a[j].Space == "xmlns" { + return false + } + + return composeAttr(a[i].Space, a[i].Key) < composeAttr(a[j].Space, a[j].Key) +} + +type c14nSpace struct { + a etree.Attr + used bool +} + +const nsSpace = "xmlns" + +// excCanonicalPrep accepts an *etree.Element and recursively transforms it into one +// which is ready for serialization to exclusive canonical form. Specifically this +// entails: +// +// 1. Stripping re-declarations of namespaces +// 2. Stripping unused namespaces +// 3. Sorting attributes into canonical order. +// +// NOTE(russell_h): Currently this function modifies the passed element. +func excCanonicalPrep(el *etree.Element, _nsAlreadyDeclared map[string]c14nSpace, inclusiveNamespaces map[string]struct{}) *etree.Element { + //Copy alreadyDeclared map (only contains namespaces) + nsAlreadyDeclared := make(map[string]c14nSpace, len(_nsAlreadyDeclared)) + for k := range _nsAlreadyDeclared { + nsAlreadyDeclared[k] = _nsAlreadyDeclared[k] + } + + //Track the namespaces used on the current element + nsUsedHere := make(map[string]struct{}) + + //Make sure to track the element namespace for the case: + // + if el.Space != "" { + nsUsedHere[el.Space] = struct{}{} + } + + toRemove := make([]string, 0, 0) + + for _, a := range el.Attr { + switch a.Space { + case nsSpace: + + //For simplicity, remove all xmlns attribues; to be added in one pass + //later. Otherwise, we need another map/set to track xmlns attributes + //that we left alone. + toRemove = append(toRemove, a.Space+":"+a.Key) + if _, ok := nsAlreadyDeclared[a.Key]; !ok { + //If we're not tracking ancestor state already for this namespace, add + //it to the map + nsAlreadyDeclared[a.Key] = c14nSpace{a: a, used: false} + } + + // This algorithm accepts a set of namespaces which should be treated + // in an inclusive fashion. Specifically that means we should keep the + // declaration of that namespace closest to the root of the tree. We can + // accomplish that be pretending it was used by this element. + _, inclusive := inclusiveNamespaces[a.Key] + if inclusive { + nsUsedHere[a.Key] = struct{}{} + } + + default: + //We only track namespaces, so ignore attributes without one. + if a.Space != "" { + nsUsedHere[a.Space] = struct{}{} + } + } + } + + //Remove all attributes so that we can add them with much-simpler logic + for _, attrK := range toRemove { + el.RemoveAttr(attrK) + } + + //For all namespaces used on the current element, declare them if they were + //not declared (and used) in an ancestor. + for k := range nsUsedHere { + spc := nsAlreadyDeclared[k] + //If previously unused, mark as used + if !spc.used { + el.Attr = append(el.Attr, spc.a) + spc.used = true + + //Assignment here is only to update the pre-existing `used` tracking value + nsAlreadyDeclared[k] = spc + } + } + + //Canonicalize all children, passing down the ancestor tracking map + for _, child := range el.ChildElements() { + excCanonicalPrep(child, nsAlreadyDeclared, inclusiveNamespaces) + } + + //Sort attributes lexicographically + sort.Sort(attrsByKey(el.Attr)) + + return el.Copy() +} + +// canonicalPrep accepts an *etree.Element and transforms it into one which is ready +// for serialization into inclusive canonical form. Specifically this +// entails: +// +// 1. Stripping re-declarations of namespaces +// 2. Sorting attributes into canonical order +// +// Inclusive canonicalization does not strip unused namespaces. +// +// TODO(russell_h): This is very similar to excCanonicalPrep - perhaps they should +// be unified into one parameterized function? +func canonicalPrep(el *etree.Element, seenSoFar map[string]struct{}) *etree.Element { + _seenSoFar := make(map[string]struct{}) + for k, v := range seenSoFar { + _seenSoFar[k] = v + } + + ne := el.Copy() + sort.Sort(attrsByKey(ne.Attr)) + if len(ne.Attr) != 0 { + for _, attr := range ne.Attr { + if attr.Space != nsSpace { + continue + } + key := attr.Space + ":" + attr.Key + if _, seen := _seenSoFar[key]; seen { + ne.RemoveAttr(attr.Space + ":" + attr.Key) + } else { + _seenSoFar[key] = struct{}{} + } + } + } + + for i, token := range ne.Child { + childElement, ok := token.(*etree.Element) + if ok { + ne.Child[i] = canonicalPrep(childElement, _seenSoFar) + } + } + + return ne +} + +func canonicalSerialize(el *etree.Element) ([]byte, error) { + doc := etree.NewDocument() + doc.SetRoot(el) + + doc.WriteSettings = etree.WriteSettings{ + CanonicalAttrVal: true, + CanonicalEndTags: true, + CanonicalText: true, + } + + return doc.WriteToBytes() +} diff --git a/vendor/github.com/russellhaering/goxmldsig/clock.go b/vendor/github.com/russellhaering/goxmldsig/clock.go new file mode 100644 index 00000000..cceaaa54 --- /dev/null +++ b/vendor/github.com/russellhaering/goxmldsig/clock.go @@ -0,0 +1,55 @@ +package dsig + +import ( + "time" + + "github.com/jonboulle/clockwork" +) + +// Clock wraps a clockwork.Clock (which could be real or fake) in order +// to default to a real clock when a nil *Clock is used. In other words, +// if you attempt to use a nil *Clock it will defer to the real system +// clock. This allows Clock to be easily added to structs with methods +// that currently reference the time package, without requiring every +// instantiation of that struct to be updated. +type Clock struct { + wrapped clockwork.Clock +} + +func (c *Clock) getWrapped() clockwork.Clock { + if c == nil { + return clockwork.NewRealClock() + } + + return c.wrapped +} + +func (c *Clock) After(d time.Duration) <-chan time.Time { + return c.getWrapped().After(d) +} + +func (c *Clock) Sleep(d time.Duration) { + c.getWrapped().Sleep(d) +} + +func (c *Clock) Now() time.Time { + return c.getWrapped().Now() +} + +func NewRealClock() *Clock { + return &Clock{ + wrapped: clockwork.NewRealClock(), + } +} + +func NewFakeClock(wrapped clockwork.Clock) *Clock { + return &Clock{ + wrapped: wrapped, + } +} + +func NewFakeClockAt(t time.Time) *Clock { + return &Clock{ + wrapped: clockwork.NewFakeClockAt(t), + } +} diff --git a/vendor/github.com/russellhaering/goxmldsig/keystore.go b/vendor/github.com/russellhaering/goxmldsig/keystore.go new file mode 100644 index 00000000..81487f08 --- /dev/null +++ b/vendor/github.com/russellhaering/goxmldsig/keystore.go @@ -0,0 +1,63 @@ +package dsig + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "math/big" + "time" +) + +type X509KeyStore interface { + GetKeyPair() (privateKey *rsa.PrivateKey, cert []byte, err error) +} + +type X509CertificateStore interface { + Certificates() (roots []*x509.Certificate, err error) +} + +type MemoryX509CertificateStore struct { + Roots []*x509.Certificate +} + +func (mX509cs *MemoryX509CertificateStore) Certificates() ([]*x509.Certificate, error) { + return mX509cs.Roots, nil +} + +type MemoryX509KeyStore struct { + privateKey *rsa.PrivateKey + cert []byte +} + +func (ks *MemoryX509KeyStore) GetKeyPair() (*rsa.PrivateKey, []byte, error) { + return ks.privateKey, ks.cert, nil +} + +func RandomKeyStoreForTest() X509KeyStore { + key, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + panic(err) + } + + now := time.Now() + + template := &x509.Certificate{ + SerialNumber: big.NewInt(0), + NotBefore: now.Add(-5 * time.Minute), + NotAfter: now.Add(365 * 24 * time.Hour), + + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{}, + BasicConstraintsValid: true, + } + + cert, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + if err != nil { + panic(err) + } + + return &MemoryX509KeyStore{ + privateKey: key, + cert: cert, + } +} diff --git a/vendor/github.com/russellhaering/goxmldsig/sign.go b/vendor/github.com/russellhaering/goxmldsig/sign.go new file mode 100644 index 00000000..1fcdee62 --- /dev/null +++ b/vendor/github.com/russellhaering/goxmldsig/sign.go @@ -0,0 +1,186 @@ +package dsig + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + _ "crypto/sha1" + _ "crypto/sha256" + "encoding/base64" + "errors" + "fmt" + + "github.com/beevik/etree" +) + +type SigningContext struct { + Hash crypto.Hash + KeyStore X509KeyStore + IdAttribute string + Prefix string + Canonicalizer Canonicalizer +} + +func NewDefaultSigningContext(ks X509KeyStore) *SigningContext { + return &SigningContext{ + Hash: crypto.SHA256, + KeyStore: ks, + IdAttribute: DefaultIdAttr, + Prefix: DefaultPrefix, + Canonicalizer: MakeC14N11Canonicalizer(), + } +} + +func (ctx *SigningContext) SetSignatureMethod(algorithmID string) error { + hash, ok := signatureMethodsByIdentifier[algorithmID] + if !ok { + return fmt.Errorf("Unknown SignatureMethod: %s", algorithmID) + } + + ctx.Hash = hash + + return nil +} + +func (ctx *SigningContext) digest(el *etree.Element) ([]byte, error) { + canonical, err := ctx.Canonicalizer.Canonicalize(el) + if err != nil { + return nil, err + } + + hash := ctx.Hash.New() + _, err = hash.Write(canonical) + if err != nil { + return nil, err + } + + return hash.Sum(nil), nil +} + +func (ctx *SigningContext) constructSignedInfo(el *etree.Element, enveloped bool) (*etree.Element, error) { + digestAlgorithmIdentifier, ok := digestAlgorithmIdentifiers[ctx.Hash] + if !ok { + return nil, errors.New("unsupported hash mechanism") + } + + signatureMethodIdentifier, ok := signatureMethodIdentifiers[ctx.Hash] + if !ok { + return nil, errors.New("unsupported signature method") + } + + digest, err := ctx.digest(el) + if err != nil { + return nil, err + } + + signedInfo := &etree.Element{ + Tag: SignedInfoTag, + Space: ctx.Prefix, + } + + // /SignedInfo/CanonicalizationMethod + canonicalizationMethod := ctx.createNamespacedElement(signedInfo, CanonicalizationMethodTag) + canonicalizationMethod.CreateAttr(AlgorithmAttr, string(ctx.Canonicalizer.Algorithm())) + + // /SignedInfo/SignatureMethod + signatureMethod := ctx.createNamespacedElement(signedInfo, SignatureMethodTag) + signatureMethod.CreateAttr(AlgorithmAttr, signatureMethodIdentifier) + + // /SignedInfo/Reference + reference := ctx.createNamespacedElement(signedInfo, ReferenceTag) + + dataId := el.SelectAttrValue(DefaultIdAttr, "") + if dataId == "" { + return nil, errors.New("Missing data ID") + } + + reference.CreateAttr(URIAttr, "#"+dataId) + + // /SignedInfo/Reference/Transforms + transforms := ctx.createNamespacedElement(reference, TransformsTag) + if enveloped { + envelopedTransform := ctx.createNamespacedElement(transforms, TransformTag) + envelopedTransform.CreateAttr(AlgorithmAttr, EnvelopedSignatureAltorithmId.String()) + } + canonicalizationAlgorithm := ctx.createNamespacedElement(transforms, TransformTag) + canonicalizationAlgorithm.CreateAttr(AlgorithmAttr, string(ctx.Canonicalizer.Algorithm())) + + // /SignedInfo/Reference/DigestMethod + digestMethod := ctx.createNamespacedElement(reference, DigestMethodTag) + digestMethod.CreateAttr(AlgorithmAttr, digestAlgorithmIdentifier) + + // /SignedInfo/Reference/DigestValue + digestValue := ctx.createNamespacedElement(reference, DigestValueTag) + digestValue.SetText(base64.StdEncoding.EncodeToString(digest)) + + return signedInfo, nil +} + +func (ctx *SigningContext) constructSignature(el *etree.Element, enveloped bool) (*etree.Element, error) { + signedInfo, err := ctx.constructSignedInfo(el, enveloped) + if err != nil { + return nil, err + } + + sig := &etree.Element{ + Tag: SignatureTag, + Space: ctx.Prefix, + } + + xmlns := "xmlns" + if ctx.Prefix != "" { + xmlns += ":" + ctx.Prefix + } + + sig.CreateAttr(xmlns, Namespace) + + sig.Child = append(sig.Child, signedInfo) + + // Must propagate down the attributes to the 'SignedInfo' before digesting + for _, attr := range sig.Attr { + signedInfo.CreateAttr(attr.Space+":"+attr.Key, attr.Value) + } + + digest, err := ctx.digest(signedInfo) + if err != nil { + return nil, err + } + + key, cert, err := ctx.KeyStore.GetKeyPair() + if err != nil { + return nil, err + } + + rawSignature, err := rsa.SignPKCS1v15(rand.Reader, key, ctx.Hash, digest) + if err != nil { + return nil, err + } + + signatureValue := ctx.createNamespacedElement(sig, SignatureValueTag) + signatureValue.SetText(base64.StdEncoding.EncodeToString(rawSignature)) + + keyInfo := ctx.createNamespacedElement(sig, KeyInfoTag) + x509Data := ctx.createNamespacedElement(keyInfo, X509DataTag) + x509Certificate := ctx.createNamespacedElement(x509Data, X509CertificateTag) + x509Certificate.SetText(base64.StdEncoding.EncodeToString(cert)) + + return sig, nil +} + +func (ctx *SigningContext) createNamespacedElement(el *etree.Element, tag string) *etree.Element { + child := el.CreateElement(tag) + child.Space = ctx.Prefix + return child +} + +func (ctx *SigningContext) SignEnveloped(el *etree.Element) (*etree.Element, error) { + sig, err := ctx.constructSignature(el, true) + if err != nil { + return nil, err + } + + ret := el.Copy() + ret.Child = append(ret.Child, sig) + + return ret, nil +} diff --git a/vendor/github.com/russellhaering/goxmldsig/tls_keystore.go b/vendor/github.com/russellhaering/goxmldsig/tls_keystore.go new file mode 100644 index 00000000..c98f312c --- /dev/null +++ b/vendor/github.com/russellhaering/goxmldsig/tls_keystore.go @@ -0,0 +1,34 @@ +package dsig + +import ( + "crypto/rsa" + "crypto/tls" + "fmt" +) + +//Well-known errors +var ( + ErrNonRSAKey = fmt.Errorf("Private key was not RSA") + ErrMissingCertificates = fmt.Errorf("No public certificates provided") +) + +//TLSCertKeyStore wraps the stdlib tls.Certificate to return its contained key +//and certs. +type TLSCertKeyStore tls.Certificate + +//GetKeyPair implements X509KeyStore using the underlying tls.Certificate +func (d TLSCertKeyStore) GetKeyPair() (*rsa.PrivateKey, []byte, error) { + pk, ok := d.PrivateKey.(*rsa.PrivateKey) + + if !ok { + return nil, nil, ErrNonRSAKey + } + + if len(d.Certificate) < 1 { + return nil, nil, ErrMissingCertificates + } + + crt := d.Certificate[0] + + return pk, crt, nil +} diff --git a/vendor/github.com/russellhaering/goxmldsig/validate.go b/vendor/github.com/russellhaering/goxmldsig/validate.go new file mode 100644 index 00000000..51a3d0b4 --- /dev/null +++ b/vendor/github.com/russellhaering/goxmldsig/validate.go @@ -0,0 +1,397 @@ +package dsig + +import ( + "bytes" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "errors" + "fmt" + "regexp" + + "github.com/beevik/etree" +) + +var uriRegexp = regexp.MustCompile("^#[a-zA-Z_][\\w.-]*$") + +type ValidationContext struct { + CertificateStore X509CertificateStore + IdAttribute string + Clock *Clock +} + +func NewDefaultValidationContext(certificateStore X509CertificateStore) *ValidationContext { + return &ValidationContext{ + CertificateStore: certificateStore, + IdAttribute: DefaultIdAttr, + } +} + +// TODO(russell_h): More flexible namespace support. This might barely work. +func inNamespace(el *etree.Element, ns string) bool { + for _, attr := range el.Attr { + if attr.Value == ns { + if attr.Space == "" && attr.Key == "xmlns" { + return el.Space == "" + } else if attr.Space == "xmlns" { + return el.Space == attr.Key + } + } + } + + return false +} + +func childPath(space, tag string) string { + if space == "" { + return "./" + tag + } else { + return "./" + space + ":" + tag + } +} + +// The RemoveElement method on etree.Element isn't recursive... +func recursivelyRemoveElement(tree, el *etree.Element) bool { + if tree.RemoveChild(el) != nil { + return true + } + + for _, child := range tree.Child { + if childElement, ok := child.(*etree.Element); ok { + if recursivelyRemoveElement(childElement, el) { + return true + } + } + } + + return false +} + +// transform applies the passed set of transforms to the specified root element. +// +// The functionality of transform is currently very limited and purpose-specific. +// +// NOTE(russell_h): Ideally this wouldn't mutate the root passed to it, and would +// instead return a copy. Unfortunately copying the tree makes it difficult to +// correctly locate the signature. I'm opting, for now, to simply mutate the root +// parameter. +func (ctx *ValidationContext) transform(root, sig *etree.Element, transforms []*etree.Element) (*etree.Element, Canonicalizer, error) { + if len(transforms) != 2 { + return nil, nil, errors.New("Expected Enveloped and C14N transforms") + } + + var canonicalizer Canonicalizer + + for _, transform := range transforms { + algo := transform.SelectAttr(AlgorithmAttr) + if algo == nil { + return nil, nil, errors.New("Missing Algorithm attribute") + } + + switch AlgorithmID(algo.Value) { + case EnvelopedSignatureAltorithmId: + if !recursivelyRemoveElement(root, sig) { + return nil, nil, errors.New("Error applying canonicalization transform: Signature not found") + } + + case CanonicalXML10ExclusiveAlgorithmId: + var prefixList string + ins := transform.FindElement(childPath("", InclusiveNamespacesTag)) + if ins != nil { + prefixListEl := ins.SelectAttr(PrefixListAttr) + if prefixListEl != nil { + prefixList = prefixListEl.Value + } + } + + canonicalizer = MakeC14N10ExclusiveCanonicalizerWithPrefixList(prefixList) + + case CanonicalXML11AlgorithmId: + canonicalizer = MakeC14N11Canonicalizer() + + default: + return nil, nil, errors.New("Unknown Transform Algorithm: " + algo.Value) + } + } + + if canonicalizer == nil { + return nil, nil, errors.New("Expected canonicalization transform") + } + + return root, canonicalizer, nil +} + +func (ctx *ValidationContext) digest(el *etree.Element, digestAlgorithmId string, canonicalizer Canonicalizer) ([]byte, error) { + data, err := canonicalizer.Canonicalize(el) + if err != nil { + return nil, err + } + + digestAlgorithm, ok := digestAlgorithmsByIdentifier[digestAlgorithmId] + if !ok { + return nil, errors.New("Unknown digest algorithm: " + digestAlgorithmId) + } + + hash := digestAlgorithm.New() + _, err = hash.Write(data) + if err != nil { + return nil, err + } + + return hash.Sum(nil), nil +} + +func (ctx *ValidationContext) verifySignedInfo(signatureElement *etree.Element, canonicalizer Canonicalizer, signatureMethodId string, cert *x509.Certificate, sig []byte) error { + signedInfo := signatureElement.FindElement(childPath(signatureElement.Space, SignedInfoTag)) + if signedInfo == nil { + return errors.New("Missing SignedInfo") + } + + // Any attributes from the 'Signature' element must be pushed down into the 'SignedInfo' element before it is canonicalized + for _, attr := range signatureElement.Attr { + signedInfo.CreateAttr(attr.Space+":"+attr.Key, attr.Value) + } + + // Canonicalize the xml + canonical, err := canonicalizer.Canonicalize(signedInfo) + if err != nil { + return err + } + + signatureAlgorithm, ok := signatureMethodsByIdentifier[signatureMethodId] + if !ok { + return errors.New("Unknown signature method: " + signatureMethodId) + } + + hash := signatureAlgorithm.New() + _, err = hash.Write(canonical) + if err != nil { + return err + } + + hashed := hash.Sum(nil) + + pubKey, ok := cert.PublicKey.(*rsa.PublicKey) + if !ok { + return errors.New("Invalid public key") + } + + // Verify that the private key matching the public key from the cert was what was used to sign the 'SignedInfo' and produce the 'SignatureValue' + err = rsa.VerifyPKCS1v15(pubKey, signatureAlgorithm, hashed[:], sig) + if err != nil { + return err + } + + return nil +} + +func (ctx *ValidationContext) validateSignature(el *etree.Element, cert *x509.Certificate) (*etree.Element, error) { + el = el.Copy() + + // Verify the document minus the signedInfo against the 'DigestValue' + // Find the 'Signature' element + sig := el.FindElement(SignatureTag) + + if sig == nil { + return nil, errors.New("Missing Signature") + } + + if !inNamespace(sig, Namespace) { + return nil, errors.New("Signature element is in the wrong namespace") + } + + // Get the 'SignedInfo' element + signedInfo := sig.FindElement(childPath(sig.Space, SignedInfoTag)) + if signedInfo == nil { + return nil, errors.New("Missing SignedInfo") + } + + reference := signedInfo.FindElement(childPath(sig.Space, ReferenceTag)) + if reference == nil { + return nil, errors.New("Missing Reference") + } + + transforms := reference.FindElement(childPath(sig.Space, TransformsTag)) + if transforms == nil { + return nil, errors.New("Missing Transforms") + } + + uri := reference.SelectAttr("URI") + if uri == nil { + // TODO(russell_h): It is permissible to leave this out. We should be + // able to fall back to finding the referenced element some other way. + return nil, errors.New("Reference is missing URI attribute") + } + + if !uriRegexp.MatchString(uri.Value) { + return nil, errors.New("Invalid URI: " + uri.Value) + } + + // Get the element referenced in the 'SignedInfo' + referencedElement := el.FindElement(fmt.Sprintf("//[@%s='%s']", ctx.IdAttribute, uri.Value[1:])) + if referencedElement == nil { + return nil, errors.New("Unable to find referenced element: " + uri.Value) + } + + // Perform all transformations listed in the 'SignedInfo' + // Basically, this means removing the 'SignedInfo' + transformed, canonicalizer, err := ctx.transform(referencedElement, sig, transforms.ChildElements()) + if err != nil { + return nil, err + } + + digestMethod := reference.FindElement(childPath(sig.Space, DigestMethodTag)) + if digestMethod == nil { + return nil, errors.New("Missing DigestMethod") + } + + digestValue := reference.FindElement(childPath(sig.Space, DigestValueTag)) + if digestValue == nil { + return nil, errors.New("Missing DigestValue") + } + + digestAlgorithmAttr := digestMethod.SelectAttr(AlgorithmAttr) + if digestAlgorithmAttr == nil { + return nil, errors.New("Missing DigestMethod Algorithm attribute") + } + + // Digest the transformed XML and compare it to the 'DigestValue' from the 'SignedInfo' + digest, err := ctx.digest(transformed, digestAlgorithmAttr.Value, canonicalizer) + if err != nil { + return nil, err + } + + decodedDigestValue, err := base64.StdEncoding.DecodeString(digestValue.Text()) + if err != nil { + return nil, err + } + + if !bytes.Equal(digest, decodedDigestValue) { + return nil, errors.New("Signature could not be verified") + } + + //Verify the signed info + signatureMethod := signedInfo.FindElement(childPath(sig.Space, SignatureMethodTag)) + if signatureMethod == nil { + return nil, errors.New("Missing SignatureMethod") + } + + signatureMethodAlgorithmAttr := signatureMethod.SelectAttr(AlgorithmAttr) + if digestAlgorithmAttr == nil { + return nil, errors.New("Missing SignatureMethod Algorithm attribute") + } + + // Decode the 'SignatureValue' so we can compare against it + signatureValue := sig.FindElement(childPath(sig.Space, SignatureValueTag)) + if signatureValue == nil { + return nil, errors.New("Missing SignatureValue") + } + + decodedSignature, err := base64.StdEncoding.DecodeString(signatureValue.Text()) + + if err != nil { + return nil, errors.New("Could not decode signature") + } + // Actually verify the 'SignedInfo' was signed by a trusted source + err = ctx.verifySignedInfo(sig, canonicalizer, signatureMethodAlgorithmAttr.Value, cert, decodedSignature) + if err != nil { + return nil, err + } + + return transformed, nil +} + +func contains(roots []*x509.Certificate, cert *x509.Certificate) bool { + for _, root := range roots { + if root.Equal(cert) { + return true + } + } + return false +} + +func (ctx *ValidationContext) verifyCertificate(el *etree.Element) (*x509.Certificate, error) { + now := ctx.Clock.Now() + el = el.Copy() + + idAttr := el.SelectAttr(DefaultIdAttr) + if idAttr == nil || idAttr.Value == "" { + return nil, errors.New("Missing ID attribute") + } + + signatureElements := el.FindElements("//" + SignatureTag) + var signatureElement *etree.Element + + // Find the Signature element that references the whole Response element + for _, e := range signatureElements { + e2 := e.Copy() + + signedInfo := e2.FindElement(childPath(e2.Space, SignedInfoTag)) + if signedInfo == nil { + return nil, errors.New("Missing SignedInfo") + } + + referenceElement := signedInfo.FindElement(childPath(e2.Space, ReferenceTag)) + if referenceElement == nil { + return nil, errors.New("Missing Reference Element") + } + + uriAttr := referenceElement.SelectAttr(URIAttr) + if uriAttr == nil || uriAttr.Value == "" { + return nil, errors.New("Missing URI attribute") + } + + if uriAttr.Value[1:] == idAttr.Value { + signatureElement = e + break + } + } + + if signatureElement == nil { + return nil, errors.New("Missing signature referencing the top-level element") + } + + // Get the x509 element from the signature + x509Element := signatureElement.FindElement("//" + childPath(signatureElement.Space, X509CertificateTag)) + if x509Element == nil { + return nil, errors.New("Missing x509 Element") + } + + x509Text := "-----BEGIN CERTIFICATE-----\n" + x509Element.Text() + "\n-----END CERTIFICATE-----" + block, _ := pem.Decode([]byte(x509Text)) + if block == nil { + return nil, errors.New("Failed to parse certificate PEM") + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, err + } + + roots, err := ctx.CertificateStore.Certificates() + if err != nil { + return nil, err + } + + // Verify that the certificate is one we trust + if !contains(roots, cert) { + return nil, errors.New("Could not verify certificate against trusted certs") + } + + if now.Before(cert.NotBefore) || now.After(cert.NotAfter) { + return nil, errors.New("Cert is not valid at this time") + } + + return cert, nil +} + +func (ctx *ValidationContext) Validate(el *etree.Element) (*etree.Element, error) { + cert, err := ctx.verifyCertificate(el) + + if err != nil { + return nil, err + } + + return ctx.validateSignature(el, cert) +} diff --git a/vendor/github.com/russellhaering/goxmldsig/xml_constants.go b/vendor/github.com/russellhaering/goxmldsig/xml_constants.go new file mode 100644 index 00000000..5c9cb693 --- /dev/null +++ b/vendor/github.com/russellhaering/goxmldsig/xml_constants.go @@ -0,0 +1,78 @@ +package dsig + +import "crypto" + +const ( + DefaultPrefix = "ds" + Namespace = "http://www.w3.org/2000/09/xmldsig#" +) + +// Tags +const ( + SignatureTag = "Signature" + SignedInfoTag = "SignedInfo" + CanonicalizationMethodTag = "CanonicalizationMethod" + SignatureMethodTag = "SignatureMethod" + ReferenceTag = "Reference" + TransformsTag = "Transforms" + TransformTag = "Transform" + DigestMethodTag = "DigestMethod" + DigestValueTag = "DigestValue" + SignatureValueTag = "SignatureValue" + KeyInfoTag = "KeyInfo" + X509DataTag = "X509Data" + X509CertificateTag = "X509Certificate" + InclusiveNamespacesTag = "InclusiveNamespaces" +) + +const ( + AlgorithmAttr = "Algorithm" + URIAttr = "URI" + DefaultIdAttr = "ID" + PrefixListAttr = "PrefixList" +) + +type AlgorithmID string + +func (id AlgorithmID) String() string { + return string(id) +} + +const ( + RSASHA1SignatureMethod = "http://www.w3.org/2000/09/xmldsig#rsa-sha1" + RSASHA256SignatureMethod = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" + RSASHA512SignatureMethod = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512" +) + +//Well-known signature algorithms +const ( + // Supported canonicalization algorithms + CanonicalXML10ExclusiveAlgorithmId AlgorithmID = "http://www.w3.org/2001/10/xml-exc-c14n#" + CanonicalXML11AlgorithmId AlgorithmID = "http://www.w3.org/2006/12/xml-c14n11" + + EnvelopedSignatureAltorithmId AlgorithmID = "http://www.w3.org/2000/09/xmldsig#enveloped-signature" +) + +var digestAlgorithmIdentifiers = map[crypto.Hash]string{ + crypto.SHA1: "http://www.w3.org/2000/09/xmldsig#sha1", + crypto.SHA256: "http://www.w3.org/2001/04/xmlenc#sha256", + crypto.SHA512: "http://www.w3.org/2001/04/xmlenc#sha512", +} + +var digestAlgorithmsByIdentifier = map[string]crypto.Hash{} +var signatureMethodsByIdentifier = map[string]crypto.Hash{} + +func init() { + for hash, id := range digestAlgorithmIdentifiers { + digestAlgorithmsByIdentifier[id] = hash + } + for hash, id := range signatureMethodIdentifiers { + signatureMethodsByIdentifier[id] = hash + } +} + +var signatureMethodIdentifiers = map[crypto.Hash]string{ + crypto.SHA1: RSASHA1SignatureMethod, + crypto.SHA256: RSASHA256SignatureMethod, + crypto.SHA512: RSASHA512SignatureMethod, +}