mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/yunionio/cloudpods.git
synced 2026-09-20 08:03:53 +08:00
feature: support SAML idp
This commit is contained in:
26
cmd/samldemo/main.go
Normal file
26
cmd/samldemo/main.go
Normal 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
2
go.mod
@@ -24,6 +24,7 @@ require (
|
||||
github.com/aokoli/goutils v1.0.1
|
||||
github.com/aws/aws-sdk-go v1.21.4
|
||||
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect
|
||||
github.com/beevik/etree v1.1.0 // indirect
|
||||
github.com/benbjohnson/clock v1.0.0
|
||||
github.com/bitly/go-simplejson v0.5.0
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
|
||||
@@ -70,6 +71,7 @@ require (
|
||||
github.com/kr/pty v1.1.5
|
||||
github.com/lib/pq v1.2.0 // indirect
|
||||
github.com/libvirt/libvirt-go-xml v5.2.0+incompatible
|
||||
github.com/ma314smith/signedxml v0.0.0-20200410192636-c342a2d0ae60
|
||||
github.com/mattn/go-runewidth v0.0.4 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.10.0 // indirect
|
||||
github.com/mattn/go-tty v0.0.0-20181127064339-e4f871175a2f // indirect
|
||||
|
||||
4
go.sum
4
go.sum
@@ -150,6 +150,8 @@ github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f h1:ZNv7
|
||||
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc=
|
||||
github.com/bazelbuild/bazel-gazelle v0.0.0-20181012220611-c728ce9f663e/go.mod h1:uHBSeeATKpVazAACZBDPL/Nk/UhQDDsJWDlqYJo8/Us=
|
||||
github.com/bazelbuild/buildtools v0.0.0-20180226164855-80c7f0d45d7e/go.mod h1:5JP0TXzWDHXv8qvxRC4InIazwdyDseBDbzESUMKk1yU=
|
||||
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
|
||||
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
||||
github.com/benbjohnson/clock v1.0.0 h1:78Jk/r6m4wCi6sndMpty7A//t4dw/RW5fV4ZgDVfX1w=
|
||||
github.com/benbjohnson/clock v1.0.0/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
@@ -511,6 +513,8 @@ github.com/lucas-clemente/aes12 v0.0.0-20171027163421-cd47fb39b79f/go.mod h1:JpH
|
||||
github.com/lucas-clemente/quic-clients v0.1.0/go.mod h1:y5xVIEoObKqULIKivu+gD/LU90pL73bTdtQjPBvtCBk=
|
||||
github.com/lucas-clemente/quic-go v0.10.2/go.mod h1:hvaRS9IHjFLMq76puFJeWNfmn+H70QZ/CXoxqw9bzao=
|
||||
github.com/lucas-clemente/quic-go-certificates v0.0.0-20160823095156-d2f86524cced/go.mod h1:NCcRLrOTZbzhZvixZLlERbJtDtYsmMw8Jc4vS8Z0g58=
|
||||
github.com/ma314smith/signedxml v0.0.0-20200410192636-c342a2d0ae60 h1:q5rqPuvxdOzg1NC6sls3XSpRokQ5KAmLIYdLW1/kyOo=
|
||||
github.com/ma314smith/signedxml v0.0.0-20200410192636-c342a2d0ae60/go.mod h1:KEgVcb43+f5KFUH/x6Vd3NROG0AIL2CuKMrIqYsmx6E=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
|
||||
@@ -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" {
|
||||
|
||||
59
pkg/util/samlutils/consts.go
Normal file
59
pkg/util/samlutils/consts.go
Normal 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,
|
||||
}
|
||||
)
|
||||
15
pkg/util/samlutils/demo/doc.go
Normal file
15
pkg/util/samlutils/demo/doc.go
Normal 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"
|
||||
413
pkg/util/samlutils/demo/service.go
Normal file
413
pkg/util/samlutils/demo/service.go
Normal 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
15
pkg/util/samlutils/doc.go
Normal 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"
|
||||
81
pkg/util/samlutils/encrypt.go
Normal file
81
pkg/util/samlutils/encrypt.go
Normal 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
|
||||
}
|
||||
15
pkg/util/samlutils/idp/doc.go
Normal file
15
pkg/util/samlutils/idp/doc.go
Normal 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"
|
||||
302
pkg/util/samlutils/idp/idp.go
Normal file
302
pkg/util/samlutils/idp/idp.go
Normal 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
|
||||
}
|
||||
56
pkg/util/samlutils/idp/sp.go
Normal file
56
pkg/util/samlutils/idp/sp.go
Normal 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
|
||||
}
|
||||
59
pkg/util/samlutils/instance.go
Normal file
59
pkg/util/samlutils/instance.go
Normal 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)
|
||||
}
|
||||
138
pkg/util/samlutils/metadata.go
Normal file
138
pkg/util/samlutils/metadata.go
Normal 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
|
||||
}
|
||||
670
pkg/util/samlutils/metadata_test.go
Normal file
670
pkg/util/samlutils/metadata_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
352
pkg/util/samlutils/response.go
Normal file
352
pkg/util/samlutils/response.go
Normal 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)
|
||||
}
|
||||
125
pkg/util/samlutils/response_test.go
Normal file
125
pkg/util/samlutils/response_test.go
Normal 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
439
pkg/util/samlutils/types.go
Normal 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
128
pkg/util/samlutils/util.go
Normal 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
|
||||
}
|
||||
@@ -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
14
vendor/github.com/beevik/etree/.travis.yml
generated
vendored
Normal 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
10
vendor/github.com/beevik/etree/CONTRIBUTORS
generated
vendored
Normal 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
24
vendor/github.com/beevik/etree/LICENSE
generated
vendored
Normal 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
205
vendor/github.com/beevik/etree/README.md
generated
vendored
Normal file
@@ -0,0 +1,205 @@
|
||||
[](https://travis-ci.org/beevik/etree)
|
||||
[](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
109
vendor/github.com/beevik/etree/RELEASE_NOTES.md
generated
vendored
Normal 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
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
276
vendor/github.com/beevik/etree/helpers.go
generated
vendored
Normal 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("&")
|
||||
case '<':
|
||||
esc = []byte("<")
|
||||
case '>':
|
||||
if m == escapeCanonicalAttr {
|
||||
continue
|
||||
}
|
||||
esc = []byte(">")
|
||||
case '\'':
|
||||
if m != escapeNormal {
|
||||
continue
|
||||
}
|
||||
esc = []byte("'")
|
||||
case '"':
|
||||
if m == escapeCanonicalText {
|
||||
continue
|
||||
}
|
||||
esc = []byte(""")
|
||||
case '\t':
|
||||
if m != escapeCanonicalAttr {
|
||||
continue
|
||||
}
|
||||
esc = []byte("	")
|
||||
case '\n':
|
||||
if m != escapeCanonicalAttr {
|
||||
continue
|
||||
}
|
||||
esc = []byte("
")
|
||||
case '\r':
|
||||
if m == escapeNormal {
|
||||
continue
|
||||
}
|
||||
esc = []byte("
")
|
||||
default:
|
||||
if !isInCharacterRange(r) || (r == 0xFFFD && width == 1) {
|
||||
esc = []byte("\uFFFD")
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
w.WriteString(s[last : i-width])
|
||||
w.Write(esc)
|
||||
last = i
|
||||
}
|
||||
w.WriteString(s[last:])
|
||||
}
|
||||
|
||||
func isInCharacterRange(r rune) bool {
|
||||
return r == 0x09 ||
|
||||
r == 0x0A ||
|
||||
r == 0x0D ||
|
||||
r >= 0x20 && r <= 0xD7FF ||
|
||||
r >= 0xE000 && r <= 0xFFFD ||
|
||||
r >= 0x10000 && r <= 0x10FFFF
|
||||
}
|
||||
582
vendor/github.com/beevik/etree/path.go
generated
vendored
Normal file
582
vendor/github.com/beevik/etree/path.go
generated
vendored
Normal 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
24
vendor/github.com/ma314smith/signedxml/.gitignore
generated
vendored
Normal 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
14
vendor/github.com/ma314smith/signedxml/.travis.yml
generated
vendored
Normal 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
21
vendor/github.com/ma314smith/signedxml/LICENSE.md
generated
vendored
Normal 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
111
vendor/github.com/ma314smith/signedxml/README.md
generated
vendored
Normal file
@@ -0,0 +1,111 @@
|
||||
## signedxml
|
||||
|
||||
[](https://travis-ci.org/ma314smith/signedxml)
|
||||
[](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.
|
||||
39
vendor/github.com/ma314smith/signedxml/envelopedsignature.go
generated
vendored
Normal file
39
vendor/github.com/ma314smith/signedxml/envelopedsignature.go
generated
vendored
Normal 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
|
||||
}
|
||||
302
vendor/github.com/ma314smith/signedxml/exclusivecanonicalization.go
generated
vendored
Normal file
302
vendor/github.com/ma314smith/signedxml/exclusivecanonicalization.go
generated
vendored
Normal 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
335
vendor/github.com/ma314smith/signedxml/signedxml.go
generated
vendored
Normal 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
176
vendor/github.com/ma314smith/signedxml/signer.go
generated
vendored
Normal 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
205
vendor/github.com/ma314smith/signedxml/validator.go
generated
vendored
Normal 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
4
vendor/modules.txt
vendored
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user