diff --git a/cmd/samldemo/main.go b/cmd/samldemo/main.go new file mode 100644 index 0000000000..98fe23475e --- /dev/null +++ b/cmd/samldemo/main.go @@ -0,0 +1,26 @@ +// Copyright 2019 Yunion +// +// 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. + +package main + +import ( + "yunion.io/x/onecloud/pkg/util/atexit" + "yunion.io/x/onecloud/pkg/util/samlutils/demo" +) + +func main() { + defer atexit.Handle() + + demo.StartServer() +} diff --git a/go.mod b/go.mod index 3810925ee4..c0f070bbea 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( github.com/aokoli/goutils v1.0.1 github.com/aws/aws-sdk-go v1.21.4 github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect + github.com/beevik/etree v1.1.0 // indirect github.com/benbjohnson/clock v1.0.0 github.com/bitly/go-simplejson v0.5.0 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect @@ -70,6 +71,7 @@ require ( github.com/kr/pty v1.1.5 github.com/lib/pq v1.2.0 // indirect github.com/libvirt/libvirt-go-xml v5.2.0+incompatible + github.com/ma314smith/signedxml v0.0.0-20200410192636-c342a2d0ae60 github.com/mattn/go-runewidth v0.0.4 // indirect github.com/mattn/go-sqlite3 v1.10.0 // indirect github.com/mattn/go-tty v0.0.0-20181127064339-e4f871175a2f // indirect diff --git a/go.sum b/go.sum index 15b75df5c1..18f4ed1350 100644 --- a/go.sum +++ b/go.sum @@ -150,6 +150,8 @@ github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f h1:ZNv7 github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= github.com/bazelbuild/bazel-gazelle v0.0.0-20181012220611-c728ce9f663e/go.mod h1:uHBSeeATKpVazAACZBDPL/Nk/UhQDDsJWDlqYJo8/Us= github.com/bazelbuild/buildtools v0.0.0-20180226164855-80c7f0d45d7e/go.mod h1:5JP0TXzWDHXv8qvxRC4InIazwdyDseBDbzESUMKk1yU= +github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= +github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/benbjohnson/clock v1.0.0 h1:78Jk/r6m4wCi6sndMpty7A//t4dw/RW5fV4ZgDVfX1w= github.com/benbjohnson/clock v1.0.0/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -511,6 +513,8 @@ github.com/lucas-clemente/aes12 v0.0.0-20171027163421-cd47fb39b79f/go.mod h1:JpH github.com/lucas-clemente/quic-clients v0.1.0/go.mod h1:y5xVIEoObKqULIKivu+gD/LU90pL73bTdtQjPBvtCBk= github.com/lucas-clemente/quic-go v0.10.2/go.mod h1:hvaRS9IHjFLMq76puFJeWNfmn+H70QZ/CXoxqw9bzao= github.com/lucas-clemente/quic-go-certificates v0.0.0-20160823095156-d2f86524cced/go.mod h1:NCcRLrOTZbzhZvixZLlERbJtDtYsmMw8Jc4vS8Z0g58= +github.com/ma314smith/signedxml v0.0.0-20200410192636-c342a2d0ae60 h1:q5rqPuvxdOzg1NC6sls3XSpRokQ5KAmLIYdLW1/kyOo= +github.com/ma314smith/signedxml v0.0.0-20200410192636-c342a2d0ae60/go.mod h1:KEgVcb43+f5KFUH/x6Vd3NROG0AIL2CuKMrIqYsmx6E= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= diff --git a/pkg/appsrv/send.go b/pkg/appsrv/send.go index d0169f61d2..1c02f368dc 100644 --- a/pkg/appsrv/send.go +++ b/pkg/appsrv/send.go @@ -73,8 +73,18 @@ func SendHeader(w http.ResponseWriter, hdr http.Header) { } func SendXml(w http.ResponseWriter, hdr http.Header, obj interface{}) { + SendXmlWithIndent(w, hdr, obj, false) +} + +func SendXmlWithIndent(w http.ResponseWriter, hdr http.Header, obj interface{}, indent bool) { if !gotypes.IsNil(obj) { - xmlBytes, err := xml.Marshal(obj) + var xmlBytes []byte + var err error + if indent { + xmlBytes, err = xml.MarshalIndent(obj, "", " ") + } else { + xmlBytes, err = xml.Marshal(obj) + } if err == nil { for k, v := range hdr { if k != "Content-Type" && k != "Content-Length" { diff --git a/pkg/util/samlutils/consts.go b/pkg/util/samlutils/consts.go new file mode 100644 index 0000000000..541d34263a --- /dev/null +++ b/pkg/util/samlutils/consts.go @@ -0,0 +1,59 @@ +// Copyright 2019 Yunion +// +// 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. + +package samlutils + +const ( + XMLNS_MD = "urn:oasis:names:tc:SAML:2.0:metadata" + XMLNS_DS = "http://www.w3.org/2000/09/xmldsig#" + XMLNS_PROTO = "urn:oasis:names:tc:SAML:2.0:protocol" + XMLNS_ASSERT = "urn:oasis:names:tc:SAML:2.0:assertion" + + PROTOCOL_SAML2 = "urn:oasis:names:tc:SAML:2.0:protocol" + + KEY_USE_SIGNING = "signing" + KEY_USE_ENCRYPTION = "encryption" + + NAME_ID_FORMAT_PERSISTENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" + NAME_ID_FORMAT_TRANSIENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + NAME_ID_FORMAT_EMAIL = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + NAME_ID_FORMAT_UNSPEC = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" + NAME_ID_FORMAT_X509 = "urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName" + NAME_ID_FORMAT_WINDOWS = "urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName" + NAME_ID_FORMAT_KERBEROS = "urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos" + NAME_ID_FORMAT_ENTITY = "urn:oasis:names:tc:SAML:2.0:nameid-format:entity" + + SAML2_VERSION = "2.0" + + STATUS_SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success" + + BINDING_HTTP_POST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + BINDING_HTTP_REDIRECT = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + + HTML_SAML_FORM_TOKEN = "$FORM$" + DEFAULT_HTML_TEMPLATE = `$FORM$` +) + +var ( + NAME_ID_FORMATS = []string{ + NAME_ID_FORMAT_PERSISTENT, + NAME_ID_FORMAT_TRANSIENT, + NAME_ID_FORMAT_EMAIL, + NAME_ID_FORMAT_UNSPEC, + NAME_ID_FORMAT_X509, + NAME_ID_FORMAT_WINDOWS, + NAME_ID_FORMAT_KERBEROS, + NAME_ID_FORMAT_ENTITY, + } +) diff --git a/pkg/util/samlutils/demo/doc.go b/pkg/util/samlutils/demo/doc.go new file mode 100644 index 0000000000..220a34214c --- /dev/null +++ b/pkg/util/samlutils/demo/doc.go @@ -0,0 +1,15 @@ +// Copyright 2019 Yunion +// +// 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. + +package demo // import "yunion.io/x/onecloud/pkg/util/samlutils/demo" diff --git a/pkg/util/samlutils/demo/service.go b/pkg/util/samlutils/demo/service.go new file mode 100644 index 0000000000..55d8026dcf --- /dev/null +++ b/pkg/util/samlutils/demo/service.go @@ -0,0 +1,413 @@ +// Copyright 2019 Yunion +// +// 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. + +package demo + +import ( + "context" + "fmt" + "net/http" + "net/url" + "os" + "strings" + + "yunion.io/x/log" + "yunion.io/x/pkg/errors" + "yunion.io/x/structarg" + + "yunion.io/x/onecloud/pkg/appsrv" + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/util/fileutils2" + "yunion.io/x/onecloud/pkg/util/httputils" + "yunion.io/x/onecloud/pkg/util/samlutils" + "yunion.io/x/onecloud/pkg/util/samlutils/idp" +) + +type Options struct { + Help bool `help:"show help"` + Cert string `help:"certificate file"` + Key string `help:"certificate private key file"` + Port int `help:"listening port"` + Entity string `help:"SAML entityID"` + SpMeta []string `help:"ServiceProvider metadata filename"` +} + +func showErrorAndExit(e error) { + fmt.Fprintf(os.Stderr, "%s", e) + fmt.Fprintln(os.Stderr) + os.Exit(1) +} + +func StartServer() { + err := prepareServer() + if err != nil { + showErrorAndExit(err) + } else { + fmt.Println("exit cleanly") + } +} + +func prepareServer() error { + parser, err := structarg.NewArgumentParser( + &Options{}, + "samldemo", + "A demo SAML 2.0 https server", + `See "ipmicli help COMMAND" for help on a specific command.`, + ) + if err != nil { + return errors.Wrap(err, "NewArgumentParser") + } + + err = parser.ParseArgs(os.Args[1:], false) + options := parser.Options().(*Options) + + if options.Help { + fmt.Print(parser.HelpString()) + return nil + } + + if len(options.Entity) == 0 { + return errors.Wrap(httperrors.ErrInputParameter, "empty entityID") + } + if options.Port <= 0 { + return errors.Wrap(httperrors.ErrInputParameter, "port must be positive integer") + } + if len(options.Key) == 0 { + return errors.Wrap(httperrors.ErrInputParameter, "key file must be present") + } + if !fileutils2.Exists(options.Key) { + return errors.Wrapf(httperrors.ErrInputParameter, "key %s not found", options.Key) + } + if len(options.Cert) == 0 { + return errors.Wrap(httperrors.ErrInputParameter, "cert file must be present") + } + if !fileutils2.Exists(options.Cert) { + return errors.Wrapf(httperrors.ErrInputParameter, "cert %s not found", options.Cert) + } + + app := appsrv.NewApplication("samldemo", 4, false) + + saml, err := samlutils.NewSAMLInstance(options.Entity, options.Cert, options.Key) + if err != nil { + return errors.Wrap(err, "NewSAMLInstance") + } + + spFunc := func(ctx context.Context, sp *idp.SSAMLServiceProvider) samlutils.SSAMLSpInitiatedLoginData { + log.Debugf("Recive SP initiated Login: %s", sp.GetEntityId()) + data := samlutils.SSAMLSpInitiatedLoginData{} + switch sp.GetEntityId() { + case "https://auth.huaweicloud.com/": // 华为云 SSO + data.NameId = "yunionoss" + data.NameIdFormat = samlutils.NAME_ID_FORMAT_TRANSIENT + data.AudienceRestriction = sp.GetEntityId() + for k, v := range map[string]string{ + // "xUserId": "052d45a3e70010440f92c000d9e3f260", + // "xAccountId": "052d45a3e70010440f92c000d9e3f260", + // "bpId": "c58a60a2e0a046c8afa77286924c2b0d", + // "name": "yunionoss", + // "email": "qiujian@yunion.cn", + // "mobile": "13811299225", + "User": "ec2admin", + "Group": "ec2admin", + } { + data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{ + Name: k, FriendlyName: k, + NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", + Values: []string{v}, + }) + } + case "https://samltest.id/saml/sp": // samltest.id SSO + data.NameId = "yunion" + data.NameIdFormat = samlutils.NAME_ID_FORMAT_TRANSIENT + data.AudienceRestriction = sp.GetEntityId() + for _, v := range []struct { + name string + friendlyName string + value string + }{ + { + name: "urn:oid:0.9.2342.19200300.100.1.1", + friendlyName: "uid", + value: "9646D89D-F5E7-F0E4-C545A9B2F4B7956B", + }, + { + name: "urn:oid:0.9.2342.19200300.100.1.3", + friendlyName: "mail", + value: "samltest@yunion.io", + }, + { + name: "urn:oid:2.5.4.4", + friendlyName: "sn", + value: "Jian", + }, + { + name: "urn:oid:2.5.4.42", + friendlyName: "givenName", + value: "Jian", + }, + } { + data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{ + Name: v.name, + FriendlyName: v.friendlyName, + NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", + Values: []string{v.value}, + }) + } + case "cloud.tencent.com": // 腾讯云 role SSO + data.NameId = "cvmcosreadonly" + data.NameIdFormat = samlutils.NAME_ID_FORMAT_TRANSIENT + data.AudienceRestriction = "https://cloud.tencent.com" + for _, v := range []struct { + name string + friendlyName string + value string + }{ + { + name: "https://cloud.tencent.com/SAML/Attributes/Role", + friendlyName: "RoleEntitlement", + value: "qcs::cam::uin/100008182714:roleName/cvmcosreadonly,qcs::cam::uin/100008182714:saml-provider/saml.yunion.io", + }, + { + name: "https://cloud.tencent.com/SAML/Attributes/RoleSessionName", + friendlyName: "RoleSessionName", + value: "cvmcosreadonly", + }, + } { + data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{ + Name: v.name, + FriendlyName: v.friendlyName, + Values: []string{v.value}, + }) + } + case "google.com/a/yunion-hk.com": + data.NameId = "qiujian" + data.NameIdFormat = samlutils.NAME_ID_FORMAT_TRANSIENT + data.AudienceRestriction = sp.GetEntityId() + for k, v := range map[string]string{ + "user.email": "qiujian@yunion-hk.com", + } { + data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{ + Name: k, FriendlyName: k, + NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", + Values: []string{v}, + }) + } + case "google.com": + data.NameId = "qiujian@yunion-hk.com" + data.NameIdFormat = samlutils.NAME_ID_FORMAT_EMAIL + data.AudienceRestriction = sp.GetEntityId() + for k, v := range map[string]string{ + "user.email": "qiujian@yunion-hk.com", + } { + data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{ + Name: k, FriendlyName: k, + NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", + Values: []string{v}, + }) + } + } + return data + } + + idpFunc := func(ctx context.Context, sp *idp.SSAMLServiceProvider, state string) samlutils.SSAMLIdpInitiatedLoginData { + log.Debugf("Recive IDP initiated Login: %s", sp.GetEntityId()) + data := samlutils.SSAMLIdpInitiatedLoginData{} + switch sp.GetEntityId() { + case "urn:alibaba:cloudcomputing": // 阿里云role SSO + data.NameId = "ecsossreadonly" + data.NameIdFormat = samlutils.NAME_ID_FORMAT_PERSISTENT + data.AudienceRestriction = sp.GetEntityId() + for k, v := range map[string]string{ + "https://www.aliyun.com/SAML-Role/Attributes/Role": "acs:ram::1123247935774897:role/administrator,acs:ram::1123247935774897:saml-provider/saml.yunion.io", + "https://www.aliyun.com/SAML-Role/Attributes/RoleSessionName": "ecsossreadonly", + "https://www.aliyun.com/SAML-Role/Attributes/SessionDuration": "1800", + } { + data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{ + Name: k, + Values: []string{v}, + }) + } + data.RelayState = "https://homenew.console.aliyun.com/" + case "urn:amazon:webservices:cn-north-1": // AWS CN role SSO + data.NameId = "ec2s3readonly" + data.NameIdFormat = samlutils.NAME_ID_FORMAT_PERSISTENT + data.AudienceRestriction = "https://signin.amazonaws.cn/saml" + for _, v := range []struct { + name string + friendlyName string + value string + }{ + { + name: "https://aws.amazon.com/SAML/Attributes/Role", + friendlyName: "RoleEntitlement", + value: "arn:aws-cn:iam::248697896586:role/ec2s3readonly,arn:aws-cn:iam::248697896586:saml-provider/saml.yunion.io", + }, + { + name: "https://aws.amazon.com/SAML/Attributes/RoleSessionName", + friendlyName: "RoleSessionName", + value: "ec2s3readonly", + }, + { + name: "urn:oid:1.3.6.1.4.1.5923.1.1.1.3", + friendlyName: "eduPersonOrgDN", + value: "ec2s3readonly", + }, + } { + data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{ + Name: v.name, + FriendlyName: v.friendlyName, + Values: []string{v.value}, + }) + } + data.RelayState = "https://console.amazonaws.cn/" + case "urn:amazon:webservices": // AWS Global role SSO + data.NameId = "ec2s3readonly" + data.NameIdFormat = samlutils.NAME_ID_FORMAT_PERSISTENT + data.AudienceRestriction = "https://signin.aws.amazon.com/saml" + for _, v := range []struct { + name string + friendlyName string + value string + }{ + { + name: "https://aws.amazon.com/SAML/Attributes/Role", + friendlyName: "RoleEntitlement", + value: "arn:aws:iam::285906155448:role/ec2s3readonly,arn:aws:iam::285906155448:saml-provider/saml.yunion.cn", + }, + { + name: "https://aws.amazon.com/SAML/Attributes/RoleSessionName", + friendlyName: "RoleSessionName", + value: "ec2s3readonly", + }, + { + name: "urn:oid:1.3.6.1.4.1.5923.1.1.1.3", + friendlyName: "eduPersonOrgDN", + value: "ec2s3readonly", + }, + } { + data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{ + Name: v.name, + FriendlyName: v.friendlyName, + Values: []string{v.value}, + }) + } + data.RelayState = "https://console.aws.amazon.com/" + case "cloud.tencent.com": // 腾讯云 role SSO + data.NameId = "cvmcosreadonly" + data.NameIdFormat = samlutils.NAME_ID_FORMAT_TRANSIENT + data.AudienceRestriction = "https://cloud.tencent.com" + for _, v := range []struct { + name string + friendlyName string + value string + }{ + { + name: "https://cloud.tencent.com/SAML/Attributes/Role", + friendlyName: "RoleEntitlement", + value: "qcs::cam::uin/100008182714:roleName/cvmcosreadonly,qcs::cam::uin/100008182714:saml-provider/saml.yunion.io", + }, + { + name: "https://cloud.tencent.com/SAML/Attributes/RoleSessionName", + friendlyName: "RoleSessionName", + value: "cvmcosreadonly", + }, + } { + data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{ + Name: v.name, + FriendlyName: v.friendlyName, + Values: []string{v.value}, + }) + } + data.RelayState = "https://console.cloud.tencent.com/" + } + return data + } + + logoutFunc := func(ctx context.Context) string { + return fmt.Sprintf(`

成功退出登录,重新登录

`, httputils.JoinPath(options.Entity, "SAML/idp")) + } + + idpInst := idp.NewIdpInstance(saml, spFunc, idpFunc, logoutFunc) + for _, spMetaFile := range options.SpMeta { + err := idpInst.AddSPMetadataFile(spMetaFile) + if err != nil { + return errors.Wrapf(err, "AddSPMetadataFile %s", spMetaFile) + } + } + idpInst.AddHandlers(app, "SAML/idp") + idpInst.SetHtmlTemplate(`

正在跳转到云控制台,请等待。。。

$FORM$`) + + app.AddHandler("GET", "SAML/idp", func(ctx context.Context, w http.ResponseWriter, r *http.Request) { + idpInitUrl := httputils.JoinPath(options.Entity, "SAML/idp/sso") + + htmlBuf := strings.Builder{} + htmlBuf.WriteString(`
    `) + // IDP initiated + for _, v := range []struct { + name string + entityID string + }{ + { + name: "Aliyun Role SSO", + entityID: "urn:alibaba:cloudcomputing", + }, + { + name: "AWS CN Role SSO", + entityID: "urn:amazon:webservices:cn-north-1", + }, + { + name: "AWS Global Role SSO", + entityID: "urn:amazon:webservices", + }, + { + name: "Tencent Cloud Role SSO", + entityID: "cloud.tencent.com", + }, + } { + htmlBuf.WriteString(fmt.Sprintf(`
  1. %s (IDP-Initiated)
  2. `, idpInitUrl, url.QueryEscape(v.entityID), v.name)) + } + + for _, v := range []struct { + name string + url string + }{ + /*{ + name: "Huawei cloud partner SSO", + url: "https://auth.huaweicloud.com/authui/saml/login?xAccountType=yunion_IDP&isFirstLogin=false&service=https%3a%2f%2fconsole.huaweicloud.com%2fiam%2f", + },*/ + { + name: "Huawei cloud SSO", + url: "https://auth.huaweicloud.com/authui/federation/websso?domain_id=052d45a3e70010440f92c000d9e3f260&idp=yunion&protocol=saml", + }, + { + name: "Tencent cloud SSO", + url: "https://cloud.tencent.com/login/forwardIdp/100008182714/saml.yunion.io", + }, + { + name: "Google cloud SSO", + url: "https://www.google.com/a/yunion-hk.com/ServiceLogin?continue=https://console.cloud.google.com", + }, + } { + htmlBuf.WriteString(fmt.Sprintf(`
  3. %s (SP-Initiated)
  4. `, v.url, v.name)) + } + + htmlBuf.WriteString(`
