feature: support SAML idp

This commit is contained in:
Qiu Jian
2020-06-16 09:55:05 +08:00
parent 8d81393c03
commit 3b7a1e9467
38 changed files with 6823 additions and 1 deletions

26
cmd/samldemo/main.go Normal file
View File

@@ -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()
}

2
go.mod
View File

@@ -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

4
go.sum
View File

@@ -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=

View File

@@ -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" {

View File

@@ -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 = `<!DOCTYPE html><html lang="en-US"><body>$FORM$</body></html>`
)
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,
}
)

View File

@@ -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"

View File

@@ -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(`<!DOCTYPE html><html lang="zh_CN"><head><meta charset="utf-8"><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head><body><h1>成功退出登录,<a href="%s">重新登录</a></h1></body></html>`, 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(`<!DOCTYPE html><html lang="zh_CN"><head><meta charset="utf-8"><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head><body><h1>正在跳转到云控制台,请等待。。。</h1>$FORM$</body></html>`)
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(`<!doctype html><html lang=en><body><ol>`)
// 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(`<li><a href="%s?EntityID=%s">%s (IDP-Initiated)</a></li>`, 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(`<li><a href="%s">%s (SP-Initiated)</a></li>`, v.url, v.name))
}
htmlBuf.WriteString(`</ol></body></html>`)
appsrv.SendHTML(w, htmlBuf.String())
})
addr := fmt.Sprintf(":%d", options.Port)
app.ListenAndServeTLS(addr, options.Cert, options.Key)
return nil
}

15
pkg/util/samlutils/doc.go Normal file
View File

@@ -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"

View File

@@ -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
}

View File

@@ -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"

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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 = `
<!-- This is the metadata for the SAMLtest SP, named by entityID -->
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" ID="SAMLtestSP" entityID="https://samltest.id/saml/sp">
<!-- This list enumerates the cryptographic algorithms acceptable to this SP -->
<md:Extensions xmlns:alg="urn:oasis:names:tc:SAML:metadata:algsupport">
<alg:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha512"/>
<alg:DigestMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#sha384"/>
<alg:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<alg:DigestMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#sha224"/>
<alg:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<alg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512"/>
<alg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384"/>
<alg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"/>
<alg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha224"/>
<alg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"/>
<alg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"/>
<alg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<alg:SigningMethod Algorithm="http://www.w3.org/2009/xmldsig11#dsa-sha256"/>
<alg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1"/>
<alg:SigningMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
<alg:SigningMethod Algorithm="http://www.w3.org/2000/09/xmldsig#dsa-sha1"/>
</md:Extensions>
<md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:Extensions>
<!-- The location to redirect users to for invocation of an AuthnRequest -->
<init:RequestInitiator xmlns:init="urn:oasis:names:tc:SAML:profiles:SSO:request-init" Binding="urn:oasis:names:tc:SAML:profiles:SSO:request-init" Location="https://samltest.id/Shibboleth.sso/Login"/>
<!-- Display information about this SP that the IdP can present to users -->
<mdui:UIInfo xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui">
<mdui:DisplayName xml:lang="en">SAMLtest SP</mdui:DisplayName>
<mdui:Description xml:lang="en">A free and basic SP for testing SAML deployments</mdui:Description>
<mdui:Logo height="90" width="225">https://samltest.id/saml/logo.png</mdui:Logo>
</mdui:UIInfo>
</md:Extensions>
<!-- A certificate containing the public key for verification of signed messages from this SP.
This is rarely used because the SP sends few signed messages, but using a separate key is better
security hygiene. In practice, many SP's use only one key for both encryption and signature.
Most SAML implementations don't rely on the rest of the certificate's contents. -->
<md:KeyDescriptor use="signing">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>
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=
</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<!-- A certificate containing the public key for encryption of messages sent to the SAMLtest SP.
This key is crucial for securing assertions from IdP's. Multiple encryption keys can be listed
and this will often be necessary for key rollovers. -->
<md:KeyDescriptor use="encryption">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>
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=
</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
<md:EncryptionMethod Algorithm="http://www.w3.org/2009/xmlenc11#aes128-gcm"/>
<md:EncryptionMethod Algorithm="http://www.w3.org/2009/xmlenc11#aes192-gcm"/>
<md:EncryptionMethod Algorithm="http://www.w3.org/2009/xmlenc11#aes256-gcm"/>
<md:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/>
<md:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes192-cbc"/>
<md:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes256-cbc"/>
<md:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc"/>
<md:EncryptionMethod Algorithm="http://www.w3.org/2009/xmlenc11#rsa-oaep"/>
<md:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"/>
</md:KeyDescriptor>
<!-- These endpoints tell IdP's where to send messages, either directly or via
a browser redirect. The locations must match the address of the SP as seen from the outside
world if this host is behind a reverse proxy. -->
<md:ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://samltest.id/Shibboleth.sso/Artifact/SOAP" index="1"/>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://samltest.id/Shibboleth.sso/SLO/SOAP"/>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://samltest.id/Shibboleth.sso/SLO/Redirect"/>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://samltest.id/Shibboleth.sso/SLO/POST"/>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://samltest.id/Shibboleth.sso/SLO/Artifact"/>
<!-- The primary endpoint to which SAML assertions will be delivered. -->
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://samltest.id/Shibboleth.sso/SAML2/POST" index="1"/>
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign" Location="https://samltest.id/Shibboleth.sso/SAML2/POST-SimpleSign" index="2"/>
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://samltest.id/Shibboleth.sso/SAML2/Artifact" index="3"/>
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:PAOS" Location="https://samltest.id/Shibboleth.sso/SAML2/ECP" index="4"/>
</md:SPSSODescriptor>
</md:EntityDescriptor>
`
idpMetadata = `<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" ID="SAMLtestIdP" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:shibmd="urn:mace:shibboleth:metadata:1.0" xmlns:xml="http://www.w3.org/XML/1998/namespace" xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui" entityID="https://samltest.id/saml/idp">
<IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol urn:mace:shibboleth:1.0">
<Extensions>
<!-- An enumeration of the domains this IdP is able to assert scoped attributes, which are
typically those with a @ delimiter, like mail. Most IdP's serve only a single domain. It's crucial
for the SP to check received attribute values match permitted domains to prevent a recognized IdP from
sending attribute values for which a different recognized IdP is authoritative. -->
<shibmd:Scope regexp="false">samltest.id</shibmd:Scope>
<!-- Display information about this IdP that can be used by SP's and discovery
services to identify the IdP meaningfully for end users -->
<mdui:UIInfo>
<mdui:DisplayName xml:lang="en">SAMLtest IdP</mdui:DisplayName>
<mdui:Description xml:lang="en">A free and basic IdP for testing SAML deployments</mdui:Description>
<mdui:Logo height="90" width="225">https://samltest.id/saml/logo.png</mdui:Logo>
</mdui:UIInfo>
</Extensions>
<KeyDescriptor use="signing">
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>
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
</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</KeyDescriptor>
<KeyDescriptor use="signing">
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>
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==
</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</KeyDescriptor>
<KeyDescriptor use="encryption">
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>
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==
</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</KeyDescriptor>
<!-- An endpoint for artifact resolution. Please see Wikipedia for more details about SAML
artifacts and when you may find them useful. -->
<ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://samltest.id/idp/profile/SAML2/SOAP/ArtifactResolution" index="1" />
<!-- A set of endpoints where the IdP can receive logout messages. These must match the public
facing addresses if this IdP is hosted behind a reverse proxy. -->
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://samltest.id/idp/profile/SAML2/Redirect/SLO"/>
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://samltest.id/idp/profile/SAML2/POST/SLO"/>
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign" Location="https://samltest.id/idp/profile/SAML2/POST-SimpleSign/SLO"/>
<!-- A set of endpoints the SP can send AuthnRequests to in order to trigger user authentication. -->
<SingleSignOnService Binding="urn:mace:shibboleth:1.0:profiles:AuthnRequest" Location="https://samltest.id/idp/profile/Shibboleth/SSO"/>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://samltest.id/idp/profile/SAML2/POST/SSO"/>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign" Location="https://samltest.id/idp/profile/SAML2/POST-SimpleSign/SSO"/>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://samltest.id/idp/profile/SAML2/Redirect/SSO"/>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://samltest.id/idp/profile/SAML2/SOAP/ECP"/>
</IDPSSODescriptor>
</EntityDescriptor>`
hwSpMetadata = `<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" ID="M349b8f2417d474387360e922ef39baa" entityID="https://auth.huaweicloud.com/">
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<ds:Reference URI="#M349b8f2417d474387360e922ef39baa">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<ds:DigestValue>diBMwyuN633Q/kBf0M+SQZ4fNCI=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>
k6+6QX8T5+S5wEIWlxwvd5a48xLWmp4Jd0bcs33vNaQVQ93YiztGcroUUqnLYK8uqAsmk2ZP4NtKFL/MyqoznrStglSi7uycwX6X3fswMwvkHJ9UVhH6Gp2Txl2JQ9/0vXy/tlXjQNFbfqzLhqxKC/K/PmKd684XkPYXlkiEJa6nGibu0gse9rvP2hE0G9bDMfdiyQnanfBvN+oNRte3xyI3DWh2P/jDTAPJMYzGM76JIneS8jLPa4gKDz5KcumG/8JV7GUWTDTZZ4ftujsjEuUFdPzKqwMYogMXOnTt8wbisSF8ZlLSMH/TIj6xAO9aSkEAo8HtlHJbekYlVA5Tz5IamLLzfaPpf7+NghLSuATIwY8/pvBZ8qhY8PVOjzRCeoEZJUrlFOcZ2CvO9zVKYdkEa1JC3mUW7CgeQc7G2/9niub7Vu00eyp9AAd9nkPfLoiWam8/yg2TBPRRJ9VKsv2UMFuITWrcFJayjezlTzY3dI3tI8lfoIMEVRaEP1v1D8XbxnxiaiKZXsGQHtpTSLc1ZL441jeZDLa661raUJDlGA6mBc1QukJklEocGg+Q+FeU33MJCaN/rIZCGvrjIZNng3yKFw1+R7/CqeJJlFWw0hPJQGiy6wfrDYSmhwuXJ2vQn3dTHjlT3kWsniiZtuOUi2TjHbq8gVHqlxmPxSs=
</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>
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=
</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</ds:Signature>
<md:SPSSODescriptor AuthnRequestsSigned="true" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:KeyDescriptor use="signing">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>
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=
</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:KeyDescriptor use="encryption">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>
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=
</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:ArtifactResolutionService xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://auth.huaweicloud.com/authui/saml/SAMLAssertionConsumer" index="0" isDefault="true"/>
<md:SingleLogoutService xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://auth.huaweicloud.com/authui/saml/LogoutServiceHTTPRedirect" ResponseLocation="https://auth.huaweicloud.com/authui/saml/LogoutServiceHTTPRedirectResponse"/>
<md:SingleLogoutService xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://auth.huaweicloud.com/authui/saml/LogoutServiceHTTPPost" ResponseLocation="https://auth.huaweicloud.com/authui/saml/LogoutServiceHTTPPostResponse"/>
<md:ManageNameIDService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://auth.huaweicloud.com/authui/saml/ManageNameIDServiceHTTPPost" ResponseLocation="https://auth.huaweicloud.com/authui/saml/ManageNameIDServiceHTTPPostResponse"/>
<md:ManageNameIDService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://auth.huaweicloud.com/authui/saml/ManageNameIDServiceHTTPRedirect" ResponseLocation="https://auth.huaweicloud.com/authui/saml/ManageNameIDServiceHTTPRedirectResponse"/>
<md:ManageNameIDService xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://auth.huaweicloud.com/authui/saml/ManageNameIDServiceSOAP"/>
<md:NameIDFormat xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata">
urn:oasis:names:tc:SAML:2.0:nameid-format:transient
</md:NameIDFormat>
<md:AssertionConsumerService xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://auth.huaweicloud.com/authui/saml/SAMLAssertionConsumer" index="0" isDefault="true"/>
</md:SPSSODescriptor>
</md:EntityDescriptor>`
hwIdpMetadata = `<md:EntityDescriptor
xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="https://www.test.com">
<md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:KeyDescriptor use="signing">
<ds:KeyInfo
xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>
MIICsDCCAhmgAwIBAgIJAKNbH+B0Vm9HMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMTgxMDMwMDIxMzA4WhcNMzMxMDMxMDIxMzA4WjBF
MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
gQDIIZtsLpqLDpXB1LI8tbtwoeOyJbM2PIxJTOqRm1ZM0r7rpvt4kFCgAd68gAsl
YAEeSqUawxV3FUgt62DLMOT2auwBcpywVW7L/ZF4IUziwuFQLWdw5NIGMP5lpt1M
HSel8k4paokoXAwZ2B+Vtku+kDTGLc3cp1T5/ClYE/ofdQIDAQABo4GnMIGkMB0G
A1UdDgQWBBRVZlu4B6TzuNHasJz5tHoMilKLdjB1BgNVHSMEbjBsgBRVZlu4B6Tz
uNHasJz5tHoMilKLdqFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUt
U3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAKNbH+B0
Vm9HMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAhyVdBqW4r94XdwMy
LK42mwqNnHy4WjM8eq9X5FhBckZX+TyM909iH2AsMjpkv8BDIxTiX6tpmNyYhOCp
vCPMmQHl9450maIA7At//sEgL94FNRJbTYkme7F3xI90X0htMr23Yan31lRwdj53
DgagnkMlzQ8QccUXrdQgzXzKb0w=</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:KeyDescriptor use="encryption">
<ds:KeyInfo
xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>
MIICsDCCAhmgAwIBAgIJAKNbH+B0Vm9HMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMTgxMDMwMDIxMzA4WhcNMzMxMDMxMDIxMzA4WjBF
MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
gQDIIZtsLpqLDpXB1LI8tbtwoeOyJbM2PIxJTOqRm1ZM0r7rpvt4kFCgAd68gAsl
YAEeSqUawxV3FUgt62DLMOT2auwBcpywVW7L/ZF4IUziwuFQLWdw5NIGMP5lpt1M
HSel8k4paokoXAwZ2B+Vtku+kDTGLc3cp1T5/ClYE/ofdQIDAQABo4GnMIGkMB0G
A1UdDgQWBBRVZlu4B6TzuNHasJz5tHoMilKLdjB1BgNVHSMEbjBsgBRVZlu4B6Tz
uNHasJz5tHoMilKLdqFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUt
U3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAKNbH+B0
Vm9HMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAhyVdBqW4r94XdwMy
LK42mwqNnHy4WjM8eq9X5FhBckZX+TyM909iH2AsMjpkv8BDIxTiX6tpmNyYhOCp
vCPMmQHl9450maIA7At//sEgL94FNRJbTYkme7F3xI90X0htMr23Yan31lRwdj53
DgagnkMlzQ8QccUXrdQgzXzKb0w=
</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://www.test.com/saml/logout"/>
<md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://www.test.com/saml/login"/>
</md:IDPSSODescriptor>
</md:EntityDescriptor>`
aliyunSpMeta = `<?xml version="1.0"?>
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="urn:alibaba:cloudcomputing">
<SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol" WantAssertionsSigned="true">
<KeyDescriptor use="signing">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>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=</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</KeyDescriptor>
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:entity</NameIDFormat>
<AssertionConsumerService index="1" isDefault="true" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://signin.aliyun.com/saml-role/sso"/>
<AttributeConsumingService index="1">
<ServiceName xml:lang="en">Alibaba Cloud Console Single Sign-On</ServiceName>
<RequestedAttribute isRequired="true" Name="https://www.aliyun.com/SAML-Role/Attributes/Role" FriendlyName="RoleEntitlement"/>
<RequestedAttribute isRequired="true" Name="https://www.aliyun.com/SAML-Role/Attributes/RoleSessionName" FriendlyName="RoleSessionName"/>
<RequestedAttribute isRequired="false" Name="https://www.aliyun.com/SAML-Role/Attributes/SessionDuration" FriendlyName="SessionDuration"/>
</AttributeConsumingService>
</SPSSODescriptor>
<Organization>
<OrganizationName xml:lang="en">Alibaba Cloud Computing Co. Ltd.</OrganizationName>
<OrganizationDisplayName xml:lang="en">AlibabaCloud</OrganizationDisplayName>
<OrganizationURL xml:lang="en">https://www.aliyun.com</OrganizationURL>
</Organization>
</EntityDescriptor>`
aliyunUserMeta = `<?xml version="1.0" encoding="UTF-8"?>
<md:EntityDescriptor ID="https___signin.aliyun.com_1661281931531610_saml_SSO" entityID="https://signin.aliyun.com/1661281931531610/saml/SSO" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"><md:SPSSODescriptor WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"><md:KeyDescriptor use="signing"><ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:X509Data><ds:X509Certificate>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==</ds:X509Certificate></ds:X509Data></ds:KeyInfo></md:KeyDescriptor><md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat><md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://signin.aliyun.com/saml/SSO" index="0" isDefault="true"/></md:SPSSODescriptor></md:EntityDescriptor>`
)
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 = `<?xml version="1.0" encoding="UTF-8"?>
<!-- InResponseTo需要与samlRequest请求的AuthnRequest中的ID配置项保持一致 -->
<!-- Destination需要与SPMetadata中AssertionConsumerService标签下的Location的值保持一致 -->
<saml2p:Response Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" ID="_d794dc393ae6724e236003bf0b917cf0" Destination="https://auth.huaweicloud.com/authui/saml/SAMLAssertionConsumer"
InResponseTo="_dck4mm08qmdhc8k4nuir07hghetdqqg8umg5" IssueInstant="2018-10-30T08:21:41.740Z" Version="2.0"
xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol">
<!-- 必须与IDP Metadata.xml中的entityID保持一致 -->
<saml2:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity"
xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">https://www.test.com</saml2:Issuer>
<saml2p:Status>
<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
<saml2p:StatusMessage>urn:oasis:names:tc:SAML:2.0:status:Success</saml2p:StatusMessage>
</saml2p:Status>
<saml2:Assertion ID="_2320c40ac7b5e857b2d0d4ea0c8758c3" IssueInstant="2018-10-30T08:21:41.740Z" Version="2.0"
xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<!-- 必须与IDP Metadata.xml的entityID保持一致 -->
<saml2:Issuer>https://www.test.com</saml2:Issuer>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
<!-- URI #号后面的值必须和Assertion标签中的ID保持一致 -->
<ds:Reference URI="#_2320c40ac7b5e857b2d0d4ea0c8758c3">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
<ec:InclusiveNamespaces PrefixList="xsd"
xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#" />
</ds:Transform>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
<!-- DigestValue的值为Assertion标签对象做的摘要摘要算法和DigestMethod一致 -->
<ds:DigestValue>rFxrycznfGNYOnprZIFJJou4ro0Mz65+43MIR5F0+H4=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<!-- 合作伙伴签名值,具体生成方法请参见下文描述 -->
<ds:SignatureValue>
YqTWQngAPfGqQmWa610PM7LeefqWdKuveUVINrqL67NoHJIDa2WxLwdVzoJIlJh64QiNPr6+ndmL DCMgIC5F/9ijuzhIICZcc6lHNIjy6EsPkKRjfo9oeoVAqLgG/kmVQYeHLBID0y11RNXXpAVY4nhJ 26KiIVGt7ywyKAmhichE+eW/UYAGiOI5vkfgD2gZUGV+yPkv64k7xK4yAH3mL2NaCPuw/90e4enm iUx0YuazDwM5FiRUSMpcJs0rcNmS6clWAUcCzbOx+y2vJGtTjHb7k3UsmpnTop5eYNp94+sDPEat 8FaV4SgafMEL5z54gpe8+//9yOWEvlBs1b0RYg==
</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<!-- 合作伙伴公钥证书必须与IDP Metadata.xml的公钥证书保持一致 -->
<ds:X509Certificate>
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhK3L160NjP9EhBGQOC2s4r+Wc62bkRkc nUxfhiZwCwJdQCykzuLOAoATnfoEamV5W25xtSS5kFs+4OC0mYVpKcI3SWoydX+UE5Qik5UfJ8Dt G1AvSEKhSluyO9axrV5Uv089jMxBnlm/R+xND73WcZM11yIbKJEZSTCEDfh+KnFbMw108umFMden RZCrNWUJoSp/90XeG0V2Nmj7Fkq72skSifwIASLRq9KqLbmh1QwUX+AoWpHK/jRUBustMBmG1n1i
AqpD4EBjjBOB27k1wXZ30+IoJt8IZmfSZRFoNn5VFWXNeEmZ1aQvGSvd3Tyyw2/Wr+w/8Mags69C mpeX6QIDAQAB
</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</ds:Signature>
<saml2:Subject>
<!-- NameQualifier取值必须与SP Metadata.xml的entityID保持一致 -->
<saml2:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" NameQualifier="https://auth.huaweicloud.com/">Some NameID value</saml2:NameID>
<saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<!-- InResponseTo需要与samlRequest请求的AuthnRequest中的ID配置项保持一致 -->
<saml2:SubjectConfirmationData InResponseTo="_dck4mm08qmdhc8k4nuir07hghetdqqg8umg5" NotBefore="2018-10-28T08:21:41.740Z" NotOnOrAfter="2018-11-01T08:21:41.740Z" Recipient="https://auth.huaweicloud.com/authui/saml/SAMLAssertionConsumer" />
</saml2:SubjectConfirmation>
</saml2:Subject>
<saml2:Conditions NotBefore="2018-10-28T08:21:41.740Z" NotOnOrAfter="2018-11-01T08:21:41.740Z">
<saml2:AudienceRestriction>
<!-- 必须与SP Metadata.xml的entityID保持一致 -->
<saml2:Audience>https://auth.huaweicloud.com/</saml2:Audience>
</saml2:AudienceRestriction>
</saml2:Conditions>
<saml2:AttributeStatement>
<!-- <saml2:AttributeValue></saml2:AttributeValue>之间的取值请参见下表的说明 -->
<saml2:Attribute FriendlyName="xUserId" Name="xUserId" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml2:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xsd:string">*******</saml2:AttributeValue>
</saml2:Attribute>
<!-- xAccountId和xUserId属性的值须一致 -->
<saml2:Attribute FriendlyName="xAccountId" Name="xAccountId" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml2:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xsd:string">********</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute FriendlyName="bpId" Name="bpId" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml2:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xsd:string">******</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute FriendlyName="email" Name="email" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml2:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xsd:string" />
</saml2:Attribute>
<saml2:Attribute FriendlyName="name" Name="name" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml2:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xsd:string">******</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute FriendlyName="mobile" Name="mobile" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml2:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xsd:string">*****</saml2:AttributeValue>
</saml2:Attribute>
</saml2:AttributeStatement>
<saml2:AuthnStatement AuthnInstant="2018-10-30T08:21:41.741Z">
<!-- 必须与SP Metadata.xml的entityID保持一致 -->
<saml2:SubjectLocality Address="https://auth.huaweicloud.com/" />
<saml2:AuthnContext>
<saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml2:AuthnContextClassRef>
</saml2:AuthnContext>
</saml2:AuthnStatement>
</saml2:Assertion>
</saml2p:Response>`
resp2 = `<samlp:Response
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
ID="identifier_2"
InResponseTo="identifier_1"
Version="2.0"
IssueInstant="2004-12-05T09:22:05Z"
Destination="https://sp.example.com/SAML2/SSO/POST">
<saml:Issuer>https://idp.example.org/SAML2</saml:Issuer>
<samlp:Status>
<samlp:StatusCode
Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>
<saml:Assertion
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
ID="identifier_3"
Version="2.0"
IssueInstant="2004-12-05T09:22:05Z">
<saml:Issuer>https://idp.example.org/SAML2</saml:Issuer>
<!-- a POSTed assertion MUST be signed -->
<ds:Signature
xmlns:ds="http://www.w3.org/2000/09/xmldsig#">...</ds:Signature>
<saml:Subject>
<saml:NameID
Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">
3f7b3dcf-1674-4ecd-92c8-1544f346baf8
</saml:NameID>
<saml:SubjectConfirmation
Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData
InResponseTo="identifier_1"
Recipient="https://sp.example.com/SAML2/SSO/POST"
NotOnOrAfter="2004-12-05T09:27:05Z"/>
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions
NotBefore="2004-12-05T09:17:05Z"
NotOnOrAfter="2004-12-05T09:27:05Z">
<saml:AudienceRestriction>
<saml:Audience>https://sp.example.com/SAML2</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement
AuthnInstant="2004-12-05T09:22:00Z"
SessionIndex="identifier_3">
<saml:AuthnContext>
<saml:AuthnContextClassRef>
urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
</saml:Assertion>
</samlp:Response>`
)
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)
}
}
}
}