`) + appsrv.SendHTML(w, htmlBuf.String()) + }) + + addr := fmt.Sprintf(":%d", options.Port) + app.ListenAndServeTLS(addr, options.Cert, options.Key) + + return nil +} diff --git a/pkg/util/samlutils/doc.go b/pkg/util/samlutils/doc.go new file mode 100644 index 0000000000..5e131aa2cb --- /dev/null +++ b/pkg/util/samlutils/doc.go @@ -0,0 +1,15 @@ +// Copyright 2019 Yunion +// +// 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. + +package samlutils // import "yunion.io/x/onecloud/pkg/util/samlutils" diff --git a/pkg/util/samlutils/encrypt.go b/pkg/util/samlutils/encrypt.go new file mode 100644 index 0000000000..4ab0c07377 --- /dev/null +++ b/pkg/util/samlutils/encrypt.go @@ -0,0 +1,81 @@ +// Copyright 2019 Yunion +// +// 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. + +package samlutils + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "io/ioutil" + + "yunion.io/x/log" + "yunion.io/x/pkg/errors" + + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/util/seclib2" +) + +func decodePrivateKey(keyString []byte) (*rsa.PrivateKey, error) { + block, _ := pem.Decode(keyString) + log.Debugf("pem.Decode privateKey data: type=%s header: %s", block.Type, block.Headers) + privKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err == nil { + return privKey.(*rsa.PrivateKey), nil + } + priv, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err == nil { + return priv, nil + } + return nil, errors.Wrap(httperrors.ErrInvalidFormat, "not a valid private key") +} + +func (saml *SSAMLInstance) parseKeys() error { + privData, err := ioutil.ReadFile(saml.privateKeyFile) + if err != nil { + return errors.Wrapf(err, "ioutil.ReadFile %s", saml.privateKeyFile) + } + saml.privateKey, err = decodePrivateKey(privData) + if err != nil { + return errors.Wrap(err, "decodePrivateKey") + } + + certData, err := ioutil.ReadFile(saml.certFile) + if err != nil { + return errors.Wrapf(err, "ioutil.Readfile %s", saml.certFile) + } + + var block *pem.Block + saml.certs = make([]*x509.Certificate, 0) + first := true + for { + block, certData = pem.Decode(certData) + if block == nil { + break + } + if first { + first = false + saml.certString = seclib2.CleanCertificate(string(pem.EncodeToMemory(block))) + + log.Debugf("cert: %s", saml.certString) + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return errors.Wrap(err, "x509.ParseCertificate") + } + saml.certs = append(saml.certs, cert) + } + + return nil +} diff --git a/pkg/util/samlutils/idp/doc.go b/pkg/util/samlutils/idp/doc.go new file mode 100644 index 0000000000..24b820be86 --- /dev/null +++ b/pkg/util/samlutils/idp/doc.go @@ -0,0 +1,15 @@ +// Copyright 2019 Yunion +// +// 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. + +package idp // import "yunion.io/x/onecloud/pkg/util/samlutils/idp" diff --git a/pkg/util/samlutils/idp/idp.go b/pkg/util/samlutils/idp/idp.go new file mode 100644 index 0000000000..5b79dca9aa --- /dev/null +++ b/pkg/util/samlutils/idp/idp.go @@ -0,0 +1,302 @@ +// Copyright 2019 Yunion +// +// 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. + +package idp + +import ( + "context" + "encoding/base64" + "encoding/xml" + "io/ioutil" + "net/http" + "strings" + + "yunion.io/x/log" + "yunion.io/x/pkg/errors" + + "yunion.io/x/onecloud/pkg/appctx" + "yunion.io/x/onecloud/pkg/appsrv" + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/util/httputils" + "yunion.io/x/onecloud/pkg/util/samlutils" +) + +type OnSpInitiatedLogin func(ctx context.Context, sp *SSAMLServiceProvider) samlutils.SSAMLSpInitiatedLoginData +type OnIdpInitiatedLogin func(ctx context.Context, sp *SSAMLServiceProvider, state string) samlutils.SSAMLIdpInitiatedLoginData +type OnLogout func(ctx context.Context) string + +type SSAMLIdpInstance struct { + saml *samlutils.SSAMLInstance + + metadataPath string + redirectLoginPath string + redirectLogoutPath string + idpInitiatedSSOPath string + + serviceProviders []*SSAMLServiceProvider + + onSpInitiatedLogin OnSpInitiatedLogin + onIdpInitiatedLogin OnIdpInitiatedLogin + onLogout OnLogout + + htmlTemplate string +} + +func NewIdpInstance(saml *samlutils.SSAMLInstance, spLoginFunc OnSpInitiatedLogin, idpLoginFunc OnIdpInitiatedLogin, logoutFunc OnLogout) *SSAMLIdpInstance { + return &SSAMLIdpInstance{ + saml: saml, + onSpInitiatedLogin: spLoginFunc, + onIdpInitiatedLogin: idpLoginFunc, + onLogout: logoutFunc, + } +} + +func (idp *SSAMLIdpInstance) AddHandlers(app *appsrv.Application, prefix string) { + idp.metadataPath = httputils.JoinPath(prefix, "metadata") + idp.redirectLoginPath = httputils.JoinPath(prefix, "redirect/login") + idp.redirectLogoutPath = httputils.JoinPath(prefix, "redirect/logout") + idp.idpInitiatedSSOPath = httputils.JoinPath(prefix, "sso") + + app.AddHandler("GET", idp.metadataPath, idp.metadataHandler) + app.AddHandler("GET", idp.redirectLoginPath, idp.redirectLoginHandler) + app.AddHandler("GET", idp.redirectLogoutPath, idp.redirectLogoutHandler) + app.AddHandler("GET", idp.idpInitiatedSSOPath, idp.idpInitiatedSSOHandler) + + log.Infof("IDP metadata: %s", idp.getMetadataUrl()) + log.Infof("IDP redirect login: %s", idp.getRedirectLoginUrl()) + log.Infof("IDP redirect logout: %s", idp.getRedirectLogoutUrl()) + log.Infof("IDP initated SSO: %s", idp.getIdpInitiatedSSOUrl()) +} + +func (idp *SSAMLIdpInstance) SetHtmlTemplate(tmp string) error { + if strings.Index(tmp, samlutils.HTML_SAML_FORM_TOKEN) < 0 { + return errors.Wrapf(httperrors.ErrInvalidFormat, "no %s found", samlutils.HTML_SAML_FORM_TOKEN) + } + idp.htmlTemplate = tmp + return nil +} + +func (idp *SSAMLIdpInstance) AddSPMetadataFile(filename string) error { + metadata, err := ioutil.ReadFile(filename) + if err != nil { + return errors.Wrap(err, "ioutil.ReadFile") + } + return idp.AddSPMetadata(metadata) +} + +func (idp *SSAMLIdpInstance) AddSPMetadata(metadata []byte) error { + ed, err := samlutils.ParseMetadata(metadata) + if err != nil { + return errors.Wrap(err, "samlutils.ParseMetadata") + } + sp := &SSAMLServiceProvider{desc: ed} + err = sp.IsValid() + if err != nil { + return errors.Wrap(err, "NewSAMLServiceProvider") + } + log.Debugf("Register SP metadata: %s", sp.GetEntityId()) + idp.serviceProviders = append(idp.serviceProviders, sp) + return nil +} + +func (idp *SSAMLIdpInstance) getMetadataUrl() string { + return httputils.JoinPath(idp.saml.GetEntityId(), idp.metadataPath) +} + +func (idp *SSAMLIdpInstance) getRedirectLoginUrl() string { + return httputils.JoinPath(idp.saml.GetEntityId(), idp.redirectLoginPath) +} + +func (idp *SSAMLIdpInstance) getRedirectLogoutUrl() string { + return httputils.JoinPath(idp.saml.GetEntityId(), idp.redirectLogoutPath) +} + +func (idp *SSAMLIdpInstance) getIdpInitiatedSSOUrl() string { + return httputils.JoinPath(idp.saml.GetEntityId(), idp.idpInitiatedSSOPath) +} + +func (idp *SSAMLIdpInstance) metadataHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { + desc := idp.getMetadata(ctx) + appsrv.SendXmlWithIndent(w, nil, desc, true) +} + +func (idp *SSAMLIdpInstance) redirectLoginHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { + _, query, _ := appsrv.FetchEnv(ctx, w, r) + input := samlutils.SIdpRedirectLoginInput{} + err := query.Unmarshal(&input) + if err != nil { + httperrors.InputParameterError(w, "query.Unmarshal error %s", err) + return + } + log.Debugf("recv input %s", input) + respHtml, err := idp.processLoginRequest(ctx, input) + if err != nil { + httperrors.InputParameterError(w, "parse parameter error %s", err) + return + } + appsrv.SendHTML(w, respHtml) +} + +func (idp *SSAMLIdpInstance) redirectLogoutHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { + log.Debugf("logout: %s", r.Header) + html := idp.onLogout(ctx) + appsrv.SendHTML(w, html) +} + +func (idp *SSAMLIdpInstance) idpInitiatedSSOHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { + _, query, _ := appsrv.FetchEnv(ctx, w, r) + input := samlutils.SIdpInitiatedLoginInput{} + err := query.Unmarshal(&input) + if err != nil { + httperrors.InputParameterError(w, "unmarshal input fail %s", err) + return + } + respHtml, err := idp.processIdpInitiatedLogin(ctx, input) + if err != nil { + httperrors.GeneralServerError(w, err) + return + } + appsrv.SendHTML(w, respHtml) +} + +func (idp *SSAMLIdpInstance) getMetadata(ctx context.Context) samlutils.EntityDescriptor { + input := samlutils.SSAMLIdpMetadataInput{ + EntityId: idp.saml.GetEntityId(), + CertString: idp.saml.GetCertString(), + RedirectLoginUrl: idp.getRedirectLoginUrl(), + RedirectLogoutUrl: idp.getRedirectLogoutUrl(), + } + hostId := appctx.AppContextHostId(ctx) + return samlutils.NewIdpMetadata(hostId, input) +} + +func (idp *SSAMLIdpInstance) processLoginRequest(ctx context.Context, input samlutils.SIdpRedirectLoginInput) (string, error) { + plainText, err := samlutils.SAMLDecode(input.SAMLRequest) + if err != nil { + return "", errors.Wrap(err, "samlutils.SAMLDecode") + } + + log.Debugf("AuthnRequest: %s", string(plainText)) + + authReq := samlutils.AuthnRequest{} + err = xml.Unmarshal(plainText, &authReq) + if err != nil { + return "", errors.Wrap(err, "xml.Unmarshal") + } + + sp := idp.getServiceProvider(authReq.Issuer.Issuer) + if sp == nil { + return "", errors.Wrapf(httperrors.ErrResourceNotFound, "issuer %s not found", authReq.Issuer.Issuer) + } + + if len(authReq.Destination) > 0 && authReq.Destination != idp.getRedirectLoginUrl() { + return "", errors.Wrapf(httperrors.ErrInputParameter, "Destination not match: get %s want %s", authReq.Destination, idp.getRedirectLoginUrl()) + } + + if authReq.AssertionConsumerServiceURL != sp.GetPostAssertionConsumerServiceUrl() { + return "", errors.Wrapf(httperrors.ErrInputParameter, "AssertionConsumerServiceURL not match: get %s want %s", authReq.AssertionConsumerServiceURL, sp.GetPostAssertionConsumerServiceUrl()) + } + + resp, err := idp.getLoginResponse(ctx, authReq, sp) + if err != nil { + return "", errors.Wrap(err, "getLoginResponse") + } + + form, err := idp.samlResponse2Form(authReq.AssertionConsumerServiceURL, resp, input.RelayState) + if err != nil { + return "", errors.Wrap(err, "samlResponse2Form") + } + + return form, nil +} + +func (idp *SSAMLIdpInstance) samlResponse2Form(url string, resp *samlutils.Response, state string) (string, error) { + respXml, err := xml.Marshal(resp) + if err != nil { + return "", errors.Wrap(err, "xml.Marshal") + } + + signed, err := idp.saml.SignXML(string(respXml)) + if err != nil { + return "", errors.Wrap(err, "saml.SignXML") + } + + log.Debugf("ResponseXML: %s", signed) + + samlResp := base64.StdEncoding.EncodeToString([]byte(signed)) + + form := samlutils.SAMLForm(url, map[string]string{ + "SAMLResponse": samlResp, + "RelayState": state, + }) + template := samlutils.DEFAULT_HTML_TEMPLATE + if len(idp.htmlTemplate) > 0 { + template = idp.htmlTemplate + } + form = strings.Replace(template, samlutils.HTML_SAML_FORM_TOKEN, form, 1) + return form, nil +} + +func (idp *SSAMLIdpInstance) getServiceProvider(eId string) *SSAMLServiceProvider { + for _, sp := range idp.serviceProviders { + if sp.GetEntityId() == eId { + return sp + } + } + return nil +} + +func (idp *SSAMLIdpInstance) getLoginResponse(ctx context.Context, req samlutils.AuthnRequest, sp *SSAMLServiceProvider) (*samlutils.Response, error) { + data := idp.onSpInitiatedLogin(ctx, sp) + input := samlutils.SSAMLResponseInput{ + IssuerCertString: idp.saml.GetCertString(), + IssuerEntityId: idp.saml.GetEntityId(), + RequestID: req.ID, + RequestEntityId: req.Issuer.Issuer, + AssertionConsumerServiceURL: req.AssertionConsumerServiceURL, + SSAMLSpInitiatedLoginData: data, + } + resp := samlutils.NewResponse(input) + return &resp, nil +} + +func (idp *SSAMLIdpInstance) processIdpInitiatedLogin(ctx context.Context, input samlutils.SIdpInitiatedLoginInput) (string, error) { + sp := idp.getServiceProvider(input.EntityID) + if sp == nil { + return "", errors.Wrapf(httperrors.ErrResourceNotFound, "issuer %s not found", input.EntityID) + } + var state []byte + if len(input.State) > 0 { + var err error + state, err = samlutils.SAMLDecode(input.State) + if err != nil { + return "", errors.Wrapf(httperrors.ErrInputParameter, "invalid state %s: %s", input.State, err) + } + } + data := idp.onIdpInitiatedLogin(ctx, sp, string(state)) + respInput := samlutils.SSAMLResponseInput{ + IssuerCertString: idp.saml.GetCertString(), + IssuerEntityId: idp.saml.GetEntityId(), + RequestID: "", + RequestEntityId: sp.GetEntityId(), + AssertionConsumerServiceURL: sp.GetPostAssertionConsumerServiceUrl(), + SSAMLSpInitiatedLoginData: data.SSAMLSpInitiatedLoginData, + } + resp := samlutils.NewResponse(respInput) + form, err := idp.samlResponse2Form(sp.GetPostAssertionConsumerServiceUrl(), &resp, data.RelayState) + if err != nil { + return "", errors.Wrap(err, "samlResponse2Form") + } + return form, nil +} diff --git a/pkg/util/samlutils/idp/sp.go b/pkg/util/samlutils/idp/sp.go new file mode 100644 index 0000000000..cc2ae96668 --- /dev/null +++ b/pkg/util/samlutils/idp/sp.go @@ -0,0 +1,56 @@ +// Copyright 2019 Yunion +// +// 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. + +package idp + +import ( + "context" + + "yunion.io/x/pkg/errors" + + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/util/samlutils" +) + +type ISAMLSpDriver interface { + RenderResponse(ctx context.Context, resp samlutils.Response) samlutils.Response +} + +type SSAMLServiceProvider struct { + desc samlutils.EntityDescriptor + driver ISAMLSpDriver +} + +func (sp *SSAMLServiceProvider) GetEntityId() string { + return sp.desc.EntityId +} + +func (sp *SSAMLServiceProvider) GetPostAssertionConsumerServiceUrl() string { + for _, srv := range sp.desc.SPSSODescriptor.AssertionConsumerServices { + if srv.Binding == samlutils.BINDING_HTTP_POST { + return srv.Location + } + } + return "" +} + +func (sp *SSAMLServiceProvider) IsValid() error { + if sp.GetEntityId() == "" { + return errors.Wrap(httperrors.ErrInputParameter, "empty entityID") + } + if sp.GetPostAssertionConsumerServiceUrl() == "" { + return errors.Wrap(httperrors.ErrInputParameter, "empty HTTP_Post AssertionConsumerServiceUrl") + } + return nil +} diff --git a/pkg/util/samlutils/instance.go b/pkg/util/samlutils/instance.go new file mode 100644 index 0000000000..22e38ddf39 --- /dev/null +++ b/pkg/util/samlutils/instance.go @@ -0,0 +1,59 @@ +// Copyright 2019 Yunion +// +// 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. + +package samlutils + +import ( + "crypto/rsa" + "crypto/x509" + + "yunion.io/x/pkg/errors" +) + +type SSAMLInstance struct { + entityID string + + privateKeyFile string + certFile string + certString string + + privateKey *rsa.PrivateKey + + certs []*x509.Certificate +} + +func NewSAMLInstance(entityID string, cert, key string) (*SSAMLInstance, error) { + saml := SSAMLInstance{ + privateKeyFile: key, + certFile: cert, + entityID: entityID, + } + err := saml.parseKeys() + if err != nil { + return nil, errors.Wrap(err, "saml.parseKeys") + } + return &saml, nil +} + +func (saml *SSAMLInstance) GetEntityId() string { + return saml.entityID +} + +func (saml *SSAMLInstance) GetCertString() string { + return "\n" + saml.certString + "\n" +} + +func (saml *SSAMLInstance) SignXML(xmlstr string) (string, error) { + return SignXML(xmlstr, saml.privateKey) +} diff --git a/pkg/util/samlutils/metadata.go b/pkg/util/samlutils/metadata.go new file mode 100644 index 0000000000..4b01321c2b --- /dev/null +++ b/pkg/util/samlutils/metadata.go @@ -0,0 +1,138 @@ +// Copyright 2019 Yunion +// +// 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. + +package samlutils + +import ( + "encoding/xml" + + "yunion.io/x/pkg/errors" +) + +func ParseMetadata(data []byte) (EntityDescriptor, error) { + ed := EntityDescriptor{} + err := xml.Unmarshal(data, &ed) + if err != nil { + return ed, errors.Wrap(err, "xml.Unmarshal") + } + return ed, nil +} + +type SSAMLIdpMetadataInput struct { + EntityId string + CertString string + RedirectLoginUrl string + RedirectLogoutUrl string +} + +func NewIdpMetadata(id string, input SSAMLIdpMetadataInput) EntityDescriptor { + desc := EntityDescriptor{ + XMLName: xml.Name{ + Space: XMLNS_MD, + Local: "EntityDescriptor", + }, + EntityId: input.EntityId, + IDPSSODescriptor: &SSODescriptor{ + XMLName: xml.Name{ + Space: XMLNS_MD, + Local: "IDPSSODescriptor", + }, + ProtocolSupportEnumeration: PROTOCOL_SAML2, + KeyDescriptors: []KeyDescriptor{ + { + XMLName: xml.Name{ + Space: XMLNS_MD, + Local: "KeyDescriptor", + }, + Use: KEY_USE_SIGNING, + KeyInfo: KeyInfo{ + XMLName: xml.Name{ + Space: XMLNS_DS, + Local: "KeyInfo", + }, + X509Data: X509Data{ + XMLName: xml.Name{ + Space: XMLNS_DS, + Local: "X509Data", + }, + X509Certificate: X509Certificate{ + XMLName: xml.Name{ + Space: XMLNS_DS, + Local: "X509Certificate", + }, + Cert: input.CertString, + }, + }, + }, + }, + { + XMLName: xml.Name{ + Space: XMLNS_MD, + Local: "KeyDescriptor", + }, + Use: KEY_USE_ENCRYPTION, + KeyInfo: KeyInfo{ + XMLName: xml.Name{ + Space: XMLNS_DS, + Local: "KeyInfo", + }, + X509Data: X509Data{ + XMLName: xml.Name{ + Space: XMLNS_DS, + Local: "X509Data", + }, + X509Certificate: X509Certificate{ + XMLName: xml.Name{ + Space: XMLNS_DS, + Local: "X509Certificate", + }, + Cert: input.CertString, + }, + }, + }, + }, + }, + SingleLogoutServices: []SSAMLService{ + { + XMLName: xml.Name{ + Space: XMLNS_MD, + Local: "SingleLogoutService", + }, + Binding: BINDING_HTTP_REDIRECT, + Location: input.RedirectLogoutUrl, + }, + }, + NameIDFormat: []SSAMLNameIDFormat{ + { + XMLName: xml.Name{ + Space: XMLNS_MD, + Local: "NameIDFormat", + }, + Format: NAME_ID_FORMAT_TRANSIENT, + }, + }, + SingleSignOnServices: []SSAMLService{ + { + XMLName: xml.Name{ + Space: XMLNS_MD, + Local: "SingleSignOnService", + }, + Binding: BINDING_HTTP_REDIRECT, + Location: input.RedirectLoginUrl, + }, + }, + }, + } + return desc +} diff --git a/pkg/util/samlutils/metadata_test.go b/pkg/util/samlutils/metadata_test.go new file mode 100644 index 0000000000..54e07799e6 --- /dev/null +++ b/pkg/util/samlutils/metadata_test.go @@ -0,0 +1,670 @@ +// Copyright 2019 Yunion +// +// 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. + +package samlutils + +import ( + "encoding/xml" + "testing" +) + +var ( + spMetadata1 = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SAMLtest SP + A free and basic SP for testing SAML deployments + https://samltest.id/saml/logo.png + + + + + + + + +MIIERTCCAq2gAwIBAgIJAKmtzjCD1+tqMA0GCSqGSIb3DQEBCwUAMDUxMzAxBgNV +BAMTKmlwLTE3Mi0zMS0yOC02NC51cy13ZXN0LTIuY29tcHV0ZS5pbnRlcm5hbDAe +Fw0xODA4MTgyMzI0MjNaFw0yODA4MTUyMzI0MjNaMDUxMzAxBgNVBAMTKmlwLTE3 +Mi0zMS0yOC02NC51cy13ZXN0LTIuY29tcHV0ZS5pbnRlcm5hbDCCAaIwDQYJKoZI +hvcNAQEBBQADggGPADCCAYoCggGBALhUlY3SkIOze+l8y6dBzM6p7B8OykJWlwiz +szU16Lih8D7KLhNJfahoVxbPxB3YFM/81PJLOeK2krvJ5zY6CJyQY3sPQAkZKI7I +8qq9lmZ2g4QPqybNstXS6YUXJNUt/ixbbK/N97+LKTiSutbD1J7AoFnouMuLjlhN +5VRZ43jez4xLSHVZaYuUFKn01Y9oLKbj46LQnZnJCAGpTgPqEQJr6GpVGw43bKyU +pGoaPrdDRgRgtPMUWgFDkgcI3QiV1lsKfBs1t1E2UA7ACFnlJZpEuBtwgivzo3Ve +itiSaF3Jxh25EY5/vABpcgQQRz3RH2l8MMKdRsxb8VT3yh2S+CX55s+cN67LiCPr +6f2u+KS1iKfB9mWN6o2S4lcmo82HIBbsuXJV0oA1HrGMyyc4Y9nng/I8iuAp8or1 +JrWRHQ+8NzO85DWK0rtvtLPxkvw0HK32glyuOP/9F05Z7+tiVIgn67buC0EdoUm1 +RSpibqmB1ST2PikslOlVbJuy4Ah93wIDAQABo1gwVjA1BgNVHREELjAsgippcC0x +NzItMzEtMjgtNjQudXMtd2VzdC0yLmNvbXB1dGUuaW50ZXJuYWwwHQYDVR0OBBYE +FAdsTxYfulJ5yunYtgYJHC9IcevzMA0GCSqGSIb3DQEBCwUAA4IBgQB3J6i7Krei +HL8NPMglfWLHk1PZOgvIEEpKL+GRebvcbyqgcuc3VVPylq70VvGqhJxp1q/mzLfr +aUiypzfWFGm9zfwIg0H5TqRZYEPTvgIhIICjaDWRwZBDJG8D5G/KoV60DlUG0crP +BlIuCCr/SRa5ZoDQqvucTfr3Rx4Ha6koXFSjoSXllR+jn4GnInhm/WH137a+v35P +UcffNxfuehoGn6i4YeXF3cwJK4e35cOFW+dLbnaLk+Ty7HOGvpw86h979C6mJ9qE +HYgq9rQyzlSPbLZGZSgVcIezunOaOsWm81BsXRNNJjzHGCqKf8RMhd8oZP55+2/S +VRBwnkGyUNCuDPrJcymC95ZT2NW/KeWkz28HF2i31xQmecT2r3lQRSM8acvOXQsN +EDCDvJvCzJT9c2AnsnO24r6arPXs/UWAxOI+MjclXPLkLD6uTHV+Oo8XZ7bOjegD +5hL6/bKUWnNMurQNGrmi/jvqsCFLDKftl7ajuxKjtodnSuwhoY7NQy8= + + + + + + + + + +MIIERTCCAq2gAwIBAgIJAKGA/tV7hXUvMA0GCSqGSIb3DQEBCwUAMDUxMzAxBgNV +BAMTKmlwLTE3Mi0zMS0yOC02NC51cy13ZXN0LTIuY29tcHV0ZS5pbnRlcm5hbDAe +Fw0xODA4MTgyMzI0MjVaFw0yODA4MTUyMzI0MjVaMDUxMzAxBgNVBAMTKmlwLTE3 +Mi0zMS0yOC02NC51cy13ZXN0LTIuY29tcHV0ZS5pbnRlcm5hbDCCAaIwDQYJKoZI +hvcNAQEBBQADggGPADCCAYoCggGBANoi7TtbPz5DD5b+pGj2bWHUWcOm135Dl+kf +KWcJV6x4Z4VRMa33nwSfFg6U0DhPaA6rYr8BfcmCIY4V4cGlJkLNsYbgbZNnrLh2 +3mj7jkaUeyv/DlGtLBcqr0gP6eDtcOf3MMGAkhROcicMj6i+uF6hqLDh4eNcpqEV +DVn+ADBsosIPiAx+RkcyZkfAF3UeGEV5WTSiQw7qYpI7x+c4ViiBzV4waBgXjvNN +72Dqlc01AylpmMKaUPfxIpPC+Ctr0bHu5xn7NxMS8Zt5NDWsP9T15qrpYatW68sX +VyE5nJRYpiRiRbo8i7QpUEya+TkXEI8PVD3KBw9UwhqL8qPPe0T+EeaawF6BVRTE +Pc+Mn4lGBr4cCFcGk/PLHeyksgPdjNmO1g7y5TWQzu21WzkXRTWJq7wGwWeW6Nrc +NqweYPLbXEo0JlmHqunkUs+NsLQAFqSPX02P2xzkA/eOU2o/jN4jAPNpzqxJouvm +iWGXl8Qy4U7vQZ0tGvlTDSltATOQ/QIDAQABo1gwVjA1BgNVHREELjAsgippcC0x +NzItMzEtMjgtNjQudXMtd2VzdC0yLmNvbXB1dGUuaW50ZXJuYWwwHQYDVR0OBBYE +FBBtS9YNKSIwViH37GJCTxjNBzLAMA0GCSqGSIb3DQEBCwUAA4IBgQDWXcaI7zMn +hGsLVTUA6dgzZCa88QkN/Z6n7lCY2oaKj1neBAWA1Mxg7GBJsmLOrHN8ie0D/uKA +F+7NqKCXYqd0PpTX7c1NICL92DvbugG/Ow50j5Dw6rU4Y8dPS7Y/T1ddbT2F9/5l +HCIWP/O2E9HREJ0JAIbu/Mi0CE1qui2aSJMDWKuiGK63M/7fvP51m6xSJOfZBhmj +gllIwEhIzfh4hVPhH0C7iqVls34UyLCZ8IZOCuGPJyTaJN6Pi3Uo1Otkz/1igN5M +pQhVaeYG7SMgha6skTLrVXTt4CuMVsOZ6cG3kHqw8XZoRld+I50iyHqansf5qwzm +NoPeXyjGRFQzV/EH3SUu8eAISTt9pfirwjKsVNHrmMRnQEB/hJYYbTWSsvdS8ghw +7a/A0EKQPVaZGCP/hcpt9JMMb66y2L8VgBbb6aTsR+Uabf6aiMnj1UBMUz9yaMka +kKM7e66uHdXUDZ/s8F5rPOGCK+O8O6EsLRf8XetRWLa1TXRDkJZVPX4= + + + + + + + + + + + + + + + + + + + + + + + + + + + + +` + + idpMetadata = ` + + + + + + samltest.id + + + + SAMLtest IdP + A free and basic IdP for testing SAML deployments + https://samltest.id/saml/logo.png + + + + + + + +MIIDETCCAfmgAwIBAgIUZRpDhkNKl5eWtJqk0Bu1BgTTargwDQYJKoZIhvcNAQEL +BQAwFjEUMBIGA1UEAwwLc2FtbHRlc3QuaWQwHhcNMTgwODI0MjExNDEwWhcNMzgw +ODI0MjExNDEwWjAWMRQwEgYDVQQDDAtzYW1sdGVzdC5pZDCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAJrh9/PcDsiv3UeL8Iv9rf4WfLPxuOm9W6aCntEA +8l6c1LQ1Zyrz+Xa/40ZgP29ENf3oKKbPCzDcc6zooHMji2fBmgXp6Li3fQUzu7yd ++nIC2teejijVtrNLjn1WUTwmqjLtuzrKC/ePoZyIRjpoUxyEMJopAd4dJmAcCq/K +k2eYX9GYRlqvIjLFoGNgy2R4dWwAKwljyh6pdnPUgyO/WjRDrqUBRFrLQJorR2kD +c4seZUbmpZZfp4MjmWMDgyGM1ZnR0XvNLtYeWAyt0KkSvFoOMjZUeVK/4xR74F8e +8ToPqLmZEg9ZUx+4z2KjVK00LpdRkH9Uxhh03RQ0FabHW6UCAwEAAaNXMFUwHQYD +VR0OBBYEFJDbe6uSmYQScxpVJhmt7PsCG4IeMDQGA1UdEQQtMCuCC3NhbWx0ZXN0 +LmlkhhxodHRwczovL3NhbWx0ZXN0LmlkL3NhbWwvaWRwMA0GCSqGSIb3DQEBCwUA +A4IBAQBNcF3zkw/g51q26uxgyuy4gQwnSr01Mhvix3Dj/Gak4tc4XwvxUdLQq+jC +cxr2Pie96klWhY/v/JiHDU2FJo9/VWxmc/YOk83whvNd7mWaNMUsX3xGv6AlZtCO +L3JhCpHjiN+kBcMgS5jrtGgV1Lz3/1zpGxykdvS0B4sPnFOcaCwHe2B9SOCWbDAN +JXpTjz1DmJO4ImyWPJpN1xsYKtm67Pefxmn0ax0uE2uuzq25h0xbTkqIQgJzyoE/ +DPkBFK1vDkMfAW11dQ0BXatEnW7Gtkc0lh2/PIbHWj4AzxYMyBf5Gy6HSVOftwjC +voQR2qr2xJBixsg+MIORKtmKHLfU + + + + + + + + + +MIIDEjCCAfqgAwIBAgIVAMECQ1tjghafm5OxWDh9hwZfxthWMA0GCSqGSIb3DQEB +CwUAMBYxFDASBgNVBAMMC3NhbWx0ZXN0LmlkMB4XDTE4MDgyNDIxMTQwOVoXDTM4 +MDgyNDIxMTQwOVowFjEUMBIGA1UEAwwLc2FtbHRlc3QuaWQwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQC0Z4QX1NFKs71ufbQwoQoW7qkNAJRIANGA4iM0 +ThYghul3pC+FwrGv37aTxWXfA1UG9njKbbDreiDAZKngCgyjxj0uJ4lArgkr4AOE +jj5zXA81uGHARfUBctvQcsZpBIxDOvUUImAl+3NqLgMGF2fktxMG7kX3GEVNc1kl +bN3dfYsaw5dUrw25DheL9np7G/+28GwHPvLb4aptOiONbCaVvh9UMHEA9F7c0zfF +/cL5fOpdVa54wTI0u12CsFKt78h6lEGG5jUs/qX9clZncJM7EFkN3imPPy+0HC8n +spXiH/MZW8o2cqWRkrw3MzBZW3Ojk5nQj40V6NUbjb7kfejzAgMBAAGjVzBVMB0G +A1UdDgQWBBQT6Y9J3Tw/hOGc8PNV7JEE4k2ZNTA0BgNVHREELTArggtzYW1sdGVz +dC5pZIYcaHR0cHM6Ly9zYW1sdGVzdC5pZC9zYW1sL2lkcDANBgkqhkiG9w0BAQsF +AAOCAQEASk3guKfTkVhEaIVvxEPNR2w3vWt3fwmwJCccW98XXLWgNbu3YaMb2RSn +7Th4p3h+mfyk2don6au7Uyzc1Jd39RNv80TG5iQoxfCgphy1FYmmdaSfO8wvDtHT +TNiLArAxOYtzfYbzb5QrNNH/gQEN8RJaEf/g/1GTw9x/103dSMK0RXtl+fRs2nbl +D1JJKSQ3AdhxK/weP3aUPtLxVVJ9wMOQOfcy02l+hHMb6uAjsPOpOVKqi3M8XmcU +ZOpx4swtgGdeoSpeRyrtMvRwdcciNBp9UZome44qZAYH1iqrpmmjsfI9pJItsgWu +3kXPjhSfj1AJGR1l9JGvJrHki1iHTA== + + + + + + + + + +MIIDEjCCAfqgAwIBAgIVAPVbodo8Su7/BaHXUHykx0Pi5CFaMA0GCSqGSIb3DQEB +CwUAMBYxFDASBgNVBAMMC3NhbWx0ZXN0LmlkMB4XDTE4MDgyNDIxMTQwOVoXDTM4 +MDgyNDIxMTQwOVowFjEUMBIGA1UEAwwLc2FtbHRlc3QuaWQwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCQb+1a7uDdTTBBFfwOUun3IQ9nEuKM98SmJDWa +MwM877elswKUTIBVh5gB2RIXAPZt7J/KGqypmgw9UNXFnoslpeZbA9fcAqqu28Z4 +sSb2YSajV1ZgEYPUKvXwQEmLWN6aDhkn8HnEZNrmeXihTFdyr7wjsLj0JpQ+VUlc +4/J+hNuU7rGYZ1rKY8AA34qDVd4DiJ+DXW2PESfOu8lJSOteEaNtbmnvH8KlwkDs +1NvPTsI0W/m4SK0UdXo6LLaV8saIpJfnkVC/FwpBolBrRC/Em64UlBsRZm2T89ca +uzDee2yPUvbBd5kLErw+sC7i4xXa2rGmsQLYcBPhsRwnmBmlAgMBAAGjVzBVMB0G +A1UdDgQWBBRZ3exEu6rCwRe5C7f5QrPcAKRPUjA0BgNVHREELTArggtzYW1sdGVz +dC5pZIYcaHR0cHM6Ly9zYW1sdGVzdC5pZC9zYW1sL2lkcDANBgkqhkiG9w0BAQsF +AAOCAQEABZDFRNtcbvIRmblnZItoWCFhVUlq81ceSQddLYs8DqK340//hWNAbYdj +WcP85HhIZnrw6NGCO4bUipxZXhiqTA/A9d1BUll0vYB8qckYDEdPDduYCOYemKkD +dmnHMQWs9Y6zWiYuNKEJ9mf3+1N8knN/PK0TYVjVjXAf2CnOETDbLtlj6Nqb8La3 +sQkYmU+aUdopbjd5JFFwbZRaj6KiHXHtnIRgu8sUXNPrgipUgZUOVhP0C0N5OfE4 +JW8ZBrKgQC/6vJ2rSa9TlzI6JAa5Ww7gMXMP9M+cJUNQklcq+SBnTK8G+uBHgPKR +zBDsMIEzRtQZm4GIoHJae4zmnCekkQ== + + + + + + + + + + + + + + + + + + + + + + + + +` + + hwSpMetadata = ` + + + + + + + + + + +diBMwyuN633Q/kBf0M+SQZ4fNCI= + + + +k6+6QX8T5+S5wEIWlxwvd5a48xLWmp4Jd0bcs33vNaQVQ93YiztGcroUUqnLYK8uqAsmk2ZP4NtKFL/MyqoznrStglSi7uycwX6X3fswMwvkHJ9UVhH6Gp2Txl2JQ9/0vXy/tlXjQNFbfqzLhqxKC/K/PmKd684XkPYXlkiEJa6nGibu0gse9rvP2hE0G9bDMfdiyQnanfBvN+oNRte3xyI3DWh2P/jDTAPJMYzGM76JIneS8jLPa4gKDz5KcumG/8JV7GUWTDTZZ4ftujsjEuUFdPzKqwMYogMXOnTt8wbisSF8ZlLSMH/TIj6xAO9aSkEAo8HtlHJbekYlVA5Tz5IamLLzfaPpf7+NghLSuATIwY8/pvBZ8qhY8PVOjzRCeoEZJUrlFOcZ2CvO9zVKYdkEa1JC3mUW7CgeQc7G2/9niub7Vu00eyp9AAd9nkPfLoiWam8/yg2TBPRRJ9VKsv2UMFuITWrcFJayjezlTzY3dI3tI8lfoIMEVRaEP1v1D8XbxnxiaiKZXsGQHtpTSLc1ZL441jeZDLa661raUJDlGA6mBc1QukJklEocGg+Q+FeU33MJCaN/rIZCGvrjIZNng3yKFw1+R7/CqeJJlFWw0hPJQGiy6wfrDYSmhwuXJ2vQn3dTHjlT3kWsniiZtuOUi2TjHbq8gVHqlxmPxSs= + + + + +MIIF6TCCA9GgAwIBAgIEPHSCijANBgkqhkiG9w0BAQsFADCBpDELMAkGA1UEBhMCQ04xEjAQBgNV BAgTCUd1YW5nRG9uZzERMA8GA1UEBxMIU2hlblpoZW4xJTAjBgNVBAoTHEh1YXdlaSBUZWNobm9s b2dpZXMgQ28uLCBMdGQxKDAmBgNVBAsTH1NlcnZpY2UgUHJvdmlkZXIgT3BlcmF0aW9uIERlcHQx HTAbBgNVBAMTFGF1dGguaHVhd2VpY2xvdWQuY29tMB4XDTE4MDYyMTEzMjUwMFoXDTI4MDYxODEz MjUwMFowgaQxCzAJBgNVBAYTAkNOMRIwEAYDVQQIEwlHdWFuZ0RvbmcxETAPBgNVBAcTCFNoZW5a aGVuMSUwIwYDVQQKExxIdWF3ZWkgVGVjaG5vbG9naWVzIENvLiwgTHRkMSgwJgYDVQQLEx9TZXJ2 aWNlIFByb3ZpZGVyIE9wZXJhdGlvbiBEZXB0MR0wGwYDVQQDExRhdXRoLmh1YXdlaWNsb3VkLmNv bTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJisHObeLOPs2mJ4eJBcruMZl9FjvRQe DmofxgOcIcmybt6qlDqAv7275JMMQfQFcEoxH3GqcCTYqvSaSnHPaJB1xljPIKAWtd7p1ymevcSy F2HdCZ8gPJKg3Q6+ZjwTipKS/nZr7xmpUQ0WvwRkgZ7zfslAW+y5PCgkDrgmEjG92rLArj8iPhmu jajXaTPQKVHuZMxBzS735uo4yjVwYE3+0mE4HTZjK+6n6Ffu+JhLzhcKGulmrT/6qHisMbIXAZyE egBDavomb+5zu/CUQwii5IAPrRTwwegYpG4+uYJ2cHfrUdHqw9lSCSbQzu1yW1AS4zB16sjoHZdV rxYyktlswNmJ1/MyRH5bO90e2kvVwV4l34Hi5HEFvFFjL8TAsbN4mGvA9fgohXp30x97UdPVV8Ji NNAKZjdZSEdG9xqrTRfe4+LQg/hzLNSwsko3nDnH8qhCgtb8qIipQ3s7niCa53AQWYR82lEViols /dbWU9qYeldVvGNAgJSqHLB7qLwcQW78+2V1446KhQqzqPeLI4ANGaLFKw8fGzgh85RKOjrIetb4 wAOZmhrrUJrRg47DYQQjNv3glDg53ijLPFunzRqUoqLphrZ1XpEA4y21OtTP6OMYAM0lSOj1gjvb ubTDo8XOQs5YGtTOyHn4CQ4GR8NNo7UrwVEmoZHA+AKbAgMBAAGjITAfMB0GA1UdDgQWBBTt05QW 9dyXi7eMekKOH0bn4xKrkDANBgkqhkiG9w0BAQsFAAOCAgEACv9zgzUgxxQ8t9ldOXmirxzSOrHx MCL8SKsu+c+Y4hoHma5LFjylv6x76NWTAFSE6GgqfuNI/gPj/2AWqObAvsHd8lsPjJ96ZoSaTmS8 NrtU6HuT1Lc+CmVfeGd3/G+KspQECjg2JeBrfyEw9B8KUAQV20DQukGfAHtKKQPOZmKm0Qm3ExWC eXz1TR2KP+Lrhny/yG43g4iVUKq65HFHs5cRzRk0iR0/NLpggl5+Op0rxMxBn+bCrnJBi0n9/PIM fWNhkEBl+B++EifPUQxQaOEsnxTgFo1O4ksK9hDFcLbr+1qCDgVIMkyC1xMBBikCgLvIdzy3SXBN ndIEUq+QgOORxLlq1WqrfLFO22TxZm8XaB5g36UMk5PGoVHGnjALReAHjC0C5sIiKMJSgQOPd71X mQSsw3G9NsMKf/H3xJXkq/b672ls/l1JBslm52DAk2k5UlLkf/1p4I7WHOfm5ZNpDjj1rTP6SiAc tWLtqXIU28fLa2sA+zHXA5acDGOm6eIrMme5HpsV/KoUOW1MXGugK59zofeueCFDGRfbyoS2lj0S W+CbJVa72CLf3xPh2nWH0cK9de+wyCx8uI0KGPyV4I9/XBLHhvkb3XPaUfnkzYkcrG/39cOaxuPF z+haXwI1lvI964zvmTgwdDjdf/0asA09S7EEK2KyzXUREM4= + + + + + + + + + +MIIF6TCCA9GgAwIBAgIEPHSCijANBgkqhkiG9w0BAQsFADCBpDELMAkGA1UEBhMCQ04xEjAQBgNV BAgTCUd1YW5nRG9uZzERMA8GA1UEBxMIU2hlblpoZW4xJTAjBgNVBAoTHEh1YXdlaSBUZWNobm9s b2dpZXMgQ28uLCBMdGQxKDAmBgNVBAsTH1NlcnZpY2UgUHJvdmlkZXIgT3BlcmF0aW9uIERlcHQx HTAbBgNVBAMTFGF1dGguaHVhd2VpY2xvdWQuY29tMB4XDTE4MDYyMTEzMjUwMFoXDTI4MDYxODEz MjUwMFowgaQxCzAJBgNVBAYTAkNOMRIwEAYDVQQIEwlHdWFuZ0RvbmcxETAPBgNVBAcTCFNoZW5a aGVuMSUwIwYDVQQKExxIdWF3ZWkgVGVjaG5vbG9naWVzIENvLiwgTHRkMSgwJgYDVQQLEx9TZXJ2 aWNlIFByb3ZpZGVyIE9wZXJhdGlvbiBEZXB0MR0wGwYDVQQDExRhdXRoLmh1YXdlaWNsb3VkLmNv bTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJisHObeLOPs2mJ4eJBcruMZl9FjvRQe DmofxgOcIcmybt6qlDqAv7275JMMQfQFcEoxH3GqcCTYqvSaSnHPaJB1xljPIKAWtd7p1ymevcSy F2HdCZ8gPJKg3Q6+ZjwTipKS/nZr7xmpUQ0WvwRkgZ7zfslAW+y5PCgkDrgmEjG92rLArj8iPhmu jajXaTPQKVHuZMxBzS735uo4yjVwYE3+0mE4HTZjK+6n6Ffu+JhLzhcKGulmrT/6qHisMbIXAZyE egBDavomb+5zu/CUQwii5IAPrRTwwegYpG4+uYJ2cHfrUdHqw9lSCSbQzu1yW1AS4zB16sjoHZdV rxYyktlswNmJ1/MyRH5bO90e2kvVwV4l34Hi5HEFvFFjL8TAsbN4mGvA9fgohXp30x97UdPVV8Ji NNAKZjdZSEdG9xqrTRfe4+LQg/hzLNSwsko3nDnH8qhCgtb8qIipQ3s7niCa53AQWYR82lEViols /dbWU9qYeldVvGNAgJSqHLB7qLwcQW78+2V1446KhQqzqPeLI4ANGaLFKw8fGzgh85RKOjrIetb4 wAOZmhrrUJrRg47DYQQjNv3glDg53ijLPFunzRqUoqLphrZ1XpEA4y21OtTP6OMYAM0lSOj1gjvb ubTDo8XOQs5YGtTOyHn4CQ4GR8NNo7UrwVEmoZHA+AKbAgMBAAGjITAfMB0GA1UdDgQWBBTt05QW 9dyXi7eMekKOH0bn4xKrkDANBgkqhkiG9w0BAQsFAAOCAgEACv9zgzUgxxQ8t9ldOXmirxzSOrHx MCL8SKsu+c+Y4hoHma5LFjylv6x76NWTAFSE6GgqfuNI/gPj/2AWqObAvsHd8lsPjJ96ZoSaTmS8 NrtU6HuT1Lc+CmVfeGd3/G+KspQECjg2JeBrfyEw9B8KUAQV20DQukGfAHtKKQPOZmKm0Qm3ExWC eXz1TR2KP+Lrhny/yG43g4iVUKq65HFHs5cRzRk0iR0/NLpggl5+Op0rxMxBn+bCrnJBi0n9/PIM fWNhkEBl+B++EifPUQxQaOEsnxTgFo1O4ksK9hDFcLbr+1qCDgVIMkyC1xMBBikCgLvIdzy3SXBN ndIEUq+QgOORxLlq1WqrfLFO22TxZm8XaB5g36UMk5PGoVHGnjALReAHjC0C5sIiKMJSgQOPd71X mQSsw3G9NsMKf/H3xJXkq/b672ls/l1JBslm52DAk2k5UlLkf/1p4I7WHOfm5ZNpDjj1rTP6SiAc tWLtqXIU28fLa2sA+zHXA5acDGOm6eIrMme5HpsV/KoUOW1MXGugK59zofeueCFDGRfbyoS2lj0S W+CbJVa72CLf3xPh2nWH0cK9de+wyCx8uI0KGPyV4I9/XBLHhvkb3XPaUfnkzYkcrG/39cOaxuPF z+haXwI1lvI964zvmTgwdDjdf/0asA09S7EEK2KyzXUREM4= + + + + + + + + +MIIF6TCCA9GgAwIBAgIEPHSCijANBgkqhkiG9w0BAQsFADCBpDELMAkGA1UEBhMCQ04xEjAQBgNV BAgTCUd1YW5nRG9uZzERMA8GA1UEBxMIU2hlblpoZW4xJTAjBgNVBAoTHEh1YXdlaSBUZWNobm9s b2dpZXMgQ28uLCBMdGQxKDAmBgNVBAsTH1NlcnZpY2UgUHJvdmlkZXIgT3BlcmF0aW9uIERlcHQx HTAbBgNVBAMTFGF1dGguaHVhd2VpY2xvdWQuY29tMB4XDTE4MDYyMTEzMjUwMFoXDTI4MDYxODEz MjUwMFowgaQxCzAJBgNVBAYTAkNOMRIwEAYDVQQIEwlHdWFuZ0RvbmcxETAPBgNVBAcTCFNoZW5a aGVuMSUwIwYDVQQKExxIdWF3ZWkgVGVjaG5vbG9naWVzIENvLiwgTHRkMSgwJgYDVQQLEx9TZXJ2 aWNlIFByb3ZpZGVyIE9wZXJhdGlvbiBEZXB0MR0wGwYDVQQDExRhdXRoLmh1YXdlaWNsb3VkLmNv bTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJisHObeLOPs2mJ4eJBcruMZl9FjvRQe DmofxgOcIcmybt6qlDqAv7275JMMQfQFcEoxH3GqcCTYqvSaSnHPaJB1xljPIKAWtd7p1ymevcSy F2HdCZ8gPJKg3Q6+ZjwTipKS/nZr7xmpUQ0WvwRkgZ7zfslAW+y5PCgkDrgmEjG92rLArj8iPhmu jajXaTPQKVHuZMxBzS735uo4yjVwYE3+0mE4HTZjK+6n6Ffu+JhLzhcKGulmrT/6qHisMbIXAZyE egBDavomb+5zu/CUQwii5IAPrRTwwegYpG4+uYJ2cHfrUdHqw9lSCSbQzu1yW1AS4zB16sjoHZdV rxYyktlswNmJ1/MyRH5bO90e2kvVwV4l34Hi5HEFvFFjL8TAsbN4mGvA9fgohXp30x97UdPVV8Ji NNAKZjdZSEdG9xqrTRfe4+LQg/hzLNSwsko3nDnH8qhCgtb8qIipQ3s7niCa53AQWYR82lEViols /dbWU9qYeldVvGNAgJSqHLB7qLwcQW78+2V1446KhQqzqPeLI4ANGaLFKw8fGzgh85RKOjrIetb4 wAOZmhrrUJrRg47DYQQjNv3glDg53ijLPFunzRqUoqLphrZ1XpEA4y21OtTP6OMYAM0lSOj1gjvb ubTDo8XOQs5YGtTOyHn4CQ4GR8NNo7UrwVEmoZHA+AKbAgMBAAGjITAfMB0GA1UdDgQWBBTt05QW 9dyXi7eMekKOH0bn4xKrkDANBgkqhkiG9w0BAQsFAAOCAgEACv9zgzUgxxQ8t9ldOXmirxzSOrHx MCL8SKsu+c+Y4hoHma5LFjylv6x76NWTAFSE6GgqfuNI/gPj/2AWqObAvsHd8lsPjJ96ZoSaTmS8 NrtU6HuT1Lc+CmVfeGd3/G+KspQECjg2JeBrfyEw9B8KUAQV20DQukGfAHtKKQPOZmKm0Qm3ExWC eXz1TR2KP+Lrhny/yG43g4iVUKq65HFHs5cRzRk0iR0/NLpggl5+Op0rxMxBn+bCrnJBi0n9/PIM fWNhkEBl+B++EifPUQxQaOEsnxTgFo1O4ksK9hDFcLbr+1qCDgVIMkyC1xMBBikCgLvIdzy3SXBN ndIEUq+QgOORxLlq1WqrfLFO22TxZm8XaB5g36UMk5PGoVHGnjALReAHjC0C5sIiKMJSgQOPd71X mQSsw3G9NsMKf/H3xJXkq/b672ls/l1JBslm52DAk2k5UlLkf/1p4I7WHOfm5ZNpDjj1rTP6SiAc tWLtqXIU28fLa2sA+zHXA5acDGOm6eIrMme5HpsV/KoUOW1MXGugK59zofeueCFDGRfbyoS2lj0S W+CbJVa72CLf3xPh2nWH0cK9de+wyCx8uI0KGPyV4I9/XBLHhvkb3XPaUfnkzYkcrG/39cOaxuPF z+haXwI1lvI964zvmTgwdDjdf/0asA09S7EEK2KyzXUREM4= + + + + + + + + + + + +urn:oasis:names:tc:SAML:2.0:nameid-format:transient + + + +` + hwIdpMetadata = ` + + + + + +MIICsDCCAhmgAwIBAgIJAKNbH+B0Vm9HMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTgxMDMwMDIxMzA4WhcNMzMxMDMxMDIxMzA4WjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB +gQDIIZtsLpqLDpXB1LI8tbtwoeOyJbM2PIxJTOqRm1ZM0r7rpvt4kFCgAd68gAsl +YAEeSqUawxV3FUgt62DLMOT2auwBcpywVW7L/ZF4IUziwuFQLWdw5NIGMP5lpt1M +HSel8k4paokoXAwZ2B+Vtku+kDTGLc3cp1T5/ClYE/ofdQIDAQABo4GnMIGkMB0G +A1UdDgQWBBRVZlu4B6TzuNHasJz5tHoMilKLdjB1BgNVHSMEbjBsgBRVZlu4B6Tz +uNHasJz5tHoMilKLdqFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUt +U3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAKNbH+B0 +Vm9HMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAhyVdBqW4r94XdwMy +LK42mwqNnHy4WjM8eq9X5FhBckZX+TyM909iH2AsMjpkv8BDIxTiX6tpmNyYhOCp +vCPMmQHl9450maIA7At//sEgL94FNRJbTYkme7F3xI90X0htMr23Yan31lRwdj53 +DgagnkMlzQ8QccUXrdQgzXzKb0w= + + + + + + + +MIICsDCCAhmgAwIBAgIJAKNbH+B0Vm9HMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTgxMDMwMDIxMzA4WhcNMzMxMDMxMDIxMzA4WjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB +gQDIIZtsLpqLDpXB1LI8tbtwoeOyJbM2PIxJTOqRm1ZM0r7rpvt4kFCgAd68gAsl +YAEeSqUawxV3FUgt62DLMOT2auwBcpywVW7L/ZF4IUziwuFQLWdw5NIGMP5lpt1M +HSel8k4paokoXAwZ2B+Vtku+kDTGLc3cp1T5/ClYE/ofdQIDAQABo4GnMIGkMB0G +A1UdDgQWBBRVZlu4B6TzuNHasJz5tHoMilKLdjB1BgNVHSMEbjBsgBRVZlu4B6Tz +uNHasJz5tHoMilKLdqFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUt +U3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAKNbH+B0 +Vm9HMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAhyVdBqW4r94XdwMy +LK42mwqNnHy4WjM8eq9X5FhBckZX+TyM909iH2AsMjpkv8BDIxTiX6tpmNyYhOCp +vCPMmQHl9450maIA7At//sEgL94FNRJbTYkme7F3xI90X0htMr23Yan31lRwdj53 +DgagnkMlzQ8QccUXrdQgzXzKb0w= + + + + + + urn:oasis:names:tc:SAML:2.0:nameid-format:transient + + +` + aliyunSpMeta = ` + + + + + + MIIDXjCCAkagAwIBAgIEXHToLjANBgkqhkiG9w0BAQsFADBwMQswCQYDVQQGEwJDTjERMA8GA1UE + CBMISGFuZ3pob3UxKTAnBgNVBAsTIEFsaWJhYmEgQ2xvdWQgQ29tcHV0aW5nIENvLiBMdGQuMSMw + IQYDVQQDExp1cm46YWxpYmFiYTpjbG91ZGNvbXB1dGluZzAgFw0xOTAyMjYwNzE4MDZaGA8yMTE5 + MDIwMjA3MTgwNlowcDELMAkGA1UEBhMCQ04xETAPBgNVBAgTCEhhbmd6aG91MSkwJwYDVQQLEyBB + bGliYWJhIENsb3VkIENvbXB1dGluZyBDby4gTHRkLjEjMCEGA1UEAxMadXJuOmFsaWJhYmE6Y2xv + dWRjb21wdXRpbmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDDjD53ZENEHoYNXAOf + OIbVBJhj7SCWKmdjnbnxq8WAFWEeZtS6hZLPpWh1z7b0NjJkvCf5oFVqBJYbbW5kEKV+9CpV6VHZ + qOXmsIRlkvZB+Wnc3SduwiiiUR9JojSPxVQvSf4WLT+HDASlrBztuRV2vHj9utLbvy+6bgVBqF8g + emL9Pcif1robDH8HlqUcADXLAt18E4MbToldVoHjpFc6fAKUXujWH5feAL8g0CKlmf/JVlHLEtu4 + vKPxBQ8sgkysk6EnrjXl6Q4a4t+vbPG5uczA1ouTkDupMCRlaWHIHaJL/AoDGabn8sVXdaVJUKC5 + 54FNkRznBhRQll+Nuc2rAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAE8k4S4HvOglthJwF3aMQXGi + LKW6Becs9SljA0/5VtZQDrDf2By/1BIMvWfZ/dFnO+MylDLVdS6XWvWat/DW0fOGxU4s1WNfshX7 + DJDGR2G1XgtGoDZEYIDahUp5katAPypCkY57fGZlI0d3nq46/2qT/Zpne+pFE3DI/x8klZMniniw + YjNXbG96y/M4DYi1J7RR8mLIfVvz5o1SMGT4Ta2p/USE2M9F6O7/zc2j62dQgXiYa9OONo31RiXR + TmvGEUNuQoBZhVrFIvOnNjIfFT7Xd3CUowwJKP1floserrx4B5jScRAi9yK3x2a2lhfc+PksXfTs + aqIr5TQL4OorLEE= + + + + urn:oasis:names:tc:SAML:2.0:nameid-format:transient + urn:oasis:names:tc:SAML:2.0:nameid-format:persistent + urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified + urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName + urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName + urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos + urn:oasis:names:tc:SAML:2.0:nameid-format:entity + + + Alibaba Cloud Console Single Sign-On + + + + + + + Alibaba Cloud Computing Co. Ltd. + AlibabaCloud + https://www.aliyun.com + +` + + aliyunUserMeta = ` +MIIDUTCCAjmgAwIBAgIEIv2v9DANBgkqhkiG9w0BAQsFADBZMQswCQYDVQQGEwJDTjERMA8GA1UE +BxMISGFuZ3pob3UxFDASBgNVBAoTC0FsaWJhYmEgSW5jMQ8wDQYDVQQLEwZBcHNhcmExEDAOBgNV +BAMTB0FsaWJhYmEwHhcNMTcwMzE0MTc1OTE5WhcNMjcwMzEyMTc1OTE5WjBZMQswCQYDVQQGEwJD +TjERMA8GA1UEBxMISGFuZ3pob3UxFDASBgNVBAoTC0FsaWJhYmEgSW5jMQ8wDQYDVQQLEwZBcHNh +cmExEDAOBgNVBAMTB0FsaWJhYmEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqK2HR +tf4smv9pCQtPenFE1w6lvxsHiv0J/knvpC1BU4iAWcS8LxAElKb49QbKHuUxcwEGJfm0+zZpqS+J +I3jmGc4aHYACyL2WxtKNx/5EK1Qs5ugCipn7g+ySOqXxc/Rv2S7muw6LTrGVTT7vo09EUDkZM34s +TupuU7tzX0ktYhimxwskG9o7bvZuQKQf66gN8l/DUzyUl59/0wA1+x5A5B3pvaABCA6dq4mi8mtJ +fTXcqWm06+FgVNPgKo59uP6y08rQJXjKDwLIf0owuoiRrPLR5JKC1vQ6PSz0cGv8tGUts5dr/0zG +FHy4h3aufQiXCSi44WUB3FejQQfgEiBdAgMBAAGjITAfMB0GA1UdDgQWBBShWN61nZsWz9MYnSrV +kCkJnSdFtDANBgkqhkiG9w0BAQsFAAOCAQEAMMAl+C3oyI6kZNmvX05Sb0q6UAM8wqjFKbPhSSiy +srjVZwjEjiZnOSnoX8vO07fsZpcVmByHzGXWuBxxKCviCpQCS9hyOTF6bvAoXwe37h02Uhv3tKI0 +7FRkXJA7HeB0HEuHPCBxxWVWJfgtkeUETnGV06CrUlGON7Du3h37EUzfTqmKhlsqKeK8uqw3gLYq +Bp6ULrP1PbNo2AaHMYaZhFL1dSUtNYvekZppregZKMIDqtEm6Pwpw2lj8gjTC40PQ0GuXEeTsfE5 +dhw42xc9RkyUg1Go04k9Z/UMxTX0KVMiRZ9DF2FWjWp1AAQJ3TvZ2Ao/XOhmk4GWRehUoHr7Hw==urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddressurn:oasis:names:tc:SAML:2.0:nameid-format:transienturn:oasis:names:tc:SAML:2.0:nameid-format:persistenturn:oasis:names:tc:SAML:1.1:nameid-format:unspecified` +) + +func TestParseMetadata(t *testing.T) { + cases := []struct { + In string + }{ + { + In: spMetadata1, + }, + { + In: idpMetadata, + }, + { + In: hwSpMetadata, + }, + { + In: hwIdpMetadata, + }, + { + In: aliyunSpMeta, + }, + { + In: aliyunUserMeta, + }, + } + for _, c := range cases { + ed, err := ParseMetadata([]byte(c.In)) + if err != nil { + t.Errorf("ParseMetadata fail %s", err) + } else { + xmlstr, err := xml.MarshalIndent(ed, "", " ") + if err != nil { + t.Errorf("xml.Marshal fail %s", err) + } else { + t.Logf("%s", xmlstr) + } + } + } +} + +var ( + resp1 = ` + + + + + https://www.test.com + + + urn:oasis:names:tc:SAML:2.0:status:Success + + + + https://www.test.com + + + + + + + + + + + + + + + rFxrycznfGNYOnprZIFJJou4ro0Mz65+43MIR5F0+H4= + + + + + YqTWQngAPfGqQmWa610PM7LeefqWdKuveUVINrqL67NoHJIDa2WxLwdVzoJIlJh64QiNPr6+ndmL DCMgIC5F/9ijuzhIICZcc6lHNIjy6EsPkKRjfo9oeoVAqLgG/kmVQYeHLBID0y11RNXXpAVY4nhJ 26KiIVGt7ywyKAmhichE+eW/UYAGiOI5vkfgD2gZUGV+yPkv64k7xK4yAH3mL2NaCPuw/90e4enm iUx0YuazDwM5FiRUSMpcJs0rcNmS6clWAUcCzbOx+y2vJGtTjHb7k3UsmpnTop5eYNp94+sDPEat 8FaV4SgafMEL5z54gpe8+//9yOWEvlBs1b0RYg== + + + + + +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhK3L160NjP9EhBGQOC2s4r+Wc62bkRkc nUxfhiZwCwJdQCykzuLOAoATnfoEamV5W25xtSS5kFs+4OC0mYVpKcI3SWoydX+UE5Qik5UfJ8Dt G1AvSEKhSluyO9axrV5Uv089jMxBnlm/R+xND73WcZM11yIbKJEZSTCEDfh+KnFbMw108umFMden RZCrNWUJoSp/90XeG0V2Nmj7Fkq72skSifwIASLRq9KqLbmh1QwUX+AoWpHK/jRUBustMBmG1n1i + AqpD4EBjjBOB27k1wXZ30+IoJt8IZmfSZRFoNn5VFWXNeEmZ1aQvGSvd3Tyyw2/Wr+w/8Mags69C mpeX6QIDAQAB + + + + + + + Some NameID value + + + + + + + + + https://auth.huaweicloud.com/ + + + + + + ******* + + + + ******** + + + ****** + + + + + + ****** + + + ***** + + + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified + + + +` + resp2 = ` + https://idp.example.org/SAML2 + + + + + https://idp.example.org/SAML2 + + ... + + + 3f7b3dcf-1674-4ecd-92c8-1544f346baf8 + + + + + + + + https://sp.example.com/SAML2 + + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport + + + + + ` +) + +func TestParseResponse(t *testing.T) { + cases := []struct { + In string + }{ + { + In: resp1, + }, + { + In: resp2, + }, + } + for _, c := range cases { + resp := Response{} + err := xml.Unmarshal([]byte(c.In), &resp) + if err != nil { + t.Errorf("xml.Unmarshal %s", err) + } else { + xmlstr, err := xml.MarshalIndent(resp, "", " ") + if err != nil { + t.Errorf("xml.Marshal fail %s", err) + } else { + t.Logf("%s", xmlstr) + } + } + } +} diff --git a/pkg/util/samlutils/response.go b/pkg/util/samlutils/response.go new file mode 100644 index 0000000000..a7305b2c3d --- /dev/null +++ b/pkg/util/samlutils/response.go @@ -0,0 +1,352 @@ +// Copyright 2019 Yunion +// +// 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. + +package samlutils + +import ( + "encoding/xml" + "time" + + "yunion.io/x/pkg/util/timeutils" + "yunion.io/x/pkg/utils" +) + +type SSAMLResponseAttribute struct { + Name string + NameFormat string + FriendlyName string + Values []string +} + +type SSAMLSpInitiatedLoginData struct { + NameId string + NameIdFormat string + + AudienceRestriction string + + Attributes []SSAMLResponseAttribute +} + +type SSAMLIdpInitiatedLoginData struct { + SSAMLSpInitiatedLoginData + + RelayState string +} + +type SSAMLResponseInput struct { + IssuerEntityId string + + RequestEntityId string + RequestID string + + AssertionConsumerServiceURL string + + IssuerCertString string + + SSAMLSpInitiatedLoginData +} + +func NewResponse(input SSAMLResponseInput) Response { + // since := timeutils.IsoTime(time.Now().UTC().Add(-time.Minute * 60 * 24)) + until := timeutils.IsoTime(time.Now().UTC().Add(time.Minute * 60 * 24)) + + respId := "_" + utils.GenRequestId(16) + assertId := "_" + utils.GenRequestId(16) + now := timeutils.IsoTime(time.Now().UTC()) + issuerFormat := NAME_ID_FORMAT_ENTITY + + issuer := Issuer{ + XMLName: xml.Name{ + Space: XMLNS_ASSERT, + Local: "Issuer", + }, + Format: &issuerFormat, + Issuer: input.IssuerEntityId, + } + + var responseTo *string + if len(input.RequestID) > 0 { + responseTo = &input.RequestID + } + + resp := Response{ + XMLName: xml.Name{ + Space: XMLNS_PROTO, + Local: "Response", + }, + ID: respId, + InResponseTo: responseTo, + Version: SAML2_VERSION, + IssueInstant: now, + Destination: input.AssertionConsumerServiceURL, + Issuer: issuer, + Status: Status{ + XMLName: xml.Name{ + Space: XMLNS_PROTO, + Local: "Status", + }, + StatusCode: StatusCode{ + XMLName: xml.Name{ + Space: XMLNS_PROTO, + Local: "StatusCode", + }, + Value: STATUS_SUCCESS, + }, + StatusMessage: &StatusMessage{ + XMLName: xml.Name{ + Space: XMLNS_PROTO, + Local: "StatusMessage", + }, + Message: STATUS_SUCCESS, + }, + }, + Assertion: Assertion{ + XMLName: xml.Name{ + Space: XMLNS_ASSERT, + Local: "Assertion", + }, + ID: assertId, + Version: SAML2_VERSION, + IssueInstant: now, + Issuer: issuer, + Signature: &Signature{ + XMLName: xml.Name{ + Space: XMLNS_DS, + Local: "Signature", + }, + SignedInfo: SignedInfo{ + XMLName: xml.Name{ + Space: XMLNS_DS, + Local: "SignedInfo", + }, + CanonicalizationMethod: EncryptionMethod{ + XMLName: xml.Name{ + Space: XMLNS_DS, + Local: "CanonicalizationMethod", + }, + Algorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + }, + SignatureMethod: EncryptionMethod{ + XMLName: xml.Name{ + Space: XMLNS_DS, + Local: "SignatureMethod", + }, + Algorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + }, + Reference: Reference{ + XMLName: xml.Name{ + Space: XMLNS_DS, + Local: "Reference", + }, + URI: "#" + assertId, + Transforms: Transforms{ + XMLName: xml.Name{ + Space: XMLNS_DS, + Local: "Transforms", + }, + Transforms: []EncryptionMethod{ + { + XMLName: xml.Name{ + Space: XMLNS_DS, + Local: "Transform", + }, + Algorithm: "http://www.w3.org/2000/09/xmldsig#enveloped-signature", + }, + { + XMLName: xml.Name{ + Space: XMLNS_DS, + Local: "Transform", + }, + Algorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + }, + }, + }, + DigestMethod: EncryptionMethod{ + XMLName: xml.Name{ + Space: XMLNS_DS, + Local: "DigestMethod", + }, + Algorithm: "http://www.w3.org/2001/04/xmlenc#sha256", + }, + DigestValue: SSAMLValue{ + XMLName: xml.Name{ + Space: XMLNS_DS, + Local: "DigestValue", + }, + Value: "", + }, + }, + }, + SignatureValue: SSAMLValue{ + XMLName: xml.Name{ + Space: XMLNS_DS, + Local: "SignatureValue", + }, + Value: "", + }, + KeyInfo: KeyInfo{ + XMLName: xml.Name{ + Space: XMLNS_DS, + Local: "KeyInfo", + }, + X509Data: X509Data{ + XMLName: xml.Name{ + Space: XMLNS_DS, + Local: "X509Data", + }, + X509Certificate: X509Certificate{ + XMLName: xml.Name{ + Space: XMLNS_DS, + Local: "X509Certificate", + }, + Cert: input.IssuerCertString, + }, + }, + }, + }, + Subject: Subject{ + XMLName: xml.Name{ + Space: XMLNS_ASSERT, + Local: "Subject", + }, + NameID: NameID{ + XMLName: xml.Name{ + Space: XMLNS_ASSERT, + Local: "NameID", + }, + Format: input.NameIdFormat, + NameQualifier: &input.RequestEntityId, + Value: input.NameId, + }, + SubjectConfirmation: SubjectConfirmation{ + XMLName: xml.Name{ + Space: XMLNS_ASSERT, + Local: "SubjectConfirmation", + }, + Method: "urn:oasis:names:tc:SAML:2.0:cm:bearer", + SubjectConfirmationData: SubjectConfirmationData{ + XMLName: xml.Name{ + Space: XMLNS_ASSERT, + Local: "SubjectConfirmationData", + }, + InResponseTo: responseTo, + Recipient: input.AssertionConsumerServiceURL, + NotOnOrAfter: until, + }, + }, + }, + Conditions: Conditions{ + XMLName: xml.Name{ + Space: XMLNS_ASSERT, + Local: "Conditions", + }, + NotOnOrAfter: until, + AudienceRestrictions: []AudienceRestriction{}, + }, + AttributeStatement: &AttributeStatement{ + XMLName: xml.Name{ + Space: XMLNS_ASSERT, + Local: "AttributeStatement", + }, + Attributes: []Attribute{}, + }, + AuthnStatement: AuthnStatement{ + XMLName: xml.Name{ + Space: XMLNS_ASSERT, + Local: "AuthnStatement", + }, + AuthnInstant: now, + SessionIndex: assertId, + SubjectLocality: &SubjectLocality{ + XMLName: xml.Name{ + Space: XMLNS_ASSERT, + Local: "SubjectLocality", + }, + Address: input.RequestEntityId, + }, + AuthnContext: AuthnContext{ + XMLName: xml.Name{ + Space: XMLNS_ASSERT, + Local: "AuthnContext", + }, + AuthnContextClassRef: AuthnContextClassRef{ + XMLName: xml.Name{ + Space: XMLNS_ASSERT, + Local: "AuthnContextClassRef", + }, + // Value: "urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified", + Value: "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport", + }, + }, + }, + }, + } + + if len(input.AudienceRestriction) > 0 { + resp.AddAudienceRestriction(input.AudienceRestriction) + } + + for _, attr := range input.Attributes { + resp.AddAttribute(attr.Name, attr.FriendlyName, attr.NameFormat, attr.Values) + } + + return resp +} + +// AddAttribute add strong attribute to the Response +func (r *Response) AddAttribute(name string, friendlyName string, nameFormat string, values []string) { + attr := Attribute{ + XMLName: xml.Name{ + Space: XMLNS_ASSERT, + Local: "Attribute", + }, + Name: name, + AttributeValues: []AttributeValue{}, + } + if len(friendlyName) > 0 { + attr.FriendlyName = &friendlyName + } + if len(nameFormat) > 0 { + attr.NameFormat = &nameFormat + } + for _, value := range values { + attrValue := AttributeValue{ + XMLName: xml.Name{ + Space: XMLNS_ASSERT, + Local: "AttributeValue", + }, + Type: "xs:string", + Value: value, + } + attr.AttributeValues = append(attr.AttributeValues, attrValue) + } + r.Assertion.AttributeStatement.Attributes = append(r.Assertion.AttributeStatement.Attributes, attr) +} + +func (r *Response) AddAudienceRestriction(value string) { + restrict := AudienceRestriction{ + XMLName: xml.Name{ + Space: XMLNS_ASSERT, + Local: "AudienceRestriction", + }, + Audience: Audience{ + XMLName: xml.Name{ + Space: XMLNS_ASSERT, + Local: "Audience", + }, + Value: value, + }, + } + r.Assertion.Conditions.AudienceRestrictions = append(r.Assertion.Conditions.AudienceRestrictions, restrict) +} diff --git a/pkg/util/samlutils/response_test.go b/pkg/util/samlutils/response_test.go new file mode 100644 index 0000000000..c5c0435c86 --- /dev/null +++ b/pkg/util/samlutils/response_test.go @@ -0,0 +1,125 @@ +// Copyright 2019 Yunion +// +// 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. + +package samlutils + +import ( + "encoding/xml" + "testing" +) + +func TestNewResponse(t *testing.T) { + privateKeyString := `-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDKpaC0XoinzuQO +bt/VsWap0OVqN8O3cBFS41Ss9GGbjn1ZIg+D14OxywZdrRA17MKxfjhO4F+d2n8H +Ih5ZofGURKgOH8KrEIPcii5HLk1ZVnLXXbinExwG26OPAID9rYNSdBhVeR4pqHm7 +SBnBXV+s83eIekAuPE6lIOF4irA3kOXxKhInYAygnx2fNR6p98suNda+F1VTO8HV +WoAvjJoQ0iAA6/eHmUv1gPtJ+odjJPH7m6FrF3voJIiwmgFua01qkn6tZH53hBc2 +Dkbt5PxqI1KC0C79wq1ufehkk9MR5NTMxIwxbc3PcTbmwduGelKOsloLK/tGu5g8 +Kq5UJE4ZAgMBAAECggEBAJmRtNybe1I4Hmm1qlkl7Fgqn4DEK8Sa3/YBowzC0ilx +bRqcDkfqjbmx0uwwl8VV3CFoNsHHlY5po7RDLd7dM9cZxIWXmg3LITKDYRi+RQ27 +zqHZO3MZrzafQi6/wgD8ejWFF1/Gvo3xR/ceZ6461aOaie5aPsMLHspSxat05p/k +C4NkBvjHdYt4PiH873PLOI6mpt8N5mnkYmer8S5XX02GyQI4OGnHuCqQsHtlhgj7 +XeSB2biDNrcYfL1Kp1E8vEGBYqhohVWVWo+rP5zyoWNxQUXOVDE5zQanLrktjno5 +uzHkK1zKGcOlh7s40pV3qgcQFRTWt4RRP5wBasKc9wECgYEA948ikAVBiAiEPDha +hlNboaUKYeB6HxKSUZhqaGY0+7OWSyBhQBALCZRk4ccscysj4VUur+6FwC7lleRj +tG+S7ukyiQEeH0c0NAfnIlSd+LFQREsj4MVLn+Sl/8edNIl/UH+69pDtVU6Az/2u +DF0FloGL5XZ0WLIVcRZdUjt8HfkCgYEA0Y54DPN5PcLKci0Plioc0qLb33kp6F+w +JJhOLJv+tbmeNVA0EM+BwTHlHF8MRsreybwSHoUYX61vkR2AlGLo8D5TlTEA1VSD +k4w5Yic3cjQzOFMWQczpGdBwV37inHOU2phOp2kKs8LyT/gJw5f6b34R7zfRc8ry +uAEXaTg4OSECgYBb37ESBgFV/OMmfjuKUnFVQiziOi7YTUokIg6LhDLxnqqOYwv0 +fH+8JGh0Kjji3QXJ4JUdEcZtlnn58PLXyfib1cu9cL6/GOvUy4IKCaE+5H9HeSNt +jYsNYgwBKxG6p7SqKV03mH2cBTBlAF6RlAw42QcUN6viJuUyPPyRQiZD8QKBgQCO +N28X8wC8Pn9gH16tnaTz+pzXvAYJ8y66lzaupaumLvPE4MqFAh7gO3lu2L6fKL0s +EdwGJHOXM0A9LtV9XucRbGsTHC+hl/q33vluuIizk+OS/ShkvakQ4NntN2qZnQNP +mv/+M5aUyt/iD8aonHLUya1oOOyH9hrlb7Aws3vMoQKBgQDNH4xxS7wxnf7GcTnt +96tHvjBVHXa0SK1XkbFjgIEDU+xeEUteYs/9jW4AMA/lqsfBXUwhF3JAVoRUWm0M +d95jkAi6q2icuGogkftyv+LgUvhSUm6N+8eQ+YvQfgsNBqxlSudjpP0V+S2v73vp +8egFWKkL+H0upowTbr0T0OUfog== +-----END PRIVATE KEY----- +` + certString := `MIIE/DCCA+SgAwIBAgIQQHdQar3faUJLgHcCgO4z3zANBgkqhkiG9w0BAQsFADCB +nzELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB0JlaWppbmcxETAPBgNVBAcMCENoYW95 +YW5nMSMwIQYDVQQKDBpZdW5pb24gVGVjaG5vbG9neSBDby4gTHRkLjERMA8GA1UE +CwwIT25lQ2xvdWQxFDASBgNVBAMMC0AxNTkxNzIxNDc5MR0wGwYJKoZIhvcNAQkB +Fg5pbmZvQHl1bmlvbi5jbjAeFw0yMDA2MDkxNjUxMjBaFw0yMjA4MTgxNjUxMjBa +MIGXMQswCQYDVQQGEwJDTjEQMA4GA1UECAwHQmVpamluZzERMA8GA1UEBwwIQ2hh +b3lhbmcxIzAhBgNVBAoMGll1bmlvbiBUZWNobm9sb2d5IENvLiBMdGQuMREwDwYD +VQQLDAhPbmVDbG91ZDEMMAoGA1UEAwwDaWRwMR0wGwYJKoZIhvcNAQkBFg5pbmZv +QHl1bmlvbi5jbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMqloLRe +iKfO5A5u39WxZqnQ5Wo3w7dwEVLjVKz0YZuOfVkiD4PXg7HLBl2tEDXswrF+OE7g +X53afwciHlmh8ZREqA4fwqsQg9yKLkcuTVlWctdduKcTHAbbo48AgP2tg1J0GFV5 +HimoebtIGcFdX6zzd4h6QC48TqUg4XiKsDeQ5fEqEidgDKCfHZ81Hqn3yy411r4X +VVM7wdVagC+MmhDSIADr94eZS/WA+0n6h2Mk8fuboWsXe+gkiLCaAW5rTWqSfq1k +fneEFzYORu3k/GojUoLQLv3CrW596GST0xHk1MzEjDFtzc9xNubB24Z6Uo6yWgsr ++0a7mDwqrlQkThkCAwEAAaOCATgwggE0MAkGA1UdEwQCMAAwHQYDVR0OBBYEFFie +B9nsyKQ0A+oUWqiJkkURLJMqMIHUBgNVHSMEgcwwgcmAFISskqtTpDD4otifwvuG +bFwyvZ0zoYGlpIGiMIGfMQswCQYDVQQGEwJDTjEQMA4GA1UECAwHQmVpamluZzER +MA8GA1UEBwwIQ2hhb3lhbmcxIzAhBgNVBAoMGll1bmlvbiBUZWNobm9sb2d5IENv +LiBMdGQuMREwDwYDVQQLDAhPbmVDbG91ZDEUMBIGA1UEAwwLQDE1OTE3MjE0Nzkx +HTAbBgkqhkiG9w0BCQEWDmluZm9AeXVuaW9uLmNuggkA7uFZmhG6rz0wEwYDVR0l +BAwwCgYIKwYBBQUHAwEwCwYDVR0PBAQDAgWgMA8GA1UdEQQIMAaHBH8AAAEwDQYJ +KoZIhvcNAQELBQADggEBAKc7gUyrAMto3O8/Qi/m23aHxYc33GVMRrDPNS7asSWO +l+3IiWc2L61OiWhmK75t1leMRZU5hEuxAb3Rq6TwHVQWD3mAuIrss+Dsfhs0Y/wa +miIYV+v4rXuu3CusBxQo8NRuNW+f/F5cR0WVBDDMm5NDQKPVdeQ5uszqtWN62tPq +zvi1nRqaRKZQbzENGmqLwfMQCm0fsUzw6mdZb137S04MuSnvb1hTyhQ492uqoTlE +KM6qMM9M9PWLW8PiZxiHxtLdO5BIXOXY//xU9xD58sOIAePuc322+TbMBtRZAyqg +OZ+tB0Nzk2RJp4luPEqxoBcDVpcuiyfu3s3Jtnr3Lns=` + + input := SSAMLResponseInput{ + NameId: "testUser", + NameIdFormat: NAME_ID_FORMAT_TRANSIENT, + RequestID: "_dck4mm08qmdhc8k4nuir07hghetdqqg8umg5", + RequestEntityId: "https://auth.huaweicloud.com/", + IssuerEntityId: "https://saml.yunion.io/", + IssuerCertString: certString, + AssertionConsumerServiceURL: "https://auth.huaweicloud.com/authui/saml/SAMLAssertionConsumer", + } + resp := NewResponse(input) + resp.AddAudienceRestriction(input.RequestEntityId) + for k, v := range map[string]string{ + "xUserId": "userXXXX", + "xAccountId": "accountXXXX", + "bpId": "bpXXXXX", + "email": "xxxx@yunion.io", + "name": "xxxx", + "mobile": "13812341234", + } { + resp.AddAttribute(k, k, "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", []string{v}) + } + + respXml, err := xml.MarshalIndent(resp, "", " ") + if err != nil { + t.Fatalf("xml.MarshalIndent fail %s", err) + } + + privateKey, err := decodePrivateKey([]byte(privateKeyString)) + if err != nil { + t.Fatalf("decodePrivateKey fail %s", err) + } + + signed, err := SignXML(string(respXml), privateKey) + if err != nil { + t.Fatalf("SignXML fail %s", err) + } + + t.Logf("signed XML: %s", signed) + + validXMLs, err := ValidateXML(signed) + if err != nil { + t.Fatalf("ValidateXML fail %s", err) + } + + t.Logf("validated xml: %s", validXMLs) +} diff --git a/pkg/util/samlutils/types.go b/pkg/util/samlutils/types.go new file mode 100644 index 0000000000..9d05bc9eeb --- /dev/null +++ b/pkg/util/samlutils/types.go @@ -0,0 +1,439 @@ +// Copyright 2019 Yunion +// +// 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. + +package samlutils + +import "encoding/xml" + +type DigestMethod struct { + XMLName xml.Name + + Algorithm string `xml:"Algorithm,attr"` +} + +type SigningMethod struct { + XMLName xml.Name + + Algorithm string `xml:"Algorithm,attr"` +} + +type RequestInitiator struct { + XMLName xml.Name + + Binding string `xml:"Binding,attr"` + Location string `xml:"Location,attr"` +} + +type SXMLText struct { + XMLName xml.Name + + Lang string `xml:"lang,attr"` + Text string `xml:",innerxml"` +} + +type SXMLLogo struct { + XMLName xml.Name + + Height string `xml:"height,attr"` + Width string `xml:"width,attr"` + URL string `xml:",innerxml"` +} + +type SSAMLUIInfo struct { + XMLName xml.Name + + DisplayName SXMLText `xml:"DisplayName"` + Description SXMLText `xml:"Description"` + Logo SXMLLogo `xml:"Logo"` +} + +type SSAMLScope struct { + XMLName xml.Name + + Regexp string `xml:"regexp,attr"` + Scope string `xml:",innerxml"` +} + +type Extensions struct { + XMLName xml.Name + + // Alg string `xml:"alg,attr"` + // MDAttr string `xml:"mdattr,attr"` + // MDRPI string `xml:"mdrpi,attr"` + + // EntityAttributes string `xml:"EntityAttributes"` + + SigningMethods []SigningMethod `xml:"SigningMethod"` + DigestMethods []DigestMethod `xml:"DigestMethod"` + + RequestInitiator *RequestInitiator `xml:"RequestInitiator"` + + UIInfo *SSAMLUIInfo `xml:"UIInfo"` + + Scope *SSAMLScope `xml:"Scope"` +} + +type X509Certificate struct { + XMLName xml.Name + + Cert string `xml:",innerxml"` +} + +type X509Data struct { + XMLName xml.Name + + X509Certificate X509Certificate `xml:"X509Certificate"` +} + +type KeyInfo struct { + XMLName xml.Name + + X509Data X509Data `xml:"X509Data"` +} + +type EncryptionMethod struct { + XMLName xml.Name + + Algorithm string `xml:"Algorithm,attr"` +} + +type KeyDescriptor struct { + XMLName xml.Name + + Use string `xml:"use,attr"` + + KeyInfo KeyInfo `xml:"KeyInfo"` + + EncryptionMethods []EncryptionMethod `xml:"EncryptionMethod"` +} + +type SSAMLService struct { + XMLName xml.Name + + Binding string `xml:"Binding,attr"` + Location string `xml:"Location,attr"` + Index *string `xml:"index,attr"` + IsDefault *string `xml:"isDefault,attr"` +} + +type SSAMLNameIDFormat struct { + XMLName xml.Name + + Format string `xml:",innerxml"` +} + +type RequestedAttribute struct { + XMLName xml.Name + + IsRequired string `xml:"isRequired,attr"` + Name string `xml:"Name,attr"` + FriendlyName string `xml:"FriendlyName,attr"` +} + +type AttributeConsumingService struct { + XMLName xml.Name + + Index string `xml:"index,attr"` + + ServiceName SXMLText `xml:"ServiceName"` + + RequestedAttribute []RequestedAttribute `xml:"RequestedAttribute"` +} + +type SSODescriptor struct { + XMLName xml.Name + + AuthnRequestsSigned *string `xml:"AuthnRequestsSigned,attr"` + WantAssertionsSigned *string `xml:"WantAssertionsSigned,attr"` + ProtocolSupportEnumeration string `xml:"protocolSupportEnumeration,attr"` + + Extensions *Extensions `xml:"Extensions"` + + KeyDescriptors []KeyDescriptor `xml:"KeyDescriptor"` + + ArtifactResolutionServices []SSAMLService `xml:"ArtifactResolutionService"` + + SingleLogoutServices []SSAMLService `xml:"SingleLogoutService"` + ManageNameIDServices []SSAMLService `xml:"ManageNameIDService"` + + NameIDFormat []SSAMLNameIDFormat `xml:"NameIDFormat"` + SingleSignOnServices []SSAMLService `xml:"SingleSignOnService"` + + AssertionConsumerServices []SSAMLService `xml:"AssertionConsumerService"` + + AttributeConsumingService []AttributeConsumingService `xml:"AttributeConsumingService"` +} + +type SSAMLValue struct { + XMLName xml.Name + + Value string `xml:",innerxml"` +} + +type Transforms struct { + XMLName xml.Name + + Transforms []EncryptionMethod `xml:"Transform"` +} + +type Reference struct { + XMLName xml.Name + + URI string `xml:"URI,attr"` + + Transforms Transforms `xml:"Transforms"` + DigestMethod EncryptionMethod `xml:"DigestMethod"` + DigestValue SSAMLValue `xml:"DigestValue"` +} + +type SignedInfo struct { + XMLName xml.Name + + CanonicalizationMethod EncryptionMethod `xml:"CanonicalizationMethod"` + SignatureMethod EncryptionMethod `xml:"SignatureMethod"` + + Reference Reference `xml:"Reference"` +} + +type Signature struct { + XMLName xml.Name + + SignedInfo SignedInfo `xml:"SignedInfo"` + SignatureValue SSAMLValue `xml:"SignatureValue"` + KeyInfo KeyInfo `xml:"KeyInfo"` +} + +type Organization struct { + XMLName xml.Name + + OrganizationName SXMLText `xml:"OrganizationName"` + OrganizationDisplayName SXMLText `xml:"OrganizationDisplayName"` + OrganizationURL SXMLText `xml:"OrganizationURL"` +} + +type EntityDescriptor struct { + XMLName xml.Name + + // Id *string `xml:"ID,attr"` + EntityId string `xml:"entityID,attr"` + + Extensions *Extensions `xml:"Extensions"` + Signature *Signature `xml:"Signature"` + + SPSSODescriptor *SSODescriptor `xml:"SPSSODescriptor"` + IDPSSODescriptor *SSODescriptor `xml:"IDPSSODescriptor"` + + Organization *Organization `xml:"Organization"` +} + +type SIdpRedirectLoginInput struct { + SAMLRequest string `json:"SAMLRequest"` + RelayState string `json:"RelayState"` + SigAlg string `json:"SigAlg"` + Signature string `json:"Signature"` +} + +type SIdpInitiatedLoginInput struct { + EntityID string `json:"EntityID"` + State string `json:"State"` +} + +type Issuer struct { + XMLName xml.Name + + Format *string `xml:"Format,attr"` + + Issuer string `xml:",innerxml"` +} + +type NameIDPolicy struct { + XMLName xml.Name + + AllowCreate string `xml:"AllowCreate,attr"` + Format string `xml:"Format,attr"` + SPNameQualifier string `xml:"SPNameQualifier,attr"` +} + +type AuthnRequest struct { + XMLName xml.Name + + AssertionConsumerServiceURL string `xml:"AssertionConsumerServiceURL,attr"` + Destination string `xml:"Destination,attr"` + ForceAuthn string `xml:"ForceAuthn,attr"` + ID string `xml:"ID,attr"` + IsPassive string `xml:"IsPassive,attr"` + IssueInstant string `xml:"IssueInstant,attr"` + ProtocolBinding string `xml:"ProtocolBinding,attr"` + Version string `xml:"Version,attr"` + + Issuer Issuer `xml:"Issuer"` + NameIDPolicy NameIDPolicy `xml:"NameIDPolicy"` +} + +type StatusCode struct { + XMLName xml.Name + + Value string `xml:"Value,attr"` +} + +type StatusMessage struct { + XMLName xml.Name + + Message string `xml:",innerxml"` +} + +type Status struct { + XMLName xml.Name + + StatusCode StatusCode `xml:"StatusCode"` + StatusMessage *StatusMessage `xml:"StatusMessage"` +} + +type Response struct { + XMLName xml.Name + + ID string `xml:"ID,attr"` + InResponseTo *string `xml:"InResponseTo,attr"` + Version string `xml:"Version,attr"` + IssueInstant string `xml:"IssueInstant,attr"` + Destination string `xml:"Destination,attr"` + + Issuer Issuer `xml:"Issuer"` + Status Status `xml:"Status"` + + Assertion Assertion `xml:"Assertion"` +} + +type Assertion struct { + XMLName xml.Name + + ID string `xml:"ID,attr"` + Version string `xml:"Version,attr"` + IssueInstant string `xml:"IssueInstant,attr"` + + Issuer Issuer `xml:"Issuer"` + Signature *Signature `xml:"Signature"` + Subject Subject `xml:"Subject"` + Conditions Conditions `xml:"Conditions"` + AttributeStatement *AttributeStatement `xml:"AttributeStatement"` + AuthnStatement AuthnStatement `xml:"AuthnStatement"` +} + +type Subject struct { + XMLName xml.Name + + NameID NameID `xml:"NameID"` + + SubjectConfirmation SubjectConfirmation `xml:"SubjectConfirmation"` +} + +type NameID struct { + XMLName xml.Name + + Format string `xml:"Format,attr"` + NameQualifier *string `xml:"NameQualifier,attr"` + + Value string `xml:",innerxml"` +} + +type SubjectConfirmation struct { + XMLName xml.Name + + Method string `xml:"Method,attr"` + + SubjectConfirmationData SubjectConfirmationData `xml:"SubjectConfirmationData"` +} + +type SubjectConfirmationData struct { + XMLName xml.Name + + InResponseTo *string `xml:"InResponseTo,attr"` + Recipient string `xml:"Recipient,attr"` + NotBefore *string `xml:"NotBefore,attr"` + NotOnOrAfter string `xml:"NotOnOrAfter,attr"` +} + +type Conditions struct { + XMLName xml.Name + + NotBefore *string `xml:"NotBefore,attr"` + NotOnOrAfter string `xml:"NotOnOrAfter,attr"` + + AudienceRestrictions []AudienceRestriction `xml:"AudienceRestriction"` +} + +type AudienceRestriction struct { + XMLName xml.Name + + Audience Audience `xml:"Audience"` +} + +type Audience struct { + XMLName xml.Name + + Value string `xml:",innerxml"` +} + +type AttributeStatement struct { + XMLName xml.Name + + Attributes []Attribute `xml:"Attribute"` +} + +type Attribute struct { + XMLName xml.Name + + FriendlyName *string `xml:"FriendlyName,attr"` + Name string `xml:"Name,attr"` + NameFormat *string `xml:"NameFormat,attr"` + + AttributeValues []AttributeValue `xml:"AttributeValue"` +} + +type AttributeValue struct { + XMLName xml.Name + + Type string `xml:"type,attr"` + + Value string `xml:",innerxml"` +} + +type AuthnStatement struct { + XMLName xml.Name + + AuthnInstant string `xml:"AuthnInstant,attr"` + SessionIndex string `xml:"SessionIndex,attr"` + + SubjectLocality *SubjectLocality `xml:"SubjectLocality"` + + AuthnContext AuthnContext `xml:"AuthnContext"` +} + +type SubjectLocality struct { + XMLName xml.Name + + Address string `xml:"Address,attr"` +} + +type AuthnContext struct { + XMLName xml.Name + + AuthnContextClassRef AuthnContextClassRef `xml:"AuthnContextClassRef"` +} + +type AuthnContextClassRef struct { + XMLName xml.Name + + Value string `xml:",innerxml"` +} diff --git a/pkg/util/samlutils/util.go b/pkg/util/samlutils/util.go new file mode 100644 index 0000000000..5391d95a13 --- /dev/null +++ b/pkg/util/samlutils/util.go @@ -0,0 +1,128 @@ +// Copyright 2019 Yunion +// +// 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. + +package samlutils + +import ( + "bytes" + "compress/flate" + "crypto/rsa" + "encoding/base64" + "fmt" + "io" + "strings" + + "github.com/ma314smith/signedxml" + + "yunion.io/x/pkg/errors" +) + +func compressString(in string) string { + buf := new(bytes.Buffer) + compressor, _ := flate.NewWriter(buf, 9) + compressor.Write([]byte(in)) + compressor.Close() + return buf.String() +} + +func decompressString(in string) string { + buf := new(bytes.Buffer) + decompressor := flate.NewReader(strings.NewReader(in)) + io.Copy(buf, decompressor) + decompressor.Close() + return buf.String() +} + +func compress(in []byte) ([]byte, error) { + buf := new(bytes.Buffer) + compressor, _ := flate.NewWriter(buf, 9) + _, err := compressor.Write(in) + if err != nil { + return nil, errors.Wrap(err, "compressor.Write") + } + compressor.Close() + return buf.Bytes(), nil +} + +func decompress(in []byte) ([]byte, error) { + buf := new(bytes.Buffer) + decompressor := flate.NewReader(bytes.NewReader(in)) + _, err := io.Copy(buf, decompressor) + if err != nil { + return nil, errors.Wrap(err, "io.Copy") + } + decompressor.Close() + return buf.Bytes(), nil +} + +func SAMLDecode(input string) ([]byte, error) { + reqBytes, err := base64.StdEncoding.DecodeString(input) + if err != nil { + return nil, errors.Wrap(err, "base64.StdEncoding.DecodeString") + } + plainText, err := decompress(reqBytes) + if err != nil { + return nil, errors.Wrap(err, "decompress") + } + return plainText, nil +} + +func SAMLEncode(input []byte) (string, error) { + comp, err := compress(input) + if err != nil { + return "", errors.Wrap(err, "compress") + } + return base64.StdEncoding.EncodeToString(comp), nil +} + +func SAMLForm(action string, attrs map[string]string) string { + form := strings.Builder{} + // form.WriteString(``) + form.WriteString(`
`) + for k, v := range attrs { + form.WriteString(fmt.Sprintf("", k, v)) + } + form.WriteString(``) + form.WriteString("
`) + // form.WriteString(``) + return form.String() +} + +func SignXML(xmlstr string, privateKey *rsa.PrivateKey) (string, error) { + signer, err := signedxml.NewSigner(string(xmlstr)) + if err != nil { + return "", errors.Wrap(err, "signedxml.NewSigner") + } + signed, err := signer.Sign(privateKey) + if err != nil { + return "", errors.Wrap(err, "signer.Sign") + } + return signed, nil +} + +func ValidateXML(signed string) ([]string, error) { + validator, err := signedxml.NewValidator(signed) + if err != nil { + return nil, errors.Wrap(err, "signedxml.NewValidator") + } + validXMLs, err := validator.ValidateReferences() + if err != nil { + return nil, errors.Wrap(err, "validator.ValidateReferences") + } + return validXMLs, nil +} diff --git a/pkg/util/seclib2/certfile.go b/pkg/util/seclib2/certfile.go index 33537d4229..46141e4891 100644 --- a/pkg/util/seclib2/certfile.go +++ b/pkg/util/seclib2/certfile.go @@ -17,6 +17,7 @@ package seclib2 import ( "fmt" "io/ioutil" + "regexp" "strings" ) @@ -55,3 +56,11 @@ func MergeCaCertFiles(cafile string, certfile string) (string, error) { return tmpfile.Name(), nil } + +func CleanCertificate(cert string) string { + re := regexp.MustCompile("---(.*)CERTIFICATE(.*)---") + cert = re.ReplaceAllString(cert, "") + cert = strings.Trim(cert, " \n") + // cert = strings.Replace(cert, "\n", "", -1) + return cert +} diff --git a/vendor/github.com/beevik/etree/.travis.yml b/vendor/github.com/beevik/etree/.travis.yml new file mode 100644 index 0000000000..f4cb25d473 --- /dev/null +++ b/vendor/github.com/beevik/etree/.travis.yml @@ -0,0 +1,14 @@ +language: go +sudo: false + +go: + - 1.11.x + - tip + +matrix: + allow_failures: + - go: tip + +script: + - go vet ./... + - go test -v ./... diff --git a/vendor/github.com/beevik/etree/CONTRIBUTORS b/vendor/github.com/beevik/etree/CONTRIBUTORS new file mode 100644 index 0000000000..03211a85eb --- /dev/null +++ b/vendor/github.com/beevik/etree/CONTRIBUTORS @@ -0,0 +1,10 @@ +Brett Vickers (beevik) +Felix Geisendörfer (felixge) +Kamil Kisiel (kisielk) +Graham King (grahamking) +Matt Smith (ma314smith) +Michal Jemala (michaljemala) +Nicolas Piganeau (npiganeau) +Chris Brown (ccbrown) +Earncef Sequeira (earncef) +Gabriel de Labachelerie (wuzuf) diff --git a/vendor/github.com/beevik/etree/LICENSE b/vendor/github.com/beevik/etree/LICENSE new file mode 100644 index 0000000000..26f1f7751b --- /dev/null +++ b/vendor/github.com/beevik/etree/LICENSE @@ -0,0 +1,24 @@ +Copyright 2015-2019 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/README.md b/vendor/github.com/beevik/etree/README.md new file mode 100644 index 0000000000..08ec26b0ac --- /dev/null +++ b/vendor/github.com/beevik/etree/README.md @@ -0,0 +1,205 @@ +[![Build Status](https://travis-ci.org/beevik/etree.svg?branch=master)](https://travis-ci.org/beevik/etree) +[![GoDoc](https://godoc.org/github.com/beevik/etree?status.svg)](https://godoc.org/github.com/beevik/etree) + +etree +===== + +The etree package is a lightweight, pure go package that expresses XML in +the form of an element tree. Its design was inspired by the Python +[ElementTree](http://docs.python.org/2/library/xml.etree.elementtree.html) +module. + +Some of the package's capabilities and features: + +* Represents XML documents as trees of elements for easy traversal. +* Imports, serializes, modifies or creates XML documents from scratch. +* Writes and reads XML to/from files, byte slices, strings and io interfaces. +* Performs simple or complex searches with lightweight XPath-like query APIs. +* Auto-indents XML using spaces or tabs for better readability. +* Implemented in pure go; depends only on standard go libraries. +* Built on top of the go [encoding/xml](http://golang.org/pkg/encoding/xml) + package. + +### Creating an XML document + +The following example creates an XML document from scratch using the etree +package and outputs its indented contents to stdout. +```go +doc := etree.NewDocument() +doc.CreateProcInst("xml", `version="1.0" encoding="UTF-8"`) +doc.CreateProcInst("xml-stylesheet", `type="text/xsl" href="style.xsl"`) + +people := doc.CreateElement("People") +people.CreateComment("These are all known people") + +jon := people.CreateElement("Person") +jon.CreateAttr("name", "Jon") + +sally := people.CreateElement("Person") +sally.CreateAttr("name", "Sally") + +doc.Indent(2) +doc.WriteTo(os.Stdout) +``` + +Output: +```xml + + + + + + + +``` + +### Reading an XML file + +Suppose you have a file on disk called `bookstore.xml` containing the +following data: + +```xml + + + + Everyday Italian + Giada De Laurentiis + 2005 + 30.00 + + + + Harry Potter + J K. Rowling + 2005 + 29.99 + + + + XQuery Kick Start + James McGovern + Per Bothner + Kurt Cagle + James Linn + Vaidyanathan Nagarajan + 2003 + 49.99 + + + + Learning XML + Erik T. Ray + 2003 + 39.95 + + + +``` + +This code reads the file's contents into an etree document. +```go +doc := etree.NewDocument() +if err := doc.ReadFromFile("bookstore.xml"); err != nil { + panic(err) +} +``` + +You can also read XML from a string, a byte slice, or an `io.Reader`. + +### Processing elements and attributes + +This example illustrates several ways to access elements and attributes using +etree selection queries. +```go +root := doc.SelectElement("bookstore") +fmt.Println("ROOT element:", root.Tag) + +for _, book := range root.SelectElements("book") { + fmt.Println("CHILD element:", book.Tag) + if title := book.SelectElement("title"); title != nil { + lang := title.SelectAttrValue("lang", "unknown") + fmt.Printf(" TITLE: %s (%s)\n", title.Text(), lang) + } + for _, attr := range book.Attr { + fmt.Printf(" ATTR: %s=%s\n", attr.Key, attr.Value) + } +} +``` +Output: +``` +ROOT element: bookstore +CHILD element: book + TITLE: Everyday Italian (en) + ATTR: category=COOKING +CHILD element: book + TITLE: Harry Potter (en) + ATTR: category=CHILDREN +CHILD element: book + TITLE: XQuery Kick Start (en) + ATTR: category=WEB +CHILD element: book + TITLE: Learning XML (en) + ATTR: category=WEB +``` + +### Path queries + +This example uses etree's path functions to select all book titles that fall +into the category of 'WEB'. The double-slash prefix in the path causes the +search for book elements to occur recursively; book elements may appear at any +level of the XML hierarchy. +```go +for _, t := range doc.FindElements("//book[@category='WEB']/title") { + fmt.Println("Title:", t.Text()) +} +``` + +Output: +``` +Title: XQuery Kick Start +Title: Learning XML +``` + +This example finds the first book element under the root bookstore element and +outputs the tag and text of each of its child elements. +```go +for _, e := range doc.FindElements("./bookstore/book[1]/*") { + fmt.Printf("%s: %s\n", e.Tag, e.Text()) +} +``` + +Output: +``` +title: Everyday Italian +author: Giada De Laurentiis +year: 2005 +price: 30.00 +``` + +This example finds all books with a price of 49.99 and outputs their titles. +```go +path := etree.MustCompilePath("./bookstore/book[p:price='49.99']/title") +for _, e := range doc.FindElementsPath(path) { + fmt.Println(e.Text()) +} +``` + +Output: +``` +XQuery Kick Start +``` + +Note that this example uses the FindElementsPath function, which takes as an +argument a pre-compiled path object. Use precompiled paths when you plan to +search with the same path more than once. + +### Other features + +These are just a few examples of the things the etree package can do. See the +[documentation](http://godoc.org/github.com/beevik/etree) for a complete +description of its capabilities. + +### Contributing + +This project accepts contributions. Just fork the repo and submit a pull +request! diff --git a/vendor/github.com/beevik/etree/RELEASE_NOTES.md b/vendor/github.com/beevik/etree/RELEASE_NOTES.md new file mode 100644 index 0000000000..ee59d7abf4 --- /dev/null +++ b/vendor/github.com/beevik/etree/RELEASE_NOTES.md @@ -0,0 +1,109 @@ +Release v1.1.0 +============== + +**New Features** + +* New attribute helpers. + * Added the `Element.SortAttrs` method, which lexicographically sorts an + element's attributes by key. +* New `ReadSettings` properties. + * Added `Entity` for the support of custom entity maps. +* New `WriteSettings` properties. + * Added `UseCRLF` to allow the output of CR-LF newlines instead of the + default LF newlines. This is useful on Windows systems. +* Additional support for text and CDATA sections. + * The `Element.Text` method now returns the concatenation of all consecutive + character data tokens immediately following an element's opening tag. + * Added `Element.SetCData` to replace the character data immediately + following an element's opening tag with a CDATA section. + * Added `Element.CreateCData` to create and add a CDATA section child + `CharData` token to an element. + * Added `Element.CreateText` to create and add a child text `CharData` token + to an element. + * Added `NewCData` to create a parentless CDATA section `CharData` token. + * Added `NewText` to create a parentless text `CharData` + token. + * Added `CharData.IsCData` to detect if the token contains a CDATA section. + * Added `CharData.IsWhitespace` to detect if the token contains whitespace + inserted by one of the document Indent functions. + * Modified `Element.SetText` so that it replaces a run of consecutive + character data tokens following the element's opening tag (instead of just + the first one). +* New "tail text" support. + * Added the `Element.Tail` method, which returns the text immediately + following an element's closing tag. + * Added the `Element.SetTail` method, which modifies the text immediately + following an element's closing tag. +* New element child insertion and removal methods. + * Added the `Element.InsertChildAt` method, which inserts a new child token + before the specified child token index. + * Added the `Element.RemoveChildAt` method, which removes the child token at + the specified child token index. +* New element and attribute queries. + * Added the `Element.Index` method, which returns the element's index within + its parent element's child token list. + * Added the `Element.NamespaceURI` method to return the namespace URI + associated with an element. + * Added the `Attr.NamespaceURI` method to return the namespace URI + associated with an element. + * Added the `Attr.Element` method to return the element that an attribute + belongs to. +* New Path filter functions. + * Added `[local-name()='val']` to keep elements whose unprefixed tag matches + the desired value. + * Added `[name()='val']` to keep elements whose full tag matches the desired + value. + * Added `[namespace-prefix()='val']` to keep elements whose namespace prefix + matches the desired value. + * Added `[namespace-uri()='val']` to keep elements whose namespace URI + matches the desired value. + +**Bug Fixes** + +* A default XML `CharSetReader` is now used to prevent failed parsing of XML + documents using certain encodings. + ([Issue](https://github.com/beevik/etree/issues/53)). +* All characters are now properly escaped according to XML parsing rules. + ([Issue](https://github.com/beevik/etree/issues/55)). +* The `Document.Indent` and `Document.IndentTabs` functions no longer insert + empty string `CharData` tokens. + +**Deprecated** + +* `Element` + * The `InsertChild` method is deprecated. Use `InsertChildAt` instead. + * The `CreateCharData` method is deprecated. Use `CreateText` instead. +* `CharData` + * The `NewCharData` method is deprecated. Use `NewText` instead. + + +Release v1.0.1 +============== + +**Changes** + +* Added support for absolute etree Path queries. An absolute path begins with + `/` or `//` and begins its search from the element's document root. +* Added [`GetPath`](https://godoc.org/github.com/beevik/etree#Element.GetPath) + and [`GetRelativePath`](https://godoc.org/github.com/beevik/etree#Element.GetRelativePath) + functions to the [`Element`](https://godoc.org/github.com/beevik/etree#Element) + type. + +**Breaking changes** + +* A path starting with `//` is now interpreted as an absolute path. + Previously, it was interpreted as a relative path starting from the element + whose + [`FindElement`](https://godoc.org/github.com/beevik/etree#Element.FindElement) + method was called. To remain compatible with this release, all paths + prefixed with `//` should be prefixed with `.//` when called from any + element other than the document's root. +* [**edit 2/1/2019**]: Minor releases should not contain breaking changes. + Even though this breaking change was very minor, it was a mistake to include + it in this minor release. In the future, all breaking changes will be + limited to major releases (e.g., version 2.0.0). + +Release v1.0.0 +============== + +Initial release. diff --git a/vendor/github.com/beevik/etree/etree.go b/vendor/github.com/beevik/etree/etree.go new file mode 100644 index 0000000000..9e24f90126 --- /dev/null +++ b/vendor/github.com/beevik/etree/etree.go @@ -0,0 +1,1453 @@ +// Copyright 2015-2019 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" + "sort" + "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) + + // Permissive allows input containing common mistakes such as missing tags + // or attribute values. Default: false. + Permissive bool + + // Entity to be passed to standard xml.Decoder. Default: nil. + Entity map[string]string +} + +// newReadSettings creates a default ReadSettings record. +func newReadSettings() ReadSettings { + return ReadSettings{ + CharsetReader: func(label string, input io.Reader) (io.Reader, error) { + return input, nil + }, + Permissive: false, + } +} + +// 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 + + // When outputting indented XML, use a carriage return and linefeed + // ("\r\n") as a new-line delimiter instead of just a linefeed ("\n"). + // This is useful on Windows-based systems. + UseCRLF bool +} + +// newWriteSettings creates a default WriteSettings record. +func newWriteSettings() WriteSettings { + return WriteSettings{ + CanonicalEndTags: false, + CanonicalText: false, + CanonicalAttrVal: false, + UseCRLF: false, + } +} + +// A Token is an empty interface that represents an Element, CharData, +// Comment, Directive, or ProcInst. +type Token interface { + Parent() *Element + Index() int + dup(parent *Element) Token + setParent(parent *Element) + setIndex(index int) + 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 prefix and tag + Attr []Attr // key-value attribute pairs + Child []Token // child tokens (elements, comments, etc.) + parent *Element // parent element + index int // token index in parent's children +} + +// An Attr represents a key-value attribute of an XML element. +type Attr struct { + Space, Key string // The attribute's namespace prefix and key + Value string // The attribute value string + element *Element // element containing the attribute +} + +// charDataFlags are used with CharData tokens to store additional settings. +type charDataFlags uint8 + +const ( + // The CharData was created by an indent function as whitespace. + whitespaceFlag charDataFlags = 1 << iota + + // The CharData contains a CDATA section. + cdataFlag +) + +// CharData can be used to represent character data or a CDATA section within +// an XML document. +type CharData struct { + Data string + parent *Element + index int + flags charDataFlags +} + +// A Comment represents an XML comment. +type Comment struct { + Data string + parent *Element + index int +} + +// A Directive represents an XML directive. +type Directive struct { + Data string + parent *Element + index int +} + +// A ProcInst represents an XML processing instruction. +type ProcInst struct { + Target string + Inst string + parent *Element + index int +} + +// 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) + } + + p := &d.Element + e.setParent(p) + + // If there is already a root element, replace it. + for i, t := range p.Child { + if _, ok := t.(*Element); ok { + t.setParent(nil) + t.setIndex(-1) + p.Child[i] = e + e.setIndex(i) + return + } + } + + // No existing root element, so add it. + p.addChild(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) +} + +// 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 character data +// tokens containing newlines 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 "" } + case d.WriteSettings.UseCRLF == true: + indent = func(depth int) string { return indentCRLF(depth*spaces, indentSpaces) } + default: + indent = func(depth int) string { return indentLF(depth*spaces, indentSpaces) } + } + d.Element.indent(0, indent) +} + +// IndentTabs modifies the document's element tree by inserting CharData +// tokens containing newlines and tabs for indentation. One tab is used per +// indentation level. +func (d *Document) IndentTabs() { + var indent indentFunc + switch d.WriteSettings.UseCRLF { + case true: + indent = func(depth int) string { return indentCRLF(depth, indentTabs) } + default: + indent = func(depth int) string { return indentLF(depth, indentTabs) } + } + d.Element.indent(0, indent) +} + +// NewElement creates an unparented element with the specified tag. The tag +// may be prefixed by a namespace prefix 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, + index: -1, + } + 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 { + return e.dup(nil).(*Element) +} + +// FullTag returns the element e's complete tag, including namespace prefix if +// present. +func (e *Element) FullTag() string { + if e.Space == "" { + return e.Tag + } + return e.Space + ":" + e.Tag +} + +// NamespaceURI returns the XML namespace URI associated with the element. If +// the element is part of the XML default namespace, NamespaceURI returns the +// empty string. +func (e *Element) NamespaceURI() string { + if e.Space == "" { + return e.findDefaultNamespaceURI() + } + return e.findLocalNamespaceURI(e.Space) +} + +// findLocalNamespaceURI finds the namespace URI corresponding to the +// requested prefix. +func (e *Element) findLocalNamespaceURI(prefix string) string { + for _, a := range e.Attr { + if a.Space == "xmlns" && a.Key == prefix { + return a.Value + } + } + + if e.parent == nil { + return "" + } + + return e.parent.findLocalNamespaceURI(prefix) +} + +// findDefaultNamespaceURI finds the default namespace URI of the element. +func (e *Element) findDefaultNamespaceURI() string { + for _, a := range e.Attr { + if a.Space == "" && a.Key == "xmlns" { + return a.Value + } + } + + if e.parent == nil { + return "" + } + + return e.parent.findDefaultNamespaceURI() +} + +// hasText returns true if the element has character data immediately +// folllowing the element's opening tag. +func (e *Element) hasText() bool { + if len(e.Child) == 0 { + return false + } + _, ok := e.Child[0].(*CharData) + return ok +} + +// namespacePrefix returns the namespace prefix associated with the element. +func (e *Element) namespacePrefix() string { + return e.Space +} + +// name returns the tag associated with the element. +func (e *Element) name() string { + return e.Tag +} + +// Text returns all character data immediately following the element's opening +// tag. +func (e *Element) Text() string { + if len(e.Child) == 0 { + return "" + } + + text := "" + for _, ch := range e.Child { + if cd, ok := ch.(*CharData); ok { + if text == "" { + text = cd.Data + } else { + text = text + cd.Data + } + } else { + break + } + } + return text +} + +// SetText replaces all character data immediately following an element's +// opening tag with the requested string. +func (e *Element) SetText(text string) { + e.replaceText(0, text, 0) +} + +// SetCData replaces all character data immediately following an element's +// opening tag with a CDATA section. +func (e *Element) SetCData(text string) { + e.replaceText(0, text, cdataFlag) +} + +// Tail returns all character data immediately following the element's end +// tag. +func (e *Element) Tail() string { + if e.Parent() == nil { + return "" + } + + p := e.Parent() + i := e.Index() + + text := "" + for _, ch := range p.Child[i+1:] { + if cd, ok := ch.(*CharData); ok { + if text == "" { + text = cd.Data + } else { + text = text + cd.Data + } + } else { + break + } + } + return text +} + +// SetTail replaces all character data immediately following the element's end +// tag with the requested string. +func (e *Element) SetTail(text string) { + if e.Parent() == nil { + return + } + + p := e.Parent() + p.replaceText(e.Index()+1, text, 0) +} + +// replaceText is a helper function that replaces a series of chardata tokens +// starting at index i with the requested text. +func (e *Element) replaceText(i int, text string, flags charDataFlags) { + end := e.findTermCharDataIndex(i) + + switch { + case end == i: + if text != "" { + // insert a new chardata token at index i + cd := newCharData(text, flags, nil) + e.InsertChildAt(i, cd) + } + + case end == i+1: + if text == "" { + // remove the chardata token at index i + e.RemoveChildAt(i) + } else { + // replace the first and only character token at index i + cd := e.Child[i].(*CharData) + cd.Data, cd.flags = text, flags + } + + default: + if text == "" { + // remove all chardata tokens starting from index i + copy(e.Child[i:], e.Child[end:]) + removed := end - i + e.Child = e.Child[:len(e.Child)-removed] + for j := i; j < len(e.Child); j++ { + e.Child[j].setIndex(j) + } + } else { + // replace the first chardata token at index i and remove all + // subsequent chardata tokens + cd := e.Child[i].(*CharData) + cd.Data, cd.flags = text, flags + copy(e.Child[i+1:], e.Child[end:]) + removed := end - (i + 1) + e.Child = e.Child[:len(e.Child)-removed] + for j := i + 1; j < len(e.Child); j++ { + e.Child[j].setIndex(j) + } + } + } +} + +// findTermCharDataIndex finds the index of the first child token that isn't +// a CharData token. It starts from the requested start index. +func (e *Element) findTermCharDataIndex(start int) int { + for i := start; i < len(e.Child); i++ { + if _, ok := e.Child[i].(*CharData); !ok { + return i + } + } + return len(e.Child) +} + +// 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 +// prefix 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 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. +// +// Deprecated: InsertChild is deprecated. Use InsertChildAt instead. +func (e *Element) InsertChild(ex Token, t Token) { + if ex == nil || ex.Parent() != e { + e.AddChild(t) + return + } + + if t.Parent() != nil { + t.Parent().RemoveChild(t) + } + + t.setParent(e) + + i := ex.Index() + e.Child = append(e.Child, nil) + copy(e.Child[i+1:], e.Child[i:]) + e.Child[i] = t + + for j := i; j < len(e.Child); j++ { + e.Child[j].setIndex(j) + } +} + +// InsertChildAt inserts the token t into the element e's list of child tokens +// just before the requested index. If the index is greater than or equal to +// the length of the list of child tokens, the token t is added to the end of +// the list. +func (e *Element) InsertChildAt(index int, t Token) { + if index >= len(e.Child) { + e.AddChild(t) + return + } + + if t.Parent() != nil { + if t.Parent() == e && t.Index() > index { + index-- + } + t.Parent().RemoveChild(t) + } + + t.setParent(e) + + e.Child = append(e.Child, nil) + copy(e.Child[index+1:], e.Child[index:]) + e.Child[index] = t + + for j := index; j < len(e.Child); j++ { + e.Child[j].setIndex(j) + } +} + +// 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 { + if t.Parent() != e { + return nil + } + return e.RemoveChildAt(t.Index()) +} + +// RemoveChildAt removes the index-th child token from the element e. The +// removed child token is returned. If the index is out of bounds, no child is +// removed and nil is returned. +func (e *Element) RemoveChildAt(index int) Token { + if index >= len(e.Child) { + return nil + } + + t := e.Child[index] + for j := index + 1; j < len(e.Child); j++ { + e.Child[j].setIndex(j - 1) + } + e.Child = append(e.Child[:index], e.Child[index+1:]...) + t.setIndex(-1) + t.setParent(nil) + return t +} + +// 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, settings ReadSettings) (n int64, err error) { + r := newCountReader(ri) + dec := xml.NewDecoder(r) + dec.CharsetReader = settings.CharsetReader + dec.Strict = !settings.Permissive + dec.Entity = settings.Entity + 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, e) + } + stack.push(e) + case xml.EndElement: + stack.pop() + case xml.CharData: + data := string(t) + var flags charDataFlags + if isWhitespace(data) { + flags = whitespaceFlag + } + newCharData(data, flags, 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. Returns nil if no matching attribute is found. The key +// may be prefixed by a namespace prefix 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 prefix +// 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 prefix and a colon. Returns nil if no +// element with a matching tag was found. +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 prefix 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. Returns nil if no element is found using the path. 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. Returns nil if no element is found using the path. +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) +} + +// GetPath returns the absolute path of the element. +func (e *Element) GetPath() string { + path := []string{} + for seg := e; seg != nil; seg = seg.Parent() { + if seg.Tag != "" { + path = append(path, seg.Tag) + } + } + + // Reverse the path. + for i, j := 0, len(path)-1; i < j; i, j = i+1, j-1 { + path[i], path[j] = path[j], path[i] + } + + return "/" + strings.Join(path, "/") +} + +// GetRelativePath returns the path of the element relative to the source +// element. If the two elements are not part of the same element tree, then +// GetRelativePath returns the empty string. +func (e *Element) GetRelativePath(source *Element) string { + var path []*Element + + if source == nil { + return "" + } + + // Build a reverse path from the element toward the root. Stop if the + // source element is encountered. + var seg *Element + for seg = e; seg != nil && seg != source; seg = seg.Parent() { + path = append(path, seg) + } + + // If we found the source element, reverse the path and compose the + // string. + if seg == source { + if len(path) == 0 { + return "." + } + parts := []string{} + for i := len(path) - 1; i >= 0; i-- { + parts = append(parts, path[i].Tag) + } + return "./" + strings.Join(parts, "/") + } + + // The source wasn't encountered, so climb from the source element toward + // the root of the tree until an element in the reversed path is + // encountered. + + findPathIndex := func(e *Element, path []*Element) int { + for i, ee := range path { + if e == ee { + return i + } + } + return -1 + } + + climb := 0 + for seg = source; seg != nil; seg = seg.Parent() { + i := findPathIndex(seg, path) + if i >= 0 { + path = path[:i] // truncate at found segment + break + } + climb++ + } + + // No element in the reversed path was encountered, so the two elements + // must not be part of the same tree. + if seg == nil { + return "" + } + + // Reverse the (possibly truncated) path and prepend ".." segments to + // climb. + parts := []string{} + for i := 0; i < climb; i++ { + parts = append(parts, "..") + } + for i := len(path) - 1; i >= 0; i-- { + parts = append(parts, path[i].Tag) + } + return strings.Join(parts, "/") +} + +// 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 NL+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 { + s := indent(depth) + if s != "" { + newCharData(s, whitespaceFlag, e) + } + } + firstNonCharData = false + } + + e.addChild(c) + + // Recursively process child elements. + if ce, ok := c.(*Element); ok { + ce.indent(depth+1, indent) + } + } + + // Insert NL+indent before the last child. + if !isCharData { + if !firstNonCharData || depth > 0 { + s := indent(depth - 1) + if s != "" { + newCharData(s, whitespaceFlag, 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.IsWhitespace() { + 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.IsWhitespace() { + continue + } + newChild[j] = c + newChild[j].setIndex(j) + 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, + index: e.index, + } + 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 +} + +// Index returns the index of this element within its parent element's +// list of child tokens. If this element has no parent element, the index +// is -1. +func (e *Element) Index() int { + return e.index +} + +// setParent replaces the element token's parent. +func (e *Element) setParent(parent *Element) { + e.parent = parent +} + +// setIndex sets the element token's index within its parent's Child slice. +func (e *Element) setIndex(index int) { + e.index = index +} + +// writeTo serializes the element to the writer w. +func (e *Element) writeTo(w *bufio.Writer, s *WriteSettings) { + w.WriteByte('<') + w.WriteString(e.FullTag()) + 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{'<', '/'}) + w.WriteString(e.FullTag()) + w.WriteByte('>') + } else { + if s.CanonicalEndTags { + w.Write([]byte{'>', '<', '/'}) + w.WriteString(e.FullTag()) + w.WriteByte('>') + } else { + w.Write([]byte{'/', '>'}) + } + } +} + +// addChild adds a child token to the element e. +func (e *Element) addChild(t Token) { + t.setIndex(len(e.Child)) + e.Child = append(e.Child, t) +} + +// CreateAttr creates an attribute and adds it to element e. The key may be +// prefixed by a namespace prefix 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, e) +} + +// createAttr is a helper function that creates attributes. +func (e *Element) createAttr(space, key, value string, parent *Element) *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: space, + Key: key, + Value: value, + element: parent, + } + e.Attr = append(e.Attr, a) + return &e.Attr[len(e.Attr)-1] +} + +// RemoveAttr removes and returns a copy of the first attribute of the element +// whose key matches the given key. The key may be prefixed by a namespace +// prefix and a colon. If a matching 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 &Attr{ + Space: a.Space, + Key: a.Key, + Value: a.Value, + element: nil, + } + } + } + return nil +} + +// SortAttrs sorts the element's attributes lexicographically by key. +func (e *Element) SortAttrs() { + sort.Sort(byAttr(e.Attr)) +} + +type byAttr []Attr + +func (a byAttr) Len() int { + return len(a) +} + +func (a byAttr) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} + +func (a byAttr) Less(i, j int) bool { + sp := strings.Compare(a[i].Space, a[j].Space) + if sp == 0 { + return strings.Compare(a[i].Key, a[j].Key) < 0 + } + return sp < 0 +} + +// FullKey returns the attribute a's complete key, including namespace prefix +// if present. +func (a *Attr) FullKey() string { + if a.Space == "" { + return a.Key + } + return a.Space + ":" + a.Key +} + +// Element returns the element containing the attribute. +func (a *Attr) Element() *Element { + return a.element +} + +// NamespaceURI returns the XML namespace URI associated with the attribute. +// If the element is part of the XML default namespace, NamespaceURI returns +// the empty string. +func (a *Attr) NamespaceURI() string { + return a.element.NamespaceURI() +} + +// writeTo serializes the attribute to the writer. +func (a *Attr) writeTo(w *bufio.Writer, s *WriteSettings) { + w.WriteString(a.FullKey()) + w.WriteString(`="`) + var m escapeMode + if s.CanonicalAttrVal { + m = escapeCanonicalAttr + } else { + m = escapeNormal + } + escapeString(w, a.Value, m) + w.WriteByte('"') +} + +// NewText creates a parentless CharData token containing character data. +func NewText(text string) *CharData { + return newCharData(text, 0, nil) +} + +// NewCData creates a parentless XML character CDATA section. +func NewCData(data string) *CharData { + return newCharData(data, cdataFlag, nil) +} + +// NewCharData creates a parentless CharData token containing character data. +// +// Deprecated: NewCharData is deprecated. Instead, use NewText, which does the +// same thing. +func NewCharData(data string) *CharData { + return newCharData(data, 0, nil) +} + +// newCharData creates a character data token and binds it to a parent +// element. If parent is nil, the CharData token remains unbound. +func newCharData(data string, flags charDataFlags, parent *Element) *CharData { + c := &CharData{ + Data: data, + parent: parent, + index: -1, + flags: flags, + } + if parent != nil { + parent.addChild(c) + } + return c +} + +// CreateText creates a CharData token containing character data and adds it +// as a child of element e. +func (e *Element) CreateText(text string) *CharData { + return newCharData(text, 0, e) +} + +// CreateCData creates a CharData token containing a CDATA section and adds it +// as a child of element e. +func (e *Element) CreateCData(data string) *CharData { + return newCharData(data, cdataFlag, e) +} + +// CreateCharData creates a CharData token containing character data and adds +// it as a child of element e. +// +// Deprecated: CreateCharData is deprecated. Instead, use CreateText, which +// does the same thing. +func (e *Element) CreateCharData(data string) *CharData { + return newCharData(data, 0, e) +} + +// dup duplicates the character data. +func (c *CharData) dup(parent *Element) Token { + return &CharData{ + Data: c.Data, + flags: c.flags, + parent: parent, + index: c.index, + } +} + +// IsCData returns true if the character data token is to be encoded as a +// CDATA section. +func (c *CharData) IsCData() bool { + return (c.flags & cdataFlag) != 0 +} + +// IsWhitespace returns true if the character data token was created by one of +// the document Indent methods to contain only whitespace. +func (c *CharData) IsWhitespace() bool { + return (c.flags & whitespaceFlag) != 0 +} + +// Parent returns the character data token's parent element, or nil if it has +// no parent. +func (c *CharData) Parent() *Element { + return c.parent +} + +// Index returns the index of this CharData token within its parent element's +// list of child tokens. If this CharData token has no parent element, the +// index is -1. +func (c *CharData) Index() int { + return c.index +} + +// setParent replaces the character data token's parent. +func (c *CharData) setParent(parent *Element) { + c.parent = parent +} + +// setIndex sets the CharData token's index within its parent element's Child +// slice. +func (c *CharData) setIndex(index int) { + c.index = index +} + +// writeTo serializes character data to the writer. +func (c *CharData) writeTo(w *bufio.Writer, s *WriteSettings) { + if c.IsCData() { + w.WriteString(``) + } else { + var m escapeMode + if s.CanonicalText { + m = escapeCanonicalText + } else { + m = escapeNormal + } + escapeString(w, c.Data, m) + } +} + +// 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, + index: -1, + } + 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, + index: c.index, + } +} + +// Parent returns comment token's parent element, or nil if it has no parent. +func (c *Comment) Parent() *Element { + return c.parent +} + +// Index returns the index of this Comment token within its parent element's +// list of child tokens. If this Comment token has no parent element, the +// index is -1. +func (c *Comment) Index() int { + return c.index +} + +// setParent replaces the comment token's parent. +func (c *Comment) setParent(parent *Element) { + c.parent = parent +} + +// setIndex sets the Comment token's index within its parent element's Child +// slice. +func (c *Comment) setIndex(index int) { + c.index = index +} + +// 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, + index: -1, + } + 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, + index: d.index, + } +} + +// Parent returns directive token's parent element, or nil if it has no +// parent. +func (d *Directive) Parent() *Element { + return d.parent +} + +// Index returns the index of this Directive token within its parent element's +// list of child tokens. If this Directive token has no parent element, the +// index is -1. +func (d *Directive) Index() int { + return d.index +} + +// setParent replaces the directive token's parent. +func (d *Directive) setParent(parent *Element) { + d.parent = parent +} + +// setIndex sets the Directive token's index within its parent element's Child +// slice. +func (d *Directive) setIndex(index int) { + d.index = index +} + +// 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, + index: -1, + } + 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, + index: p.index, + } +} + +// Parent returns processing instruction token's parent element, or nil if it +// has no parent. +func (p *ProcInst) Parent() *Element { + return p.parent +} + +// Index returns the index of this ProcInst token within its parent element's +// list of child tokens. If this ProcInst token has no parent element, the +// index is -1. +func (p *ProcInst) Index() int { + return p.index +} + +// setParent replaces the processing instruction token's parent. +func (p *ProcInst) setParent(parent *Element) { + p.parent = parent +} + +// setIndex sets the processing instruction token's index within its parent +// element's Child slice. +func (p *ProcInst) setIndex(index int) { + p.index = index +} + +// 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 0000000000..825e14e914 --- /dev/null +++ b/vendor/github.com/beevik/etree/helpers.go @@ -0,0 +1,276 @@ +// Copyright 2015-2019 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 ( + "bufio" + "io" + "strings" + "unicode/utf8" +) + +// 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 indentCRLF and indentLF +const ( + indentSpaces = "\r\n " + indentTabs = "\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t" +) + +// indentCRLF returns a CRLF newline followed by n copies of the first +// non-CRLF character in the source string. +func indentCRLF(n int, source string) string { + switch { + case n < 0: + return source[:2] + case n < len(source)-1: + return source[:n+2] + default: + return source + strings.Repeat(source[2:3], n-len(source)+2) + } +} + +// indentLF returns a LF newline followed by n copies of the first non-LF +// character in the source string. +func indentLF(n int, source string) string { + switch { + case n < 0: + return source[1:2] + case n < len(source)-1: + return source[1 : n+2] + default: + return source[1:] + strings.Repeat(source[2:3], n-len(source)+2) + } +} + +// 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 +} + +type escapeMode byte + +const ( + escapeNormal escapeMode = iota + escapeCanonicalText + escapeCanonicalAttr +) + +// escapeString writes an escaped version of a string to the writer. +func escapeString(w *bufio.Writer, s string, m escapeMode) { + var esc []byte + last := 0 + for i := 0; i < len(s); { + r, width := utf8.DecodeRuneInString(s[i:]) + i += width + switch r { + case '&': + esc = []byte("&") + case '<': + esc = []byte("<") + case '>': + if m == escapeCanonicalAttr { + continue + } + esc = []byte(">") + case '\'': + if m != escapeNormal { + continue + } + esc = []byte("'") + case '"': + if m == escapeCanonicalText { + continue + } + esc = []byte(""") + case '\t': + if m != escapeCanonicalAttr { + continue + } + esc = []byte(" ") + case '\n': + if m != escapeCanonicalAttr { + continue + } + esc = []byte(" ") + case '\r': + if m == escapeNormal { + continue + } + esc = []byte(" ") + default: + if !isInCharacterRange(r) || (r == 0xFFFD && width == 1) { + esc = []byte("\uFFFD") + break + } + continue + } + w.WriteString(s[last : i-width]) + w.Write(esc) + last = i + } + w.WriteString(s[last:]) +} + +func isInCharacterRange(r rune) bool { + return r == 0x09 || + r == 0x0A || + r == 0x0D || + r >= 0x20 && r <= 0xD7FF || + r >= 0xE000 && r <= 0xFFFD || + r >= 0x10000 && r <= 0x10FFFF +} diff --git a/vendor/github.com/beevik/etree/path.go b/vendor/github.com/beevik/etree/path.go new file mode 100644 index 0000000000..82db0ac556 --- /dev/null +++ b/vendor/github.com/beevik/etree/path.go @@ -0,0 +1,582 @@ +// Copyright 2015-2019 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 a string that represents a search path through an etree starting +from the document root or an arbitrary element. Paths are used with the +Element object's Find* methods to locate and return desired elements. + +A Path consists of a series of slash-separated "selectors", each of which may +be modified by one or more bracket-enclosed "filters". Selectors are used to +traverse the etree from element to element, while filters are used to narrow +the list of candidate elements at each node. + +Although etree Path strings are similar to XPath strings +(https://www.w3.org/TR/1999/REC-xpath-19991116/), they have a more limited set +of selectors and filtering options. + +The following selectors are supported by etree Path strings: + + . Select the current element. + .. Select the parent of the current element. + * Select all child elements of the current element. + / Select the root element when used at the start of a path. + // Select all descendants of the current element. + tag Select all child elements with a name matching the tag. + +The following basic filters are supported by etree Path strings: + + [@attrib] Keep elements with an attribute named attrib. + [@attrib='val'] Keep elements with an attribute named attrib and value matching val. + [tag] Keep elements with a child element named tag. + [tag='val'] Keep elements with a child element named tag and text matching val. + [n] Keep the n-th element, where n is a numeric index starting from 1. + +The following function filters are also supported: + + [text()] Keep elements with non-empty text. + [text()='val'] Keep elements whose text matches val. + [local-name()='val'] Keep elements whose un-prefixed tag matches val. + [name()='val'] Keep elements whose full tag exactly matches val. + [namespace-prefix()='val'] Keep elements whose namespace prefix matches val. + [namespace-uri()='val'] Keep elements whose namespace URI matches val. + +Here are some examples of Path strings: + +- Select the bookstore child element of the root element: + /bookstore + +- Beginning from the root element, select the title elements of all +descendant book elements having a 'category' attribute of 'WEB': + //book[@category='WEB']/title + +- Beginning from the current element, select the first descendant +book element with a title child element containing the text 'Great +Expectations': + .//book[title='Great Expectations'][1] + +- Beginning from the current element, select all child elements of +book elements with an attribute 'language' set to 'english': + ./book/*[@language='english'] + +- Beginning from the current element, select all child elements of +book elements containing the text 'special': + ./book/*[text()='special'] + +- Beginning from the current element, select all descendant book +elements whose title child element has a 'language' attribute of 'french': + .//book/title[@language='french']/.. + +- Beginning from the current element, select all book elements +belonging to the http://www.w3.org/TR/html4/ namespace: + .//book[namespace-uri()='http://www.w3.org/TR/html4/'] + +*/ +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 ends with //, fix it + if strings.HasSuffix(path, "//") { + path = path + "*" + } + + var segments []segment + + // Check for an absolute path + if strings.HasPrefix(path, "/") { + segments = append(segments, segment{new(selectRoot), []filter{}}) + path = path[1:] + } + + // Split path into segments + 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: []filter{}, + } + 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) + } +} + +var fnTable = map[string]struct { + hasFn func(e *Element) bool + getValFn func(e *Element) string +}{ + "local-name": {nil, (*Element).name}, + "name": {nil, (*Element).FullTag}, + "namespace-prefix": {nil, (*Element).namespacePrefix}, + "namespace-uri": {nil, (*Element).NamespaceURI}, + "text": {(*Element).hasText, (*Element).Text}, +} + +// 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'], [fn()='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 + } + + key := path[:eqindex] + value := path[eqindex+2 : rindex] + + switch { + case key[0] == '@': + return newFilterAttrVal(key[1:], value) + case strings.HasSuffix(key, "()"): + fn := key[:len(key)-2] + if t, ok := fnTable[fn]; ok && t.getValFn != nil { + return newFilterFuncVal(t.getValFn, value) + } + c.err = ErrPath("path has unknown function " + fn) + return nil + default: + return newFilterChildText(key, value) + } + } + + // Filter contains [@attr], [N], [tag] or [fn()] + switch { + case path[0] == '@': + return newFilterAttr(path[1:]) + case strings.HasSuffix(path, "()"): + fn := path[:len(path)-2] + if t, ok := fnTable[fn]; ok && t.hasFn != nil { + return newFilterFunc(t.hasFn) + } + c.err = ErrPath("path has unknown function " + fn) + return nil + 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) +} + +// selectRoot selects the element's root node. +type selectRoot struct{} + +func (s *selectRoot) apply(e *Element, p *pather) { + root := e + for root.parent != nil { + root = root.parent + } + p.candidates = append(p.candidates, root) +} + +// 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] +} + +// filterFunc filters the candidate list for elements satisfying a custom +// boolean function. +type filterFunc struct { + fn func(e *Element) bool +} + +func newFilterFunc(fn func(e *Element) bool) *filterFunc { + return &filterFunc{fn} +} + +func (f *filterFunc) apply(p *pather) { + for _, c := range p.candidates { + if f.fn(c) { + p.scratch = append(p.scratch, c) + } + } + p.candidates, p.scratch = p.scratch, p.candidates[0:0] +} + +// filterFuncVal filters the candidate list for elements containing a value +// matching the result of a custom function. +type filterFuncVal struct { + fn func(e *Element) string + val string +} + +func newFilterFuncVal(fn func(e *Element) string, value string) *filterFuncVal { + return &filterFuncVal{fn, value} +} + +func (f *filterFuncVal) apply(p *pather) { + for _, c := range p.candidates { + if f.fn(c) == f.val { + p.scratch = append(p.scratch, c) + } + } + 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/ma314smith/signedxml/.gitignore b/vendor/github.com/ma314smith/signedxml/.gitignore new file mode 100644 index 0000000000..daf913b1b3 --- /dev/null +++ b/vendor/github.com/ma314smith/signedxml/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/vendor/github.com/ma314smith/signedxml/.travis.yml b/vendor/github.com/ma314smith/signedxml/.travis.yml new file mode 100644 index 0000000000..17364db595 --- /dev/null +++ b/vendor/github.com/ma314smith/signedxml/.travis.yml @@ -0,0 +1,14 @@ +language: go +sudo: false + +go: + - 1.x + - master + +before_install: + - go get -v golang.org/x/lint/golint + +script: + - go vet ./... + - golint ./... + - go test -cover -v ./... diff --git a/vendor/github.com/ma314smith/signedxml/LICENSE.md b/vendor/github.com/ma314smith/signedxml/LICENSE.md new file mode 100644 index 0000000000..538c3831f8 --- /dev/null +++ b/vendor/github.com/ma314smith/signedxml/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Matt Smith + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/ma314smith/signedxml/README.md b/vendor/github.com/ma314smith/signedxml/README.md new file mode 100644 index 0000000000..9e8dd08269 --- /dev/null +++ b/vendor/github.com/ma314smith/signedxml/README.md @@ -0,0 +1,111 @@ +## signedxml + +[![Build Status](https://travis-ci.org/ma314smith/signedxml.svg?branch=master)](https://travis-ci.org/ma314smith/signedxml) +[![GoDoc](https://godoc.org/github.com/ma314smith/signedxml?status.svg)](https://godoc.org/github.com/ma314smith/signedxml) + +The signedxml package transforms and validates signed xml documents. The main use case is to support Single Sign On protocols like SAML and WS-Federation. + +Other packages that provide similar functionality rely on C libraries, which makes them difficult to run across platforms without significant configuration. `signedxml` is written in pure go, and can be easily used on any platform. + +### Install + +`go get github.com/ma314smith/signedxml` + +### Included Algorithms + +- Hashes + - http://www.w3.org/2001/04/xmldsig-more#md5 + - http://www.w3.org/2000/09/xmldsig#sha1 + - http://www.w3.org/2001/04/xmldsig-more#sha224 + - http://www.w3.org/2001/04/xmlenc#sha256 + - http://www.w3.org/2001/04/xmldsig-more#sha384 + - http://www.w3.org/2001/04/xmlenc#sha512 + - http://www.w3.org/2001/04/xmlenc#ripemd160 + + +- Signatures + - http://www.w3.org/2001/04/xmldsig-more#rsa-md2 + - http://www.w3.org/2001/04/xmldsig-more#rsa-md5 + - http://www.w3.org/2000/09/xmldsig#rsa-sha1 + - http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 + - http://www.w3.org/2001/04/xmldsig-more#rsa-sha384 + - http://www.w3.org/2001/04/xmldsig-more#rsa-sha512 + - http://www.w3.org/2000/09/xmldsig#dsa-sha1 + - http://www.w3.org/2000/09/xmldsig#dsa-sha256 + - http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1 + - http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256 + - http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384 + - http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512 + + +- Canonicalization Methods/Transforms + - http://www.w3.org/2000/09/xmldsig#enveloped-signature + - http://www.w3.org/2001/10/xml-exc-c14n# + - http://www.w3.org/2001/10/xml-exc-c14n#WithComments + +### Examples + +#### Validating signed XML +If your signed xml contains the signature and certificate, then you can just pass in the xml and call `Validate()`. +```go +validator, err := signedxml.NewValidator(``) +xml, err = validator.ValidateReferences() +``` +`ValidateReferences()` verifies the DigestValue and SignatureValue in the xml document, and returns the signed payload(s). If the error value is `nil`, then the signed xml is valid. + +The x509.Certificate that was successfully used to validate the xml will be available by calling: +```go +validator.SigningCert() +``` +You can then verify that you trust the certificate. You can optionally supply your trusted certificates ahead of time by assigning them to the `Certificates` property of the `Validator` object, which is an x509.Certificate array. + +#### Using an external Signature +If you need to specify an external Signature, you can use the `SetSignature()` function to assign it: +```go +validator.SetSignature(<`Signature>`) +``` + +#### Generating signed XML +It is expected that your XML contains the Signature element with all the parameters set (except DigestValue and SignatureValue). +```go +signer, err := signedxml.NewSigner(` uri lookup + for _, attr := range node.Attr { + if attr.Space == "xmlns" { + e.namespaces[attr.Key] = attr.Value + } + } + + // handle the namespace of the node itself + if node.Space != "" { + if !contains(prefixesInScope, node.Space) { + nsListToRender["xmlns:"+node.Space] = e.namespaces[node.Space] + prefixesInScope = append(prefixesInScope, node.Space) + } + } else if defaultNS != currentNS { + newDefaultNS = currentNS + elementAttributes = append(elementAttributes, + etree.Attr{Key: "xmlns", Value: currentNS}) + } + + for _, attr := range node.Attr { + // include the namespaces if they are in the inclusiveNamespacePrefixList + if attr.Space == "xmlns" { + if !contains(prefixesInScope, attr.Key) && + contains(e.inclusiveNamespacePrefixList, attr.Key) { + + nsListToRender["xmlns:"+attr.Key] = attr.Value + prefixesInScope = append(prefixesInScope, attr.Key) + } + } + + // include namespaces for qualfied attributes + if attr.Space != "" && + attr.Space != "xmlns" && + !contains(prefixesInScope, attr.Space) { + + nsListToRender["xmlns:"+attr.Space] = e.namespaces[attr.Space] + prefixesInScope = append(prefixesInScope, attr.Space) + } + + // inclued all non-namespace attributes + if attr.Space != "xmlns" && attr.Key != "xmlns" { + attrListToRender = append(attrListToRender, + attribute{ + prefix: attr.Space, + uri: e.namespaces[attr.Space], + key: attr.Key, + value: attr.Value, + }) + } + } + + // sort and add the namespace attributes first + sortedNSList := getSortedNamespaces(nsListToRender) + elementAttributes = append(elementAttributes, sortedNSList...) + // then sort and add the non-namespace attributes + sortedAttributes := getSortedAttributes(attrListToRender) + elementAttributes = append(elementAttributes, sortedAttributes...) + // replace the nodes attributes with the sorted copy + node.Attr = elementAttributes + return currentNS, prefixesInScope +} + +func contains(slice []string, value string) bool { + for _, s := range slice { + if s == value { + return true + } + } + return false +} + +// getSortedNamespaces sorts the namespace attributes by their prefix +func getSortedNamespaces(list map[string]string) []etree.Attr { + var keys []string + for k := range list { + keys = append(keys, k) + } + sort.Strings(keys) + + elem := etree.Element{} + for _, k := range keys { + elem.CreateAttr(k, list[k]) + } + + return elem.Attr +} + +// getSortedAttributes sorts attributes by their namespace URIs +func getSortedAttributes(list attributes) []etree.Attr { + sort.Sort(list) + attrs := make([]etree.Attr, len(list)) + for i, a := range list { + attrs[i] = etree.Attr{ + Space: a.prefix, + Key: a.key, + Value: a.value, + } + } + return attrs +} + +func removeTokenFromElement(token etree.Token, e *etree.Element) *etree.Token { + for i, t := range e.Child { + if t == token { + e.Child = append(e.Child[0:i], e.Child[i+1:]...) + return &t + } + } + return nil +} + +func removeTokenFromDocument(token etree.Token, d *etree.Document) *etree.Token { + for i, t := range d.Child { + if t == token { + d.Child = append(d.Child[0:i], d.Child[i+1:]...) + return &t + } + } + return nil +} + +// 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 +} diff --git a/vendor/github.com/ma314smith/signedxml/signedxml.go b/vendor/github.com/ma314smith/signedxml/signedxml.go new file mode 100644 index 0000000000..f14578c86f --- /dev/null +++ b/vendor/github.com/ma314smith/signedxml/signedxml.go @@ -0,0 +1,335 @@ +// Package signedxml transforms and validates signedxml documents +package signedxml + +import ( + "crypto" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "errors" + "fmt" + "log" + "os" + "strings" + + "github.com/beevik/etree" +) + +var logger = log.New(os.Stdout, "DEBUG-SIGNEDXML: ", log.Ldate|log.Ltime|log.Lshortfile) + +func init() { + hashAlgorithms = map[string]crypto.Hash{ + "http://www.w3.org/2001/04/xmldsig-more#md5": crypto.MD5, + "http://www.w3.org/2000/09/xmldsig#sha1": crypto.SHA1, + "http://www.w3.org/2001/04/xmldsig-more#sha224": crypto.SHA224, + "http://www.w3.org/2001/04/xmlenc#sha256": crypto.SHA256, + "http://www.w3.org/2001/04/xmldsig-more#sha384": crypto.SHA384, + "http://www.w3.org/2001/04/xmlenc#sha512": crypto.SHA512, + "http://www.w3.org/2001/04/xmlenc#ripemd160": crypto.RIPEMD160, + } + + signatureAlgorithms = map[string]x509.SignatureAlgorithm{ + "http://www.w3.org/2001/04/xmldsig-more#rsa-md2": x509.MD2WithRSA, + "http://www.w3.org/2001/04/xmldsig-more#rsa-md5": x509.MD5WithRSA, + "http://www.w3.org/2000/09/xmldsig#rsa-sha1": x509.SHA1WithRSA, + "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256": x509.SHA256WithRSA, + "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384": x509.SHA384WithRSA, + "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512": x509.SHA512WithRSA, + "http://www.w3.org/2000/09/xmldsig#dsa-sha1": x509.DSAWithSHA1, + "http://www.w3.org/2000/09/xmldsig#dsa-sha256": x509.DSAWithSHA256, + "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1": x509.ECDSAWithSHA1, + "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256": x509.ECDSAWithSHA256, + "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384": x509.ECDSAWithSHA384, + "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512": x509.ECDSAWithSHA512, + } + + CanonicalizationAlgorithms = map[string]CanonicalizationAlgorithm{ + "http://www.w3.org/2000/09/xmldsig#enveloped-signature": EnvelopedSignature{}, + "http://www.w3.org/2001/10/xml-exc-c14n#": ExclusiveCanonicalization{}, + "http://www.w3.org/2001/10/xml-exc-c14n#WithComments": ExclusiveCanonicalization{WithComments: true}, + } +} + +// CanonicalizationAlgorithm defines an interface for processing an XML +// document into a standard format. +// +// If any child elements are in the Transform node, the entire transform node +// will be passed to the Process method through the transformXML parameter as an +// XML string. This is necessary for transforms that need additional processing +// data, like XPath (http://www.w3.org/TR/xmldsig-core/#sec-XPath). If there are +// no child elements in Transform (or CanonicalizationMethod), then an empty +// string will be passed through. +type CanonicalizationAlgorithm interface { + Process(inputXML string, transformXML string) (outputXML string, err error) +} + +// CanonicalizationAlgorithms maps the CanonicalizationMethod or +// Transform Algorithm URIs to a type that implements the +// CanonicalizationAlgorithm interface. +// +// Implementations are provided for the following transforms: +// http://www.w3.org/2001/10/xml-exc-c14n# (ExclusiveCanonicalization) +// http://www.w3.org/2001/10/xml-exc-c14n#WithComments (ExclusiveCanonicalizationWithComments) +// http://www.w3.org/2000/09/xmldsig#enveloped-signature (EnvelopedSignature) +// +// Custom implementations can be added to the map +var CanonicalizationAlgorithms map[string]CanonicalizationAlgorithm +var hashAlgorithms map[string]crypto.Hash +var signatureAlgorithms map[string]x509.SignatureAlgorithm + +// signatureData provides options for verifying a signed XML document +type signatureData struct { + xml *etree.Document + signature *etree.Element + signedInfo *etree.Element + sigValue string + sigAlgorithm x509.SignatureAlgorithm + canonAlgorithm CanonicalizationAlgorithm + refIDAttribute string +} + +// SetSignature can be used to assign an external signature for the XML doc +// that Validator will verify +func (s *signatureData) SetSignature(sig string) error { + doc := etree.NewDocument() + err := doc.ReadFromString(sig) + s.signature = doc.Root() + return err +} + +func (s *signatureData) parseEnvelopedSignature() error { + sig := s.xml.FindElement(".//Signature") + if sig != nil { + s.signature = sig + } else { + return errors.New("signedxml: Unable to find a unique signature element " + + "in the xml document. The signature must either be enveloped in the " + + "xml doc or externally assigned to Validator.SetSignature") + } + return nil +} + +func (s *signatureData) parseSignedInfo() error { + s.signedInfo = nil + s.signedInfo = s.signature.SelectElement("SignedInfo") + if s.signedInfo == nil { + return errors.New("signedxml: unable to find SignedInfo element") + } + + // move the Signature level namespace down to SignedInfo so that the signature + // value will match up + if s.signedInfo.Space != "" { + attr := s.signature.SelectAttr(s.signedInfo.Space) + if attr != nil { + s.signedInfo.Attr = []etree.Attr{*attr} + } + } else { + attr := s.signature.SelectAttr("xmlns") + if attr != nil { + s.signedInfo.Attr = []etree.Attr{*attr} + } + } + + // Copy SignedInfo xmlns: into itself if it does not exist and is defined as a root attribute + root := s.xml.Root() + + if root != nil { + sigNS := root.SelectAttr("xmlns:" + s.signedInfo.Space) + if sigNS != nil { + if s.signedInfo.SelectAttr("xmlns:"+s.signedInfo.Space) == nil { + s.signedInfo.CreateAttr("xmlns:"+s.signedInfo.Space, sigNS.Value) + } + } + } + + return nil +} + +func (s *signatureData) parseSigValue() error { + s.sigValue = "" + sigValueElement := s.signature.SelectElement("SignatureValue") + if sigValueElement != nil { + s.sigValue = sigValueElement.Text() + return nil + } + return errors.New("signedxml: unable to find SignatureValue") +} + +func (s *signatureData) parseSigAlgorithm() error { + s.sigAlgorithm = x509.UnknownSignatureAlgorithm + sigMethod := s.signedInfo.SelectElement("SignatureMethod") + + var sigAlgoURI string + if sigMethod == nil { + return errors.New("signedxml: Unable to find SignatureMethod element") + } + + sigAlgoURI = sigMethod.SelectAttrValue("Algorithm", "") + if sigAlgoURI == "" { + return errors.New("signedxml: Unable to find Algorithm in " + + "SignatureMethod element") + } + + sigAlgo, ok := signatureAlgorithms[sigAlgoURI] + if ok { + s.sigAlgorithm = sigAlgo + return nil + } + + return errors.New("signedxml: Unsupported Algorithm " + sigAlgoURI + " in " + + "SignatureMethod") +} + +func (s *signatureData) parseCanonAlgorithm() error { + s.canonAlgorithm = nil + canonMethod := s.signedInfo.SelectElement("CanonicalizationMethod") + + var canonAlgoURI string + if canonMethod == nil { + return errors.New("signedxml: Unable to find CanonicalizationMethod element") + } + + canonAlgoURI = canonMethod.SelectAttrValue("Algorithm", "") + if canonAlgoURI == "" { + return errors.New("signedxml: Unable to find Algorithm in " + + "CanonicalizationMethod element") + } + + canonAlgo, ok := CanonicalizationAlgorithms[canonAlgoURI] + if ok { + s.canonAlgorithm = canonAlgo + return nil + } + + return errors.New("signedxml: Unsupported Algorithm " + canonAlgoURI + " in " + + "CanonicalizationMethod") +} + +func (s *signatureData) getReferencedXML(reference *etree.Element, inputDoc *etree.Document) (outputDoc *etree.Document, err error) { + uri := reference.SelectAttrValue("URI", "") + uri = strings.Replace(uri, "#", "", 1) + // populate doc with the referenced xml from the Reference URI + if uri == "" { + outputDoc = inputDoc + } else { + refIDAttribute := "ID" + if s.refIDAttribute != "" { + refIDAttribute = s.refIDAttribute + } + path := fmt.Sprintf(".//[@%s='%s']", refIDAttribute, uri) + e := inputDoc.FindElement(path) + if e != nil { + outputDoc = etree.NewDocument() + outputDoc.SetRoot(e.Copy()) + } else { + // SAML v1.1 Assertions use AssertionID + path := fmt.Sprintf(".//[@AssertionID='%s']", uri) + e := inputDoc.FindElement(path) + if e != nil { + outputDoc = etree.NewDocument() + outputDoc.SetRoot(e.Copy()) + } + } + } + + if outputDoc == nil { + return nil, errors.New("signedxml: unable to find refereced xml") + } + + return outputDoc, nil +} + +func getCertFromPEMString(pemString string) (*x509.Certificate, error) { + pubkey := fmt.Sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----", + pemString) + + pemBlock, _ := pem.Decode([]byte(pubkey)) + if pemBlock == nil { + return &x509.Certificate{}, errors.New("Could not parse Public Key PEM") + } + if pemBlock.Type != "PUBLIC KEY" { + return &x509.Certificate{}, errors.New("Found wrong key type") + } + + cert, err := x509.ParseCertificate(pemBlock.Bytes) + return cert, err +} + +func processTransform(transform *etree.Element, + docIn *etree.Document) (docOut *etree.Document, err error) { + + transformAlgoURI := transform.SelectAttrValue("Algorithm", "") + if transformAlgoURI == "" { + return nil, errors.New("signedxml: unable to find Algorithm in Transform") + } + + transformAlgo, ok := CanonicalizationAlgorithms[transformAlgoURI] + if !ok { + return nil, fmt.Errorf("signedxml: unable to find matching transform"+ + "algorithm for %s in CanonicalizationAlgorithms", transformAlgoURI) + } + + var transformContent string + + if transform.ChildElements() != nil { + tDoc := etree.NewDocument() + tDoc.SetRoot(transform.Copy()) + transformContent, err = tDoc.WriteToString() + if err != nil { + return nil, err + } + } + + docString, err := docIn.WriteToString() + if err != nil { + return nil, err + } + + docString, err = transformAlgo.Process(docString, transformContent) + if err != nil { + return nil, err + } + + docOut = etree.NewDocument() + docOut.ReadFromString(docString) + + return docOut, nil +} + +func calculateHash(reference *etree.Element, doc *etree.Document) (string, error) { + digestMethodElement := reference.SelectElement("DigestMethod") + if digestMethodElement == nil { + return "", errors.New("signedxml: unable to find DigestMethod") + } + + digestMethodURI := digestMethodElement.SelectAttrValue("Algorithm", "") + if digestMethodURI == "" { + return "", errors.New("signedxml: unable to find Algorithm in DigestMethod") + } + + digestAlgo, ok := hashAlgorithms[digestMethodURI] + if !ok { + return "", fmt.Errorf("signedxml: unable to find matching hash"+ + "algorithm for %s in hashAlgorithms", digestMethodURI) + } + + doc.WriteSettings.CanonicalEndTags = true + doc.WriteSettings.CanonicalText = true + doc.WriteSettings.CanonicalAttrVal = true + + h := digestAlgo.New() + docBytes, err := doc.WriteToBytes() + if err != nil { + return "", err + } + + // ioutil.WriteFile("C:/Temp/SignedXML/Suspect.xml", docBytes, 0644) + // s, _ := doc.WriteToString() + // logger.Println(s) + + h.Write(docBytes) + d := h.Sum(nil) + calculatedValue := base64.StdEncoding.EncodeToString(d) + + return calculatedValue, nil +} diff --git a/vendor/github.com/ma314smith/signedxml/signer.go b/vendor/github.com/ma314smith/signedxml/signer.go new file mode 100644 index 0000000000..d9abe473bc --- /dev/null +++ b/vendor/github.com/ma314smith/signedxml/signer.go @@ -0,0 +1,176 @@ +package signedxml + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "errors" + + "github.com/beevik/etree" +) + +var signingAlgorithms map[x509.SignatureAlgorithm]cryptoHash + +func init() { + signingAlgorithms = map[x509.SignatureAlgorithm]cryptoHash{ + // MD2 not supported + // x509.MD2WithRSA: cryptoHash{algorithm: "rsa", hash: crypto.MD2}, + x509.MD5WithRSA: cryptoHash{algorithm: "rsa", hash: crypto.MD5}, + x509.SHA1WithRSA: cryptoHash{algorithm: "rsa", hash: crypto.SHA1}, + x509.SHA256WithRSA: cryptoHash{algorithm: "rsa", hash: crypto.SHA256}, + x509.SHA384WithRSA: cryptoHash{algorithm: "rsa", hash: crypto.SHA384}, + x509.SHA512WithRSA: cryptoHash{algorithm: "rsa", hash: crypto.SHA512}, + // DSA not supported + // x509.DSAWithSHA1: cryptoHash{algorithm: "dsa", hash: crypto.SHA1}, + // x509.DSAWithSHA256:cryptoHash{algorithm: "dsa", hash: crypto.SHA256}, + // Golang ECDSA support is lacking, can't seem to load private keys + // x509.ECDSAWithSHA1: cryptoHash{algorithm: "ecdsa", hash: crypto.SHA1}, + // x509.ECDSAWithSHA256: cryptoHash{algorithm: "ecdsa", hash: crypto.SHA256}, + // x509.ECDSAWithSHA384: cryptoHash{algorithm: "ecdsa", hash: crypto.SHA384}, + // x509.ECDSAWithSHA512: cryptoHash{algorithm: "ecdsa", hash: crypto.SHA512}, + } +} + +type cryptoHash struct { + algorithm string + hash crypto.Hash +} + +// Signer provides options for signing an XML document +type Signer struct { + signatureData + privateKey interface{} +} + +// NewSigner returns a *Signer for the XML provided +func NewSigner(xml string) (*Signer, error) { + doc := etree.NewDocument() + err := doc.ReadFromString(xml) + if err != nil { + return nil, err + } + s := &Signer{signatureData: signatureData{xml: doc}} + return s, nil +} + +// Sign populates the XML digest and signature based on the parameters present and privateKey given +func (s *Signer) Sign(privateKey interface{}) (string, error) { + s.privateKey = privateKey + + if s.signature == nil { + if err := s.parseEnvelopedSignature(); err != nil { + return "", err + } + } + if err := s.parseSignedInfo(); err != nil { + return "", err + } + if err := s.parseSigAlgorithm(); err != nil { + return "", err + } + if err := s.parseCanonAlgorithm(); err != nil { + return "", err + } + if err := s.setDigest(); err != nil { + return "", err + } + if err := s.setSignature(); err != nil { + return "", err + } + + xml, err := s.xml.WriteToString() + if err != nil { + return "", err + } + return xml, nil +} + +// SetReferenceIDAttribute set the referenceIDAttribute +func (s *Signer) SetReferenceIDAttribute(refIDAttribute string) { + s.signatureData.refIDAttribute = refIDAttribute +} + +func (s *Signer) setDigest() (err error) { + references := s.signedInfo.FindElements("./Reference") + for _, ref := range references { + doc := s.xml.Copy() + transforms := ref.SelectElement("Transforms") + for _, transform := range transforms.SelectElements("Transform") { + doc, err = processTransform(transform, doc) + if err != nil { + return err + } + } + + doc, err := s.getReferencedXML(ref, doc) + if err != nil { + return err + } + + calculatedValue, err := calculateHash(ref, doc) + if err != nil { + return err + } + + digestValueElement := ref.SelectElement("DigestValue") + if digestValueElement == nil { + return errors.New("signedxml: unable to find DigestValue") + } + digestValueElement.SetText(calculatedValue) + } + return nil +} + +func (s *Signer) setSignature() error { + doc := etree.NewDocument() + doc.SetRoot(s.signedInfo.Copy()) + signedInfo, err := doc.WriteToString() + if err != nil { + return err + } + + canonSignedInfo, err := s.canonAlgorithm.Process(signedInfo, "") + if err != nil { + return err + } + + var hashed, signature []byte + //var h1, h2 *big.Int + signingAlgorithm, ok := signingAlgorithms[s.sigAlgorithm] + if !ok { + return errors.New("signedxml: unsupported algorithm") + } + + hasher := signingAlgorithm.hash.New() + hasher.Write([]byte(canonSignedInfo)) + hashed = hasher.Sum(nil) + + switch signingAlgorithm.algorithm { + case "rsa": + signature, err = rsa.SignPKCS1v15(rand.Reader, s.privateKey.(*rsa.PrivateKey), signingAlgorithm.hash, hashed) + /* + case "dsa": + h1, h2, err = dsa.Sign(rand.Reader, s.privateKey.(*dsa.PrivateKey), hashed) + case "ecdsa": + h1, h2, err = ecdsa.Sign(rand.Reader, s.privateKey.(*ecdsa.PrivateKey), hashed) + */ + } + if err != nil { + return err + } + + // DSA and ECDSA has not been validated + /* + if signature == nil && h1 != nil && h2 != nil { + signature = append(h1.Bytes(), h2.Bytes()...) + } + */ + + b64 := base64.StdEncoding.EncodeToString(signature) + sigValueElement := s.signature.SelectElement("SignatureValue") + sigValueElement.SetText(b64) + + return nil +} diff --git a/vendor/github.com/ma314smith/signedxml/validator.go b/vendor/github.com/ma314smith/signedxml/validator.go new file mode 100644 index 0000000000..eb1358096a --- /dev/null +++ b/vendor/github.com/ma314smith/signedxml/validator.go @@ -0,0 +1,205 @@ +package signedxml + +import ( + "crypto/x509" + "encoding/base64" + "errors" + "fmt" + "log" + + "github.com/beevik/etree" +) + +// Validator provides options for verifying a signed XML document +type Validator struct { + Certificates []x509.Certificate + signingCert x509.Certificate + signatureData +} + +// NewValidator returns a *Validator for the XML provided +func NewValidator(xml string) (*Validator, error) { + doc := etree.NewDocument() + err := doc.ReadFromString(xml) + if err != nil { + return nil, err + } + v := &Validator{signatureData: signatureData{xml: doc}} + return v, nil +} + +// SetReferenceIDAttribute set the referenceIDAttribute +func (v *Validator) SetReferenceIDAttribute(refIDAttribute string) { + v.signatureData.refIDAttribute = refIDAttribute +} + +// SetXML is used to assign the XML document that the Validator will verify +func (v *Validator) SetXML(xml string) error { + doc := etree.NewDocument() + err := doc.ReadFromString(xml) + v.xml = doc + return err +} + +// SigningCert returns the certificate, if any, that was used to successfully +// validate the signature of the XML document. This will be a zero value +// x509.Certificate before Validator.Validate is successfully called. +func (v *Validator) SigningCert() x509.Certificate { + return v.signingCert +} + +// Validate validates the Reference digest values, and the signature value +// over the SignedInfo. +// +// Deprecated: Use ValidateReferences instead +func (v *Validator) Validate() error { + _, err := v.ValidateReferences() + return err +} + +// ValidateReferences validates the Reference digest values, and the signature value +// over the SignedInfo. +// +// If the signature is enveloped in the XML, then it will be used. +// Otherwise, an external signature should be assigned using +// Validator.SetSignature. +// +// The references returned by this method can be used to verify what was signed. +func (v *Validator) ValidateReferences() ([]string, error) { + if err := v.loadValuesFromXML(); err != nil { + return nil, err + } + + referenced, err := v.validateReferences() + if err != nil { + return nil, err + } + + var ref []string + for _, doc := range referenced { + docStr, err := doc.WriteToString() + if err != nil { + return nil, err + } + ref = append(ref, docStr) + } + + err = v.validateSignature() + return ref, err +} + +func (v *Validator) loadValuesFromXML() error { + if v.signature == nil { + if err := v.parseEnvelopedSignature(); err != nil { + return err + } + } + if err := v.parseSignedInfo(); err != nil { + return err + } + if err := v.parseSigValue(); err != nil { + return err + } + if err := v.parseSigAlgorithm(); err != nil { + return err + } + if err := v.parseCanonAlgorithm(); err != nil { + return err + } + if err := v.loadCertificates(); err != nil { + return err + } + return nil +} + +func (v *Validator) validateReferences() (referenced []*etree.Document, err error) { + references := v.signedInfo.FindElements("./Reference") + for _, ref := range references { + doc := v.xml.Copy() + transforms := ref.SelectElement("Transforms") + for _, transform := range transforms.SelectElements("Transform") { + doc, err = processTransform(transform, doc) + if err != nil { + return nil, err + } + } + + doc, err = v.getReferencedXML(ref, doc) + if err != nil { + return nil, err + } + + referenced = append(referenced, doc) + + digestValueElement := ref.SelectElement("DigestValue") + if digestValueElement == nil { + return nil, errors.New("signedxml: unable to find DigestValue") + } + digestValue := digestValueElement.Text() + + calculatedValue, err := calculateHash(ref, doc) + if err != nil { + return nil, err + } + + if calculatedValue != digestValue { + return nil, fmt.Errorf("signedxml: Calculated digest does not match the"+ + " expected digestvalue of %s", digestValue) + } + } + return referenced, nil +} + +func (v *Validator) validateSignature() error { + doc := etree.NewDocument() + doc.SetRoot(v.signedInfo.Copy()) + signedInfo, err := doc.WriteToString() + if err != nil { + return err + } + + canonSignedInfo, err := v.canonAlgorithm.Process(signedInfo, "") + if err != nil { + return err + } + + b64, err := base64.StdEncoding.DecodeString(v.sigValue) + if err != nil { + return err + } + sig := []byte(b64) + + v.signingCert = x509.Certificate{} + for _, cert := range v.Certificates { + err := cert.CheckSignature(v.sigAlgorithm, []byte(canonSignedInfo), sig) + if err == nil { + v.signingCert = cert + return nil + } + } + + return errors.New("signedxml: Calculated signature does not match the " + + "SignatureValue provided") +} + +func (v *Validator) loadCertificates() error { + // If v.Certificates is already populated, then the client has already set it + // to the desired cert. Otherwise, let's pull the public keys from the XML + if len(v.Certificates) < 1 { + keydata := v.xml.FindElements(".//X509Certificate") + for _, key := range keydata { + cert, err := getCertFromPEMString(key.Text()) + if err != nil { + log.Printf("signedxml: Unable to load certificate: (%s). "+ + "Looking for another cert.", err) + } else { + v.Certificates = append(v.Certificates, *cert) + } + } + } + + if len(v.Certificates) < 1 { + return errors.New("signedxml: a certificate is required, but was not found") + } + return nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index c7598a083c..eaf00a1055 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -158,6 +158,8 @@ github.com/aws/aws-sdk-go/service/iam github.com/aws/aws-sdk-go/service/s3 github.com/aws/aws-sdk-go/service/sts github.com/aws/aws-sdk-go/service/sts/stsiface +# github.com/beevik/etree v1.1.0 +github.com/beevik/etree # github.com/benbjohnson/clock v1.0.0 github.com/benbjohnson/clock # github.com/beorn7/perks v1.0.0 @@ -393,6 +395,8 @@ github.com/kr/pty github.com/leodido/go-urn # github.com/libvirt/libvirt-go-xml v5.2.0+incompatible github.com/libvirt/libvirt-go-xml +# github.com/ma314smith/signedxml v0.0.0-20200410192636-c342a2d0ae60 +github.com/ma314smith/signedxml # github.com/mattn/go-colorable v0.1.2 github.com/mattn/go-colorable # github.com/mattn/go-isatty v0.0.9