View File

@@ -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)
}

View File

@@ -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)
}

439
pkg/util/samlutils/types.go Normal file
View File

@@ -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"`
}

128
pkg/util/samlutils/util.go Normal file
View File

@@ -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(`<!DOCTYPE html><html lang="en-US"><body>`)
form.WriteString(`<form id="saml_submit_form" method="POST" action="`)
form.WriteString(action)
form.WriteString(`">`)
for k, v := range attrs {
form.WriteString(fmt.Sprintf("<input type=\"hidden\" name=\"%s\" value=\"%s\" />", k, v))
}
form.WriteString(`<input type="submit" value="Submit" />`)
form.WriteString("</form><script><!--\n")
form.WriteString("document.getElementById('saml_submit_form').submit();\n")
form.WriteString(`//--></script>`)
// form.WriteString(`</body></html>`)
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
}

View File

@@ -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
}

14
vendor/github.com/beevik/etree/.travis.yml generated vendored Normal file
View File

@@ -0,0 +1,14 @@
language: go
sudo: false
go:
- 1.11.x
- tip
matrix:
allow_failures:
- go: tip
script:
- go vet ./...
- go test -v ./...

10
vendor/github.com/beevik/etree/CONTRIBUTORS generated vendored Normal file
View File

@@ -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)

24
vendor/github.com/beevik/etree/LICENSE generated vendored Normal file
View File

@@ -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.

205
vendor/github.com/beevik/etree/README.md generated vendored Normal file
View File

@@ -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
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="style.xsl"?>
<People>
<!--These are all known people-->
<Person name="Jon"/>
<Person name="Sally"/>
</People>
```
### Reading an XML file
Suppose you have a file on disk called `bookstore.xml` containing the
following data:
```xml
<bookstore xmlns:p="urn:schemas-books-com:prices">
<book category="COOKING">
<title lang="en">Everyday Italian</title>
<author>Giada De Laurentiis</author>
<year>2005</year>
<p:price>30.00</p:price>
</book>
<book category="CHILDREN">
<title lang="en">Harry Potter</title>
<author>J K. Rowling</author>
<year>2005</year>
<p:price>29.99</p:price>
</book>
<book category="WEB">
<title lang="en">XQuery Kick Start</title>
<author>James McGovern</author>
<author>Per Bothner</author>
<author>Kurt Cagle</author>
<author>James Linn</author>
<author>Vaidyanathan Nagarajan</author>
<year>2003</year>
<p:price>49.99</p:price>
</book>
<book category="WEB">
<title lang="en">Learning XML</title>
<author>Erik T. Ray</author>
<year>2003</year>
<p:price>39.95</p:price>
</book>
</bookstore>
```
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!

109
vendor/github.com/beevik/etree/RELEASE_NOTES.md generated vendored Normal file
View File

@@ -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.

1453
vendor/github.com/beevik/etree/etree.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

276
vendor/github.com/beevik/etree/helpers.go generated vendored Normal file
View File

@@ -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("&amp;")
case '<':
esc = []byte("&lt;")
case '>':
if m == escapeCanonicalAttr {
continue
}
esc = []byte("&gt;")
case '\'':
if m != escapeNormal {
continue
}
esc = []byte("&apos;")
case '"':
if m == escapeCanonicalText {
continue
}
esc = []byte("&quot;")
case '\t':
if m != escapeCanonicalAttr {
continue
}
esc = []byte("&#x9;")
case '\n':
if m != escapeCanonicalAttr {
continue
}
esc = []byte("&#xA;")
case '\r':
if m == escapeNormal {
continue
}
esc = []byte("&#xD;")
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
}

582
vendor/github.com/beevik/etree/path.go generated vendored Normal file
View File

@@ -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]
}

24
vendor/github.com/ma314smith/signedxml/.gitignore generated vendored Normal file
View File

@@ -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

14
vendor/github.com/ma314smith/signedxml/.travis.yml generated vendored Normal file
View File

@@ -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 ./...

21
vendor/github.com/ma314smith/signedxml/LICENSE.md generated vendored Normal file
View File

@@ -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.

111
vendor/github.com/ma314smith/signedxml/README.md generated vendored Normal file
View File

@@ -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(`<YourXMLString></YourXMLString>`)
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></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(`<YourXMLString></YourXMLString`)
signedXML, err := signer.Sign(`*rsa.PrivateKey object`)
```
`Sign()` will generate the DigestValue and SignatureValue, populate it in the XML, and return the signed XML string.
#### Implementing custom transforms
Additional Transform algorithms can be included by adding to the CanonicalizationAlgorithms map. This interface will need to be implemented:
```go
type CanonicalizationAlgorithm interface {
Process(inputXML string, transformXML string) (outputXML string, err error)
}
```
Simple Example:
```go
type NoChangeCanonicalization struct{}
func (n NoChangeCanonicalization) Process(inputXML string,
transformXML string) (outputXML string, err error) {
return inputXML, nil
}
signedxml.CanonicalizationAlgorithms["http://myTranform"] = NoChangeCanonicalization{}
```
See `envelopedsignature.go` and `exclusivecanonicalization.go` for examples of actual implementations.
### Using a custom reference ID attribute
It is possible to set a custom reference ID attribute for both the signer and the validator. The default value is `"ID"`
Signer example:
```go
signer.SetReferenceIDAttribute("customId")
```
Validator example:
```go
validator.SetReferenceIDAttribute("customId")
```
### Contributions
Contributions are welcome. Just fork the repo and send a pull request.

View File

@@ -0,0 +1,39 @@
package signedxml
import (
"errors"
"github.com/beevik/etree"
)
// EnvelopedSignature implements the CanonicalizationAlgorithm
// interface and is used for processing the
// http://www.w3.org/2000/09/xmldsig#enveloped-signature transform
// algorithm
type EnvelopedSignature struct{}
// Process is called to transfrom the XML using the EnvelopedSignature
// algorithm
func (e EnvelopedSignature) Process(inputXML string,
transformXML string) (outputXML string, err error) {
doc := etree.NewDocument()
doc.ReadFromString(inputXML)
sig := doc.FindElement(".//Signature")
if sig == nil {
return "", errors.New("signedxml: unable to find Signature node")
}
sigParent := sig.Parent()
elem := sigParent.RemoveChild(sig)
if elem == nil {
return "", errors.New("signedxml: unable to remove Signature element")
}
docString, err := doc.WriteToString()
if err != nil {
return "", err
}
//logger.Println(docString)
return docString, nil
}

View File

@@ -0,0 +1,302 @@
package signedxml
import (
"sort"
"strings"
"github.com/beevik/etree"
)
// the attribute and attributes structs are used to implement the sort.Interface
type attribute struct {
prefix, uri, key, value string
}
type attributes []attribute
func (a attributes) Len() int {
return len(a)
}
// Less is part of the sort.Interface, and is used to order attributes by their
// namespace URIs and then by their keys.
func (a attributes) Less(i, j int) bool {
if a[i].uri == "" && a[j].uri != "" {
return true
}
if a[j].uri == "" && a[i].uri != "" {
return false
}
iQual := a[i].uri + a[i].key
jQual := a[j].uri + a[j].key
return iQual < jQual
}
func (a attributes) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
// ExclusiveCanonicalization implements the CanonicalizationAlgorithm
// interface and is used for processing the
// http://www.w3.org/2001/10/xml-exc-c14n# and
// http://www.w3.org/2001/10/xml-exc-c14n#WithComments transform
// algorithms
type ExclusiveCanonicalization struct {
WithComments bool
inclusiveNamespacePrefixList []string
namespaces map[string]string
}
// Process is called to transfrom the XML using the ExclusiveCanonicalization
// algorithm
func (e ExclusiveCanonicalization) Process(inputXML string,
transformXML string) (outputXML string, err error) {
e.namespaces = make(map[string]string)
doc := etree.NewDocument()
doc.WriteSettings.CanonicalEndTags = true
doc.WriteSettings.CanonicalText = true
doc.WriteSettings.CanonicalAttrVal = true
err = doc.ReadFromString(inputXML)
if err != nil {
return "", err
}
e.loadPrefixList(transformXML)
e.processDocLevelNodes(doc)
e.processRecursive(doc.Root(), nil, "")
outputXML, err = doc.WriteToString()
return outputXML, err
}
func (e *ExclusiveCanonicalization) loadPrefixList(transformXML string) {
if transformXML != "" {
tDoc := etree.NewDocument()
tDoc.ReadFromString(transformXML)
inclNSNode := tDoc.Root().SelectElement("InclusiveNamespaces")
if inclNSNode != nil {
prefixList := inclNSNode.SelectAttrValue("PrefixList", "")
if prefixList != "" {
e.inclusiveNamespacePrefixList = strings.Split(prefixList, " ")
}
}
}
}
// process nodes outside of the root element
func (e ExclusiveCanonicalization) processDocLevelNodes(doc *etree.Document) {
// keep track of the previous node action to manage line returns in CharData
previousNodeRemoved := false
for i := 0; i < len(doc.Child); i++ {
c := doc.Child[i]
switch c := c.(type) {
case *etree.Comment:
if e.WithComments {
previousNodeRemoved = false
} else {
removeTokenFromDocument(c, doc)
i--
previousNodeRemoved = true
}
case *etree.CharData:
if isWhitespace(c.Data) {
if previousNodeRemoved {
removeTokenFromDocument(c, doc)
i--
previousNodeRemoved = true
} else {
c.Data = "\n"
}
}
case *etree.Directive:
removeTokenFromDocument(c, doc)
i--
previousNodeRemoved = true
case *etree.ProcInst:
// remove declaration, but leave other PI's
if c.Target == "xml" {
removeTokenFromDocument(c, doc)
i--
previousNodeRemoved = true
} else {
previousNodeRemoved = false
}
default:
previousNodeRemoved = false
}
}
// if the last line is CharData whitespace, then remove it
if c, ok := doc.Child[len(doc.Child)-1].(*etree.CharData); ok {
if isWhitespace(c.Data) {
removeTokenFromDocument(c, doc)
}
}
}
func (e ExclusiveCanonicalization) processRecursive(node *etree.Element,
prefixesInScope []string, defaultNS string) {
newDefaultNS, newPrefixesInScope :=
e.renderAttributes(node, prefixesInScope, defaultNS)
for _, child := range node.Child {
switch child := child.(type) {
case *etree.Comment:
if !e.WithComments {
removeTokenFromElement(etree.Token(child), node)
}
case *etree.Element:
e.processRecursive(child, newPrefixesInScope, newDefaultNS)
}
}
}
func (e ExclusiveCanonicalization) renderAttributes(node *etree.Element,
prefixesInScope []string, defaultNS string) (newDefaultNS string,
newPrefixesInScope []string) {
currentNS := node.SelectAttrValue("xmlns", defaultNS)
elementAttributes := []etree.Attr{}
nsListToRender := make(map[string]string)
attrListToRender := attributes{}
// load map with for prefix -> 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
}

335
vendor/github.com/ma314smith/signedxml/signedxml.go generated vendored Normal file
View File

@@ -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
}

176
vendor/github.com/ma314smith/signedxml/signer.go generated vendored Normal file
View File

@@ -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
}

205
vendor/github.com/ma314smith/signedxml/validator.go generated vendored Normal file
View File

@@ -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
}

4
vendor/modules.txt vendored
View File

@@ -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