From f8641681f6a888fe05c02d262093f1156e7df36d Mon Sep 17 00:00:00 2001 From: Sasha Klizhentas Date: Fri, 5 May 2017 15:53:05 -0700 Subject: [PATCH] SAML 2.0 initial implementation --- Godeps/Godeps.json | 35 +- constants.go | 6 + lib/auth/apiserver.go | 162 ++- lib/auth/auth.go | 452 +++++++-- lib/auth/auth_with_roles.go | 60 ++ lib/auth/clt.go | 146 +++ lib/auth/init.go | 2 +- lib/auth/new_web_user.go | 11 +- lib/auth/permissions.go | 2 + lib/client/api.go | 19 +- lib/client/weblogin.go | 24 +- lib/config/fileconf.go | 1 + lib/defaults/defaults.go | 3 + lib/services/authentication.go | 6 +- lib/services/identity.go | 103 +- lib/services/local/users.go | 156 ++- lib/services/resource.go | 15 +- lib/services/role.go | 25 + lib/services/saml.go | 713 +++++++++++++ lib/services/user.go | 42 +- lib/utils/certs.go | 157 +++ lib/utils/equals.go | 56 ++ lib/web/apiserver.go | 73 +- lib/web/saml.go | 118 +++ tool/tctl/common/collection.go | 49 +- tool/tctl/common/tctl.go | 25 +- vendor/github.com/beevik/etree/.travis.yml | 16 + vendor/github.com/beevik/etree/CONTRIBUTORS | 8 + vendor/github.com/beevik/etree/LICENSE | 24 + vendor/github.com/beevik/etree/README.md | 203 ++++ vendor/github.com/beevik/etree/etree.go | 943 ++++++++++++++++++ vendor/github.com/beevik/etree/helpers.go | 188 ++++ vendor/github.com/beevik/etree/path.go | 470 +++++++++ .../github.com/gravitational/form/.gitignore | 24 + vendor/github.com/gravitational/form/LICENSE | 201 ++++ vendor/github.com/gravitational/form/Makefile | 13 + .../github.com/gravitational/form/README.md | 9 + vendor/github.com/gravitational/form/form.go | 283 ++++++ .../gravitational/form/shippable.yaml | 19 + .../russellhaering/gosaml2/.travis.yml | 12 + .../github.com/russellhaering/gosaml2/LICENSE | 175 ++++ .../russellhaering/gosaml2/README.md | 34 + .../russellhaering/gosaml2/attribute.go | 19 + .../russellhaering/gosaml2/authn_request.go | 16 + .../russellhaering/gosaml2/build_request.go | 141 +++ .../russellhaering/gosaml2/decode_response.go | 216 ++++ .../gosaml2/retrieve_assertion.go | 93 ++ .../github.com/russellhaering/gosaml2/saml.go | 87 ++ .../russellhaering/gosaml2/test_constants.go | 376 +++++++ .../gosaml2/types/encrypted_assertion.go | 72 ++ .../gosaml2/types/encrypted_key.go | 109 ++ .../russellhaering/gosaml2/types/metadata.go | 39 + .../russellhaering/gosaml2/types/response.go | 118 +++ .../russellhaering/gosaml2/validate.go | 218 ++++ .../russellhaering/gosaml2/xml_constants.go | 54 + .../russellhaering/goxmldsig/.travis.yml | 6 + .../russellhaering/goxmldsig/LICENSE | 175 ++++ .../russellhaering/goxmldsig/README.md | 90 ++ .../russellhaering/goxmldsig/canonicalize.go | 128 +++ .../russellhaering/goxmldsig/clock.go | 55 + .../goxmldsig/etreeutils/canonicalize.go | 98 ++ .../goxmldsig/etreeutils/namespace.go | 328 ++++++ .../goxmldsig/etreeutils/sort.go | 66 ++ .../goxmldsig/etreeutils/unmarshal.go | 43 + .../russellhaering/goxmldsig/keystore.go | 63 ++ .../russellhaering/goxmldsig/sign.go | 211 ++++ .../russellhaering/goxmldsig/tls_keystore.go | 34 + .../goxmldsig/types/signature.go | 93 ++ .../russellhaering/goxmldsig/validate.go | 417 ++++++++ .../russellhaering/goxmldsig/xml_constants.go | 78 ++ vendor/github.com/satori/go.uuid/.travis.yml | 22 + vendor/github.com/satori/go.uuid/LICENSE | 20 + vendor/github.com/satori/go.uuid/README.md | 65 ++ vendor/github.com/satori/go.uuid/uuid.go | 481 +++++++++ 74 files changed, 8984 insertions(+), 130 deletions(-) create mode 100644 lib/services/saml.go create mode 100644 lib/utils/certs.go create mode 100644 lib/utils/equals.go create mode 100644 lib/web/saml.go create mode 100644 vendor/github.com/beevik/etree/.travis.yml create mode 100644 vendor/github.com/beevik/etree/CONTRIBUTORS create mode 100644 vendor/github.com/beevik/etree/LICENSE create mode 100644 vendor/github.com/beevik/etree/README.md create mode 100644 vendor/github.com/beevik/etree/etree.go create mode 100644 vendor/github.com/beevik/etree/helpers.go create mode 100644 vendor/github.com/beevik/etree/path.go create mode 100644 vendor/github.com/gravitational/form/.gitignore create mode 100644 vendor/github.com/gravitational/form/LICENSE create mode 100644 vendor/github.com/gravitational/form/Makefile create mode 100644 vendor/github.com/gravitational/form/README.md create mode 100644 vendor/github.com/gravitational/form/form.go create mode 100644 vendor/github.com/gravitational/form/shippable.yaml create mode 100644 vendor/github.com/russellhaering/gosaml2/.travis.yml create mode 100644 vendor/github.com/russellhaering/gosaml2/LICENSE create mode 100644 vendor/github.com/russellhaering/gosaml2/README.md create mode 100644 vendor/github.com/russellhaering/gosaml2/attribute.go create mode 100644 vendor/github.com/russellhaering/gosaml2/authn_request.go create mode 100644 vendor/github.com/russellhaering/gosaml2/build_request.go create mode 100644 vendor/github.com/russellhaering/gosaml2/decode_response.go create mode 100644 vendor/github.com/russellhaering/gosaml2/retrieve_assertion.go create mode 100644 vendor/github.com/russellhaering/gosaml2/saml.go create mode 100644 vendor/github.com/russellhaering/gosaml2/test_constants.go create mode 100644 vendor/github.com/russellhaering/gosaml2/types/encrypted_assertion.go create mode 100644 vendor/github.com/russellhaering/gosaml2/types/encrypted_key.go create mode 100644 vendor/github.com/russellhaering/gosaml2/types/metadata.go create mode 100644 vendor/github.com/russellhaering/gosaml2/types/response.go create mode 100644 vendor/github.com/russellhaering/gosaml2/validate.go create mode 100644 vendor/github.com/russellhaering/gosaml2/xml_constants.go create mode 100644 vendor/github.com/russellhaering/goxmldsig/.travis.yml create mode 100644 vendor/github.com/russellhaering/goxmldsig/LICENSE create mode 100644 vendor/github.com/russellhaering/goxmldsig/README.md create mode 100644 vendor/github.com/russellhaering/goxmldsig/canonicalize.go create mode 100644 vendor/github.com/russellhaering/goxmldsig/clock.go create mode 100644 vendor/github.com/russellhaering/goxmldsig/etreeutils/canonicalize.go create mode 100644 vendor/github.com/russellhaering/goxmldsig/etreeutils/namespace.go create mode 100644 vendor/github.com/russellhaering/goxmldsig/etreeutils/sort.go create mode 100644 vendor/github.com/russellhaering/goxmldsig/etreeutils/unmarshal.go create mode 100644 vendor/github.com/russellhaering/goxmldsig/keystore.go create mode 100644 vendor/github.com/russellhaering/goxmldsig/sign.go create mode 100644 vendor/github.com/russellhaering/goxmldsig/tls_keystore.go create mode 100644 vendor/github.com/russellhaering/goxmldsig/types/signature.go create mode 100644 vendor/github.com/russellhaering/goxmldsig/validate.go create mode 100644 vendor/github.com/russellhaering/goxmldsig/xml_constants.go create mode 100644 vendor/github.com/satori/go.uuid/.travis.yml create mode 100644 vendor/github.com/satori/go.uuid/LICENSE create mode 100644 vendor/github.com/satori/go.uuid/README.md create mode 100644 vendor/github.com/satori/go.uuid/uuid.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 1d22390405067..12bc35b8718c8 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -1,7 +1,7 @@ { "ImportPath": "github.com/gravitational/teleport", "GoVersion": "go1.6", - "GodepVersion": "v76", + "GodepVersion": "v74", "Packages": [ "./lib/...", "./tool/...", @@ -171,6 +171,10 @@ "Comment": "v1.5.13", "Rev": "918c42e2bcdb277aa821401c906e88254501bdf4" }, + { + "ImportPath": "github.com/beevik/etree", + "Rev": "cda1c0026246bd095961ef9a3c430e50d0e80fba" + }, { "ImportPath": "github.com/boltdb/bolt", "Comment": "v1.0-111-g04a3e85", @@ -298,6 +302,10 @@ "ImportPath": "github.com/gravitational/configure/jsonschema", "Rev": "1db4b84fe9dbbbaf40827aa714dcca17b368de2c" }, + { + "ImportPath": "github.com/gravitational/form", + "Rev": "c4048f792f70d207e6d8b9c1bf52319247f202b8" + }, { "ImportPath": "github.com/gravitational/kingpin", "Comment": "v2.1.10-6-g7856865", @@ -405,6 +413,31 @@ "ImportPath": "github.com/pquerna/otp/totp", "Rev": "54653902c20e47f3417541d35435cb6d6162e28a" }, + { + "ImportPath": "github.com/russellhaering/gosaml2", + "Rev": "1a1c11a8ec20d23bddff71f425ee124c426f08f2" + }, + { + "ImportPath": "github.com/russellhaering/gosaml2/types", + "Rev": "1a1c11a8ec20d23bddff71f425ee124c426f08f2" + }, + { + "ImportPath": "github.com/russellhaering/goxmldsig", + "Rev": "eaac44c63fe007124f8f6255b09febc906784981" + }, + { + "ImportPath": "github.com/russellhaering/goxmldsig/etreeutils", + "Rev": "eaac44c63fe007124f8f6255b09febc906784981" + }, + { + "ImportPath": "github.com/russellhaering/goxmldsig/types", + "Rev": "eaac44c63fe007124f8f6255b09febc906784981" + }, + { + "ImportPath": "github.com/satori/go.uuid", + "Comment": "v1.1.0-8-g5bf94b6", + "Rev": "5bf94b69c6b68ee1b541973bb8e1144db23a194b" + }, { "ImportPath": "github.com/tstranex/u2f", "Rev": "eb799ce68da4150b16ff5d0c89a24e2a2ad993d8" diff --git a/constants.go b/constants.go index 36c95aaa19aa1..5272d26816b28 100644 --- a/constants.go +++ b/constants.go @@ -87,6 +87,9 @@ const ( // ConnectorOIDC means connector type OIDC ConnectorOIDC = "oidc" + // ConnectorSAML means connector type SAML + ConnectorSAML = "oidc" + // DataDirParameterName is the name of the data dir configuration parameter passed // to all backends during initialization DataDirParameterName = "data_dir" @@ -115,6 +118,9 @@ const ( // OIDC means authentication will happen remotly using an OIDC connector. OIDC = "oidc" + + // SAML means authentication will happen remotly using an SAML connector. + SAML = "saml" ) const ( diff --git a/lib/auth/apiserver.go b/lib/auth/apiserver.go index c67511e286e96..a5fdbad600a21 100644 --- a/lib/auth/apiserver.go +++ b/lib/auth/apiserver.go @@ -154,6 +154,15 @@ func NewAPIServer(config *APIConfig) http.Handler { srv.POST("/:version/oidc/requests/create", srv.withAuth(srv.createOIDCAuthRequest)) srv.POST("/:version/oidc/requests/validate", srv.withAuth(srv.validateOIDCAuthCallback)) + // SAML handlers + srv.POST("/:version/saml/connectors", srv.withAuth(srv.createSAMLConnector)) + srv.PUT("/:version/saml/connectors", srv.withAuth(srv.upsertSAMLConnector)) + srv.GET("/:version/saml/connectors", srv.withAuth(srv.getSAMLConnectors)) + srv.GET("/:version/saml/connectors/:id", srv.withAuth(srv.getSAMLConnector)) + srv.DELETE("/:version/saml/connectors/:id", srv.withAuth(srv.deleteSAMLConnector)) + srv.POST("/:version/saml/requests/create", srv.withAuth(srv.createSAMLAuthRequest)) + srv.POST("/:version/saml/requests/validate", srv.withAuth(srv.validateSAMLResponse)) + // U2F srv.GET("/:version/u2f/signuptokens/:token", srv.withAuth(srv.getSignupU2FRegisterRequest)) srv.POST("/:version/u2f/users", srv.withAuth(srv.createUserWithU2FToken)) @@ -1128,7 +1137,7 @@ type oidcAuthRawResponse struct { // Username is authenticated teleport username Username string `json:"username"` // Identity contains validated OIDC identity - Identity services.OIDCIdentity `json:"identity"` + Identity services.ExternalIdentity `json:"identity"` // Web session will be generated by auth server if requested in OIDCAuthRequest Session json.RawMessage `json:"session,omitempty"` // Cert will be generated by certificate authority @@ -1173,6 +1182,157 @@ func (s *APIServer) validateOIDCAuthCallback(auth ClientI, w http.ResponseWriter return &raw, nil } +type createSAMLConnectorRawReq struct { + Connector json.RawMessage `json:"connector"` +} + +func (s *APIServer) createSAMLConnector(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) { + var req *createSAMLConnectorRawReq + if err := httplib.ReadJSON(r, &req); err != nil { + return nil, trace.Wrap(err) + } + connector, err := services.GetSAMLConnectorMarshaler().UnmarshalSAMLConnector(req.Connector) + if err != nil { + return nil, trace.Wrap(err) + } + err = auth.CreateSAMLConnector(connector) + if err != nil { + return nil, trace.Wrap(err) + } + return message("ok"), nil +} + +type upsertSAMLConnectorRawReq struct { + Connector json.RawMessage `json:"connector"` +} + +func (s *APIServer) upsertSAMLConnector(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) { + var req *upsertSAMLConnectorRawReq + if err := httplib.ReadJSON(r, &req); err != nil { + return nil, trace.Wrap(err) + } + connector, err := services.GetSAMLConnectorMarshaler().UnmarshalSAMLConnector(req.Connector) + if err != nil { + return nil, trace.Wrap(err) + } + err = auth.UpsertSAMLConnector(connector) + if err != nil { + return nil, trace.Wrap(err) + } + return message("ok"), nil +} + +func (s *APIServer) getSAMLConnector(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) { + withSecrets, _, err := httplib.ParseBool(r.URL.Query(), "with_secrets") + if err != nil { + return nil, trace.Wrap(err) + } + connector, err := auth.GetSAMLConnector(p.ByName("id"), withSecrets) + if err != nil { + return nil, trace.Wrap(err) + } + return rawMessage(services.GetSAMLConnectorMarshaler().MarshalSAMLConnector(connector, services.WithVersion(version))) +} + +func (s *APIServer) deleteSAMLConnector(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) { + err := auth.DeleteSAMLConnector(p.ByName("id")) + if err != nil { + return nil, trace.Wrap(err) + } + return message("ok"), nil +} + +func (s *APIServer) getSAMLConnectors(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) { + withSecrets, _, err := httplib.ParseBool(r.URL.Query(), "with_secrets") + if err != nil { + return nil, trace.Wrap(err) + } + connectors, err := auth.GetSAMLConnectors(withSecrets) + if err != nil { + return nil, trace.Wrap(err) + } + items := make([]json.RawMessage, len(connectors)) + for i, connector := range connectors { + data, err := services.GetSAMLConnectorMarshaler().MarshalSAMLConnector(connector, services.WithVersion(version)) + if err != nil { + return nil, trace.Wrap(err) + } + items[i] = data + } + return items, nil +} + +type createSAMLAuthRequestReq struct { + Req services.SAMLAuthRequest `json:"req"` +} + +func (s *APIServer) createSAMLAuthRequest(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) { + var req *createSAMLAuthRequestReq + if err := httplib.ReadJSON(r, &req); err != nil { + return nil, trace.Wrap(err) + } + response, err := auth.CreateSAMLAuthRequest(req.Req) + if err != nil { + return nil, trace.Wrap(err) + } + return response, nil +} + +type validateSAMLResponseReq struct { + Response string `json:"response"` +} + +// samlAuthRawResponse is returned when auth server validated callback parameters +// returned from SAML provider +type samlAuthRawResponse struct { + // Username is authenticated teleport username + Username string `json:"username"` + // Identity contains validated OIDC identity + Identity services.ExternalIdentity `json:"identity"` + // Web session will be generated by auth server if requested in OIDCAuthRequest + Session json.RawMessage `json:"session,omitempty"` + // Cert will be generated by certificate authority + Cert []byte `json:"cert,omitempty"` + // Req is original oidc auth request + Req services.SAMLAuthRequest `json:"req"` + // HostSigners is a list of signing host public keys + // trusted by proxy, used in console login + HostSigners []json.RawMessage `json:"host_signers"` +} + +func (s *APIServer) validateSAMLResponse(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) { + var req *validateSAMLResponseReq + if err := httplib.ReadJSON(r, &req); err != nil { + return nil, trace.Wrap(err) + } + response, err := auth.ValidateSAMLResponse(req.Response) + if err != nil { + return nil, trace.Wrap(err) + } + raw := samlAuthRawResponse{ + Username: response.Username, + Identity: response.Identity, + Cert: response.Cert, + Req: response.Req, + } + if response.Session != nil { + rawSession, err := services.GetWebSessionMarshaler().MarshalWebSession(response.Session, services.WithVersion(version)) + if err != nil { + return nil, trace.Wrap(err) + } + raw.Session = rawSession + } + raw.HostSigners = make([]json.RawMessage, len(response.HostSigners)) + for i, ca := range response.HostSigners { + data, err := services.GetCertAuthorityMarshaler().MarshalCertAuthority(ca, services.WithVersion(version)) + if err != nil { + return nil, trace.Wrap(err) + } + raw.HostSigners[i] = data + } + return &raw, nil +} + // HTTP GET /:version/events?query // // Query fields: diff --git a/lib/auth/auth.go b/lib/auth/auth.go index ff84006a44445..284380253a879 100644 --- a/lib/auth/auth.go +++ b/lib/auth/auth.go @@ -24,8 +24,12 @@ limitations under the License. package auth import ( + "bytes" + "compress/flate" + "encoding/base64" "encoding/json" "fmt" + "io/ioutil" "net/http" "net/url" "sync" @@ -39,11 +43,13 @@ import ( "github.com/gravitational/teleport/lib/utils" log "github.com/Sirupsen/logrus" + "github.com/beevik/etree" "github.com/coreos/go-oidc/jose" "github.com/coreos/go-oidc/oauth2" "github.com/coreos/go-oidc/oidc" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" + saml2 "github.com/russellhaering/gosaml2" "github.com/tstranex/u2f" ) @@ -107,6 +113,7 @@ func NewAuthServer(cfg *InitConfig, opts ...AuthServerOption) *AuthServer { ClusterAuthPreference: cfg.ClusterAuthPreferenceService, UniversalSecondFactorSettings: cfg.UniversalSecondFactorService, oidcClients: make(map[string]*oidcClient), + samlProviders: make(map[string]*samlProvider), DeveloperMode: cfg.DeveloperMode, } for _, o := range opts { @@ -126,10 +133,11 @@ func NewAuthServer(cfg *InitConfig, opts ...AuthServerOption) *AuthServer { // - same for users and their sessions // - checks public keys to see if they're signed by it (can be trusted or not) type AuthServer struct { - lock sync.Mutex - oidcClients map[string]*oidcClient - clock clockwork.Clock - bk backend.Backend + lock sync.Mutex + oidcClients map[string]*oidcClient + samlProviders map[string]*samlProvider + clock clockwork.Clock + bk backend.Backend // DeveloperMode should only be used during development as it does several // unsafe things like log sensitive information to console as well as @@ -721,6 +729,26 @@ func (s *AuthServer) getOIDCClient(conn services.OIDCConnector) (*oidc.Client, e return client, nil } +func (s *AuthServer) getSAMLProvider(conn services.SAMLConnector) (*saml2.SAMLServiceProvider, error) { + s.lock.Lock() + defer s.lock.Unlock() + + providerPack, ok := s.samlProviders[conn.GetName()] + if ok && providerPack.connector.Equals(conn) { + return providerPack.provider, nil + } + delete(s.samlProviders, conn.GetName()) + + serviceProvider, err := conn.GetServiceProvider() + if err != nil { + return nil, trace.Wrap(err) + } + + s.samlProviders[conn.GetName()] = &samlProvider{connector: conn, provider: serviceProvider} + + return serviceProvider, nil +} + func (s *AuthServer) UpsertOIDCConnector(connector services.OIDCConnector) error { return s.Identity.UpsertOIDCConnector(connector) } @@ -776,13 +804,202 @@ func (s *AuthServer) CreateOIDCAuthRequest(req services.OIDCAuthRequest) (*servi return &req, nil } +func (s *AuthServer) UpsertSAMLConnector(connector services.SAMLConnector) error { + return s.Identity.UpsertSAMLConnector(connector) +} + +func (s *AuthServer) DeleteSAMLConnector(connectorName string) error { + return s.Identity.DeleteSAMLConnector(connectorName) +} + +func (s *AuthServer) CreateSAMLAuthRequest(req services.SAMLAuthRequest) (*services.SAMLAuthRequest, error) { + connector, err := s.Identity.GetSAMLConnector(req.ConnectorID, true) + if err != nil { + return nil, trace.Wrap(err) + } + provider, err := s.getSAMLProvider(connector) + if err != nil { + return nil, trace.Wrap(err) + } + + doc, err := provider.BuildAuthRequestDocument() + if err != nil { + return nil, trace.Wrap(err) + } + + attr := doc.Root().SelectAttr("ID") + if attr == nil || attr.Value == "" { + return nil, trace.BadParameter("missing auth request ID") + } + + req.ID = attr.Value + req.RedirectURL, err = provider.BuildAuthURLFromDocument("", doc) + if err != nil { + return nil, trace.Wrap(err) + } + + err = s.Identity.CreateSAMLAuthRequest(req, defaults.SAMLAuthRequestTTL) + if err != nil { + return nil, trace.Wrap(err) + } + return &req, nil +} + +// ValidateOIDCAuthCallback is called by the proxy to check OIDC query parameters +// returned by OIDC Provider, if everything checks out, auth server +// will respond with OIDCAuthResponse, otherwise it will return error +func (a *AuthServer) ValidateOIDCAuthCallback(q url.Values) (*OIDCAuthResponse, error) { + if error := q.Get("error"); error != "" { + return nil, trace.OAuth2(oauth2.ErrorInvalidRequest, error, q) + } + + code := q.Get("code") + if code == "" { + return nil, trace.OAuth2( + oauth2.ErrorInvalidRequest, "code query param must be set", q) + } + + stateToken := q.Get("state") + if stateToken == "" { + return nil, trace.OAuth2( + oauth2.ErrorInvalidRequest, "missing state query param", q) + } + + req, err := a.Identity.GetOIDCAuthRequest(stateToken) + if err != nil { + return nil, trace.Wrap(err) + } + + connector, err := a.Identity.GetOIDCConnector(req.ConnectorID, true) + if err != nil { + return nil, trace.Wrap(err) + } + + oidcClient, err := a.getOIDCClient(connector) + if err != nil { + return nil, trace.Wrap(err) + } + + // extract claims from both the id token and the userinfo endpoint and merge them + claims, err := a.getClaims(oidcClient, connector.GetIssuerURL(), code) + if err != nil { + return nil, trace.OAuth2( + oauth2.ErrorUnsupportedResponseType, "unable to construct claims", q) + } + log.Debugf("[OIDC] Claims: %v", claims) + + // if we are sending acr values, make sure we also validate them + acrValue := connector.GetACR() + if acrValue != "" { + err := a.validateACRValues(acrValue, connector.GetProvider(), claims) + if err != nil { + return nil, trace.Wrap(err) + } + log.Debugf("[OIDC] ACR values %q successfully validated", acrValue) + } + + ident, err := oidc.IdentityFromClaims(claims) + if err != nil { + return nil, trace.OAuth2( + oauth2.ErrorUnsupportedResponseType, "unable to convert claims to identity", q) + } + log.Debugf("[IDENTITY] %q expires at: %v", ident.Email, ident.ExpiresAt) + + response := &OIDCAuthResponse{ + Identity: services.ExternalIdentity{ConnectorID: connector.GetName(), Username: ident.Email}, + Req: *req, + } + + log.Debugf("[OIDC] Applying %v claims to roles mappings", len(connector.GetClaimsToRoles())) + if len(connector.GetClaimsToRoles()) != 0 { + if err := a.createOIDCUser(connector, ident, claims); err != nil { + return nil, trace.Wrap(err) + } + } + + if !req.CheckUser { + return response, nil + } + + user, err := a.Identity.GetUserByOIDCIdentity(services.ExternalIdentity{ + ConnectorID: req.ConnectorID, Username: ident.Email}) + if err != nil { + return nil, trace.Wrap(err) + } + response.Username = user.GetName() + + var roles services.RoleSet + roles, err = services.FetchRoles(user.GetRoles(), a.Access) + if err != nil { + return nil, trace.Wrap(err) + } + sessionTTL := roles.AdjustSessionTTL(utils.ToTTL(a.clock, ident.ExpiresAt)) + bearerTokenTTL := utils.MinTTL(BearerTokenTTL, sessionTTL) + + if req.CreateWebSession { + sess, err := a.NewWebSession(user.GetName()) + if err != nil { + return nil, trace.Wrap(err) + } + // session will expire based on identity TTL and allowed session TTL + sess.SetExpiryTime(a.clock.Now().UTC().Add(sessionTTL)) + // bearer token will expire based on the expected session renewal + sess.SetBearerTokenExpiryTime(a.clock.Now().UTC().Add(bearerTokenTTL)) + if err := a.UpsertWebSession(user.GetName(), sess); err != nil { + return nil, trace.Wrap(err) + } + response.Session = sess + } + + if len(req.PublicKey) != 0 { + certTTL := utils.MinTTL(utils.ToTTL(a.clock, ident.ExpiresAt), req.CertTTL) + allowedLogins, err := roles.CheckLogins(certTTL) + if err != nil { + return nil, trace.Wrap(err) + } + cert, err := a.GenerateUserCert(req.PublicKey, user.GetName(), allowedLogins, certTTL, roles.CanForwardAgents()) + if err != nil { + return nil, trace.Wrap(err) + } + response.Cert = cert + + authorities, err := a.GetCertAuthorities(services.HostCA, false) + if err != nil { + return nil, trace.Wrap(err) + } + for _, authority := range authorities { + response.HostSigners = append(response.HostSigners, authority) + } + } + + return response, nil +} + +// SAMLAuthResponse is returned when auth server validated callback parameters +// returned from SAML identity provider +type SAMLAuthResponse struct { + // Username is authenticated teleport username + Username string `json:"username"` + // Identity contains validated OIDC identity + Identity services.ExternalIdentity `json:"identity"` + // Web session will be generated by auth server if requested in OIDCAuthRequest + Session services.WebSession `json:"session,omitempty"` + // Cert will be generated by certificate authority + Cert []byte `json:"cert,omitempty"` + // Req is original oidc auth request + Req services.SAMLAuthRequest `json:"req"` + // HostSigners is a list of signing host public keys + // trusted by proxy, used in console login + HostSigners []services.CertAuthority `json:"host_signers"` +} + // OIDCAuthResponse is returned when auth server validated callback parameters // returned from OIDC provider type OIDCAuthResponse struct { // Username is authenticated teleport username Username string `json:"username"` // Identity contains validated OIDC identity - Identity services.OIDCIdentity `json:"identity"` + Identity services.ExternalIdentity `json:"identity"` // Web session will be generated by auth server if requested in OIDCAuthRequest Session services.WebSession `json:"session,omitempty"` // Cert will be generated by certificate authority @@ -823,6 +1040,93 @@ func (a *AuthServer) buildRoles(connector services.OIDCConnector, ident *oidc.Id return roles, nil } +// buildSAMLRoles takes a connector and claims and returns a slice of roles. If the claims +// match a concrete roles in the connector, those roles are returned directly. If the +// claims match a template role in the connector, then that role is first created from +// the template, then returned. +func (a *AuthServer) buildSAMLRoles(connector services.SAMLConnector, assertionInfo saml2.AssertionInfo, expiresAt time.Time) ([]string, error) { + roles := connector.MapAttributes(assertionInfo) + if len(roles) == 0 { + role, err := connector.RoleFromTemplate(assertionInfo) + if err != nil { + log.Warningf("[SAML] Unable to map claims to roles or role templates for %q", connector.GetName()) + return nil, trace.AccessDenied("unable to map claims to roles or role templates for %q", connector.GetName()) + } + + // figure out ttl for role. expires = now + ttl => ttl = expires - now + ttl := expiresAt.Sub(a.clock.Now()) + + // upsert templated role + err = a.Access.UpsertRole(role, ttl) + if err != nil { + log.Warningf("[OIDC] Unable to upsert templated role for connector: %q", connector.GetName()) + return nil, trace.AccessDenied("unable to upsert templated role: %q", connector.GetName()) + } + + roles = []string{role.GetName()} + } + + return roles, nil +} + +func (a *AuthServer) createSAMLUser(connector services.SAMLConnector, assertionInfo saml2.AssertionInfo, expiresAt time.Time) error { + roles, err := a.buildSAMLRoles(connector, assertionInfo, expiresAt) + if err != nil { + return trace.Wrap(err) + } + + log.Debugf("[IDENTITY] %v/%v is a dynamic identity, generating user with roles: %v", connector.GetName(), assertionInfo.NameID, roles) + user, err := services.GetUserMarshaler().GenerateUser(&services.UserV2{ + Kind: services.KindUser, + Version: services.V2, + Metadata: services.Metadata{ + Name: assertionInfo.NameID, + Namespace: defaults.Namespace, + }, + Spec: services.UserSpecV2{ + Roles: roles, + Expires: expiresAt, + SAMLIdentities: []services.ExternalIdentity{{ConnectorID: connector.GetName(), Username: assertionInfo.NameID}}, + CreatedBy: services.CreatedBy{ + User: services.UserRef{Name: "system"}, + Time: time.Now().UTC(), + Connector: &services.ConnectorRef{ + Type: teleport.ConnectorSAML, + ID: connector.GetName(), + Identity: assertionInfo.NameID, + }, + }, + }, + }) + if err != nil { + return trace.Wrap(err) + } + + // check if a user exists already + existingUser, err := a.GetUser(assertionInfo.NameID) + if err != nil { + if !trace.IsNotFound(err) { + return trace.Wrap(err) + } + } + + // check if exisiting user is a non-oidc user, if so, return an error + if existingUser != nil { + connectorRef := existingUser.GetCreatedBy().Connector + if connectorRef == nil || connectorRef.Type != teleport.ConnectorSAML || connectorRef.ID != connector.GetName() { + return trace.AlreadyExists("user %q already exists and is not SAML user", existingUser.GetName()) + } + } + + // no non-oidc user exists, create or update the exisiting oidc user + err = a.UpsertUser(user) + if err != nil { + return trace.Wrap(err) + } + + return nil +} + func (a *AuthServer) createOIDCUser(connector services.OIDCConnector, ident *oidc.Identity, claims jose.Claims) error { roles, err := a.buildRoles(connector, ident, claims) if err != nil { @@ -840,7 +1144,7 @@ func (a *AuthServer) createOIDCUser(connector services.OIDCConnector, ident *oid Spec: services.UserSpecV2{ Roles: roles, Expires: ident.ExpiresAt, - OIDCIdentities: []services.OIDCIdentity{{ConnectorID: connector.GetName(), Email: ident.Email}}, + OIDCIdentities: []services.ExternalIdentity{{ConnectorID: connector.GetName(), Username: ident.Email}}, CreatedBy: services.CreatedBy{ User: services.UserRef{Name: "system"}, Time: time.Now().UTC(), @@ -1093,98 +1397,115 @@ func (a *AuthServer) validateACRValues(acrValue string, identityProvider string, return nil } -// ValidateOIDCAuthCallback is called by the proxy to check OIDC query parameters -// returned by OIDC Provider, if everything checks out, auth server -// will respond with OIDCAuthResponse, otherwise it will return error -func (a *AuthServer) ValidateOIDCAuthCallback(q url.Values) (*OIDCAuthResponse, error) { - if error := q.Get("error"); error != "" { - return nil, trace.OAuth2(oauth2.ErrorInvalidRequest, error, q) +func parseSAMLInResponseTo(response string) (string, error) { + raw, _ := base64.StdEncoding.DecodeString(response) + log.Debugf("SAML response:\n %v\n", string(raw)) + + doc := etree.NewDocument() + err := doc.ReadFromBytes(raw) + if err != nil { + // Attempt to inflate the response in case it happens to be compressed (as with one case at saml.oktadev.com) + buf, err := ioutil.ReadAll(flate.NewReader(bytes.NewReader(raw))) + if err != nil { + return "", trace.Wrap(err) + } + + doc = etree.NewDocument() + err = doc.ReadFromBytes(buf) + if err != nil { + return "", trace.Wrap(err) + } } - code := q.Get("code") - if code == "" { - return nil, trace.OAuth2( - oauth2.ErrorInvalidRequest, "code query param must be set", q) + if doc.Root() == nil { + return "", trace.BadParameter("unable to parse response") } - stateToken := q.Get("state") - if stateToken == "" { - return nil, trace.OAuth2( - oauth2.ErrorInvalidRequest, "missing state query param", q) + el := doc.Root() + responseTo := el.SelectAttr("InResponseTo") + if responseTo == nil { + return "", trace.BadParameter("identity provider initiated flows are not supported") + } + if responseTo.Value == "" { + return "", trace.BadParameter("InResponseTo can not be empty") } + return responseTo.Value, nil +} - req, err := a.Identity.GetOIDCAuthRequest(stateToken) +// ValidateSAMLResponse consumes attribute statements from SAML identity provider +func (a *AuthServer) ValidateSAMLResponse(samlResponse string) (*SAMLAuthResponse, error) { + requestID, err := parseSAMLInResponseTo(samlResponse) if err != nil { return nil, trace.Wrap(err) } - - connector, err := a.Identity.GetOIDCConnector(req.ConnectorID, true) + request, err := a.Identity.GetSAMLAuthRequest(requestID) if err != nil { return nil, trace.Wrap(err) } - - oidcClient, err := a.getOIDCClient(connector) + connector, err := a.Identity.GetSAMLConnector(request.ConnectorID, true) if err != nil { return nil, trace.Wrap(err) } - - // extract claims from both the id token and the userinfo endpoint and merge them - claims, err := a.getClaims(oidcClient, connector.GetIssuerURL(), code) + provider, err := a.getSAMLProvider(connector) if err != nil { - return nil, trace.OAuth2( - oauth2.ErrorUnsupportedResponseType, "unable to construct claims", q) + return nil, trace.Wrap(err) } - log.Debugf("[OIDC] Claims: %v", claims) - - // if we are sending acr values, make sure we also validate them - acrValue := connector.GetACR() - if acrValue != "" { - err := a.validateACRValues(acrValue, connector.GetProvider(), claims) - if err != nil { - return nil, trace.Wrap(err) - } - log.Debugf("[OIDC] ACR values %q successfully validated", acrValue) + assertionInfo, err := provider.RetrieveAssertionInfo(samlResponse) + if err != nil { + log.Warningf("SAML error: %v", err) + return nil, trace.AccessDenied("bad SAML response") } - ident, err := oidc.IdentityFromClaims(claims) - if err != nil { - return nil, trace.OAuth2( - oauth2.ErrorUnsupportedResponseType, "unable to convert claims to identity", q) + if assertionInfo.WarningInfo.InvalidTime { + log.Warningf("SAML error, invalid time") + return nil, trace.AccessDenied("bad SAML response") } - log.Debugf("[IDENTITY] %q expires at: %v", ident.Email, ident.ExpiresAt) - response := &OIDCAuthResponse{ - Identity: services.OIDCIdentity{ConnectorID: connector.GetName(), Email: ident.Email}, - Req: *req, + if assertionInfo.WarningInfo.NotInAudience { + log.Warningf("SAML error, not in audience") + return nil, trace.AccessDenied("bad SAML response") } - log.Debugf("[OIDC] Applying %v claims to roles mappings", len(connector.GetClaimsToRoles())) - if len(connector.GetClaimsToRoles()) != 0 { - if err := a.createOIDCUser(connector, ident, claims); err != nil { - return nil, trace.Wrap(err) - } + log.Debugf("Assertion Info: %#v\n", assertionInfo) + for key, val := range assertionInfo.Values { + log.Debugf("assertion: %s: %+v\n", key, val) } - if !req.CheckUser { - return response, nil + log.Debugf("warnings %+v\n", assertionInfo.WarningInfo) + + log.Debugf("[OIDC] Applying %v claims to roles mappings", len(connector.GetAttributesToRoles())) + if len(connector.GetAttributesToRoles()) == 0 { + return nil, trace.BadParameter("SAML does not support binding to local users") + } + // TODO(klizhentas) use SessionNotOnOrAfter to calculate expiration time + expiresAt := a.clock.Now().Add(defaults.CertDuration) + if err := a.createSAMLUser(connector, *assertionInfo, expiresAt); err != nil { + return nil, trace.Wrap(err) } - user, err := a.Identity.GetUserByOIDCIdentity(services.OIDCIdentity{ - ConnectorID: req.ConnectorID, Email: ident.Email}) + identity := services.ExternalIdentity{ + ConnectorID: request.ConnectorID, + Username: assertionInfo.NameID, + } + user, err := a.Identity.GetUserBySAMLIdentity(identity) if err != nil { return nil, trace.Wrap(err) } - response.Username = user.GetName() + response := &SAMLAuthResponse{ + Req: *request, + Identity: identity, + Username: user.GetName(), + } var roles services.RoleSet roles, err = services.FetchRoles(user.GetRoles(), a.Access) if err != nil { return nil, trace.Wrap(err) } - sessionTTL := roles.AdjustSessionTTL(utils.ToTTL(a.clock, ident.ExpiresAt)) + sessionTTL := roles.AdjustSessionTTL(utils.ToTTL(a.clock, expiresAt)) bearerTokenTTL := utils.MinTTL(BearerTokenTTL, sessionTTL) - if req.CreateWebSession { + if request.CreateWebSession { sess, err := a.NewWebSession(user.GetName()) if err != nil { return nil, trace.Wrap(err) @@ -1199,13 +1520,13 @@ func (a *AuthServer) ValidateOIDCAuthCallback(q url.Values) (*OIDCAuthResponse, response.Session = sess } - if len(req.PublicKey) != 0 { - certTTL := utils.MinTTL(utils.ToTTL(a.clock, ident.ExpiresAt), req.CertTTL) + if len(request.PublicKey) != 0 { + certTTL := utils.MinTTL(utils.ToTTL(a.clock, expiresAt), request.CertTTL) allowedLogins, err := roles.CheckLogins(certTTL) if err != nil { return nil, trace.Wrap(err) } - cert, err := a.GenerateUserCert(req.PublicKey, user.GetName(), allowedLogins, certTTL, roles.CanForwardAgents()) + cert, err := a.GenerateUserCert(request.PublicKey, user.GetName(), allowedLogins, certTTL, roles.CanForwardAgents()) if err != nil { return nil, trace.Wrap(err) } @@ -1266,6 +1587,11 @@ type oidcClient struct { config oidc.ClientConfig } +type samlProvider struct { + provider *saml2.SAMLServiceProvider + connector services.SAMLConnector +} + // oidcConfigsEqual is a struct that helps us to verify that // two oidc configs are equal func oidcConfigsEqual(a, b oidc.ClientConfig) bool { diff --git a/lib/auth/auth_with_roles.go b/lib/auth/auth_with_roles.go index 1f40ad287a3c1..923122a294831 100644 --- a/lib/auth/auth_with_roles.go +++ b/lib/auth/auth_with_roles.go @@ -498,6 +498,66 @@ func (a *AuthWithRoles) DeleteOIDCConnector(connectorID string) error { return a.authServer.DeleteOIDCConnector(connectorID) } +// +func (a *AuthWithRoles) CreateSAMLConnector(connector services.SAMLConnector) error { + if err := a.action(defaults.Namespace, services.KindSAML, services.ActionWrite); err != nil { + return trace.Wrap(err) + } + return a.authServer.UpsertSAMLConnector(connector) +} + +func (a *AuthWithRoles) UpsertSAMLConnector(connector services.SAMLConnector) error { + if err := a.action(defaults.Namespace, services.KindSAML, services.ActionWrite); err != nil { + return trace.Wrap(err) + } + return a.authServer.UpsertSAMLConnector(connector) +} + +func (a *AuthWithRoles) GetSAMLConnector(id string, withSecrets bool) (services.SAMLConnector, error) { + if withSecrets { + if err := a.action(defaults.Namespace, services.KindSAML, services.ActionWrite); err != nil { + return nil, trace.Wrap(err) + } + } else { + if err := a.action(defaults.Namespace, services.KindSAML, services.ActionRead); err != nil { + return nil, trace.Wrap(err) + } + } + return a.authServer.Identity.GetSAMLConnector(id, withSecrets) +} + +func (a *AuthWithRoles) GetSAMLConnectors(withSecrets bool) ([]services.SAMLConnector, error) { + if withSecrets { + if err := a.action(defaults.Namespace, services.KindSAML, services.ActionWrite); err != nil { + return nil, trace.Wrap(err) + } + } else { + if err := a.action(defaults.Namespace, services.KindSAML, services.ActionRead); err != nil { + return nil, trace.Wrap(err) + } + } + return a.authServer.Identity.GetSAMLConnectors(withSecrets) +} + +func (a *AuthWithRoles) CreateSAMLAuthRequest(req services.SAMLAuthRequest) (*services.SAMLAuthRequest, error) { + if err := a.action(defaults.Namespace, services.KindSAMLRequest, services.ActionWrite); err != nil { + return nil, trace.Wrap(err) + } + return a.authServer.CreateSAMLAuthRequest(req) +} + +func (a *AuthWithRoles) ValidateSAMLResponse(re string) (*SAMLAuthResponse, error) { + // auth callback is it's own authz, no need to check extra permissions + return a.authServer.ValidateSAMLResponse(re) +} + +func (a *AuthWithRoles) DeleteSAMLConnector(connectorID string) error { + if err := a.action(defaults.Namespace, services.KindSAML, services.ActionWrite); err != nil { + return trace.Wrap(err) + } + return a.authServer.DeleteSAMLConnector(connectorID) +} + func (a *AuthWithRoles) EmitAuditEvent(eventType string, fields events.EventFields) error { if err := a.action(defaults.Namespace, services.KindEvent, services.ActionWrite); err != nil { return trace.Wrap(err) diff --git a/lib/auth/clt.go b/lib/auth/clt.go index a382fdc0a5b7d..878cbfee1b76d 100644 --- a/lib/auth/clt.go +++ b/lib/auth/clt.go @@ -958,6 +958,131 @@ func (c *Client) ValidateOIDCAuthCallback(q url.Values) (*OIDCAuthResponse, erro return &response, nil } +// CreateOIDCConnector creates SAML connector +func (c *Client) CreateSAMLConnector(connector services.SAMLConnector) error { + data, err := services.GetSAMLConnectorMarshaler().MarshalSAMLConnector(connector) + if err != nil { + return trace.Wrap(err) + } + _, err = c.PostJSON(c.Endpoint("saml", "connectors"), &createSAMLConnectorRawReq{ + Connector: data, + }) + if err != nil { + return trace.Wrap(err) + } + return nil +} + +// UpsertSAMLConnector updates or creates OIDC connector +func (c *Client) UpsertSAMLConnector(connector services.SAMLConnector) error { + data, err := services.GetSAMLConnectorMarshaler().MarshalSAMLConnector(connector) + if err != nil { + return trace.Wrap(err) + } + _, err = c.PostJSON(c.Endpoint("saml", "connectors"), &upsertSAMLConnectorRawReq{ + Connector: data, + }) + if err != nil { + return trace.Wrap(err) + } + return nil +} + +// GetOIDCConnector returns SAML connector information by id +func (c *Client) GetSAMLConnector(id string, withSecrets bool) (services.SAMLConnector, error) { + if id == "" { + return nil, trace.BadParameter("missing connector id") + } + out, err := c.Get(c.Endpoint("saml", "connectors", id), + url.Values{"with_secrets": []string{fmt.Sprintf("%t", withSecrets)}}) + if err != nil { + return nil, trace.Wrap(err) + } + return services.GetSAMLConnectorMarshaler().UnmarshalSAMLConnector(out.Bytes()) +} + +// GetSAMLConnectors gets SAML connectors list +func (c *Client) GetSAMLConnectors(withSecrets bool) ([]services.SAMLConnector, error) { + out, err := c.Get(c.Endpoint("saml", "connectors"), + url.Values{"with_secrets": []string{fmt.Sprintf("%t", withSecrets)}}) + if err != nil { + return nil, err + } + var items []json.RawMessage + if err := json.Unmarshal(out.Bytes(), &items); err != nil { + return nil, trace.Wrap(err) + } + connectors := make([]services.SAMLConnector, len(items)) + for i, raw := range items { + connector, err := services.GetSAMLConnectorMarshaler().UnmarshalSAMLConnector(raw) + if err != nil { + return nil, trace.Wrap(err) + } + connectors[i] = connector + } + return connectors, nil +} + +// DeleteSAMLConnector deletes SAML connector by ID +func (c *Client) DeleteSAMLConnector(connectorID string) error { + if connectorID == "" { + return trace.BadParameter("missing connector id") + } + _, err := c.Delete(c.Endpoint("saml", "connectors", connectorID)) + return trace.Wrap(err) +} + +// CreateSAMLAuthRequest creates SAML AuthnRequest +func (c *Client) CreateSAMLAuthRequest(req services.SAMLAuthRequest) (*services.SAMLAuthRequest, error) { + out, err := c.PostJSON(c.Endpoint("saml", "requests", "create"), createSAMLAuthRequestReq{ + Req: req, + }) + if err != nil { + return nil, trace.Wrap(err) + } + var response *services.SAMLAuthRequest + if err := json.Unmarshal(out.Bytes(), &response); err != nil { + return nil, trace.Wrap(err) + } + return response, nil +} + +// ValidateSAMLResponse validates response returned by SAML identity provider +func (c *Client) ValidateSAMLResponse(re string) (*SAMLAuthResponse, error) { + out, err := c.PostJSON(c.Endpoint("saml", "requests", "validate"), validateSAMLResponseReq{ + Response: re, + }) + if err != nil { + return nil, trace.Wrap(err) + } + var rawResponse *samlAuthRawResponse + if err := json.Unmarshal(out.Bytes(), &rawResponse); err != nil { + return nil, trace.Wrap(err) + } + response := SAMLAuthResponse{ + Username: rawResponse.Username, + Identity: rawResponse.Identity, + Cert: rawResponse.Cert, + Req: rawResponse.Req, + } + if len(rawResponse.Session) != 0 { + session, err := services.GetWebSessionMarshaler().UnmarshalWebSession(rawResponse.Session) + if err != nil { + return nil, trace.Wrap(err) + } + response.Session = session + } + response.HostSigners = make([]services.CertAuthority, len(rawResponse.HostSigners)) + for i, raw := range rawResponse.HostSigners { + ca, err := services.GetCertAuthorityMarshaler().UnmarshalCertAuthority(raw) + if err != nil { + return nil, trace.Wrap(err) + } + response.HostSigners[i] = ca + } + return &response, nil +} + // EmitAuditEvent sends an auditable event to the auth server (part of evets.IAuditLog interface) func (c *Client) EmitAuditEvent(eventType string, fields events.EventFields) error { _, err := c.PostJSON(c.Endpoint("events"), &auditEventReq{ @@ -1374,6 +1499,27 @@ type IdentityService interface { // ValidateOIDCAuthCallback validates OIDC auth callback returned from redirect ValidateOIDCAuthCallback(q url.Values) (*OIDCAuthResponse, error) + // CreateSAMLConnector creates SAML connector + CreateSAMLConnector(connector services.SAMLConnector) error + + // UpsertSAMLConnector updates or creates SAML connector + UpsertSAMLConnector(connector services.SAMLConnector) error + + // GetSAMLConnector returns SAML connector information by id + GetSAMLConnector(id string, withSecrets bool) (services.SAMLConnector, error) + + // GetSAMLConnector gets SAML connectors list + GetSAMLConnectors(withSecrets bool) ([]services.SAMLConnector, error) + + // DeleteSAMLConnector deletes SAML connector by ID + DeleteSAMLConnector(connectorID string) error + + // CreateSAMLAuthRequest creates SAML AuthnRequest + CreateSAMLAuthRequest(req services.SAMLAuthRequest) (*services.SAMLAuthRequest, error) + + // ValidateSAMLResponse validates SAML auth response + ValidateSAMLResponse(re string) (*SAMLAuthResponse, error) + // GetU2FSignRequest generates request for user trying to authenticate with U2F token GetU2FSignRequest(user string, password []byte) (*u2f.SignRequest, error) diff --git a/lib/auth/init.go b/lib/auth/init.go index 60a766dacc259..9ccaf92e9686b 100644 --- a/lib/auth/init.go +++ b/lib/auth/init.go @@ -162,7 +162,7 @@ func Init(cfg InitConfig, dynamicConfig bool) (*AuthServer, *Identity, error) { log.Infof("[INIT] Set Universal Second Factor Settings: %v", cfg.U2F) } - if cfg.OIDCConnectors != nil && len(cfg.OIDCConnectors) > 0 { + if len(cfg.OIDCConnectors) > 0 { for _, connector := range cfg.OIDCConnectors { if err := asrv.UpsertOIDCConnector(connector); err != nil { return nil, nil, trace.Wrap(err) diff --git a/lib/auth/new_web_user.go b/lib/auth/new_web_user.go index 911fba42457f2..ea770dceaa06e 100644 --- a/lib/auth/new_web_user.go +++ b/lib/auth/new_web_user.go @@ -48,7 +48,7 @@ func (s *AuthServer) CreateSignupToken(userv1 services.UserV1) (string, error) { } // make sure that connectors actually exist - for _, id := range user.GetIdentities() { + for _, id := range user.GetOIDCIdentities() { if err := id.Check(); err != nil { return "", trace.Wrap(err) } @@ -57,6 +57,15 @@ func (s *AuthServer) CreateSignupToken(userv1 services.UserV1) (string, error) { } } + for _, id := range user.GetSAMLIdentities() { + if err := id.Check(); err != nil { + return "", trace.Wrap(err) + } + if _, err := s.GetSAMLConnector(id.ConnectorID, false); err != nil { + return "", trace.Wrap(err) + } + } + // TODO(rjones): TOCTOU, instead try to create signup token for user and fail // when unable to. _, err := s.GetPasswordHash(user.GetName()) diff --git a/lib/auth/permissions.go b/lib/auth/permissions.go index 66ee9d6f6983e..74e984e08e515 100644 --- a/lib/auth/permissions.go +++ b/lib/auth/permissions.go @@ -174,6 +174,8 @@ func GetCheckerForBuiltinRole(role teleport.Role) (services.AccessChecker, error services.KindProxy: services.RW(), services.KindOIDCRequest: services.RW(), services.KindOIDC: services.RO(), + services.KindSAMLRequest: services.RW(), + services.KindSAML: services.RO(), services.KindNamespace: services.RO(), services.KindEvent: services.RW(), services.KindSession: services.RW(), diff --git a/lib/client/api.go b/lib/client/api.go index f0d43442e7408..926e486450582 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -1010,11 +1010,18 @@ func (tc *TeleportClient) Login() (*CertAuthMethod, error) { return nil, trace.Wrap(err) } case teleport.OIDC: - response, err = tc.oidcLogin(pr.Auth.OIDC.Name, key.Pub) + response, err = tc.ssoLogin(pr.Auth.OIDC.Name, key.Pub, teleport.OIDC) if err != nil { return nil, trace.Wrap(err) } + // in this case identity is returned by the proxy + tc.Username = response.Username + case teleport.SAML: + response, err = tc.ssoLogin(pr.Auth.SAML.Name, key.Pub, teleport.SAML) + if err != nil { + return nil, trace.Wrap(err) + } // in this case identity is returned by the proxy tc.Username = response.Username default: @@ -1114,13 +1121,13 @@ func (tc *TeleportClient) directLogin(secondFactorType string, pub []byte) (*SSH return response, trace.Wrap(err) } -// oidcLogin opens browser window and uses OIDC redirect cycle with browser -func (tc *TeleportClient) oidcLogin(connectorID string, pub []byte) (*SSHLoginResponse, error) { - log.Infof("oidcLogin start") +// samlLogin opens browser window and uses OIDC or SAML redirect cycle with browser +func (tc *TeleportClient) ssoLogin(connectorID string, pub []byte, protocol string) (*SSHLoginResponse, error) { + log.Debugf("samlLogin start") // ask the CA (via proxy) to sign our public key: webProxyAddr := tc.Config.ProxyWebHostPort() - response, err := SSHAgentOIDCLogin(webProxyAddr, - connectorID, pub, tc.KeyTTL, tc.InsecureSkipVerify, loopbackPool(webProxyAddr)) + response, err := SSHAgentSSOLogin(webProxyAddr, + connectorID, pub, tc.KeyTTL, tc.InsecureSkipVerify, loopbackPool(webProxyAddr), protocol) return response, trace.Wrap(err) } diff --git a/lib/client/weblogin.go b/lib/client/weblogin.go index 2659519e7ecdc..a613cc9b4b669 100644 --- a/lib/client/weblogin.go +++ b/lib/client/weblogin.go @@ -59,14 +59,16 @@ type SSHLoginResponse struct { HostSigners []services.CertAuthorityV1 `json:"host_signers"` } -type OIDCLoginConsoleReq struct { +// SSOLoginConsoleReq is used to SSO for tsh +type SSOLoginConsoleReq struct { RedirectURL string `json:"redirect_url"` PublicKey []byte `json:"public_key"` CertTTL time.Duration `json:"cert_ttl"` ConnectorID string `json:"connector_id"` } -type OIDCLoginConsoleResponse struct { +// SSOLoginConsoleResponse is a response to SSO console request +type SSOLoginConsoleResponse struct { RedirectURL string `json:"redirect_url"` } @@ -117,8 +119,8 @@ type sealData struct { Nonce []byte `json:"nonce"` } -// SSHAgentOIDCLogin is used by SSH Agent (tsh) to login using OpenID connect -func SSHAgentOIDCLogin(proxyAddr, connectorID string, pubKey []byte, ttl time.Duration, insecure bool, pool *x509.CertPool) (*SSHLoginResponse, error) { +// SSHAgentSSOLogin is used by SSH Agent (tsh) to login using OpenID connect +func SSHAgentSSOLogin(proxyAddr, connectorID string, pubKey []byte, ttl time.Duration, insecure bool, pool *x509.CertPool, protocol string) (*SSHLoginResponse, error) { clt, proxyURL, err := initClient(proxyAddr, insecure, pool) if err != nil { return nil, trace.Wrap(err) @@ -197,7 +199,7 @@ func SSHAgentOIDCLogin(proxyAddr, connectorID string, pubKey []byte, ttl time.Du query.Set("secret", secret.KeyToEncodedString(keyBytes)) u.RawQuery = query.Encode() - out, err := clt.PostJSON(clt.Endpoint("webapi", "oidc", "login", "console"), OIDCLoginConsoleReq{ + out, err := clt.PostJSON(clt.Endpoint("webapi", protocol, "login", "console"), SSOLoginConsoleReq{ RedirectURL: u.String(), PublicKey: pubKey, CertTTL: ttl, @@ -207,7 +209,7 @@ func SSHAgentOIDCLogin(proxyAddr, connectorID string, pubKey []byte, ttl time.Du return nil, trace.Wrap(err) } - var re *OIDCLoginConsoleResponse + var re *SSOLoginConsoleResponse err = json.Unmarshal(out.Bytes(), &re) if err != nil { return nil, trace.Wrap(err) @@ -259,6 +261,8 @@ type AuthenticationSettings struct { U2F *U2FSettings `json:"u2f,omitempty"` // OIDC contains the OIDC Connector settings needed for authentication. OIDC *OIDCSettings `json:"oidc,omitempty"` + // SAML contains the SAML Connector settings needed for authentication. + SAML *SAMLSettings `json:"saml,omitempty"` } // U2FSettings contains the AppID for Universal Second Factor. @@ -267,6 +271,14 @@ type U2FSettings struct { AppID string `json:"app_id"` } +// SAMLSettings contains the Name and Display string for SAML +type SAMLSettings struct { + // Name is the internal name of the connector. + Name string `json:"name"` + // Display is the display name for the connector. + Display string `json:"display"` +} + // OIDCSettings contains the Name and Display string for OIDC. type OIDCSettings struct { // Name is the internal name of the connector. diff --git a/lib/config/fileconf.go b/lib/config/fileconf.go index 4f9900685cef4..7de9932014aad 100644 --- a/lib/config/fileconf.go +++ b/lib/config/fileconf.go @@ -121,6 +121,7 @@ var ( "public_addr": false, "cache": true, "ttl": false, + "issuer": false, } ) diff --git a/lib/defaults/defaults.go b/lib/defaults/defaults.go index ffaa19b8e5f8d..78fd787c899f0 100644 --- a/lib/defaults/defaults.go +++ b/lib/defaults/defaults.go @@ -120,6 +120,9 @@ const ( // OIDCAuthRequestTTL is TTL of internally stored auth request created by client OIDCAuthRequestTTL = 10 * 60 * time.Second + // SAMLAuthRequestTTL is TTL of internally stored auth request created by client + SAMLAuthRequestTTL = 10 * 60 * time.Second + // LogRotationPeriod defines how frequently to rotate the audit log file LogRotationPeriod = (time.Hour * 24) diff --git a/lib/services/authentication.go b/lib/services/authentication.go index 931f59cadb103..fa51b4e9dfdb9 100644 --- a/lib/services/authentication.go +++ b/lib/services/authentication.go @@ -135,7 +135,11 @@ func (c *AuthPreferenceV2) CheckAndSetDefaults() error { } case teleport.OIDC: if c.Spec.SecondFactor != "" { - return trace.BadParameter("second factor [%q] not supported with oidc connector") + return trace.BadParameter("second factor [%q] not supported with OIDC connector") + } + case teleport.SAML: + if c.Spec.SecondFactor != "" { + return trace.BadParameter("second factor [%q] not supported with SAML connector") } default: return trace.BadParameter("unsupported type %q", c.Spec.Type) diff --git a/lib/services/identity.go b/lib/services/identity.go index 1a3002c12f087..1ba53bb0bdc2a 100644 --- a/lib/services/identity.go +++ b/lib/services/identity.go @@ -58,7 +58,11 @@ type Identity interface { // GetUserByOIDCIdentity returns a user by it's specified OIDC Identity, returns first // user specified with this identity - GetUserByOIDCIdentity(id OIDCIdentity) (User, error) + GetUserByOIDCIdentity(id ExternalIdentity) (User, error) + + // GetUserBySAMLIdentity returns a user by it's specified OIDC Identity, returns first + // user specified with this identity + GetUserBySAMLIdentity(id ExternalIdentity) (User, error) // DeleteUser deletes a user with all the keys from the backend DeleteUser(user string) error @@ -159,6 +163,27 @@ type Identity interface { // GetOIDCAuthRequest returns OIDC auth request if found GetOIDCAuthRequest(stateToken string) (*OIDCAuthRequest, error) + + // CreateSAMLConnector creates SAML Connector + CreateSAMLConnector(connector SAMLConnector) error + + // UpsertSAMLConnector upserts SAML Connector + UpsertSAMLConnector(connector SAMLConnector) error + + // DeleteSAMLConnector deletes OIDC Connector + DeleteSAMLConnector(connectorID string) error + + // GetSAMLConnector returns OIDC connector data, withSecrets adds or removes secrets from return results + GetSAMLConnector(id string, withSecrets bool) (SAMLConnector, error) + + // GetSAMLConnectors returns registered connectors, withSecrets adds or removes secret from return results + GetSAMLConnectors(withSecrets bool) ([]SAMLConnector, error) + + // CreateSAMLAuthRequest creates new auth request + CreateSAMLAuthRequest(req SAMLAuthRequest, ttl time.Duration) error + + // GetSAMLAuthRequest returns OSAML auth request if found + GetSAMLAuthRequest(id string) (*SAMLAuthRequest, error) } // VerifyPassword makes sure password satisfies our requirements (relaxed), @@ -188,16 +213,15 @@ type SignupToken struct { // OIDCIdentity is OpenID Connect identity that is linked // to particular user and connector and lets user to log in using external // credentials, e.g. google -type OIDCIdentity struct { +type ExternalIdentity struct { // ConnectorID is id of registered OIDC connector, e.g. 'google-example.com' ConnectorID string `json:"connector_id"` - // Email is OIDC verified email claim - // e.g. bob@example.com - Email string `json:"username"` + // Username is username supplied by external identity provider + Username string `json:"username"` } -const OIDCIDentitySchema = `{ +const ExternalIdentitySchema = `{ "type": "object", "additionalProperties": false, "properties": { @@ -207,22 +231,22 @@ const OIDCIDentitySchema = `{ }` // String returns debug friendly representation of this identity -func (i *OIDCIdentity) String() string { - return fmt.Sprintf("OIDCIdentity(connectorID=%v, email=%v)", i.ConnectorID, i.Email) +func (i *ExternalIdentity) String() string { + return fmt.Sprintf("OIDCIdentity(connectorID=%v, username=%v)", i.ConnectorID, i.Username) } // Equals returns true if this identity equals to passed one -func (i *OIDCIdentity) Equals(other *OIDCIdentity) bool { - return i.ConnectorID == other.ConnectorID && i.Email == other.Email +func (i *ExternalIdentity) Equals(other *ExternalIdentity) bool { + return i.ConnectorID == other.ConnectorID && i.Username == other.Username } // Check returns nil if all parameters are great, err otherwise -func (i *OIDCIdentity) Check() error { +func (i *ExternalIdentity) Check() error { if i.ConnectorID == "" { return trace.BadParameter("ConnectorID: missing value") } - if i.Email == "" { - return trace.BadParameter("Email: missing email") + if i.Username == "" { + return trace.BadParameter("Username: missing username") } return nil } @@ -284,6 +308,59 @@ func (i *OIDCAuthRequest) Check() error { return nil } +// SAMLAuthRequest is a request to authenticate with OIDC +// provider, the state about request is managed by auth server +type SAMLAuthRequest struct { + // ID is a unique request ID + ID string `json:"id"` + + // ConnectorID is ID of OIDC connector this request uses + ConnectorID string `json:"connector_id"` + + // Type is opaque string that helps callbacks identify the request type + Type string `json:"type"` + + // CheckUser tells validator if it should expect and check user + CheckUser bool `json:"check_user"` + + // RedirectURL will be used by browser + RedirectURL string `json:"redirect_url"` + + // PublicKey is an optional public key, users want these + // keys to be signed by auth servers user CA in case + // of successfull auth + PublicKey []byte `json:"public_key"` + + // CertTTL is the TTL of the certificate user wants to get + CertTTL time.Duration `json:"cert_ttl"` + + // CreateWebSession indicates if user wants to generate a web + // session after successful authentication + CreateWebSession bool `json:"create_web_session"` + + // ClientRedirectURL is a URL client wants to be redirected + // after successfull authentication + ClientRedirectURL string `json:"client_redirect_url"` +} + +// Check returns nil if all parameters are great, err otherwise +func (i *SAMLAuthRequest) Check() error { + if i.ConnectorID == "" { + return trace.BadParameter("ConnectorID: missing value") + } + if len(i.PublicKey) != 0 { + _, _, _, _, err := ssh.ParseAuthorizedKey(i.PublicKey) + if err != nil { + return trace.BadParameter("PublicKey: bad key: %v", err) + } + if (i.CertTTL > defaults.MaxCertDuration) || (i.CertTTL < defaults.MinCertDuration) { + return trace.BadParameter("CertTTL: wrong certificate TTL") + } + } + + return nil +} + // U2F is a configuration of the U2F two factor authentication // Deprecated: Use services.UniversalSecondFactor instead. type U2F struct { diff --git a/lib/services/local/users.go b/lib/services/local/users.go index 997bc72c599f0..3a958fb4d3063 100644 --- a/lib/services/local/users.go +++ b/lib/services/local/users.go @@ -125,13 +125,30 @@ func (s *IdentityService) GetUser(user string) (services.User, error) { // GetUserByOIDCIdentity returns a user by it's specified OIDC Identity, returns first // user specified with this identity -func (s *IdentityService) GetUserByOIDCIdentity(id services.OIDCIdentity) (services.User, error) { +func (s *IdentityService) GetUserByOIDCIdentity(id services.ExternalIdentity) (services.User, error) { users, err := s.GetUsers() if err != nil { return nil, trace.Wrap(err) } for _, u := range users { - for _, uid := range u.GetIdentities() { + for _, uid := range u.GetOIDCIdentities() { + if uid.Equals(&id) { + return u, nil + } + } + } + return nil, trace.NotFound("user with identity %v not found", &id) +} + +// GetUserBySAMLCIdentity returns a user by it's specified OIDC Identity, returns first +// user specified with this identity +func (s *IdentityService) GetUserBySAMLIdentity(id services.ExternalIdentity) (services.User, error) { + users, err := s.GetUsers() + if err != nil { + return nil, trace.Wrap(err) + } + for _, u := range users { + for _, uid := range u.GetSAMLIdentities() { if uid.Equals(&id) { return u, nil } @@ -381,10 +398,12 @@ func (s *IdentityService) UpsertPassword(user string, password []byte) error { } var ( - userTokensPath = []string{"addusertokens"} - u2fRegChalPath = []string{"adduseru2fchallenges"} - connectorsPath = []string{"web", "connectors", "oidc", "connectors"} - authRequestsPath = []string{"web", "connectors", "oidc", "requests"} + userTokensPath = []string{"addusertokens"} + u2fRegChalPath = []string{"adduseru2fchallenges"} + oidcConnectorsPath = []string{"web", "connectors", "oidc", "connectors"} + oidcAuthRequestsPath = []string{"web", "connectors", "oidc", "requests"} + samlConnectorsPath = []string{"web", "connectors", "saml", "connectors"} + samlAuthRequestsPath = []string{"web", "connectors", "saml", "requests"} ) // UpsertSignupToken upserts signup token - one time token that lets user to create a user account @@ -599,7 +618,7 @@ func (s *IdentityService) UpsertOIDCConnector(connector services.OIDCConnector) return trace.Wrap(err) } ttl := backend.TTL(s.Clock(), connector.Expiry()) - err = s.UpsertVal(connectorsPath, connector.GetName(), data, ttl) + err = s.UpsertVal(oidcConnectorsPath, connector.GetName(), data, ttl) if err != nil { return trace.Wrap(err) } @@ -608,13 +627,13 @@ func (s *IdentityService) UpsertOIDCConnector(connector services.OIDCConnector) // DeleteOIDCConnector deletes OIDC Connector func (s *IdentityService) DeleteOIDCConnector(connectorID string) error { - err := s.DeleteKey(connectorsPath, connectorID) + err := s.DeleteKey(oidcConnectorsPath, connectorID) return trace.Wrap(err) } // GetOIDCConnector returns OIDC connector data, , withSecrets adds or removes client secret from return results func (s *IdentityService) GetOIDCConnector(id string, withSecrets bool) (services.OIDCConnector, error) { - data, err := s.GetVal(connectorsPath, id) + data, err := s.GetVal(oidcConnectorsPath, id) if err != nil { if trace.IsNotFound(err) { return nil, trace.NotFound("OpenID connector '%v' is not configured", id) @@ -633,7 +652,7 @@ func (s *IdentityService) GetOIDCConnector(id string, withSecrets bool) (service // GetOIDCConnectors returns registered connectors, withSecrets adds or removes client secret from return results func (s *IdentityService) GetOIDCConnectors(withSecrets bool) ([]services.OIDCConnector, error) { - connectorIDs, err := s.GetKeys(connectorsPath) + connectorIDs, err := s.GetKeys(oidcConnectorsPath) if err != nil { return nil, trace.Wrap(err) } @@ -661,7 +680,7 @@ func (s *IdentityService) CreateOIDCAuthRequest(req services.OIDCAuthRequest, tt if err != nil { return trace.Wrap(err) } - err = s.CreateVal(authRequestsPath, req.StateToken, data, ttl) + err = s.CreateVal(oidcAuthRequestsPath, req.StateToken, data, ttl) if err != nil { return trace.Wrap(err) } @@ -670,7 +689,7 @@ func (s *IdentityService) CreateOIDCAuthRequest(req services.OIDCAuthRequest, tt // GetOIDCAuthRequest returns OIDC auth request if found func (s *IdentityService) GetOIDCAuthRequest(stateToken string) (*services.OIDCAuthRequest, error) { - data, err := s.GetVal(authRequestsPath, stateToken) + data, err := s.GetVal(oidcAuthRequestsPath, stateToken) if err != nil { return nil, trace.Wrap(err) } @@ -680,3 +699,116 @@ func (s *IdentityService) GetOIDCAuthRequest(stateToken string) (*services.OIDCA } return req, nil } + +// CreateSAMLConnector creates SAML Connector +func (s *IdentityService) CreateSAMLConnector(connector services.SAMLConnector) error { + if err := connector.CheckAndSetDefaults(); err != nil { + return trace.Wrap(err) + } + data, err := services.GetSAMLConnectorMarshaler().MarshalSAMLConnector(connector) + if err != nil { + return trace.Wrap(err) + } + ttl := backend.TTL(s.Clock(), connector.Expiry()) + err = s.CreateVal(samlConnectorsPath, connector.GetName(), data, ttl) + if err != nil { + return trace.Wrap(err) + } + return nil +} + +// UpsertSAMLConnector upserts SAML Connector +func (s *IdentityService) UpsertSAMLConnector(connector services.SAMLConnector) error { + if err := connector.CheckAndSetDefaults(); err != nil { + return trace.Wrap(err) + } + data, err := services.GetSAMLConnectorMarshaler().MarshalSAMLConnector(connector) + if err != nil { + return trace.Wrap(err) + } + ttl := backend.TTL(s.Clock(), connector.Expiry()) + err = s.UpsertVal(samlConnectorsPath, connector.GetName(), data, ttl) + if err != nil { + return trace.Wrap(err) + } + return nil +} + +// DeleteSAMLConnector deletes OIDC Connector +func (s *IdentityService) DeleteSAMLConnector(connectorID string) error { + err := s.DeleteKey(samlConnectorsPath, connectorID) + return trace.Wrap(err) +} + +// GetSAMLConnector returns OIDC connector data, withSecrets adds or removes secrets from return results +func (s *IdentityService) GetSAMLConnector(id string, withSecrets bool) (services.SAMLConnector, error) { + data, err := s.GetVal(samlConnectorsPath, id) + if err != nil { + if trace.IsNotFound(err) { + return nil, trace.NotFound("SAML connector '%v' is not configured", id) + } + return nil, trace.Wrap(err) + } + conn, err := services.GetSAMLConnectorMarshaler().UnmarshalSAMLConnector(data) + if err != nil { + return nil, trace.Wrap(err) + } + if !withSecrets { + keyPair := conn.GetSigningKeyPair() + if keyPair != nil { + keyPair.PrivateKey = "" + conn.SetSigningKeyPair(keyPair) + } + } + return conn, nil +} + +// GetSAMLConnectors returns registered connectors, withSecrets adds or removes secret from return results +func (s *IdentityService) GetSAMLConnectors(withSecrets bool) ([]services.SAMLConnector, error) { + connectorIDs, err := s.GetKeys(samlConnectorsPath) + if err != nil { + return nil, trace.Wrap(err) + } + connectors := make([]services.SAMLConnector, 0, len(connectorIDs)) + for _, id := range connectorIDs { + connector, err := s.GetSAMLConnector(id, withSecrets) + if err != nil { + if !trace.IsNotFound(err) { + return nil, trace.Wrap(err) + } + // the record has expired + continue + } + connectors = append(connectors, connector) + } + return connectors, nil +} + +// CreateSAMLAuthRequest creates new auth request +func (s *IdentityService) CreateSAMLAuthRequest(req services.SAMLAuthRequest, ttl time.Duration) error { + if err := req.Check(); err != nil { + return trace.Wrap(err) + } + data, err := json.Marshal(req) + if err != nil { + return trace.Wrap(err) + } + err = s.CreateVal(samlAuthRequestsPath, req.ID, data, ttl) + if err != nil { + return trace.Wrap(err) + } + return nil +} + +// GetSAMLAuthRequest returns OSAML auth request if found +func (s *IdentityService) GetSAMLAuthRequest(id string) (*services.SAMLAuthRequest, error) { + data, err := s.GetVal(samlAuthRequestsPath, id) + if err != nil { + return nil, trace.Wrap(err) + } + var req *services.SAMLAuthRequest + if err := json.Unmarshal(data, &req); err != nil { + return nil, trace.Wrap(err) + } + return req, nil +} diff --git a/lib/services/resource.go b/lib/services/resource.go index 0be369958b857..4e35f1b0fcfcf 100644 --- a/lib/services/resource.go +++ b/lib/services/resource.go @@ -56,12 +56,18 @@ const ( // KindRole is a role resource KindRole = "role" - // KindOIDC is oidc connector resource + // KindOIDC is OIDC connector resource KindOIDC = "oidc" - // KindOIDCReques is oidc auth request resource + // KindSAML is SAML connector resource + KindSAML = "saml" + + // KindOIDCRequest is oidc auth request resource KindOIDCRequest = "oidc_request" + // KindOIDCReques is saml auth request resource + KindSAMLRequest = "saml_request" + // KindSession is a recorded session resource KindSession = "session" @@ -92,6 +98,9 @@ const ( // KindOIDCConnector is a OIDC connector resource KindOIDCConnector = "oidc" + // KindSAMLConnector is a SAML connector resource + KindSAMLConnector = "saml" + // KindAuthPreference is the type of authentication for this cluster. KindClusterAuthPreference = "cluster_auth_preference" @@ -307,6 +316,8 @@ func ParseShortcut(in string) (string, error) { return KindNode, nil case "oidc": return KindOIDCConnector, nil + case "saml": + return KindSAMLConnector, nil case "users": return KindUser, nil case "cert_authorities", "cas": diff --git a/lib/services/role.go b/lib/services/role.go index 05f4b0eceb3af..a6fec476c8991 100644 --- a/lib/services/role.go +++ b/lib/services/role.go @@ -149,6 +149,8 @@ type Role interface { SetForwardAgent(forwardAgent bool) // CheckAndSetDefaults checks and set default values for missing fields. CheckAndSetDefaults() error + // Equals returns true if roles are equal + Equals(other Role) bool } // RoleV2 represents role resource specification @@ -163,6 +165,29 @@ type RoleV2 struct { Spec RoleSpecV2 `json:"spec"` } +// Equals returns true if roles are equal +func (r *RoleV2) Equals(other Role) bool { + if !utils.StringSlicesEqual(r.GetLogins(), other.GetLogins()) { + return false + } + if !utils.StringSlicesEqual(r.GetNamespaces(), other.GetNamespaces()) { + return false + } + if !utils.StringMapsEqual(r.GetNodeLabels(), other.GetNodeLabels()) { + return false + } + if !utils.StringMapSlicesEqual(r.GetResources(), other.GetResources()) { + return false + } + if r.CanForwardAgent() != other.CanForwardAgent() { + return false + } + if r.GetMaxSessionTTL() != other.GetMaxSessionTTL() { + return false + } + return true +} + // SetResource sets resource rule func (r *RoleV2) SetResource(kind string, actions []string) { if r.Spec.Resources == nil { diff --git a/lib/services/saml.go b/lib/services/saml.go new file mode 100644 index 0000000000000..e258974fb15ba --- /dev/null +++ b/lib/services/saml.go @@ -0,0 +1,713 @@ +/* +Copyright 2015 Gravitational, Inc. + +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 services + +import ( + "bytes" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/json" + "encoding/xml" + "fmt" + "text/template" + "time" + + "github.com/gravitational/teleport/lib/defaults" + "github.com/gravitational/teleport/lib/utils" + + log "github.com/Sirupsen/logrus" + "github.com/gravitational/trace" + "github.com/jonboulle/clockwork" + saml2 "github.com/russellhaering/gosaml2" + "github.com/russellhaering/gosaml2/types" + dsig "github.com/russellhaering/goxmldsig" +) + +// SAMLConnector specifies configuration for SAML 2.0 dentity providers +type SAMLConnector interface { + // Resource provides common methods for objects + Resource + // GetDisplay returns display - friendly name for this provider. + GetDisplay() string + // SetDisplay sets friendly name for this provider. + SetDisplay(string) + // GetAttributesToRoles returns attributes to roles mapping + GetAttributesToRoles() []AttributeMapping + // SetAttributesToRoles sets attributes to roles mapping + SetAttributesToRoles(mapping []AttributeMapping) + // GetAttributes returns list of attributes expected by mappings + GetAttributes() []string + // MapAttributes maps attributes to roles + MapAttributes(assertionInfo saml2.AssertionInfo) []string + // RoleFromTemplate creates a role from a template and claims. + RoleFromTemplate(assertionInfo saml2.AssertionInfo) (Role, error) + // Check checks SAML connector for errors + CheckAndSetDefaults() error + // SetIssuer sets issuer + SetIssuer(issuer string) + // GetIssuer returns issuer + GetIssuer() string + // GetSigningKeyPair returns signing key pair + GetSigningKeyPair() *SigningKeyPair + // GetSigningKeyPair sets signing key pair + SetSigningKeyPair(k *SigningKeyPair) + // Equals returns true if the connectors are identical + Equals(other SAMLConnector) bool + // GetSSO returns SSO service + GetSSO() string + // SetSSO sets SSO service + SetSSO(string) + // GetEntityDescriptor returns XML entity descriptor of the service + GetEntityDescriptor() string + // SetEntityDescriptor sets entity descritor of the service + SetEntityDescriptor(v string) + // GetCert returns identity provider checking x509 certificate + GetCert() string + // SetCert sets identity provider checking certificate + SetCert(string) + // GetServiceProviderIssuer returns service provider issuer + GetServiceProviderIssuer() string + // SetServiceProviderIssuer sets service provider issuer + SetServiceProviderIssuer(v string) + // GetAudience returns audience + GetAudience() string + // SetAudience sets audience + SetAudience(v string) + // GetServiceProvider initialises service provider spec from settings + GetServiceProvider() (*saml2.SAMLServiceProvider, error) +} + +// NewSAMLConnector returns a new SAMLConnector based off a name and SAMLConnectorSpecV2. +func NewSAMLConnector(name string, spec SAMLConnectorSpecV2) SAMLConnector { + return &SAMLConnectorV2{ + Kind: KindSAMLConnector, + Version: V2, + Metadata: Metadata{ + Name: name, + Namespace: defaults.Namespace, + }, + Spec: spec, + } +} + +var samlConnectorMarshaler SAMLConnectorMarshaler = &TeleportSAMLConnectorMarshaler{} + +// SetSAMLConnectorMarshaler sets global user marshaler +func SetSAMLConnectorMarshaler(m SAMLConnectorMarshaler) { + marshalerMutex.Lock() + defer marshalerMutex.Unlock() + samlConnectorMarshaler = m +} + +// GetSAMLConnectorMarshaler returns currently set user marshaler +func GetSAMLConnectorMarshaler() SAMLConnectorMarshaler { + marshalerMutex.RLock() + defer marshalerMutex.RUnlock() + return samlConnectorMarshaler +} + +// SAMLConnectorMarshaler implements marshal/unmarshal of User implementations +// mostly adds support for extended versions +type SAMLConnectorMarshaler interface { + // UnmarshalSAMLConnector unmarshals connector from binary representation + UnmarshalSAMLConnector(bytes []byte) (SAMLConnector, error) + // MarshalSAMLConnector marshals connector to binary representation + MarshalSAMLConnector(c SAMLConnector, opts ...MarshalOption) ([]byte, error) +} + +// GetSAMLConnectorSchema returns schema for SAMLConnector +func GetSAMLConnectorSchema() string { + return fmt.Sprintf(SAMLConnectorV2SchemaTemplate, MetadataSchema, SAMLConnectorSpecV2Schema) +} + +type TeleportSAMLConnectorMarshaler struct{} + +// UnmarshalSAMLConnector unmarshals connector from +func (*TeleportSAMLConnectorMarshaler) UnmarshalSAMLConnector(bytes []byte) (SAMLConnector, error) { + var h ResourceHeader + err := json.Unmarshal(bytes, &h) + if err != nil { + return nil, trace.Wrap(err) + } + switch h.Version { + case V2: + var c SAMLConnectorV2 + if err := utils.UnmarshalWithSchema(GetSAMLConnectorSchema(), &c, bytes); err != nil { + return nil, trace.BadParameter(err.Error()) + } + return &c, nil + } + + return nil, trace.BadParameter("SAML connector resource version %v is not supported", h.Version) +} + +// MarshalUser marshals SAML connector into JSON +func (*TeleportSAMLConnectorMarshaler) MarshalSAMLConnector(c SAMLConnector, opts ...MarshalOption) ([]byte, error) { + cfg, err := collectOptions(opts) + if err != nil { + return nil, trace.Wrap(err) + } + type connv2 interface { + V2() *SAMLConnectorV2 + } + version := cfg.GetVersion() + switch version { + case V2: + v, ok := c.(connv2) + if !ok { + return nil, trace.BadParameter("don't know how to marshal %v", V2) + } + return json.Marshal(v.V2()) + default: + return nil, trace.BadParameter("version %v is not supported", version) + } +} + +// SAMLConnectorV2 is version 1 resource spec for SAML connector +type SAMLConnectorV2 struct { + // Kind is a resource kind + Kind string `json:"kind"` + // Version is version + Version string `json:"version"` + // Metadata is connector metadata + Metadata Metadata `json:"metadata"` + // Spec contains connector specification + Spec SAMLConnectorSpecV2 `json:"spec"` +} + +// GetServiceProviderIssuer returns service provider issuer +func (o *SAMLConnectorV2) GetServiceProviderIssuer() string { + return o.Spec.ServiceProviderIssuer +} + +// SetServiceProviderIssuer sets service provider issuer +func (o *SAMLConnectorV2) SetServiceProviderIssuer(v string) { + o.Spec.ServiceProviderIssuer = v +} + +// GetAudience returns audience +func (o *SAMLConnectorV2) GetAudience() string { + return o.Spec.Audience +} + +// SetAudience sets audience +func (o *SAMLConnectorV2) SetAudience(v string) { + o.Spec.Audience = v +} + +// GetCert returns identity provider checking x509 certificate +func (o *SAMLConnectorV2) GetCert() string { + return o.Spec.Cert +} + +// SetCert sets identity provider checking certificate +func (o *SAMLConnectorV2) SetCert(cert string) { + o.Spec.Cert = cert +} + +// GetSSO returns SSO service +func (o *SAMLConnectorV2) GetSSO() string { + return o.Spec.SSO +} + +// SetSSO sets SSO service +func (o *SAMLConnectorV2) SetSSO(sso string) { + o.Spec.SSO = sso +} + +// GetEntityDescriptor returns XML entity descriptor of the service +func (o *SAMLConnectorV2) GetEntityDescriptor() string { + return o.Spec.EntityDescriptor +} + +// SetEntityDescriptor sets entity descritor of the service +func (o *SAMLConnectorV2) SetEntityDescriptor(v string) { + o.Spec.EntityDescriptor = v +} + +// Equals returns true if the connectors are identical +func (o *SAMLConnectorV2) Equals(other SAMLConnector) bool { + if o.GetName() != other.GetName() { + return false + } + if o.GetCert() != other.GetCert() { + return false + } + if o.GetAudience() != other.GetAudience() { + return false + } + if o.GetEntityDescriptor() != other.GetEntityDescriptor() { + return false + } + if o.Expiry() != other.Expiry() { + return false + } + if o.GetIssuer() != other.GetIssuer() { + return false + } + if (o.GetSigningKeyPair() == nil && other.GetSigningKeyPair() != nil) || (o.GetSigningKeyPair() != nil && other.GetSigningKeyPair() == nil) { + return false + } + if o.GetSigningKeyPair() != nil { + a, b := o.GetSigningKeyPair(), other.GetSigningKeyPair() + if a.Cert != b.Cert || a.PrivateKey != b.PrivateKey { + return false + } + } + mappings := o.GetAttributesToRoles() + otherMappings := other.GetAttributesToRoles() + if len(mappings) != len(otherMappings) { + return false + } + for i := range mappings { + a, b := mappings[i], otherMappings[i] + if a.Name != b.Name || a.Value != b.Value || !utils.StringSlicesEqual(a.Roles, b.Roles) { + return false + } + if (a.RoleTemplate != nil && b.RoleTemplate == nil) || (a.RoleTemplate == nil && b.RoleTemplate != nil) { + return false + } + if a.RoleTemplate != nil && !a.RoleTemplate.Equals(b.RoleTemplate) { + return false + } + } + if o.GetSSO() != other.GetSSO() { + return false + } + return true +} + +// V2 returns V2 version of the resource +func (o *SAMLConnectorV2) V2() *SAMLConnectorV2 { + return o +} + +// SetDisplay sets friendly name for this provider. +func (o *SAMLConnectorV2) SetDisplay(display string) { + o.Spec.Display = display +} + +// GetMetadata returns object metadata +func (o *SAMLConnectorV2) GetMetadata() Metadata { + return o.Metadata +} + +// SetExpiry sets expiry time for the object +func (o *SAMLConnectorV2) SetExpiry(expires time.Time) { + o.Metadata.SetExpiry(expires) +} + +// Expires retuns object expiry setting +func (o *SAMLConnectorV2) Expiry() time.Time { + return o.Metadata.Expiry() +} + +// SetTTL sets Expires header using realtime clock +func (o *SAMLConnectorV2) SetTTL(clock clockwork.Clock, ttl time.Duration) { + o.Metadata.SetTTL(clock, ttl) +} + +// GetName returns the name of the connector +func (o *SAMLConnectorV2) GetName() string { + return o.Metadata.GetName() +} + +// SetName sets client secret to some value +func (o *SAMLConnectorV2) SetName(name string) { + o.Metadata.SetName(name) +} + +// SetIssuer sets issuer +func (o *SAMLConnectorV2) SetIssuer(issuer string) { + o.Spec.Issuer = issuer +} + +// GetIssuer returns issuer +func (o *SAMLConnectorV2) GetIssuer() string { + return o.Spec.Issuer +} + +// Display - Friendly name for this provider. +func (o *SAMLConnectorV2) GetDisplay() string { + if o.Spec.Display != "" { + return o.Spec.Display + } + return o.GetName() +} + +// GetAttributesToRoles returns attributes to roles mapping +func (o *SAMLConnectorV2) GetAttributesToRoles() []AttributeMapping { + return o.Spec.AttributesToRoles +} + +// SetAttributesToRoles sets attributes to roles mapping +func (o *SAMLConnectorV2) SetAttributesToRoles(mapping []AttributeMapping) { + o.Spec.AttributesToRoles = mapping +} + +// GetAttributes returns list of attributes expected by mappings +func (o *SAMLConnectorV2) GetAttributes() []string { + var out []string + for _, mapping := range o.Spec.AttributesToRoles { + out = append(out, mapping.Name) + } + return utils.Deduplicate(out) +} + +// MapClaims maps claims to roles +func (o *SAMLConnectorV2) MapAttributes(assertionInfo saml2.AssertionInfo) []string { + var roles []string + for _, mapping := range o.Spec.AttributesToRoles { + for _, attr := range assertionInfo.Values { + if attr.Name != mapping.Name { + continue + } + for _, value := range attr.Values { + if value.Value == mapping.Value { + roles = append(roles, mapping.Roles...) + } + } + } + } + return utils.Deduplicate(roles) +} + +func executeSAMLStringTemplate(raw string, assertionInfo saml2.AssertionInfo) (string, error) { + tmpl, err := template.New("dynamic-roles").Parse(raw) + if err != nil { + return "", trace.Wrap(err) + } + var buf bytes.Buffer + err = tmpl.Execute(&buf, assertionInfo) + if err != nil { + return "", trace.Wrap(err) + } + + return buf.String(), nil +} + +func executeSAMLSliceTemplate(raw []string, assertionInfo saml2.AssertionInfo) ([]string, error) { + var sl []string + + for _, v := range raw { + tmpl, err := template.New("dynamic-roles").Parse(v) + if err != nil { + return nil, trace.Wrap(err) + } + var buf bytes.Buffer + err = tmpl.Execute(&buf, assertionInfo) + if err != nil { + return nil, trace.Wrap(err) + } + + sl = append(sl, buf.String()) + } + + return sl, nil +} + +// RoleFromTemplate creates a role from a template and claims. +func (o *SAMLConnectorV2) RoleFromTemplate(assertionInfo saml2.AssertionInfo) (Role, error) { + for _, mapping := range o.Spec.AttributesToRoles { + for _, attr := range assertionInfo.Values { + // attribute name doesn't match + if attr.Name != mapping.Value { + continue + } + + // attribute value doesn't match + var matched bool + for _, val := range attr.Values { + if val.Value == mapping.Value { + matched = true + break + } + } + if !matched { + continue + } + + // claim name and value match, if a role template exists, execute template + roleTemplate := mapping.RoleTemplate + if roleTemplate != nil { + // at the moment, only allow templating for role name and logins + executedName, err := executeSAMLStringTemplate(roleTemplate.GetName(), assertionInfo) + if err != nil { + return nil, trace.Wrap(err) + } + executedLogins, err := executeSAMLSliceTemplate(roleTemplate.GetLogins(), assertionInfo) + if err != nil { + return nil, trace.Wrap(err) + } + + roleTemplate.SetName(executedName) + roleTemplate.SetLogins(executedLogins) + + // check all fields and make sure we have have a valid role + err = roleTemplate.CheckAndSetDefaults() + if err != nil { + return nil, trace.Wrap(err) + } + + return roleTemplate, nil + } + } + } + + return nil, trace.BadParameter("unable to create role from template") +} + +// GetServiceProvider initialises service provider spec from settings +func (o *SAMLConnectorV2) GetServiceProvider() (*saml2.SAMLServiceProvider, error) { + if o.Metadata.Name == "" { + return nil, trace.BadParameter("ID: missing connector name, name your connector to refer to internally e.g. okta1") + } + if o.Spec.AssertionConsumerService == "" { + return nil, trace.BadParameter("missing acs - assertion consumer service parameter, set service URL that will receive POST requests from SAML") + } + if o.Spec.ServiceProviderIssuer == "" { + o.Spec.ServiceProviderIssuer = o.Spec.AssertionConsumerService + } + if o.Spec.Audience == "" { + o.Spec.Audience = o.Spec.AssertionConsumerService + } + certStore := dsig.MemoryX509CertificateStore{ + Roots: []*x509.Certificate{}, + } + if o.Spec.EntityDescriptor != "" { + metadata := &types.EntityDescriptor{} + err := xml.Unmarshal([]byte(o.Spec.EntityDescriptor), metadata) + if err != nil { + return nil, trace.Wrap(err, "failed to parse entity_descriptor") + } + + for _, kd := range metadata.IDPSSODescriptor.KeyDescriptors { + certData, err := base64.StdEncoding.DecodeString(kd.KeyInfo.X509Data.X509Certificate.Data) + if err != nil { + return nil, trace.Wrap(err) + } + cert, err := x509.ParseCertificate(certData) + if err != nil { + return nil, trace.Wrap(err, "failed to parse certificate in metadata") + } + certStore.Roots = append(certStore.Roots, cert) + } + o.Spec.Issuer = metadata.EntityID + o.Spec.SSO = metadata.IDPSSODescriptor.SingleSignOnService.Location + } + if o.Spec.Issuer == "" { + return nil, trace.BadParameter("no issuer or entityID set, either set issuer as a paramter or via entity_descriptor spec") + } + if o.Spec.SSO == "" { + return nil, trace.BadParameter("no SSO set either explicitly or via entity_descriptor spec") + } + if o.Spec.Cert != "" { + cert, err := utils.ParseCertificatePEM([]byte(o.Spec.Cert)) + if err != nil { + return nil, trace.Wrap(err) + } + certStore.Roots = append(certStore.Roots, cert) + } + if len(certStore.Roots) == 0 { + return nil, trace.BadParameter( + "no identity provider certificate provided, either set certificate as a parameter or via entity_descriptor") + } + if o.Spec.SigningKeyPair == nil { + keyPEM, certPEM, err := utils.GenerateSelfSignedSigningCert(pkix.Name{ + Organization: []string{"Teleport OSS"}, + CommonName: "teleport.localhost.localdomain", + }, nil, 10*365*24*time.Hour) + if err != nil { + return nil, trace.Wrap(err) + } + o.Spec.SigningKeyPair = &SigningKeyPair{ + PrivateKey: string(keyPEM), + Cert: string(certPEM), + } + } + keyStore, err := utils.ParseSigningKeyStorePEM(o.Spec.SigningKeyPair.PrivateKey, o.Spec.SigningKeyPair.Cert) + if err != nil { + return nil, trace.Wrap(err) + } + // make sure claim mappings have either roles or a role template + for _, v := range o.Spec.AttributesToRoles { + hasRoles := false + if len(v.Roles) > 0 { + hasRoles = true + } + hasRoleTemplate := false + if v.RoleTemplate != nil { + hasRoleTemplate = true + } + + // we either need to have roles or role templates not both or neither + // ! ( hasRoles XOR hasRoleTemplate ) + if hasRoles == hasRoleTemplate { + return nil, trace.BadParameter("need roles or role template (not both or none)") + } + } + log.Debugf("SSO: %v\nIssuer:%v,acs:%v", o.Spec.SSO, o.Spec.Issuer, o.Spec.AssertionConsumerService) + sp := &saml2.SAMLServiceProvider{ + IdentityProviderSSOURL: o.Spec.SSO, + IdentityProviderIssuer: o.Spec.Issuer, + ServiceProviderIssuer: o.Spec.ServiceProviderIssuer, + AssertionConsumerServiceURL: o.Spec.AssertionConsumerService, + SignAuthnRequests: true, + AudienceURI: o.Spec.Audience, + IDPCertificateStore: &certStore, + SPKeyStore: keyStore, + } + return sp, nil +} + +// GetSigningKeyPair returns signing key pair +func (o *SAMLConnectorV2) GetSigningKeyPair() *SigningKeyPair { + return o.Spec.SigningKeyPair +} + +// GetSigningKeyPair sets signing key pair +func (o *SAMLConnectorV2) SetSigningKeyPair(k *SigningKeyPair) { + o.Spec.SigningKeyPair = k +} + +func (o *SAMLConnectorV2) CheckAndSetDefaults() error { + _, err := o.GetServiceProvider() + return err +} + +// SAMLConnectorV2SchemaTemplate is a template JSON Schema for user +const SAMLConnectorV2SchemaTemplate = `{ + "type": "object", + "additionalProperties": false, + "required": ["kind", "spec", "metadata", "version"], + "properties": { + "kind": {"type": "string"}, + "version": {"type": "string", "default": "v1"}, + "metadata": %v, + "spec": %v + } +}` + +// SAMLConnectorSpecV2 specifies configuration for Open ID Connect compatible external +// identity provider, e.g. google in some organisation +type SAMLConnectorSpecV2 struct { + // Issuer is identity provider issuer + Issuer string `json:"issuer"` + // SSO is URL of the identity provider SSO service + SSO string `json:"sso"` + // Cert is identity provider certificate PEM + // IDP signs responses using this certificate + Cert string `json:"cert"` + // Display controls how this connector is displayed + Display string `json:"display"` + // AssertionConsumerService is a URL for assertion consumer service + // on the service provider (Teleport's side) + AssertionConsumerService string `json:"acs"` + // Audience uniquely identifies our service provider + Audience string `json:"audience"` + // SertviceProviderIssuer is the issuer of the service provider (Teleport) + ServiceProviderIssuer string `json:"service_provider_issuer"` + // EntityDescriptor is XML with descriptor, can be used to supply configuration + // parameters in one XML files vs supplying them in the individual elelemtns + EntityDescriptor string `json:"entity_descriptor"` + // AttriburesToRoles is a list of mappings of attribute statements to roles + AttributesToRoles []AttributeMapping `json:"attributes_to_roles"` + // SigningKeyPair is x509 key pair used to sign AuthnRequest + SigningKeyPair *SigningKeyPair `json:"signing_key_pair,omitempty"` +} + +// SAMLConnectorSpecV2Schema is a JSON Schema for SAML Connector +var SAMLConnectorSpecV2Schema = fmt.Sprintf(`{ + "type": "object", + "additionalProperties": false, + "required": ["acs"], + "properties": { + "issuer": {"type": "string"}, + "sso": {"type": "string"}, + "cert": {"type": "string"}, + "display": {"type": "string"}, + "acs": {"type": "string"}, + "audience": {"type": "string"}, + "service_provider_issuer": {"type": "string"}, + "entity_descriptor": {"type": "string"}, + "attributes_to_roles": { + "type": "array", + "items": %v + }, + "signing_key_pair": %v + } +}`, AttributeMappingSchema, SigningKeyPairSchema) + +// GetAttributeNames returns a list of claim names from the claim values +func GetAttributeNames(attributes map[string]types.Attribute) []string { + var out []string + for _, attr := range attributes { + out = append(out, attr.Name) + } + return out +} + +// AttributeMapping is SAML Attribute statement mapping +// from SAML attribute statements to roles +type AttributeMapping struct { + // Name is attribute statement name + Name string `json:"name"` + // Value is attribute statement value to match + Value string `json:"value"` + // Roles is a list of teleport roles to match + Roles []string `json:"roles,omitempty"` + // RoleTemplate is a template for a role that will be filled + // with data from claims. + RoleTemplate *RoleV2 `json:"role_template,omitempty"` +} + +// AttribueMappingSchema is JSON schema for claim mapping +var AttributeMappingSchema = fmt.Sprintf(`{ + "type": "object", + "additionalProperties": false, + "required": ["name", "value" ], + "properties": { + "name": {"type": "string"}, + "value": {"type": "string"}, + "roles": { + "type": "array", + "items": { + "type": "string" + } + }, + "role_template": %v + } +}`, GetRoleSchema("")) + +// SigningKeyPairSchema +var SigningKeyPairSchema = `{ + "type": "object", + "additionalProperties": false, + "properties": { + "private_key": {"type": "string"}, + "cert": {"type": "string"} + } +}` + +// SigningKeyPair is a key pair used to sign SAML AuthnRequest +type SigningKeyPair struct { + // PrivateKey is PEM encoded x509 private key + PrivateKey string `json:"private_key"` + // Cert is certificate in OpenSSH authorized keys format + Cert string `json:"cert"` +} diff --git a/lib/services/user.go b/lib/services/user.go index c3b740c4dd099..aca6289010c9a 100644 --- a/lib/services/user.go +++ b/lib/services/user.go @@ -17,8 +17,10 @@ import ( type User interface { // Resource provides common resource properties Resource - // GetIdentities returns a list of connected OIDCIdentities - GetIdentities() []OIDCIdentity + // GetOIDCIdentities returns a list of connected OIDCIdentities + GetOIDCIdentities() []ExternalIdentity + // GetSAMLIdentities returns a list of connected OIDCIdentities + GetSAMLIdentities() []ExternalIdentity // GetRoles returns a list of roles assigned to user GetRoles() []string // String returns user @@ -215,7 +217,11 @@ func (u *UserV2) WebSessionInfo(allowedLogins []string) interface{} { type UserSpecV2 struct { // OIDCIdentities lists associated OpenID Connect identities // that let user log in using externally verified identity - OIDCIdentities []OIDCIdentity `json:"oidc_identities,omitempty"` + OIDCIdentities []ExternalIdentity `json:"oidc_identities,omitempty"` + + // SAMLIdentities lists associated SAML identities + // that let user log in using externally verified identity + SAMLIdentities []ExternalIdentity `json:"saml_identities,omitempty"` // Roles is a list of roles assigned to user Roles []string `json:"roles,omitempty"` @@ -262,6 +268,10 @@ const UserSpecV2SchemaTemplate = `{ "type": "array", "items": %v }, + "saml_identities": { + "type": "array", + "items": %v + }, "status": %v, "created_by": %v%v } @@ -287,7 +297,7 @@ func (u *UserV2) Equals(other User) bool { if u.Metadata.Name != other.GetName() { return false } - otherIdentities := other.GetIdentities() + otherIdentities := other.GetOIDCIdentities() if len(u.Spec.OIDCIdentities) != len(otherIdentities) { return false } @@ -296,6 +306,15 @@ func (u *UserV2) Equals(other User) bool { return false } } + otherSAMLIdentities := other.GetSAMLIdentities() + if len(u.Spec.SAMLIdentities) != len(otherSAMLIdentities) { + return false + } + for i := range u.Spec.SAMLIdentities { + if !u.Spec.SAMLIdentities[i].Equals(&otherSAMLIdentities[i]) { + return false + } + } return true } @@ -317,11 +336,16 @@ func (u *UserV2) GetStatus() LoginStatus { return u.Spec.Status } -// GetIdentities returns a list of connected OIDCIdentities -func (u *UserV2) GetIdentities() []OIDCIdentity { +// GetOIDCIdentities returns a list of connected OIDCIdentities +func (u *UserV2) GetOIDCIdentities() []ExternalIdentity { return u.Spec.OIDCIdentities } +// GetSAMLIdentities returns a list of connected SAMLIdentities +func (u *UserV2) GetSAMLIdentities() []ExternalIdentity { + return u.Spec.SAMLIdentities +} + // GetRoles returns a list of roles assigned to user func (u *UserV2) GetRoles() []string { return u.Spec.Roles @@ -377,7 +401,7 @@ type UserV1 struct { // OIDCIdentities lists associated OpenID Connect identities // that let user log in using externally verified identity - OIDCIdentities []OIDCIdentity `json:"oidc_identities"` + OIDCIdentities []ExternalIdentity `json:"oidc_identities"` // Status is a login status of the user Status LoginStatus `json:"status"` @@ -464,9 +488,9 @@ type UserMarshaler interface { func GetUserSchema(extensionSchema string) string { var userSchema string if extensionSchema == "" { - userSchema = fmt.Sprintf(UserSpecV2SchemaTemplate, OIDCIDentitySchema, LoginStatusSchema, CreatedBySchema, ``) + userSchema = fmt.Sprintf(UserSpecV2SchemaTemplate, ExternalIdentitySchema, ExternalIdentitySchema, LoginStatusSchema, CreatedBySchema, ``) } else { - userSchema = fmt.Sprintf(UserSpecV2SchemaTemplate, OIDCIDentitySchema, LoginStatusSchema, CreatedBySchema, ", "+extensionSchema) + userSchema = fmt.Sprintf(UserSpecV2SchemaTemplate, ExternalIdentitySchema, ExternalIdentitySchema, LoginStatusSchema, CreatedBySchema, ", "+extensionSchema) } return fmt.Sprintf(V2SchemaTemplate, MetadataSchema, userSchema) } diff --git a/lib/utils/certs.go b/lib/utils/certs.go new file mode 100644 index 0000000000000..0070368355c6c --- /dev/null +++ b/lib/utils/certs.go @@ -0,0 +1,157 @@ +/* +Copyright 2016 SPIFFE Authors +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 utils + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "time" + + log "github.com/Sirupsen/logrus" + "github.com/gravitational/trace" +) + +// ParseSigningKeyStore parses signing key store from PEM encoded key pair +func ParseSigningKeyStorePEM(keyPEM, certPEM string) (*SigningKeyStore, error) { + _, err := ParseCertificatePEM([]byte(certPEM)) + if err != nil { + return nil, trace.Wrap(err) + } + key, err := ParsePrivateKeyPEM([]byte(keyPEM)) + if err != nil { + return nil, trace.Wrap(err) + } + rsaKey, ok := key.(*rsa.PrivateKey) + if !ok { + return nil, trace.BadParameter("key of type %T is not supported, only RSA keys are supported for signatures") + } + certASN, _ := pem.Decode([]byte(certPEM)) + if certASN == nil { + return nil, trace.BadParameter("expected PEM-encoded block") + } + return &SigningKeyStore{privateKey: rsaKey, cert: certASN.Bytes}, nil +} + +// SigningKeyStore is used to sign using X509 digital signatures +type SigningKeyStore struct { + privateKey *rsa.PrivateKey + cert []byte +} + +func (ks *SigningKeyStore) GetKeyPair() (*rsa.PrivateKey, []byte, error) { + return ks.privateKey, ks.cert, nil +} + +// GenerateSelfSignedSigningCert generates self-signed certificate used for digital signatures +func GenerateSelfSignedSigningCert(entity pkix.Name, dnsNames []string, ttl time.Duration) ([]byte, []byte, error) { + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, trace.Wrap(err) + } + // to account for clock skew + notBefore := time.Now().Add(-2 * time.Minute) + notAfter := notBefore.Add(ttl) + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, nil, trace.Wrap(err) + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Issuer: entity, + Subject: entity, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageDigitalSignature, + BasicConstraintsValid: true, + DNSNames: dnsNames, + } + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + if err != nil { + return nil, nil, trace.Wrap(err) + } + + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}) + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + + return keyPEM, certPEM, nil +} + +// ParseCertificateRequestPEM parses PEM-encoded certificate signing request +func ParseCertificateRequestPEM(bytes []byte) (*x509.CertificateRequest, error) { + block, _ := pem.Decode(bytes) + if block == nil { + return nil, trace.BadParameter("expected PEM-encoded block") + } + csr, err := x509.ParseCertificateRequest(block.Bytes) + if err != nil { + return nil, trace.BadParameter(err.Error()) + } + return csr, nil +} + +// ParseCertificatePEM parses PEM-encoded certificate +func ParseCertificatePEM(bytes []byte) (*x509.Certificate, error) { + block, _ := pem.Decode(bytes) + if block == nil { + return nil, trace.BadParameter("expected PEM-encoded block") + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, trace.BadParameter(err.Error()) + } + return cert, nil +} + +// ParsePrivateKeyPEM parses PEM-encoded private key +func ParsePrivateKeyPEM(bytes []byte) (crypto.Signer, error) { + block, _ := pem.Decode(bytes) + if block == nil { + return nil, trace.BadParameter("expected PEM-encoded block") + } + return ParsePrivateKeyDER(block.Bytes) +} + +// ParsePrivateKeyDER parses unencrypted DER-encoded private key +func ParsePrivateKeyDER(der []byte) (crypto.Signer, error) { + generalKey, err := x509.ParsePKCS8PrivateKey(der) + if err != nil { + generalKey, err = x509.ParsePKCS1PrivateKey(der) + if err != nil { + generalKey, err = x509.ParseECPrivateKey(der) + if err != nil { + log.Errorf("failed to parse key: %v", err) + return nil, trace.BadParameter("failed parsing private key") + } + } + } + + switch generalKey.(type) { + case *rsa.PrivateKey: + return generalKey.(*rsa.PrivateKey), nil + case *ecdsa.PrivateKey: + return generalKey.(*ecdsa.PrivateKey), nil + } + + return nil, trace.BadParameter("unsupported private key type") +} diff --git a/lib/utils/equals.go b/lib/utils/equals.go new file mode 100644 index 0000000000000..80a90b355858a --- /dev/null +++ b/lib/utils/equals.go @@ -0,0 +1,56 @@ +/* +Copyright 2017 Gravitational, Inc. + +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 utils + +// StringSlicesEqual returns true if string slices equal +func StringSlicesEqual(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +// StringMapsEqual returns true if two strings maps are equal +func StringMapsEqual(a, b map[string]string) bool { + if len(a) != len(b) { + return false + } + for key := range a { + if a[key] != b[key] { + return false + } + } + return true +} + +// StringMapSlicesEqual returns true if two maps of string slices are equal +func StringMapSlicesEqual(a, b map[string][]string) bool { + if len(a) != len(b) { + return false + } + for key := range a { + if !StringSlicesEqual(a[key], b[key]) { + return false + } + } + return true +} diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index 99552c6fa8948..959cbd169de6f 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -193,6 +193,11 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*RewritingHandler, error) { h.POST("/webapi/oidc/login/console", httplib.MakeHandler(h.oidcLoginConsole)) h.GET("/webapi/oidc/callback", httplib.MakeHandler(h.oidcCallback)) + // SAML 2.0 handlers + h.POST("/webapi/saml/acs", httplib.MakeHandler(h.samlACS)) + h.GET("/webapi/saml/sso", httplib.MakeHandler(h.samlSSO)) + h.POST("/webapi/saml/login/console", httplib.MakeHandler(h.samlSSOConsole)) + // U2F related APIs h.GET("/webapi/u2f/signuptokens/:token", httplib.MakeHandler(h.u2fRegisterRequest)) h.POST("/webapi/u2f/users", httplib.MakeHandler(h.createNewU2FUser)) @@ -383,6 +388,30 @@ func buildOIDCConnectorSettings(authClient auth.ClientI) *client.OIDCSettings { } } +func buildSAMLConnectorSettings(authClient auth.ClientI) *client.SAMLSettings { + samlConnectors, err := authClient.GetSAMLConnectors(false) + if err != nil { + // if we have nothing set on the backend, return we have nothing + if trace.IsNotFound(err) { + return nil + } + + log.Debugf("Unable to get SAML Connectors: %v", err) + return nil + } + + if len(samlConnectors) < 1 { + log.Debugf("No SAML Connectors found") + return nil + } + + // always use the first one as only allow a single oidc connector now + return &client.SAMLSettings{ + Name: samlConnectors[0].GetName(), + Display: samlConnectors[0].GetDisplay(), + } +} + func buildAuthenticationSettings(authClient auth.ClientI) (*client.AuthenticationSettings, error) { as := &client.AuthenticationSettings{} @@ -401,6 +430,9 @@ func buildAuthenticationSettings(authClient auth.ClientI) (*client.Authenticatio if cap.GetType() == teleport.OIDC { as.OIDC = buildOIDCConnectorSettings(authClient) } + if cap.GetType() == teleport.SAML { + as.SAML = buildSAMLConnectorSettings(authClient) + } return as, nil } @@ -473,8 +505,8 @@ func (m *Handler) oidcLoginWeb(w http.ResponseWriter, r *http.Request, p httprou } func (m *Handler) oidcLoginConsole(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) { - log.Infof("oidcLoginConsole start") - var req *client.OIDCLoginConsoleReq + log.Debugf("oidcLoginConsole start") + var req *client.SSOLoginConsoleReq if err := httplib.ReadJSON(r, &req); err != nil { return nil, trace.Wrap(err) } @@ -498,15 +530,15 @@ func (m *Handler) oidcLoginConsole(w http.ResponseWriter, r *http.Request, p htt if err != nil { return nil, trace.Wrap(err) } - return &client.OIDCLoginConsoleResponse{RedirectURL: response.RedirectURL}, nil + return &client.SSOLoginConsoleResponse{RedirectURL: response.RedirectURL}, nil } func (m *Handler) oidcCallback(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) { - log.Infof("oidcCallback start") + log.Debugf("oidcCallback start") response, err := m.cfg.ProxyClient.ValidateOIDCAuthCallback(r.URL.Query()) if err != nil { - log.Infof("[OIDC] Error while processing callback: %v", err) + log.Warningf("[OIDC] Error while processing callback: %v", err) // redirect to an error page pathToError := url.URL{ @@ -529,7 +561,14 @@ func (m *Handler) oidcCallback(w http.ResponseWriter, r *http.Request, p httprou if len(response.Req.PublicKey) == 0 { return nil, trace.BadParameter("not a web or console oidc login request") } - redirectURL, err := ConstructSSHResponse(response) + redirectURL, err := ConstructSSHResponse(AuthParams{ + ClientRedirectURL: response.Req.ClientRedirectURL, + Username: response.Username, + Identity: response.Identity, + Session: response.Session, + Cert: response.Cert, + HostSigners: response.HostSigners, + }) if err != nil { return nil, trace.Wrap(err) } @@ -537,10 +576,28 @@ func (m *Handler) oidcCallback(w http.ResponseWriter, r *http.Request, p httprou return nil, nil } +// AuthParams are used to construct redirect URL containing auth +// information back to tsh login +type AuthParams struct { + // Username is authenticated teleport username + Username string + // Identity contains validated OIDC identity + Identity services.ExternalIdentity + // Web session will be generated by auth server if requested in OIDCAuthRequest + Session services.WebSession + // Cert will be generated by certificate authority + Cert []byte + // HostSigners is a list of signing host public keys + // trusted by proxy, used in console login + HostSigners []services.CertAuthority + // ClientRedirectURL is a URL to redirect client to + ClientRedirectURL string +} + // ConstructSSHResponse creates a special SSH response for SSH login method // that encodes everything using the client's secret key -func ConstructSSHResponse(response *auth.OIDCAuthResponse) (*url.URL, error) { - u, err := url.Parse(response.Req.ClientRedirectURL) +func ConstructSSHResponse(response AuthParams) (*url.URL, error) { + u, err := url.Parse(response.ClientRedirectURL) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/web/saml.go b/lib/web/saml.go new file mode 100644 index 0000000000000..354ecaf19da44 --- /dev/null +++ b/lib/web/saml.go @@ -0,0 +1,118 @@ +package web + +import ( + "net/http" + "net/url" + + "github.com/gravitational/teleport/lib/client" + "github.com/gravitational/teleport/lib/httplib" + "github.com/gravitational/teleport/lib/services" + + log "github.com/Sirupsen/logrus" + "github.com/gravitational/form" + "github.com/gravitational/trace" + "github.com/julienschmidt/httprouter" +) + +func (m *Handler) samlSSO(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) { + log.Debugf("samlSSO start") + + query := r.URL.Query() + clientRedirectURL := query.Get("redirect_url") + if clientRedirectURL == "" { + return nil, trace.BadParameter("missing redirect_url query parameter") + } + connectorID := query.Get("connector_id") + if connectorID == "" { + return nil, trace.BadParameter("missing connector_id query parameter") + } + response, err := m.cfg.ProxyClient.CreateSAMLAuthRequest( + services.SAMLAuthRequest{ + ConnectorID: connectorID, + CreateWebSession: true, + ClientRedirectURL: clientRedirectURL, + }) + if err != nil { + return nil, trace.Wrap(err) + } + log.Debugf("samlSSO redirect: %v", response.RedirectURL) + http.Redirect(w, r, response.RedirectURL, http.StatusFound) + return nil, nil +} + +func (m *Handler) samlSSOConsole(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) { + log.Debugf("samlSSOConsole start") + var req *client.SSOLoginConsoleReq + if err := httplib.ReadJSON(r, &req); err != nil { + return nil, trace.Wrap(err) + } + if req.RedirectURL == "" { + return nil, trace.BadParameter("missing RedirectURL") + } + if len(req.PublicKey) == 0 { + return nil, trace.BadParameter("missing PublicKey") + } + if req.ConnectorID == "" { + return nil, trace.BadParameter("missing ConnectorID") + } + response, err := m.cfg.ProxyClient.CreateSAMLAuthRequest( + services.SAMLAuthRequest{ + ConnectorID: req.ConnectorID, + ClientRedirectURL: req.RedirectURL, + PublicKey: req.PublicKey, + CertTTL: req.CertTTL, + }) + if err != nil { + return nil, trace.Wrap(err) + } + return &client.SSOLoginConsoleResponse{RedirectURL: response.RedirectURL}, nil +} + +func (m *Handler) samlACS(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) { + var samlResponse string + err := form.Parse(r, form.String("SAMLResponse", &samlResponse, form.Required())) + if err != nil { + return nil, trace.Wrap(err) + } + + l := log.WithFields(log.Fields{trace.Component: "SAML"}) + + response, err := m.cfg.ProxyClient.ValidateSAMLResponse(samlResponse) + if err != nil { + log.Warningf("error while processing callback: %v", err) + + // redirect to an error page + pathToError := url.URL{ + Path: "/web/msg/error/login_failed", + RawQuery: url.Values{"details": []string{"Unable to process callback from OIDC provider."}}.Encode(), + } + http.Redirect(w, r, pathToError.String(), http.StatusFound) + return nil, nil + } + // if we created web session, set session cookie and redirect to original url + if response.Req.CreateWebSession { + log.Debugf("redirecting to web browser") + if err := SetSession(w, response.Username, response.Session.GetName()); err != nil { + return nil, trace.Wrap(err) + } + http.Redirect(w, r, response.Req.ClientRedirectURL, http.StatusFound) + return nil, nil + } + l.Debugf("samlCallback redirecting to console login") + if len(response.Req.PublicKey) == 0 { + return nil, trace.BadParameter("not a web or console oidc login request") + } + redirectURL, err := ConstructSSHResponse(AuthParams{ + ClientRedirectURL: response.Req.ClientRedirectURL, + Username: response.Username, + Identity: response.Identity, + Session: response.Session, + Cert: response.Cert, + HostSigners: response.HostSigners, + }) + if err != nil { + return nil, trace.Wrap(err) + } + http.Redirect(w, r, redirectURL.String(), http.StatusFound) + return nil, nil +} diff --git a/tool/tctl/common/collection.go b/tool/tctl/common/collection.go index 40720695f08dd..64405a444e8f5 100644 --- a/tool/tctl/common/collection.go +++ b/tool/tctl/common/collection.go @@ -325,11 +325,11 @@ func (r *reverseTunnelCollection) writeYAML(w io.Writer) error { return trace.Wrap(err) } -type connectorCollection struct { +type oidcCollection struct { connectors []services.OIDCConnector } -func (c *connectorCollection) writeText(w io.Writer) error { +func (c *oidcCollection) writeText(w io.Writer) error { t := goterm.NewTable(0, 10, 5, ' ', 0) printHeader(t, []string{"Name", "Issuer URL", "Additional Scope"}) for _, conn := range c.connectors { @@ -339,7 +339,7 @@ func (c *connectorCollection) writeText(w io.Writer) error { return trace.Wrap(err) } -func (c *connectorCollection) writeJSON(w io.Writer) error { +func (c *oidcCollection) writeJSON(w io.Writer) error { data, err := json.MarshalIndent(c.toMarshal(), "", " ") if err != nil { return trace.Wrap(err) @@ -348,14 +348,53 @@ func (c *connectorCollection) writeJSON(w io.Writer) error { return trace.Wrap(err) } -func (c *connectorCollection) toMarshal() interface{} { +func (c *oidcCollection) toMarshal() interface{} { if len(c.connectors) == 1 { return c.connectors[0] } return c.connectors } -func (c *connectorCollection) writeYAML(w io.Writer) error { +func (c *oidcCollection) writeYAML(w io.Writer) error { + data, err := yaml.Marshal(c.toMarshal()) + if err != nil { + return trace.Wrap(err) + } + _, err = w.Write(data) + return trace.Wrap(err) +} + +type samlCollection struct { + connectors []services.SAMLConnector +} + +func (c *samlCollection) writeText(w io.Writer) error { + t := goterm.NewTable(0, 10, 5, ' ', 0) + printHeader(t, []string{"Name", "SSO URL"}) + for _, conn := range c.connectors { + fmt.Fprintf(t, "%v\t%v\n", conn.GetName(), conn.GetSSO()) + } + _, err := io.WriteString(w, t.String()) + return trace.Wrap(err) +} + +func (c *samlCollection) writeJSON(w io.Writer) error { + data, err := json.MarshalIndent(c.toMarshal(), "", " ") + if err != nil { + return trace.Wrap(err) + } + _, err = w.Write(data) + return trace.Wrap(err) +} + +func (c *samlCollection) toMarshal() interface{} { + if len(c.connectors) == 1 { + return c.connectors[0] + } + return c.connectors +} + +func (c *samlCollection) writeYAML(w io.Writer) error { data, err := yaml.Marshal(c.toMarshal()) if err != nil { return trace.Wrap(err) diff --git a/tool/tctl/common/tctl.go b/tool/tctl/common/tctl.go index 914ff82d1f376..8fd636286d381 100644 --- a/tool/tctl/common/tctl.go +++ b/tool/tctl/common/tctl.go @@ -957,6 +957,18 @@ func (u *CreateCommand) Create(client *auth.TunClient) error { } count += 1 switch raw.Kind { + case services.KindSAMLConnector: + conn, err := services.GetSAMLConnectorMarshaler().UnmarshalSAMLConnector(raw.Raw) + if err != nil { + return trace.Wrap(err) + } + if err := conn.CheckAndSetDefaults(); err != nil { + return trace.Wrap(err) + } + if err := client.UpsertSAMLConnector(conn); err != nil { + return trace.Wrap(err) + } + fmt.Printf("SAML connector %v upserted\n", conn.GetName()) case services.KindOIDCConnector: conn, err := services.GetOIDCConnectorMarshaler().UnmarshalOIDCConnector(raw.Raw) if err != nil { @@ -1065,6 +1077,11 @@ func (d *DeleteCommand) Delete(client *auth.TunClient) error { return trace.Wrap(err) } fmt.Printf("user %v has been deleted\n", d.ref.Name) + case services.KindSAMLConnector: + if err := client.DeleteSAMLConnector(d.ref.Name); err != nil { + return trace.Wrap(err) + } + fmt.Printf("SAML Connector %v has been deleted\n", d.ref.Name) case services.KindOIDCConnector: if err := client.DeleteOIDCConnector(d.ref.Name); err != nil { return trace.Wrap(err) @@ -1104,12 +1121,18 @@ func (g *GetCommand) getCollection(client auth.ClientI) (collection, error) { return nil, trace.BadParameter("specify resource to list, e.g. 'tctl get roles'") } switch g.ref.Kind { + case services.KindSAMLConnector: + connectors, err := client.GetSAMLConnectors(g.withSecrets) + if err != nil { + return nil, trace.Wrap(err) + } + return &samlCollection{connectors: connectors}, nil case services.KindOIDCConnector: connectors, err := client.GetOIDCConnectors(g.withSecrets) if err != nil { return nil, trace.Wrap(err) } - return &connectorCollection{connectors: connectors}, nil + return &oidcCollection{connectors: connectors}, nil case services.KindReverseTunnel: tunnels, err := client.GetReverseTunnels() if err != nil { diff --git a/vendor/github.com/beevik/etree/.travis.yml b/vendor/github.com/beevik/etree/.travis.yml new file mode 100644 index 0000000000000..7caa2477c6de0 --- /dev/null +++ b/vendor/github.com/beevik/etree/.travis.yml @@ -0,0 +1,16 @@ +language: go +sudo: false + +go: + - 1.4.2 + - 1.5.1 + - 1.6 + - tip + +matrix: + allow_failures: + - go: tip + +script: + - go vet ./... + - go test -v ./... diff --git a/vendor/github.com/beevik/etree/CONTRIBUTORS b/vendor/github.com/beevik/etree/CONTRIBUTORS new file mode 100644 index 0000000000000..084662c3a8352 --- /dev/null +++ b/vendor/github.com/beevik/etree/CONTRIBUTORS @@ -0,0 +1,8 @@ +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) diff --git a/vendor/github.com/beevik/etree/LICENSE b/vendor/github.com/beevik/etree/LICENSE new file mode 100644 index 0000000000000..e14ad682a0d30 --- /dev/null +++ b/vendor/github.com/beevik/etree/LICENSE @@ -0,0 +1,24 @@ +Copyright 2015 Brett Vickers. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER ``AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/beevik/etree/README.md b/vendor/github.com/beevik/etree/README.md new file mode 100644 index 0000000000000..28558433c80bd --- /dev/null +++ b/vendor/github.com/beevik/etree/README.md @@ -0,0 +1,203 @@ +[![Build Status](https://travis-ci.org/beevik/etree.svg?branch=master)](https://travis-ci.org/beevik/etree) +[![GoDoc](https://godoc.org/github.com/beevik/etree?status.svg)](https://godoc.org/github.com/beevik/etree) + +etree +===== + +The etree package is a lightweight, pure go package that expresses XML in +the form of an element tree. Its design was inspired by the Python +[ElementTree](http://docs.python.org/2/library/xml.etree.elementtree.html) +module. Some of the package's features include: + +* Represents XML documents as trees of elements for easy traversal. +* Imports, serializes, modifies or creates XML documents from scratch. +* Writes and reads XML to/from files, byte slices, strings and io interfaces. +* Performs simple or complex searches with lightweight XPath-like query APIs. +* Auto-indents XML using spaces or tabs for better readability. +* Implemented in pure go; depends only on standard go libraries. +* Built on top of the go [encoding/xml](http://golang.org/pkg/encoding/xml) + package. + +### Creating an XML document + +The following example creates an XML document from scratch using the etree +package and outputs its indented contents to stdout. +```go +doc := etree.NewDocument() +doc.CreateProcInst("xml", `version="1.0" encoding="UTF-8"`) +doc.CreateProcInst("xml-stylesheet", `type="text/xsl" href="style.xsl"`) + +people := doc.CreateElement("People") +people.CreateComment("These are all known people") + +jon := people.CreateElement("Person") +jon.CreateAttr("name", "Jon") + +sally := people.CreateElement("Person") +sally.CreateAttr("name", "Sally") + +doc.Indent(2) +doc.WriteTo(os.Stdout) +``` + +Output: +```xml + + + + + + + +``` + +### Reading an XML file + +Suppose you have a file on disk called `bookstore.xml` containing the +following data: + +```xml + + + + Everyday Italian + Giada De Laurentiis + 2005 + 30.00 + + + + Harry Potter + J K. Rowling + 2005 + 29.99 + + + + XQuery Kick Start + James McGovern + Per Bothner + Kurt Cagle + James Linn + Vaidyanathan Nagarajan + 2003 + 49.99 + + + + Learning XML + Erik T. Ray + 2003 + 39.95 + + + +``` + +This code reads the file's contents into an etree document. +```go +doc := etree.NewDocument() +if err := doc.ReadFromFile("bookstore.xml"); err != nil { + panic(err) +} +``` + +You can also read XML from a string, a byte slice, or an `io.Reader`. + +### Processing elements and attributes + +This example illustrates several ways to access elements and attributes using +etree selection queries. +```go +root := doc.SelectElement("bookstore") +fmt.Println("ROOT element:", root.Tag) + +for _, book := range root.SelectElements("book") { + fmt.Println("CHILD element:", book.Tag) + if title := book.SelectElement("title"); title != nil { + lang := title.SelectAttrValue("lang", "unknown") + fmt.Printf(" TITLE: %s (%s)\n", title.Text(), lang) + } + for _, attr := range book.Attr { + fmt.Printf(" ATTR: %s=%s\n", attr.Key, attr.Value) + } +} +``` +Output: +``` +ROOT element: bookstore +CHILD element: book + TITLE: Everyday Italian (en) + ATTR: category=COOKING +CHILD element: book + TITLE: Harry Potter (en) + ATTR: category=CHILDREN +CHILD element: book + TITLE: XQuery Kick Start (en) + ATTR: category=WEB +CHILD element: book + TITLE: Learning XML (en) + ATTR: category=WEB +``` + +### Path queries + +This example uses etree's path functions to select all book titles that fall +into the category of 'WEB'. The double-slash prefix in the path causes the +search for book elements to occur recursively; book elements may appear at any +level of the XML hierarchy. +```go +for _, t := range doc.FindElements("//book[@category='WEB']/title") { + fmt.Println("Title:", t.Text()) +} +``` + +Output: +``` +Title: XQuery Kick Start +Title: Learning XML +``` + +This example finds the first book element under the root bookstore element and +outputs the tag and text of each of its child elements. +```go +for _, e := range doc.FindElements("./bookstore/book[1]/*") { + fmt.Printf("%s: %s\n", e.Tag, e.Text()) +} +``` + +Output: +``` +title: Everyday Italian +author: Giada De Laurentiis +year: 2005 +price: 30.00 +``` + +This example finds all books with a price of 49.99 and outputs their titles. +```go +path := etree.MustCompilePath("./bookstore/book[p:price='49.99']/title") +for _, e := range doc.FindElementsPath(path) { + fmt.Println(e.Text()) +} +``` + +Output: +``` +XQuery Kick Start +``` + +Note that this example uses the FindElementsPath function, which takes as an +argument a pre-compiled path object. Use precompiled paths when you plan to +search with the same path more than once. + +### Other features + +These are just a few examples of the things the etree package can do. See the +[documentation](http://godoc.org/github.com/beevik/etree) for a complete +description of its capabilities. + +### Contributing + +This project accepts contributions. Just fork the repo and submit a pull +request! diff --git a/vendor/github.com/beevik/etree/etree.go b/vendor/github.com/beevik/etree/etree.go new file mode 100644 index 0000000000000..36b279f60039f --- /dev/null +++ b/vendor/github.com/beevik/etree/etree.go @@ -0,0 +1,943 @@ +// Copyright 2015 Brett Vickers. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package etree provides XML services through an Element Tree +// abstraction. +package etree + +import ( + "bufio" + "bytes" + "encoding/xml" + "errors" + "io" + "os" + "strings" +) + +const ( + // NoIndent is used with Indent to disable all indenting. + NoIndent = -1 +) + +// ErrXML is returned when XML parsing fails due to incorrect formatting. +var ErrXML = errors.New("etree: invalid XML format") + +// ReadSettings allow for changing the default behavior of the ReadFrom* +// methods. +type ReadSettings struct { + // CharsetReader to be passed to standard xml.Decoder. Default: nil. + CharsetReader func(charset string, input io.Reader) (io.Reader, error) + + // Permissive allows input containing common mistakes such as missing tags + // or attribute values. Default: false. + Permissive bool +} + +// newReadSettings creates a default ReadSettings record. +func newReadSettings() ReadSettings { + return ReadSettings{} +} + +// WriteSettings allow for changing the serialization behavior of the WriteTo* +// methods. +type WriteSettings struct { + // CanonicalEndTags forces the production of XML end tags, even for + // elements that have no child elements. Default: false. + CanonicalEndTags bool + + // CanonicalText forces the production of XML character references for + // text data characters &, <, and >. If false, XML character references + // are also produced for " and '. Default: false. + CanonicalText bool + + // CanonicalAttrVal forces the production of XML character references for + // attribute value characters &, < and ". If false, XML character + // references are also produced for > and '. Default: false. + CanonicalAttrVal bool +} + +// newWriteSettings creates a default WriteSettings record. +func newWriteSettings() WriteSettings { + return WriteSettings{ + CanonicalEndTags: false, + CanonicalText: false, + CanonicalAttrVal: false, + } +} + +// A Token is an empty interface that represents an Element, CharData, +// Comment, Directive, or ProcInst. +type Token interface { + Parent() *Element + dup(parent *Element) Token + setParent(parent *Element) + writeTo(w *bufio.Writer, s *WriteSettings) +} + +// A Document is a container holding a complete XML hierarchy. Its embedded +// element contains zero or more children, one of which is usually the root +// element. The embedded element may include other children such as +// processing instructions or BOM CharData tokens. +type Document struct { + Element + ReadSettings ReadSettings + WriteSettings WriteSettings +} + +// An Element represents an XML element, its attributes, and its child tokens. +type Element struct { + Space, Tag string // namespace and tag + Attr []Attr // key-value attribute pairs + Child []Token // child tokens (elements, comments, etc.) + parent *Element // parent element +} + +// An Attr represents a key-value attribute of an XML element. +type Attr struct { + Space, Key string // The attribute's namespace and key + Value string // The attribute value string +} + +// CharData represents character data within XML. +type CharData struct { + Data string + parent *Element + whitespace bool +} + +// A Comment represents an XML comment. +type Comment struct { + Data string + parent *Element +} + +// A Directive represents an XML directive. +type Directive struct { + Data string + parent *Element +} + +// A ProcInst represents an XML processing instruction. +type ProcInst struct { + Target string + Inst string + parent *Element +} + +// NewDocument creates an XML document without a root element. +func NewDocument() *Document { + return &Document{ + Element{Child: make([]Token, 0)}, + newReadSettings(), + newWriteSettings(), + } +} + +// Copy returns a recursive, deep copy of the document. +func (d *Document) Copy() *Document { + return &Document{*(d.dup(nil).(*Element)), d.ReadSettings, d.WriteSettings} +} + +// Root returns the root element of the document, or nil if there is no root +// element. +func (d *Document) Root() *Element { + for _, t := range d.Child { + if c, ok := t.(*Element); ok { + return c + } + } + return nil +} + +// SetRoot replaces the document's root element with e. If the document +// already has a root when this function is called, then the document's +// original root is unbound first. If the element e is bound to another +// document (or to another element within a document), then it is unbound +// first. +func (d *Document) SetRoot(e *Element) { + if e.parent != nil { + e.parent.RemoveChild(e) + } + e.setParent(&d.Element) + + for i, t := range d.Child { + if _, ok := t.(*Element); ok { + t.setParent(nil) + d.Child[i] = e + return + } + } + d.Child = append(d.Child, e) +} + +// ReadFrom reads XML from the reader r into the document d. It returns the +// number of bytes read and any error encountered. +func (d *Document) ReadFrom(r io.Reader) (n int64, err error) { + return d.Element.readFrom(r, d.ReadSettings) +} + +// ReadFromFile reads XML from the string s into the document d. +func (d *Document) ReadFromFile(filename string) error { + f, err := os.Open(filename) + if err != nil { + return err + } + defer f.Close() + _, err = d.ReadFrom(f) + return err +} + +// ReadFromBytes reads XML from the byte slice b into the document d. +func (d *Document) ReadFromBytes(b []byte) error { + _, err := d.ReadFrom(bytes.NewReader(b)) + return err +} + +// ReadFromString reads XML from the string s into the document d. +func (d *Document) ReadFromString(s string) error { + _, err := d.ReadFrom(strings.NewReader(s)) + return err +} + +// WriteTo serializes an XML document into the writer w. It +// returns the number of bytes written and any error encountered. +func (d *Document) WriteTo(w io.Writer) (n int64, err error) { + cw := newCountWriter(w) + b := bufio.NewWriter(cw) + for _, c := range d.Child { + c.writeTo(b, &d.WriteSettings) + } + err, n = b.Flush(), cw.bytes + return +} + +// WriteToFile serializes an XML document into the file named +// filename. +func (d *Document) WriteToFile(filename string) error { + f, err := os.Create(filename) + if err != nil { + return err + } + defer f.Close() + _, err = d.WriteTo(f) + return err +} + +// WriteToBytes serializes the XML document into a slice of +// bytes. +func (d *Document) WriteToBytes() (b []byte, err error) { + var buf bytes.Buffer + if _, err = d.WriteTo(&buf); err != nil { + return + } + return buf.Bytes(), nil +} + +// WriteToString serializes the XML document into a string. +func (d *Document) WriteToString() (s string, err error) { + var b []byte + if b, err = d.WriteToBytes(); err != nil { + return + } + return string(b), nil +} + +type indentFunc func(depth int) string + +// Indent modifies the document's element tree by inserting CharData entities +// containing carriage returns and indentation. The amount of indentation per +// depth level is given as spaces. Pass etree.NoIndent for spaces if you want +// no indentation at all. +func (d *Document) Indent(spaces int) { + var indent indentFunc + switch { + case spaces < 0: + indent = func(depth int) string { return "" } + default: + indent = func(depth int) string { return crIndent(depth*spaces, crsp) } + } + d.Element.indent(0, indent) +} + +// IndentTabs modifies the document's element tree by inserting CharData +// entities containing carriage returns and tabs for indentation. One tab is +// used per indentation level. +func (d *Document) IndentTabs() { + indent := func(depth int) string { return crIndent(depth, crtab) } + d.Element.indent(0, indent) +} + +// NewElement creates an unparented element with the specified tag. The tag +// may be prefixed by a namespace and a colon. +func NewElement(tag string) *Element { + space, stag := spaceDecompose(tag) + return newElement(space, stag, nil) +} + +// newElement is a helper function that creates an element and binds it to +// a parent element if possible. +func newElement(space, tag string, parent *Element) *Element { + e := &Element{ + Space: space, + Tag: tag, + Attr: make([]Attr, 0), + Child: make([]Token, 0), + parent: parent, + } + if parent != nil { + parent.addChild(e) + } + return e +} + +// Copy creates a recursive, deep copy of the element and all its attributes +// and children. The returned element has no parent but can be parented to a +// another element using AddElement, or to a document using SetRoot. +func (e *Element) Copy() *Element { + var parent *Element + return e.dup(parent).(*Element) +} + +// Text returns the characters immediately following the element's +// opening tag. +func (e *Element) Text() string { + if len(e.Child) == 0 { + return "" + } + if cd, ok := e.Child[0].(*CharData); ok { + return cd.Data + } + return "" +} + +// SetText replaces an element's subsidiary CharData text with a new string. +func (e *Element) SetText(text string) { + if len(e.Child) > 0 { + if cd, ok := e.Child[0].(*CharData); ok { + cd.Data = text + return + } + } + cd := newCharData(text, false, e) + copy(e.Child[1:], e.Child[0:]) + e.Child[0] = cd +} + +// CreateElement creates an element with the specified tag and adds it as the +// last child element of the element e. The tag may be prefixed by a namespace +// and a colon. +func (e *Element) CreateElement(tag string) *Element { + space, stag := spaceDecompose(tag) + return newElement(space, stag, e) +} + +// AddChild adds the token t as the last child of element e. If token t was +// already the child of another element, it is first removed from its current +// parent element. +func (e *Element) AddChild(t Token) { + if t.Parent() != nil { + t.Parent().RemoveChild(t) + } + t.setParent(e) + e.addChild(t) +} + +// InsertChild inserts the token t before e's existing child token ex. If ex +// is nil (or if ex is not a child of e), then t is added to the end of e's +// child token list. If token t was already the child of another element, it +// is first removed from its current parent element. +func (e *Element) InsertChild(ex Token, t Token) { + if t.Parent() != nil { + t.Parent().RemoveChild(t) + } + t.setParent(e) + + for i, c := range e.Child { + if c == ex { + e.Child = append(e.Child, nil) + copy(e.Child[i+1:], e.Child[i:]) + e.Child[i] = t + return + } + } + e.addChild(t) +} + +// RemoveChild attempts to remove the token t from element e's list of +// children. If the token t is a child of e, then it is returned. Otherwise, +// nil is returned. +func (e *Element) RemoveChild(t Token) Token { + for i, c := range e.Child { + if c == t { + e.Child = append(e.Child[:i], e.Child[i+1:]...) + c.setParent(nil) + return t + } + } + return nil +} + +// ReadFrom reads XML from the reader r and stores the result as a new child +// of element e. +func (e *Element) readFrom(ri io.Reader, settings ReadSettings) (n int64, err error) { + r := newCountReader(ri) + dec := xml.NewDecoder(r) + dec.CharsetReader = settings.CharsetReader + dec.Strict = !settings.Permissive + var stack stack + stack.push(e) + for { + t, err := dec.RawToken() + switch { + case err == io.EOF: + return r.bytes, nil + case err != nil: + return r.bytes, err + case stack.empty(): + return r.bytes, ErrXML + } + + top := stack.peek().(*Element) + + switch t := t.(type) { + case xml.StartElement: + e := newElement(t.Name.Space, t.Name.Local, top) + for _, a := range t.Attr { + e.createAttr(a.Name.Space, a.Name.Local, a.Value) + } + stack.push(e) + case xml.EndElement: + stack.pop() + case xml.CharData: + data := string(t) + newCharData(data, isWhitespace(data), top) + case xml.Comment: + newComment(string(t), top) + case xml.Directive: + newDirective(string(t), top) + case xml.ProcInst: + newProcInst(t.Target, string(t.Inst), top) + } + } +} + +// SelectAttr finds an element attribute matching the requested key and +// returns it if found. The key may be prefixed by a namespace and a colon. +func (e *Element) SelectAttr(key string) *Attr { + space, skey := spaceDecompose(key) + for i, a := range e.Attr { + if spaceMatch(space, a.Space) && skey == a.Key { + return &e.Attr[i] + } + } + return nil +} + +// SelectAttrValue finds an element attribute matching the requested key and +// returns its value if found. The key may be prefixed by a namespace and a +// colon. If the key is not found, the dflt value is returned instead. +func (e *Element) SelectAttrValue(key, dflt string) string { + space, skey := spaceDecompose(key) + for _, a := range e.Attr { + if spaceMatch(space, a.Space) && skey == a.Key { + return a.Value + } + } + return dflt +} + +// ChildElements returns all elements that are children of element e. +func (e *Element) ChildElements() []*Element { + var elements []*Element + for _, t := range e.Child { + if c, ok := t.(*Element); ok { + elements = append(elements, c) + } + } + return elements +} + +// SelectElement returns the first child element with the given tag. The tag +// may be prefixed by a namespace and a colon. +func (e *Element) SelectElement(tag string) *Element { + space, stag := spaceDecompose(tag) + for _, t := range e.Child { + if c, ok := t.(*Element); ok && spaceMatch(space, c.Space) && stag == c.Tag { + return c + } + } + return nil +} + +// SelectElements returns a slice of all child elements with the given tag. +// The tag may be prefixed by a namespace and a colon. +func (e *Element) SelectElements(tag string) []*Element { + space, stag := spaceDecompose(tag) + var elements []*Element + for _, t := range e.Child { + if c, ok := t.(*Element); ok && spaceMatch(space, c.Space) && stag == c.Tag { + elements = append(elements, c) + } + } + return elements +} + +// FindElement returns the first element matched by the XPath-like path +// string. Panics if an invalid path string is supplied. +func (e *Element) FindElement(path string) *Element { + return e.FindElementPath(MustCompilePath(path)) +} + +// FindElementPath returns the first element matched by the XPath-like path +// string. +func (e *Element) FindElementPath(path Path) *Element { + p := newPather() + elements := p.traverse(e, path) + switch { + case len(elements) > 0: + return elements[0] + default: + return nil + } +} + +// FindElements returns a slice of elements matched by the XPath-like path +// string. Panics if an invalid path string is supplied. +func (e *Element) FindElements(path string) []*Element { + return e.FindElementsPath(MustCompilePath(path)) +} + +// FindElementsPath returns a slice of elements matched by the Path object. +func (e *Element) FindElementsPath(path Path) []*Element { + p := newPather() + return p.traverse(e, path) +} + +// indent recursively inserts proper indentation between an +// XML element's child tokens. +func (e *Element) indent(depth int, indent indentFunc) { + e.stripIndent() + n := len(e.Child) + if n == 0 { + return + } + + oldChild := e.Child + e.Child = make([]Token, 0, n*2+1) + isCharData, firstNonCharData := false, true + for _, c := range oldChild { + + // Insert CR+indent before child if it's not character data. + // Exceptions: when it's the first non-character-data child, or when + // the child is at root depth. + _, isCharData = c.(*CharData) + if !isCharData { + if !firstNonCharData || depth > 0 { + newCharData(indent(depth), true, e) + } + firstNonCharData = false + } + + e.addChild(c) + + // Recursively process child elements. + if ce, ok := c.(*Element); ok { + ce.indent(depth+1, indent) + } + } + + // Insert CR+indent before the last child. + if !isCharData { + if !firstNonCharData || depth > 0 { + newCharData(indent(depth-1), true, e) + } + } +} + +// stripIndent removes any previously inserted indentation. +func (e *Element) stripIndent() { + // Count the number of non-indent child tokens + n := len(e.Child) + for _, c := range e.Child { + if cd, ok := c.(*CharData); ok && cd.whitespace { + n-- + } + } + if n == len(e.Child) { + return + } + + // Strip out indent CharData + newChild := make([]Token, n) + j := 0 + for _, c := range e.Child { + if cd, ok := c.(*CharData); ok && cd.whitespace { + continue + } + newChild[j] = c + j++ + } + e.Child = newChild +} + +// dup duplicates the element. +func (e *Element) dup(parent *Element) Token { + ne := &Element{ + Space: e.Space, + Tag: e.Tag, + Attr: make([]Attr, len(e.Attr)), + Child: make([]Token, len(e.Child)), + parent: parent, + } + for i, t := range e.Child { + ne.Child[i] = t.dup(ne) + } + for i, a := range e.Attr { + ne.Attr[i] = a + } + return ne +} + +// Parent returns the element token's parent element, or nil if it has no +// parent. +func (e *Element) Parent() *Element { + return e.parent +} + +// setParent replaces the element token's parent. +func (e *Element) setParent(parent *Element) { + e.parent = parent +} + +// writeTo serializes the element to the writer w. +func (e *Element) writeTo(w *bufio.Writer, s *WriteSettings) { + w.WriteByte('<') + if e.Space != "" { + w.WriteString(e.Space) + w.WriteByte(':') + } + w.WriteString(e.Tag) + for _, a := range e.Attr { + w.WriteByte(' ') + a.writeTo(w, s) + } + if len(e.Child) > 0 { + w.WriteString(">") + for _, c := range e.Child { + c.writeTo(w, s) + } + w.Write([]byte{'<', '/'}) + if e.Space != "" { + w.WriteString(e.Space) + w.WriteByte(':') + } + w.WriteString(e.Tag) + w.WriteByte('>') + } else { + if s.CanonicalEndTags { + w.Write([]byte{'>', '<', '/'}) + if e.Space != "" { + w.WriteString(e.Space) + w.WriteByte(':') + } + w.WriteString(e.Tag) + w.WriteByte('>') + } else { + w.Write([]byte{'/', '>'}) + } + } +} + +// addChild adds a child token to the element e. +func (e *Element) addChild(t Token) { + e.Child = append(e.Child, t) +} + +// CreateAttr creates an attribute and adds it to element e. The key may be +// prefixed by a namespace and a colon. If an attribute with the key already +// exists, its value is replaced. +func (e *Element) CreateAttr(key, value string) *Attr { + space, skey := spaceDecompose(key) + return e.createAttr(space, skey, value) +} + +// createAttr is a helper function that creates attributes. +func (e *Element) createAttr(space, key, value string) *Attr { + for i, a := range e.Attr { + if space == a.Space && key == a.Key { + e.Attr[i].Value = value + return &e.Attr[i] + } + } + a := Attr{space, key, value} + e.Attr = append(e.Attr, a) + return &e.Attr[len(e.Attr)-1] +} + +// RemoveAttr removes and returns the first attribute of the element whose key +// matches the given key. The key may be prefixed by a namespace and a colon. +// If an equal attribute does not exist, nil is returned. +func (e *Element) RemoveAttr(key string) *Attr { + space, skey := spaceDecompose(key) + for i, a := range e.Attr { + if space == a.Space && skey == a.Key { + e.Attr = append(e.Attr[0:i], e.Attr[i+1:]...) + return &a + } + } + return nil +} + +var xmlReplacerNormal = strings.NewReplacer( + "&", "&", + "<", "<", + ">", ">", + "'", "'", + `"`, """, +) + +var xmlReplacerCanonicalText = strings.NewReplacer( + "&", "&", + "<", "<", + ">", ">", + "\r", " ", +) + +var xmlReplacerCanonicalAttrVal = strings.NewReplacer( + "&", "&", + "<", "<", + `"`, """, + "\t", " ", + "\n", " ", + "\r", " ", +) + +// writeTo serializes the attribute to the writer. +func (a *Attr) writeTo(w *bufio.Writer, s *WriteSettings) { + if a.Space != "" { + w.WriteString(a.Space) + w.WriteByte(':') + } + w.WriteString(a.Key) + w.WriteString(`="`) + var r *strings.Replacer + if s.CanonicalAttrVal { + r = xmlReplacerCanonicalAttrVal + } else { + r = xmlReplacerNormal + } + w.WriteString(r.Replace(a.Value)) + w.WriteByte('"') +} + +// NewCharData creates a parentless XML character data entity. +func NewCharData(data string) *CharData { + return newCharData(data, false, nil) +} + +// newCharData creates an XML character data entity and binds it to a parent +// element. If parent is nil, the CharData token remains unbound. +func newCharData(data string, whitespace bool, parent *Element) *CharData { + c := &CharData{ + Data: data, + whitespace: whitespace, + parent: parent, + } + if parent != nil { + parent.addChild(c) + } + return c +} + +// CreateCharData creates an XML character data entity and adds it as a child +// of element e. +func (e *Element) CreateCharData(data string) *CharData { + return newCharData(data, false, e) +} + +// dup duplicates the character data. +func (c *CharData) dup(parent *Element) Token { + return &CharData{ + Data: c.Data, + whitespace: c.whitespace, + parent: parent, + } +} + +// Parent returns the character data token's parent element, or nil if it has +// no parent. +func (c *CharData) Parent() *Element { + return c.parent +} + +// setParent replaces the character data token's parent. +func (c *CharData) setParent(parent *Element) { + c.parent = parent +} + +// writeTo serializes the character data entity to the writer. +func (c *CharData) writeTo(w *bufio.Writer, s *WriteSettings) { + var r *strings.Replacer + if s.CanonicalText { + r = xmlReplacerCanonicalText + } else { + r = xmlReplacerNormal + } + w.WriteString(r.Replace(c.Data)) +} + +// NewComment creates a parentless XML comment. +func NewComment(comment string) *Comment { + return newComment(comment, nil) +} + +// NewComment creates an XML comment and binds it to a parent element. If +// parent is nil, the Comment remains unbound. +func newComment(comment string, parent *Element) *Comment { + c := &Comment{ + Data: comment, + parent: parent, + } + if parent != nil { + parent.addChild(c) + } + return c +} + +// CreateComment creates an XML comment and adds it as a child of element e. +func (e *Element) CreateComment(comment string) *Comment { + return newComment(comment, e) +} + +// dup duplicates the comment. +func (c *Comment) dup(parent *Element) Token { + return &Comment{ + Data: c.Data, + parent: parent, + } +} + +// Parent returns comment token's parent element, or nil if it has no parent. +func (c *Comment) Parent() *Element { + return c.parent +} + +// setParent replaces the comment token's parent. +func (c *Comment) setParent(parent *Element) { + c.parent = parent +} + +// writeTo serialies the comment to the writer. +func (c *Comment) writeTo(w *bufio.Writer, s *WriteSettings) { + w.WriteString("") +} + +// NewDirective creates a parentless XML directive. +func NewDirective(data string) *Directive { + return newDirective(data, nil) +} + +// newDirective creates an XML directive and binds it to a parent element. If +// parent is nil, the Directive remains unbound. +func newDirective(data string, parent *Element) *Directive { + d := &Directive{ + Data: data, + parent: parent, + } + if parent != nil { + parent.addChild(d) + } + return d +} + +// CreateDirective creates an XML directive and adds it as the last child of +// element e. +func (e *Element) CreateDirective(data string) *Directive { + return newDirective(data, e) +} + +// dup duplicates the directive. +func (d *Directive) dup(parent *Element) Token { + return &Directive{ + Data: d.Data, + parent: parent, + } +} + +// Parent returns directive token's parent element, or nil if it has no +// parent. +func (d *Directive) Parent() *Element { + return d.parent +} + +// setParent replaces the directive token's parent. +func (d *Directive) setParent(parent *Element) { + d.parent = parent +} + +// writeTo serializes the XML directive to the writer. +func (d *Directive) writeTo(w *bufio.Writer, s *WriteSettings) { + w.WriteString("") +} + +// NewProcInst creates a parentless XML processing instruction. +func NewProcInst(target, inst string) *ProcInst { + return newProcInst(target, inst, nil) +} + +// newProcInst creates an XML processing instruction and binds it to a parent +// element. If parent is nil, the ProcInst remains unbound. +func newProcInst(target, inst string, parent *Element) *ProcInst { + p := &ProcInst{ + Target: target, + Inst: inst, + parent: parent, + } + if parent != nil { + parent.addChild(p) + } + return p +} + +// CreateProcInst creates a processing instruction and adds it as a child of +// element e. +func (e *Element) CreateProcInst(target, inst string) *ProcInst { + return newProcInst(target, inst, e) +} + +// dup duplicates the procinst. +func (p *ProcInst) dup(parent *Element) Token { + return &ProcInst{ + Target: p.Target, + Inst: p.Inst, + parent: parent, + } +} + +// Parent returns processing instruction token's parent element, or nil if it +// has no parent. +func (p *ProcInst) Parent() *Element { + return p.parent +} + +// setParent replaces the processing instruction token's parent. +func (p *ProcInst) setParent(parent *Element) { + p.parent = parent +} + +// writeTo serializes the processing instruction to the writer. +func (p *ProcInst) writeTo(w *bufio.Writer, s *WriteSettings) { + w.WriteString("") +} diff --git a/vendor/github.com/beevik/etree/helpers.go b/vendor/github.com/beevik/etree/helpers.go new file mode 100644 index 0000000000000..4f8350e70c6be --- /dev/null +++ b/vendor/github.com/beevik/etree/helpers.go @@ -0,0 +1,188 @@ +// Copyright 2015 Brett Vickers. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package etree + +import ( + "io" + "strings" +) + +// A simple stack +type stack struct { + data []interface{} +} + +func (s *stack) empty() bool { + return len(s.data) == 0 +} + +func (s *stack) push(value interface{}) { + s.data = append(s.data, value) +} + +func (s *stack) pop() interface{} { + value := s.data[len(s.data)-1] + s.data[len(s.data)-1] = nil + s.data = s.data[:len(s.data)-1] + return value +} + +func (s *stack) peek() interface{} { + return s.data[len(s.data)-1] +} + +// A fifo is a simple first-in-first-out queue. +type fifo struct { + data []interface{} + head, tail int +} + +func (f *fifo) add(value interface{}) { + if f.len()+1 >= len(f.data) { + f.grow() + } + f.data[f.tail] = value + if f.tail++; f.tail == len(f.data) { + f.tail = 0 + } +} + +func (f *fifo) remove() interface{} { + value := f.data[f.head] + f.data[f.head] = nil + if f.head++; f.head == len(f.data) { + f.head = 0 + } + return value +} + +func (f *fifo) len() int { + if f.tail >= f.head { + return f.tail - f.head + } + return len(f.data) - f.head + f.tail +} + +func (f *fifo) grow() { + c := len(f.data) * 2 + if c == 0 { + c = 4 + } + buf, count := make([]interface{}, c), f.len() + if f.tail >= f.head { + copy(buf[0:count], f.data[f.head:f.tail]) + } else { + hindex := len(f.data) - f.head + copy(buf[0:hindex], f.data[f.head:]) + copy(buf[hindex:count], f.data[:f.tail]) + } + f.data, f.head, f.tail = buf, 0, count +} + +// countReader implements a proxy reader that counts the number of +// bytes read from its encapsulated reader. +type countReader struct { + r io.Reader + bytes int64 +} + +func newCountReader(r io.Reader) *countReader { + return &countReader{r: r} +} + +func (cr *countReader) Read(p []byte) (n int, err error) { + b, err := cr.r.Read(p) + cr.bytes += int64(b) + return b, err +} + +// countWriter implements a proxy writer that counts the number of +// bytes written by its encapsulated writer. +type countWriter struct { + w io.Writer + bytes int64 +} + +func newCountWriter(w io.Writer) *countWriter { + return &countWriter{w: w} +} + +func (cw *countWriter) Write(p []byte) (n int, err error) { + b, err := cw.w.Write(p) + cw.bytes += int64(b) + return b, err +} + +// isWhitespace returns true if the byte slice contains only +// whitespace characters. +func isWhitespace(s string) bool { + for i := 0; i < len(s); i++ { + if c := s[i]; c != ' ' && c != '\t' && c != '\n' && c != '\r' { + return false + } + } + return true +} + +// spaceMatch returns true if namespace a is the empty string +// or if namespace a equals namespace b. +func spaceMatch(a, b string) bool { + switch { + case a == "": + return true + default: + return a == b + } +} + +// spaceDecompose breaks a namespace:tag identifier at the ':' +// and returns the two parts. +func spaceDecompose(str string) (space, key string) { + colon := strings.IndexByte(str, ':') + if colon == -1 { + return "", str + } + return str[:colon], str[colon+1:] +} + +// Strings used by crIndent +const ( + crsp = "\n " + crtab = "\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t" +) + +// crIndent returns a carriage return followed by n copies of the +// first non-CR character in the source string. +func crIndent(n int, source string) string { + switch { + case n < 0: + return source[:1] + case n < len(source): + return source[:n+1] + default: + return source + strings.Repeat(source[1:2], n-len(source)+1) + } +} + +// nextIndex returns the index of the next occurrence of sep in s, +// starting from offset. It returns -1 if the sep string is not found. +func nextIndex(s, sep string, offset int) int { + switch i := strings.Index(s[offset:], sep); i { + case -1: + return -1 + default: + return offset + i + } +} + +// isInteger returns true if the string s contains an integer. +func isInteger(s string) bool { + for i := 0; i < len(s); i++ { + if (s[i] < '0' || s[i] > '9') && !(i == 0 && s[i] == '-') { + return false + } + } + return true +} diff --git a/vendor/github.com/beevik/etree/path.go b/vendor/github.com/beevik/etree/path.go new file mode 100644 index 0000000000000..126eb154f5162 --- /dev/null +++ b/vendor/github.com/beevik/etree/path.go @@ -0,0 +1,470 @@ +// Copyright 2015 Brett Vickers. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package etree + +import ( + "strconv" + "strings" +) + +/* +A Path is an object that represents an optimized version of an +XPath-like search string. Although path strings are XPath-like, +only the following limited syntax is supported: + + . Selects the current element + .. Selects the parent of the current element + * Selects all child elements + // Selects all descendants of the current element + tag Selects all child elements with the given tag + [#] Selects the element of the given index (1-based, + negative starts from the end) + [@attrib] Selects all elements with the given attribute + [@attrib='val'] Selects all elements with the given attribute set to val + [tag] Selects all elements with a child element named tag + [tag='val'] Selects all elements with a child element named tag + and text equal to val + +Examples: + +Select the title elements of all descendant book elements having a +'category' attribute of 'WEB': + //book[@category='WEB']/title + +Select the first book element with a title child containing the text +'Great Expectations': + .//book[title='Great Expectations'][1] + +Starting from the current element, select all children of book elements +with an attribute 'language' set to 'english': + ./book/*[@language='english'] + +Select all descendant book elements whose title element has an attribute +'language' set to 'french': + //book/title[@language='french']/.. +*/ +type Path struct { + segments []segment +} + +// ErrPath is returned by path functions when an invalid etree path is provided. +type ErrPath string + +// Error returns the string describing a path error. +func (err ErrPath) Error() string { + return "etree: " + string(err) +} + +// CompilePath creates an optimized version of an XPath-like string that +// can be used to query elements in an element tree. +func CompilePath(path string) (Path, error) { + var comp compiler + segments := comp.parsePath(path) + if comp.err != ErrPath("") { + return Path{nil}, comp.err + } + return Path{segments}, nil +} + +// MustCompilePath creates an optimized version of an XPath-like string that +// can be used to query elements in an element tree. Panics if an error +// occurs. Use this function to create Paths when you know the path is +// valid (i.e., if it's hard-coded). +func MustCompilePath(path string) Path { + p, err := CompilePath(path) + if err != nil { + panic(err) + } + return p +} + +// A segment is a portion of a path between "/" characters. +// It contains one selector and zero or more [filters]. +type segment struct { + sel selector + filters []filter +} + +func (seg *segment) apply(e *Element, p *pather) { + seg.sel.apply(e, p) + for _, f := range seg.filters { + f.apply(p) + } +} + +// A selector selects XML elements for consideration by the +// path traversal. +type selector interface { + apply(e *Element, p *pather) +} + +// A filter pares down a list of candidate XML elements based +// on a path filter in [brackets]. +type filter interface { + apply(p *pather) +} + +// A pather is helper object that traverses an element tree using +// a Path object. It collects and deduplicates all elements matching +// the path query. +type pather struct { + queue fifo + results []*Element + inResults map[*Element]bool + candidates []*Element + scratch []*Element // used by filters +} + +// A node represents an element and the remaining path segments that +// should be applied against it by the pather. +type node struct { + e *Element + segments []segment +} + +func newPather() *pather { + return &pather{ + results: make([]*Element, 0), + inResults: make(map[*Element]bool), + candidates: make([]*Element, 0), + scratch: make([]*Element, 0), + } +} + +// traverse follows the path from the element e, collecting +// and then returning all elements that match the path's selectors +// and filters. +func (p *pather) traverse(e *Element, path Path) []*Element { + for p.queue.add(node{e, path.segments}); p.queue.len() > 0; { + p.eval(p.queue.remove().(node)) + } + return p.results +} + +// eval evalutes the current path node by applying the remaining +// path's selector rules against the node's element. +func (p *pather) eval(n node) { + p.candidates = p.candidates[0:0] + seg, remain := n.segments[0], n.segments[1:] + seg.apply(n.e, p) + + if len(remain) == 0 { + for _, c := range p.candidates { + if in := p.inResults[c]; !in { + p.inResults[c] = true + p.results = append(p.results, c) + } + } + } else { + for _, c := range p.candidates { + p.queue.add(node{c, remain}) + } + } +} + +// A compiler generates a compiled path from a path string. +type compiler struct { + err ErrPath +} + +// parsePath parses an XPath-like string describing a path +// through an element tree and returns a slice of segment +// descriptors. +func (c *compiler) parsePath(path string) []segment { + // If path starts or ends with //, fix it + if strings.HasPrefix(path, "//") { + path = "." + path + } + if strings.HasSuffix(path, "//") { + path = path + "*" + } + + // Paths cannot be absolute + if strings.HasPrefix(path, "/") { + c.err = ErrPath("paths cannot be absolute.") + return nil + } + + // Split path into segment objects + var segments []segment + for _, s := range splitPath(path) { + segments = append(segments, c.parseSegment(s)) + if c.err != ErrPath("") { + break + } + } + return segments +} + +func splitPath(path string) []string { + pieces := make([]string, 0) + start := 0 + inquote := false + for i := 0; i+1 <= len(path); i++ { + if path[i] == '\'' { + inquote = !inquote + } else if path[i] == '/' && !inquote { + pieces = append(pieces, path[start:i]) + start = i + 1 + } + } + return append(pieces, path[start:]) +} + +// parseSegment parses a path segment between / characters. +func (c *compiler) parseSegment(path string) segment { + pieces := strings.Split(path, "[") + seg := segment{ + sel: c.parseSelector(pieces[0]), + filters: make([]filter, 0), + } + for i := 1; i < len(pieces); i++ { + fpath := pieces[i] + if fpath[len(fpath)-1] != ']' { + c.err = ErrPath("path has invalid filter [brackets].") + break + } + seg.filters = append(seg.filters, c.parseFilter(fpath[:len(fpath)-1])) + } + return seg +} + +// parseSelector parses a selector at the start of a path segment. +func (c *compiler) parseSelector(path string) selector { + switch path { + case ".": + return new(selectSelf) + case "..": + return new(selectParent) + case "*": + return new(selectChildren) + case "": + return new(selectDescendants) + default: + return newSelectChildrenByTag(path) + } +} + +// parseFilter parses a path filter contained within [brackets]. +func (c *compiler) parseFilter(path string) filter { + if len(path) == 0 { + c.err = ErrPath("path contains an empty filter expression.") + return nil + } + + // Filter contains [@attr='val'] or [tag='val']? + eqindex := strings.Index(path, "='") + if eqindex >= 0 { + rindex := nextIndex(path, "'", eqindex+2) + if rindex != len(path)-1 { + c.err = ErrPath("path has mismatched filter quotes.") + return nil + } + switch { + case path[0] == '@': + return newFilterAttrVal(path[1:eqindex], path[eqindex+2:rindex]) + default: + return newFilterChildText(path[:eqindex], path[eqindex+2:rindex]) + } + } + + // Filter contains [@attr], [N] or [tag] + switch { + case path[0] == '@': + return newFilterAttr(path[1:]) + case isInteger(path): + pos, _ := strconv.Atoi(path) + switch { + case pos > 0: + return newFilterPos(pos - 1) + default: + return newFilterPos(pos) + } + default: + return newFilterChild(path) + } +} + +// selectSelf selects the current element into the candidate list. +type selectSelf struct{} + +func (s *selectSelf) apply(e *Element, p *pather) { + p.candidates = append(p.candidates, e) +} + +// selectParent selects the element's parent into the candidate list. +type selectParent struct{} + +func (s *selectParent) apply(e *Element, p *pather) { + if e.parent != nil { + p.candidates = append(p.candidates, e.parent) + } +} + +// selectChildren selects the element's child elements into the +// candidate list. +type selectChildren struct{} + +func (s *selectChildren) apply(e *Element, p *pather) { + for _, c := range e.Child { + if c, ok := c.(*Element); ok { + p.candidates = append(p.candidates, c) + } + } +} + +// selectDescendants selects all descendant child elements +// of the element into the candidate list. +type selectDescendants struct{} + +func (s *selectDescendants) apply(e *Element, p *pather) { + var queue fifo + for queue.add(e); queue.len() > 0; { + e := queue.remove().(*Element) + p.candidates = append(p.candidates, e) + for _, c := range e.Child { + if c, ok := c.(*Element); ok { + queue.add(c) + } + } + } +} + +// selectChildrenByTag selects into the candidate list all child +// elements of the element having the specified tag. +type selectChildrenByTag struct { + space, tag string +} + +func newSelectChildrenByTag(path string) *selectChildrenByTag { + s, l := spaceDecompose(path) + return &selectChildrenByTag{s, l} +} + +func (s *selectChildrenByTag) apply(e *Element, p *pather) { + for _, c := range e.Child { + if c, ok := c.(*Element); ok && spaceMatch(s.space, c.Space) && s.tag == c.Tag { + p.candidates = append(p.candidates, c) + } + } +} + +// filterPos filters the candidate list, keeping only the +// candidate at the specified index. +type filterPos struct { + index int +} + +func newFilterPos(pos int) *filterPos { + return &filterPos{pos} +} + +func (f *filterPos) apply(p *pather) { + if f.index >= 0 { + if f.index < len(p.candidates) { + p.scratch = append(p.scratch, p.candidates[f.index]) + } + } else { + if -f.index <= len(p.candidates) { + p.scratch = append(p.scratch, p.candidates[len(p.candidates)+f.index]) + } + } + p.candidates, p.scratch = p.scratch, p.candidates[0:0] +} + +// filterAttr filters the candidate list for elements having +// the specified attribute. +type filterAttr struct { + space, key string +} + +func newFilterAttr(str string) *filterAttr { + s, l := spaceDecompose(str) + return &filterAttr{s, l} +} + +func (f *filterAttr) apply(p *pather) { + for _, c := range p.candidates { + for _, a := range c.Attr { + if spaceMatch(f.space, a.Space) && f.key == a.Key { + p.scratch = append(p.scratch, c) + break + } + } + } + p.candidates, p.scratch = p.scratch, p.candidates[0:0] +} + +// filterAttrVal filters the candidate list for elements having +// the specified attribute with the specified value. +type filterAttrVal struct { + space, key, val string +} + +func newFilterAttrVal(str, value string) *filterAttrVal { + s, l := spaceDecompose(str) + return &filterAttrVal{s, l, value} +} + +func (f *filterAttrVal) apply(p *pather) { + for _, c := range p.candidates { + for _, a := range c.Attr { + if spaceMatch(f.space, a.Space) && f.key == a.Key && f.val == a.Value { + p.scratch = append(p.scratch, c) + break + } + } + } + p.candidates, p.scratch = p.scratch, p.candidates[0:0] +} + +// filterChild filters the candidate list for elements having +// a child element with the specified tag. +type filterChild struct { + space, tag string +} + +func newFilterChild(str string) *filterChild { + s, l := spaceDecompose(str) + return &filterChild{s, l} +} + +func (f *filterChild) apply(p *pather) { + for _, c := range p.candidates { + for _, cc := range c.Child { + if cc, ok := cc.(*Element); ok && + spaceMatch(f.space, cc.Space) && + f.tag == cc.Tag { + p.scratch = append(p.scratch, c) + } + } + } + p.candidates, p.scratch = p.scratch, p.candidates[0:0] +} + +// filterChildText filters the candidate list for elements having +// a child element with the specified tag and text. +type filterChildText struct { + space, tag, text string +} + +func newFilterChildText(str, text string) *filterChildText { + s, l := spaceDecompose(str) + return &filterChildText{s, l, text} +} + +func (f *filterChildText) apply(p *pather) { + for _, c := range p.candidates { + for _, cc := range c.Child { + if cc, ok := cc.(*Element); ok && + spaceMatch(f.space, cc.Space) && + f.tag == cc.Tag && + f.text == cc.Text() { + p.scratch = append(p.scratch, c) + } + } + } + p.candidates, p.scratch = p.scratch, p.candidates[0:0] +} diff --git a/vendor/github.com/gravitational/form/.gitignore b/vendor/github.com/gravitational/form/.gitignore new file mode 100644 index 0000000000000..daf913b1b347a --- /dev/null +++ b/vendor/github.com/gravitational/form/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/vendor/github.com/gravitational/form/LICENSE b/vendor/github.com/gravitational/form/LICENSE new file mode 100644 index 0000000000000..9faf6baa64fc5 --- /dev/null +++ b/vendor/github.com/gravitational/form/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2015 Gravitational, Inc. + + 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. \ No newline at end of file diff --git a/vendor/github.com/gravitational/form/Makefile b/vendor/github.com/gravitational/form/Makefile new file mode 100644 index 0000000000000..f0d580b815e66 --- /dev/null +++ b/vendor/github.com/gravitational/form/Makefile @@ -0,0 +1,13 @@ +clean: + find . -name flymake_* -delete + +test: clean + go vet ./... # note that I added vetting step here to to see if there are anything outstanding + go test -v ./... -cover + +cover: clean + go test -v . -coverprofile=/tmp/coverage.out + go tool cover -html=/tmp/coverage.out + +sloccount: + find . -path ./Godeps -prune -o -name "*.go" -print0 | xargs -0 wc -l diff --git a/vendor/github.com/gravitational/form/README.md b/vendor/github.com/gravitational/form/README.md new file mode 100644 index 0000000000000..e54e70842521e --- /dev/null +++ b/vendor/github.com/gravitational/form/README.md @@ -0,0 +1,9 @@ +# Form + +[![Build Status](https://api.shippable.com/projects/5505eecf5ab6cc13529b4a98/badge?branchName=master)](https://app.shippable.com/projects/5505eecf5ab6cc13529b4a98/builds/latest) + +Form is a package for handling HTTP web forms input. See godoc for details: + +```go +godoc github.com/gravitational/form +``` diff --git a/vendor/github.com/gravitational/form/form.go b/vendor/github.com/gravitational/form/form.go new file mode 100644 index 0000000000000..4975ec725e7d2 --- /dev/null +++ b/vendor/github.com/gravitational/form/form.go @@ -0,0 +1,283 @@ +/* +Copyright 2015 Gravitational, Inc. + +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. +*/ +// Form is a minimalist HTTP web form parser library based on functional arguments. +package form + +import ( + "fmt" + "mime" + "mime/multipart" + "net/http" + "time" + + "strconv" +) + +// Param is a functional argument parameter passed to the Parse function +type Param func(r *http.Request) error + +// Parse takes http.Request and form arguments that it needs to extract +// +// import ( +// "github.com/gravitational/form" +// ) +// +// var duration time.Duration +// var count int +// name := "default" // a simple way to set default argument +// +// err := form.Parse(r, +// form.Duration("duration", &duration), +// form.Int("count", &count, Required()), // notice the "Required" argument +// form.String("name", &name), +// ) +// +// if err != nil { +// // handle error here +// } +func Parse(r *http.Request, params ...Param) error { + mtype, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err != nil { + return err + } + if mtype == "multipart/form-data" { + if err := r.ParseMultipartForm(maxMemoryBytes); err != nil { + return err + } + } else { + if err := r.ParseForm(); err != nil { + return err + } + } + for _, p := range params { + if err := p(r); err != nil { + return err + } + } + return nil +} + +// Duration extracts duration expressed as a Go duration string e.g. "1s" +func Duration(name string, out *time.Duration, predicates ...Predicate) Param { + return func(r *http.Request) error { + for _, p := range predicates { + if err := p.Pass(name, r); err != nil { + return err + } + } + v := r.Form.Get(name) + if v == "" { + return nil + } + d, err := time.ParseDuration(v) + if err != nil { + return &BadParameterError{Param: name, Message: err.Error()} + } + *out = d + return nil + } +} + +// Time extracts duration expressed as in RFC 3339 format +func Time(name string, out *time.Time, predicates ...Predicate) Param { + return func(r *http.Request) error { + for _, p := range predicates { + if err := p.Pass(name, r); err != nil { + return err + } + } + v := r.Form.Get(name) + if v == "" { + return nil + } + var t time.Time + if err := t.UnmarshalText([]byte(v)); err != nil { + return &BadParameterError{Param: name, Message: err.Error()} + } + *out = t + return nil + } +} + +// String extracts the argument by name as is without any changes +func String(name string, out *string, predicates ...Predicate) Param { + return func(r *http.Request) error { + for _, p := range predicates { + if err := p.Pass(name, r); err != nil { + return err + } + } + *out = r.Form.Get(name) + return nil + } +} + +// Int extracts the integer argument in decimal format e.g. "10" +func Int(name string, out *int, predicates ...Predicate) Param { + return func(r *http.Request) error { + v := r.Form.Get(name) + for _, p := range predicates { + if err := p.Pass(name, r); err != nil { + return err + } + } + if v == "" { + return nil + } + p, err := strconv.Atoi(v) + if err != nil { + return &BadParameterError{Param: name, Message: err.Error()} + } + *out = p + return nil + } +} + +// StringSlice extracts the string slice of arguments by name +func StringSlice(name string, out *[]string, predicates ...Predicate) Param { + return func(r *http.Request) error { + for _, p := range predicates { + if err := p.Pass(name, r); err != nil { + return err + } + } + *out = make([]string, len(r.Form[name])) + copy(*out, r.Form[name]) + return nil + } +} + +// FileSlice reads the files uploaded with name parameter and initialized +// the slice of files. The files should be closed by the callee after +// usage, by executing f.Close() on each of them +// files slice will be nil if there's an error +func FileSlice(name string, files *Files, predicates ...Predicate) Param { + return func(r *http.Request) error { + err := r.ParseMultipartForm(maxMemoryBytes) + if err != nil { + return err + } + if r.MultipartForm == nil && r.MultipartForm.File == nil { + return fmt.Errorf("missing form") + } + for _, p := range predicates { + if err := p.Pass(name, r); err != nil { + return err + } + } + + fhs := r.MultipartForm.File[name] + if len(fhs) == 0 { + *files = []*FileWrapper{} + return nil + } + + *files = make([]*FileWrapper, len(fhs)) + for i, fh := range fhs { + f, err := fh.Open() + if err != nil { + files.Close() + return err + } + (*files)[i] = &FileWrapper{f, fh.Filename} + } + return nil + } +} + +// Predicate provides an extensible way to check various conditions on a variable +// e.g. setting minimums and maximums, or parsing some regular expressions +type Predicate interface { + Pass(param string, r *http.Request) error +} + +// PredicateFunc takes a func and converts it into a Predicate-compatible interface +type PredicateFunc func(param string, r *http.Request) error + +func (p PredicateFunc) Pass(param string, r *http.Request) error { + return p(param, r) +} + +// Required checker parameter ensures that the parameter is indeed supplied by user +// it returns MissingParameterError when parameter is not present +func Required() Predicate { + return PredicateFunc(func(param string, r *http.Request) error { + if r.Form.Get(param) == "" { + return &MissingParameterError{Param: param} + } + return nil + }) +} + +// MissingParameterError is an error that indicates that required parameter was not +// supplied by user. +type MissingParameterError struct { + Param string +} + +func (p *MissingParameterError) Error() string { + return fmt.Sprintf("missing required parameter: '%v'", p.Param) +} + +// BadParameterError is returned whenever the parameter format does not match +// required restrictions. +type BadParameterError struct { + Param string // Param is a paramter name + Message string // Message is an error message presented to user +} + +func (p *BadParameterError) Error() string { + return fmt.Sprintf("bad parameter '%v', error: %v", p.Param, p.Message) +} + +const maxMemoryBytes = 64 * 1024 + +type FileWrapper struct { + multipart.File + name string +} + +// Name returns file name as set during upload +func (f *FileWrapper) Name() string { + return f.name +} + +// Files is a slice of multipart.File that provides additional +// convenient method to close all files as a single operation +type Files []*FileWrapper + +func (fs *Files) Close() error { + e := &FilesCloseError{} + for _, f := range *fs { + if f != nil { + if err := f.Close(); err != nil { + e.Errors = append(e.Errors, err) + } + } + } + if len(e.Errors) != 0 { + return e + } + return nil +} + +type FilesCloseError struct { + Errors []error +} + +func (p *FilesCloseError) Error() string { + return fmt.Sprintf("failed to close files, error: %v", p.Errors) +} diff --git a/vendor/github.com/gravitational/form/shippable.yaml b/vendor/github.com/gravitational/form/shippable.yaml new file mode 100644 index 0000000000000..6f4cfe4bef1aa --- /dev/null +++ b/vendor/github.com/gravitational/form/shippable.yaml @@ -0,0 +1,19 @@ +language: go + +go: + - 1.4 + +build_image: shippableimages/ubuntu1404_go + +before_install: + - source $HOME/.gvm/scripts/gvm + - gvm install go$SHIPPABLE_GO_VERSION + - gvm use go$SHIPPABLE_GO_VERSION + - export GOPATH=/root/workspace/ + - go get gopkg.in/check.v1 + - go get golang.org/x/tools/cmd/cover + - go get golang.org/x/tools/cmd/vet + +script: + - echo $GOPATH + - make test \ No newline at end of file diff --git a/vendor/github.com/russellhaering/gosaml2/.travis.yml b/vendor/github.com/russellhaering/gosaml2/.travis.yml new file mode 100644 index 0000000000000..5845d69d005e2 --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/.travis.yml @@ -0,0 +1,12 @@ +language: go + +go: + - 1.5 + - 1.6 + - 1.7 + - 1.8 + - tip + +matrix: + allow_failures: + - go: tip diff --git a/vendor/github.com/russellhaering/gosaml2/LICENSE b/vendor/github.com/russellhaering/gosaml2/LICENSE new file mode 100644 index 0000000000000..67db8588217f2 --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/LICENSE @@ -0,0 +1,175 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/vendor/github.com/russellhaering/gosaml2/README.md b/vendor/github.com/russellhaering/gosaml2/README.md new file mode 100644 index 0000000000000..2cefd9d7e0e2b --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/README.md @@ -0,0 +1,34 @@ +# gosaml2 + +[![Build Status](https://travis-ci.org/russellhaering/gosaml2.svg?branch=master)](https://travis-ci.org/russellhaering/gosaml2) +[![GoDoc](https://godoc.org/github.com/russellhaering/gosaml2?status.svg)](https://godoc.org/github.com/russellhaering/gosaml2) + +SAML 2.0 implemementation for Service Providers based on [etree](https://github.com/beevik/etree) +and [goxmldsig](https://github.com/russellhaering/goxmldsig), a pure Go +implementation of XML digital signatures. + +## Installation + +Install `gosaml2` into your `$GOPATH` using `go get`: + +``` +go get github.com/russellhaering/gosaml2 +``` + +## Example + +See [demo.go](s2example/demo.go). + +## Supported Identity Providers + +This library is meant to be a generic SAML implementation. If you find a +standards compliant identity provider that it doesn't work with please +submit a bug or pull request. + +The following identity providers have been tested: + +* Okta +* Auth0 +* Shibboleth +* Ipsilon +* OneLogin diff --git a/vendor/github.com/russellhaering/gosaml2/attribute.go b/vendor/github.com/russellhaering/gosaml2/attribute.go new file mode 100644 index 0000000000000..0109cf94da223 --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/attribute.go @@ -0,0 +1,19 @@ +package saml2 + +import "github.com/russellhaering/gosaml2/types" + +// Values is a convenience wrapper for a map of strings to Attributes, which +// can be used for easy access to the string values of Attribute lists. +type Values map[string]types.Attribute + +// Get is a safe method (nil maps will not panic) for returning the first value +// for an attribute at a key, or the empty string if none exists. +func (vals Values) Get(k string) string { + if vals == nil { + return "" + } + if v, ok := vals[k]; ok && len(v.Values) > 0 { + return string(v.Values[0].Value) + } + return "" +} diff --git a/vendor/github.com/russellhaering/gosaml2/authn_request.go b/vendor/github.com/russellhaering/gosaml2/authn_request.go new file mode 100644 index 0000000000000..9f8360adc7ac3 --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/authn_request.go @@ -0,0 +1,16 @@ +package saml2 + +import "time" + +// AuthNRequest is the go struct representation of an authentication request +type AuthNRequest struct { + ID string `xml:",attr"` + Version string `xml:",attr"` + ProtocolBinding string `xml:",attr"` + AssertionConsumerServiceURL string `xml:",attr"` + + IssueInstant time.Time `xml:",attr"` + + Destination string `xml:",attr"` + Issuer string +} diff --git a/vendor/github.com/russellhaering/gosaml2/build_request.go b/vendor/github.com/russellhaering/gosaml2/build_request.go new file mode 100644 index 0000000000000..c6aa3b9ac6596 --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/build_request.go @@ -0,0 +1,141 @@ +package saml2 + +import ( + "bytes" + "compress/flate" + "encoding/base64" + "fmt" + "net/http" + "net/url" + + "github.com/beevik/etree" + "github.com/satori/go.uuid" +) + +const issueInstantFormat = "2006-01-02T15:04:05Z" + +func (sp *SAMLServiceProvider) BuildAuthRequestDocument() (*etree.Document, error) { + authnRequest := &etree.Element{ + Space: "samlp", + Tag: "AuthnRequest", + } + + authnRequest.CreateAttr("xmlns:samlp", "urn:oasis:names:tc:SAML:2.0:protocol") + authnRequest.CreateAttr("xmlns:saml", "urn:oasis:names:tc:SAML:2.0:assertion") + + authnRequest.CreateAttr("ID", "_"+uuid.NewV4().String()) + authnRequest.CreateAttr("Version", "2.0") + authnRequest.CreateAttr("ProtocolBinding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST") + authnRequest.CreateAttr("AssertionConsumerServiceURL", sp.AssertionConsumerServiceURL) + authnRequest.CreateAttr("IssueInstant", sp.Clock.Now().UTC().Format(issueInstantFormat)) + authnRequest.CreateAttr("Destination", sp.IdentityProviderSSOURL) + + // NOTE(russell_h): In earlier versions we mistakenly sent the IdentityProviderIssuer + // in the AuthnRequest. For backwards compatibility we will fall back to that + // behavior when ServiceProviderIssuer isn't set. + if sp.ServiceProviderIssuer != "" { + authnRequest.CreateElement("saml:Issuer").SetText(sp.ServiceProviderIssuer) + } else { + authnRequest.CreateElement("saml:Issuer").SetText(sp.IdentityProviderIssuer) + } + + nameIdPolicy := authnRequest.CreateElement("samlp:NameIDPolicy") + nameIdPolicy.CreateAttr("AllowCreate", "true") + nameIdPolicy.CreateAttr("Format", sp.NameIdFormat) + + if sp.RequestedAuthnContext != nil { + requestedAuthnContext := authnRequest.CreateElement("samlp:RequestedAuthnContext") + requestedAuthnContext.CreateAttr("Comparison", sp.RequestedAuthnContext.Comparison) + + for _, context := range sp.RequestedAuthnContext.Contexts { + authnContextClassRef := requestedAuthnContext.CreateElement("saml:AuthnContextClassRef") + authnContextClassRef.SetText(context) + } + } + + doc := etree.NewDocument() + + if sp.SignAuthnRequests { + ctx := sp.SigningContext() + signed, err := ctx.SignEnveloped(authnRequest) + if err != nil { + return nil, err + } + + doc.SetRoot(signed) + } else { + doc.SetRoot(authnRequest) + } + return doc, nil +} + +// BuildAuthRequest builds for identity provider +func (sp *SAMLServiceProvider) BuildAuthRequest() (string, error) { + doc, err := sp.BuildAuthRequestDocument() + if err != nil { + return "", err + } + return doc.WriteToString() +} + +func (sp *SAMLServiceProvider) BuildAuthURLFromDocument(relayState string, doc *etree.Document) (string, error) { + parsedUrl, err := url.Parse(sp.IdentityProviderSSOURL) + if err != nil { + return "", err + } + + authnRequest, err := doc.WriteToString() + if err != nil { + return "", err + } + + buf := &bytes.Buffer{} + + fw, err := flate.NewWriter(buf, flate.DefaultCompression) + if err != nil { + return "", fmt.Errorf("flate NewWriter error: %v", err) + } + + _, err = fw.Write([]byte(authnRequest)) + if err != nil { + return "", fmt.Errorf("flate.Writer Write error: %v", err) + } + + err = fw.Close() + if err != nil { + return "", fmt.Errorf("flate.Writer Close error: %v", err) + } + + qs := parsedUrl.Query() + + qs.Add("SAMLRequest", base64.StdEncoding.EncodeToString(buf.Bytes())) + + if relayState != "" { + qs.Add("RelayState", relayState) + } + + parsedUrl.RawQuery = qs.Encode() + return parsedUrl.String(), nil +} + +// BuildAuthURL builds redirect URL to be sent to principal +func (sp *SAMLServiceProvider) BuildAuthURL(relayState string) (string, error) { + doc, err := sp.BuildAuthRequestDocument() + if err != nil { + return "", err + } + return sp.BuildAuthURLFromDocument(relayState, doc) +} + +// AuthRedirect takes a ResponseWriter and Request from an http interaction and +// redirects to the SAMLServiceProvider's configured IdP, including the +// relayState provided, if any. +func (sp *SAMLServiceProvider) AuthRedirect(w http.ResponseWriter, r *http.Request, relayState string) (err error) { + url, err := sp.BuildAuthURL(relayState) + if err != nil { + return err + } + + http.Redirect(w, r, url, http.StatusFound) + return nil +} diff --git a/vendor/github.com/russellhaering/gosaml2/decode_response.go b/vendor/github.com/russellhaering/gosaml2/decode_response.go new file mode 100644 index 0000000000000..06e5d9799b4df --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/decode_response.go @@ -0,0 +1,216 @@ +package saml2 + +import ( + "bytes" + "compress/flate" + "crypto/tls" + "encoding/base64" + "fmt" + "io/ioutil" + + "encoding/xml" + + "github.com/beevik/etree" + "github.com/russellhaering/gosaml2/types" + dsig "github.com/russellhaering/goxmldsig" + "github.com/russellhaering/goxmldsig/etreeutils" +) + +func (sp *SAMLServiceProvider) validationContext() *dsig.ValidationContext { + ctx := dsig.NewDefaultValidationContext(sp.IDPCertificateStore) + ctx.Clock = sp.Clock + return ctx +} + +// validateResponseAttributes validates a SAML Response's tag and attributes. It does +// not inspect child elements of the Response at all. +func (sp *SAMLServiceProvider) validateResponseAttributes(response *types.Response) error { + if response.Destination != sp.AssertionConsumerServiceURL { + return ErrInvalidValue{ + Key: DestinationAttr, + Expected: sp.AssertionConsumerServiceURL, + Actual: response.Destination, + } + } + + if response.Version != "2.0" { + return ErrInvalidValue{ + Reason: ReasonUnsupported, + Key: "SAML version", + Expected: "2.0", + Actual: response.Version, + } + } + + return nil +} + +func (sp *SAMLServiceProvider) unmarshalResponse(el *etree.Element) (*types.Response, error) { + response := &types.Response{} + + doc := etree.NewDocument() + doc.SetRoot(el) + data, err := doc.WriteToBytes() + if err != nil { + return nil, err + } + + err = xml.Unmarshal(data, response) + if err != nil { + return nil, err + } + + return response, nil +} + +func (sp *SAMLServiceProvider) getDecryptCert() (*tls.Certificate, error) { + if sp.SPKeyStore == nil { + return nil, fmt.Errorf("no decryption certs available") + } + + //This is the tls.Certificate we'll use to decrypt any encrypted assertions + var decryptCert tls.Certificate + + switch crt := sp.SPKeyStore.(type) { + case dsig.TLSCertKeyStore: + // Get the tls.Certificate directly if possible + decryptCert = tls.Certificate(crt) + + default: + + //Otherwise, construct one from the results of GetKeyPair + pk, cert, err := sp.SPKeyStore.GetKeyPair() + if err != nil { + return nil, fmt.Errorf("error getting keypair: %v", err) + } + + decryptCert = tls.Certificate{ + Certificate: [][]byte{cert}, + PrivateKey: pk, + } + } + + return &decryptCert, nil +} + +func (sp *SAMLServiceProvider) decryptAssertions(response *types.Response) error { + for _, ea := range response.EncryptedAssertions { + decryptCert, err := sp.getDecryptCert() + if err != nil { + return err + } + + assertion, err := ea.Decrypt(decryptCert) + if err != nil { + return err + } + + response.Assertions = append(response.Assertions, *assertion) + } + + return nil +} + +func (sp *SAMLServiceProvider) validateElementSignature(el *etree.Element) (*etree.Element, error) { + return sp.validationContext().Validate(el) +} + +func (sp *SAMLServiceProvider) validateAssertionSignatures(el *etree.Element) error { + validateAssertion := func(ctx etreeutils.NSContext, unverifiedAssertion *etree.Element) error { + if unverifiedAssertion.Parent() != el { + return fmt.Errorf("found assertion with unexpected parent element: %s", unverifiedAssertion.Parent().Tag) + } + + detatched, err := etreeutils.NSDetatch(ctx, unverifiedAssertion) + if err != nil { + return err + } + + assertion, err := sp.validationContext().Validate(detatched) + if err != nil { + return err + } + + // Replace the original unverified Assertion with the verified one. Note that + // at this point only the Assertion (and not the parent Response) can be trusted + // as having been signed by the IdP. + if el.RemoveChild(unverifiedAssertion) == nil { + // Out of an abundance of caution, check to make sure an Assertion was actually + // removed. If it wasn't a programming error has occurred. + panic("unable to remove assertion") + } + + el.AddChild(assertion) + + return nil + } + + return etreeutils.NSFindIterate(el, SAMLAssertionNamespace, AssertionTag, validateAssertion) +} + +//ValidateEncodedResponse both decodes and validates, based on SP +//configuration, an encoded, signed response. It will also appropriately +//decrypt a response if the assertion was encrypted +func (sp *SAMLServiceProvider) ValidateEncodedResponse(encodedResponse string) (*types.Response, error) { + raw, err := base64.StdEncoding.DecodeString(encodedResponse) + if err != nil { + return nil, err + } + + doc := etree.NewDocument() + err = doc.ReadFromBytes(raw) + if err != nil { + // Attempt to inflate the response in case it happens to be compressed (as with one case at saml.oktadev.com) + buf, err := ioutil.ReadAll(flate.NewReader(bytes.NewReader(raw))) + if err != nil { + return nil, err + } + + doc = etree.NewDocument() + err = doc.ReadFromBytes(buf) + if err != nil { + return nil, err + } + } + + if doc.Root() == nil { + return nil, fmt.Errorf("unable to parse response") + } + + el := doc.Root() + + if !sp.SkipSignatureValidation { + el, err = sp.validateElementSignature(el) + if err == dsig.ErrMissingSignature { + // The Response wasn't signed. It is possible that the Assertion inside of + // the Response was signed. + + // Unfortunately we just blew away our Response + el = doc.Root() + + err = sp.validateAssertionSignatures(el) + if err != nil { + return nil, err + } + } else if err != nil || el == nil { + return nil, err + } + } + + decodedResponse, err := sp.unmarshalResponse(el) + if err != nil { + return nil, err + } + + err = sp.decryptAssertions(decodedResponse) + if err != nil { + return nil, err + } + + err = sp.Validate(decodedResponse) + if err != nil { + return nil, err + } + + return decodedResponse, nil +} diff --git a/vendor/github.com/russellhaering/gosaml2/retrieve_assertion.go b/vendor/github.com/russellhaering/gosaml2/retrieve_assertion.go new file mode 100644 index 0000000000000..58815212478b5 --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/retrieve_assertion.go @@ -0,0 +1,93 @@ +package saml2 + +import "fmt" + +//ErrMissingElement is the error type that indicates an element and/or attribute is +//missing. It provides a structured error that can be more appropriately acted +//upon. +type ErrMissingElement struct { + Tag, Attribute string +} + +type ErrVerification struct { + Cause error +} + +func (e ErrVerification) Error() string { + return fmt.Sprintf("error validating response: %s", e.Cause.Error()) +} + +//ErrMissingAssertion indicates that an appropriate assertion element could not +//be found in the SAML Response +var ( + ErrMissingAssertion = ErrMissingElement{Tag: AssertionTag} +) + +func (e ErrMissingElement) Error() string { + if e.Attribute != "" { + return fmt.Sprintf("missing %s attribute on %s element", e.Attribute, e.Tag) + } + return fmt.Sprintf("missing %s element", e.Tag) +} + +//RetrieveAssertionInfo takes an encoded response and returns the AssertionInfo +//contained, or an error message if an error has been encountered. +func (sp *SAMLServiceProvider) RetrieveAssertionInfo(encodedResponse string) (*AssertionInfo, error) { + assertionInfo := &AssertionInfo{ + Values: make(Values), + } + + response, err := sp.ValidateEncodedResponse(encodedResponse) + if err != nil { + return nil, ErrVerification{Cause: err} + } + + // TODO: Support multiple assertions + if len(response.Assertions) == 0 { + return nil, ErrMissingAssertion + } + + assertion := response.Assertions[0] + + warningInfo, err := sp.VerifyAssertionConditions(&assertion) + if err != nil { + return nil, err + } + + //Get the NameID + subject := assertion.Subject + if subject == nil { + return nil, ErrMissingElement{Tag: SubjectTag} + } + + nameID := subject.NameID + if nameID == nil { + return nil, ErrMissingElement{Tag: NameIdTag} + } + + assertionInfo.NameID = nameID.Value + + //Get the actual assertion attributes + attributeStatement := assertion.AttributeStatement + if attributeStatement == nil && !sp.AllowMissingAttributes { + return nil, ErrMissingElement{Tag: AttributeStatementTag} + } + + if attributeStatement != nil { + for _, attribute := range attributeStatement.Attributes { + assertionInfo.Values[attribute.Name] = attribute + } + } + + if assertion.AuthnStatement != nil { + if assertion.AuthnStatement.AuthnInstant != nil { + assertionInfo.AuthnInstant = assertion.AuthnStatement.AuthnInstant + } + if assertion.AuthnStatement.SessionNotOnOrAfter != nil { + assertionInfo.SessionNotOnOrAfter = assertion.AuthnStatement.SessionNotOnOrAfter + } + } + + assertionInfo.WarningInfo = warningInfo + return assertionInfo, nil +} diff --git a/vendor/github.com/russellhaering/gosaml2/saml.go b/vendor/github.com/russellhaering/gosaml2/saml.go new file mode 100644 index 0000000000000..c9a870a6da80e --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/saml.go @@ -0,0 +1,87 @@ +package saml2 + +import ( + "sync" + "time" + + dsig "github.com/russellhaering/goxmldsig" +) + +type SAMLServiceProvider struct { + IdentityProviderSSOURL string + IdentityProviderIssuer string + + AssertionConsumerServiceURL string + ServiceProviderIssuer string + + SignAuthnRequests bool + SignAuthnRequestsAlgorithm string + + // RequestedAuthnContext allows service providers to require that the identity + // provider use specific authentication mechanisms. Leaving this unset will + // permit the identity provider to choose the auth method. To maximize compatibility + // with identity providers it is recommended to leave this unset. + RequestedAuthnContext *RequestedAuthnContext + AudienceURI string + IDPCertificateStore dsig.X509CertificateStore + SPKeyStore dsig.X509KeyStore + NameIdFormat string + SkipSignatureValidation bool + AllowMissingAttributes bool + Clock *dsig.Clock + signingContextMu sync.RWMutex + signingContext *dsig.SigningContext +} + +// RequestedAuthnContext controls which authentication mechanisms are requested of +// the identity provider. It is generally sufficient to omit this and let the +// identity provider select an authentication mechansim. +type RequestedAuthnContext struct { + // The RequestedAuthnContext comparison policy to use. See the section 3.3.2.2.1 + // of the SAML 2.0 specification for details. Constants named AuthnPolicyMatch* + // contain standardized values. + Comparison string + + // Contexts will be passed as AuthnContextClassRefs. For example, to force password + // authentication on some identity providers, Contexts should have a value of + // []string{AuthnContextPasswordProtectedTransport}, and Comparison should have a + // value of AuthnPolicyMatchExact. + Contexts []string +} + +func (sp *SAMLServiceProvider) SigningContext() *dsig.SigningContext { + sp.signingContextMu.RLock() + signingContext := sp.signingContext + sp.signingContextMu.RUnlock() + + if signingContext != nil { + return signingContext + } + + sp.signingContextMu.Lock() + defer sp.signingContextMu.Unlock() + + sp.signingContext = dsig.NewDefaultSigningContext(sp.SPKeyStore) + sp.signingContext.SetSignatureMethod(sp.SignAuthnRequestsAlgorithm) + return sp.signingContext +} + +type ProxyRestriction struct { + Count int + Audience []string +} + +type WarningInfo struct { + OneTimeUse bool + ProxyRestriction *ProxyRestriction + NotInAudience bool + InvalidTime bool +} + +type AssertionInfo struct { + NameID string + Values Values + WarningInfo *WarningInfo + AuthnInstant *time.Time + SessionNotOnOrAfter *time.Time +} diff --git a/vendor/github.com/russellhaering/gosaml2/test_constants.go b/vendor/github.com/russellhaering/gosaml2/test_constants.go new file mode 100644 index 0000000000000..a35c5cde446fa --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/test_constants.go @@ -0,0 +1,376 @@ +package saml2 + +var idpCertificate = ` +-----BEGIN CERTIFICATE----- +MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+a +-----END CERTIFICATE----- +` + +const rawResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7http://www.okta.com/exk5zt0r12Edi4rD20h7FsWGCBC+t/LaVkUKUvRQpzyZTmlxUzw4R9FOzXPPJRw=hS50WgYs/cn3uxmhrza/0/0QW3H7bwdjPZ2hQmG7IeSd7awTOghBqdrjvaPfQ7tRW+UK6ewMgIBVKG6jV3qYAWeW2U70hMb7hE9qJqBKyYyimmhVWULx1HB2YmlU1wmispywoPlXQ6gj0iWaL2RFI83vUp7X50eZ6dELqoJVZpzQI065Tt0TG7UuKUW1flYsbiS9NaXnuw+mcrBW25ZA9F5CLePHki01ZzUw+XtNmKthEb7SR30mzPoj08Dji22daYvGu82IR01wIZPoQJPCGMT6y2xC/pQPqGljAg/vUa+gaYgaMaAVYxhk/hfgMUBlOeKACBaGTmygab1Nz5KvPg==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com123urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.comphoebesimon` + +const manInTheMiddledResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7ijTqmVmDy7ssK+rvmJaCQ6AQaFaXz+HIN/r6O37B0eQ=G09fAYXGDLK+/jAekHsNL0RLo40Xm6+VwXmUj0IDIrvIIv/mJU5VD6ylOLnPezLDBVY9BJst1YCz+8krdvmQ8Stkd6qiN2bN/5KpCdika111YGpeNdMmg/E57ZG3S895hTNJQYOfCwhPFUtQuXLkspOaw81pcqOTr+bVSofJ8uQP7cVQa/ANxbjKAj0fhAuxAvZfiqPms5Stv4sNGpzULUDJl87CoEleHExGmpTsI7Qt3EvGToPMZXPHF4MGvuC0Z2ZD4iI6Pr7xk98t54PJtAX2qJu1tZqBJmL0Qcq5spl9W3yC1tAZuDeFLm1C4/T9crO2Q5WILP/tkw/yJ+ZttQ==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+ahttp://www.okta.com/exk5zt0r12Edi4rD20h7zln6sheEO2JBdanrT5mZtJZ192tGHavuBpCFHQsJFVg=dHh6TWbnjtImyrfjPTX5QzE/6Vm/HsRWVvWWlvFAddf/CvhO4Kc5j8C7hvQoYMLhYuZMFFSReGysuDy5IscOJwTGhhcvb238qHSGGs6q8OUBCsmLSDAbIaGA++LV/tkUZ2ridGIi0yT81UOl1oT1batlHsK3eMyxkpnFmvBzIm4tGTzRkOPpYRLeiM9bxbKI+DM/623DCXyBCLYBzJo1O6QE02aLajwRMi/vmiV4LSiGlFcY9TtDCafdVJRv0tIQ25BQoT4feuHdr6S8xOSpGgRYH5ECamVOt4e079XdEkVUiSzQokiUkgDlTXEyerPLOVsOk4PW5nRs86sXIiGL5w==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiH9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com123urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.com` + +const alteredReferenceURIResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7ijTqmVmDy7ssK+rvmJaCQ6AQaFaXz+HIN/r6O37B0eQ=G09fAYXGDLK+/jAekHsNL0RLo40Xm6+VwXmUj0IDIrvIIv/mJU5VD6ylOLnPezLDBVY9BJst1YCz+8krdvmQ8Stkd6qiN2bN/5KpCdika111YGpeNdMmg/E57ZG3S895hTNJQYOfCwhPFUtQuXLkspOaw81pcqOTr+bVSofJ8uQP7cVQa/ANxbjKAj0fhAuxAvZfiqPms5Stv4sNGpzULUDJl87CoEleHExGmpTsI7Qt3EvGToPMZXPHF4MGvuC0Z2ZD4iI6Pr7xk98t54PJtAX2qJu1tZqBJmL0Qcq5spl9W3yC1tAZuDeFLm1C4/T9crO2Q5WILP/tkw/yJ+ZttQ==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+ahttp://www.okta.com/exk5zt0r12Edi4rD20h7zln6sheEO2JBdanrT5mZtJZ192tGHavuBpCFHQsJFVg=dHh6TWbnjtImyrfjPTX5QzE/6Vm/HsRWVvWWlvFAddf/CvhO4Kc5j8C7hvQoYMLhYuZMFFSReGysuDy5IscOJwTGhhcvb238qHSGGs6q8OUBCsmLSDAbIaGA++LV/tkUZ2ridGIi0yT81UOl1oT1batlHsK3eMyxkpnFmvBzIm4tGTzRkOPpYRLeiM9bxbKI+DM/623DCXyBCLYBzJo1O6QE02aLajwRMi/vmiV4LSiGlFcY9TtDCafdVJRv0tIQ25BQoT4feuHdr6S8xOSpGgRYH5ECamVOt4e079XdEkVUiSzQokiUkgDlTXEyerPLOVsOk4PW5nRs86sXIiGL5w==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiH9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com123urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.com` + +const alteredSignedInfoResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7ijTqmVmDy7ssK+rvmJaCQ6AQaFaXz+HIN/r6O37B0eQ=G09fAYXGDLK+/jAekHsNL0RLo40Xm6+VwXmUj0IDIrvIIv/mJU5VD6ylOLnPezLDBVY9BJst1YCz+8krdvmQ8Stkd6qiN2bN/5KpCdika111YGpeNdMmg/E57ZG3S895hTNJQYOfCwhPFUtQuXLkspOaw81pcqOTr+bVSofJ8uQP7cVQa/ANxbjKAj0fhAuxAvZfiqPms5Stv4sNGpzULUDJl87CoEleHExGmpTsI7Qt3EvGToPMZXPHF4MGvuC0Z2ZD4iI6Pr7xk98t54PJtAX2qJu1tZqBJmL0Qcq5spl9W3yC1tAZuDeFLm1C4/T9crO2Q5WILP/tkw/yJ+ZttQ==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEV +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+ahttp://www.okta.com/exk5zt0r12Edi4rD20h7zln6sheEO2JBdanrT5mZtJZ192tGHavuBpCFHQsJFVg=dHh6TWbnjtImyrfjPTX5QzE/6Vm/HsRWVvWWlvFAddf/CvhO4Kc5j8C7hvQoYMLhYuZMFFSReGysuDy5IscOJwTGhhcvb238qHSGGs6q8OUBCsmLSDAbIaGA++LV/tkUZ2ridGIi0yT81UOl1oT1batlHsK3eMyxkpnFmvBzIm4tGTzRkOPpYRLeiM9bxbKI+DM/623DCXyBCLYBzJo1O6QE02aLajwRMi/vmiV4LSiGlFcY9TtDCafdVJRv0tIQ25BQoT4feuHdr6S8xOSpGgRYH5ECamVOt4e079XdEkVUiSzQokiUkgDlTXEyerPLOVsOk4PW5nRs86sXIiGL5w==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiH9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com123urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.com` + +const alteredRecipientResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7http://www.okta.com/exk5zt0r12Edi4rD20h7FsWGCBC+t/LaVkUKUvRQpzyZTmlxUzw4R9FOzXPPJRw=hS50WgYs/cn3uxmhrza/0/0QW3H7bwdjPZ2hQmG7IeSd7awTOghBqdrjvaPfQ7tRW+UK6ewMgIBVKG6jV3qYAWeW2U70hMb7hE9qJqBKyYyimmhVWULx1HB2YmlU1wmispywoPlXQ6gj0iWaL2RFI83vUp7X50eZ6dELqoJVZpzQI065Tt0TG7UuKUW1flYsbiS9NaXnuw+mcrBW25ZA9F5CLePHki01ZzUw+XtNmKthEb7SR30mzPoj08Dji22daYvGu82IR01wIZPoQJPCGMT6y2xC/pQPqGljAg/vUa+gaYgaMaAVYxhk/hfgMUBlOeKACBaGTmygab1Nz5KvPg==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com123urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.comphoebe.simon@scaleft.com` + +const alteredSubjectConfirmationMethodResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7http://www.okta.com/exk5zt0r12Edi4rD20h7FsWGCBC+t/LaVkUKUvRQpzyZTmlxUzw4R9FOzXPPJRw=hS50WgYs/cn3uxmhrza/0/0QW3H7bwdjPZ2hQmG7IeSd7awTOghBqdrjvaPfQ7tRW+UK6ewMgIBVKG6jV3qYAWeW2U70hMb7hE9qJqBKyYyimmhVWULx1HB2YmlU1wmispywoPlXQ6gj0iWaL2RFI83vUp7X50eZ6dELqoJVZpzQI065Tt0TG7UuKUW1flYsbiS9NaXnuw+mcrBW25ZA9F5CLePHki01ZzUw+XtNmKthEb7SR30mzPoj08Dji22daYvGu82IR01wIZPoQJPCGMT6y2xC/pQPqGljAg/vUa+gaYgaMaAVYxhk/hfgMUBlOeKACBaGTmygab1Nz5KvPg==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com123urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.comphoebe.simon@scaleft.com` + +const alteredDestinationResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7http://www.okta.com/exk5zt0r12Edi4rD20h7FsWGCBC+t/LaVkUKUvRQpzyZTmlxUzw4R9FOzXPPJRw=hS50WgYs/cn3uxmhrza/0/0QW3H7bwdjPZ2hQmG7IeSd7awTOghBqdrjvaPfQ7tRW+UK6ewMgIBVKG6jV3qYAWeW2U70hMb7hE9qJqBKyYyimmhVWULx1HB2YmlU1wmispywoPlXQ6gj0iWaL2RFI83vUp7X50eZ6dELqoJVZpzQI065Tt0TG7UuKUW1flYsbiS9NaXnuw+mcrBW25ZA9F5CLePHki01ZzUw+XtNmKthEb7SR30mzPoj08Dji22daYvGu82IR01wIZPoQJPCGMT6y2xC/pQPqGljAg/vUa+gaYgaMaAVYxhk/hfgMUBlOeKACBaGTmygab1Nz5KvPg==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com123urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.comphoebe.simon@scaleft.com` + +const alteredVersionResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7http://www.okta.com/exk5zt0r12Edi4rD20h7FsWGCBC+t/LaVkUKUvRQpzyZTmlxUzw4R9FOzXPPJRw=hS50WgYs/cn3uxmhrza/0/0QW3H7bwdjPZ2hQmG7IeSd7awTOghBqdrjvaPfQ7tRW+UK6ewMgIBVKG6jV3qYAWeW2U70hMb7hE9qJqBKyYyimmhVWULx1HB2YmlU1wmispywoPlXQ6gj0iWaL2RFI83vUp7X50eZ6dELqoJVZpzQI065Tt0TG7UuKUW1flYsbiS9NaXnuw+mcrBW25ZA9F5CLePHki01ZzUw+XtNmKthEb7SR30mzPoj08Dji22daYvGu82IR01wIZPoQJPCGMT6y2xC/pQPqGljAg/vUa+gaYgaMaAVYxhk/hfgMUBlOeKACBaGTmygab1Nz5KvPg==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com123urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.comphoebe.simon@scaleft.com` + +const missingIDResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7mj+xyS5DtKVNbbFq4caWhGcrirqNzv7mIHNzHQH/f60=GA1URoMOE5EFfkHYimGXm7Ecph/m0s135VyF9Wut6NSpuZdQ2crM1IslvKCRjkE09rZgagQQMAThUcOFuX35dZPz9J4Ihpt1juhfGv1AV8I8jiOKFETj65MiPabDEi8+P6YWf4qNujAJXHKJIa/MFXBqoKR/imLQT8eu1nhVBQGYqWwZePddfXO2JYk2ce7mtnyMT0dUVb+o+tlEDYa7ri9fj4JL/z1XX7yrbVZxn2mdKPJtSSP8uHNOWSM6j1vp4oK+KSDviBfiVLlVA58noz5GyFtp642h+LV2quKbncMFfnfB1kfHLK/xaz9UaDBy+bHK4oGzSpVhZqcOzzliKA==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+ahttp://www.okta.com/exk5zt0r12Edi4rD20h7FsWGCBC+t/LaVkUKUvRQpzyZTmlxUzw4R9FOzXPPJRw=hS50WgYs/cn3uxmhrza/0/0QW3H7bwdjPZ2hQmG7IeSd7awTOghBqdrjvaPfQ7tRW+UK6ewMgIBVKG6jV3qYAWeW2U70hMb7hE9qJqBKyYyimmhVWULx1HB2YmlU1wmispywoPlXQ6gj0iWaL2RFI83vUp7X50eZ6dELqoJVZpzQI065Tt0TG7UuKUW1flYsbiS9NaXnuw+mcrBW25ZA9F5CLePHki01ZzUw+XtNmKthEb7SR30mzPoj08Dji22daYvGu82IR01wIZPoQJPCGMT6y2xC/pQPqGljAg/vUa+gaYgaMaAVYxhk/hfgMUBlOeKACBaGTmygab1Nz5KvPg==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com123urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.comphoebe.simon@scaleft.com` + +const assertionInfoResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7http://www.okta.com/exk5zt0r12Edi4rD20h7FsWGCBC+t/LaVkUKUvRQpzyZTmlxUzw4R9FOzXPPJRw=hS50WgYs/cn3uxmhrza/0/0QW3H7bwdjPZ2hQmG7IeSd7awTOghBqdrjvaPfQ7tRW+UK6ewMgIBVKG6jV3qYAWeW2U70hMb7hE9qJqBKyYyimmhVWULx1HB2YmlU1wmispywoPlXQ6gj0iWaL2RFI83vUp7X50eZ6dELqoJVZpzQI065Tt0TG7UuKUW1flYsbiS9NaXnuw+mcrBW25ZA9F5CLePHki01ZzUw+XtNmKthEb7SR30mzPoj08Dji22daYvGu82IR01wIZPoQJPCGMT6y2xC/pQPqGljAg/vUa+gaYgaMaAVYxhk/hfgMUBlOeKACBaGTmygab1Nz5KvPg==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com123urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.comphoebe.simon@scaleft.com` + +const assertionInfoModifiedAudienceResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7http://www.okta.com/exk5zt0r12Edi4rD20h7FsWGCBC+t/LaVkUKUvRQpzyZTmlxUzw4R9FOzXPPJRw=hS50WgYs/cn3uxmhrza/0/0QW3H7bwdjPZ2hQmG7IeSd7awTOghBqdrjvaPfQ7tRW+UK6ewMgIBVKG6jV3qYAWeW2U70hMb7hE9qJqBKyYyimmhVWULx1HB2YmlU1wmispywoPlXQ6gj0iWaL2RFI83vUp7X50eZ6dELqoJVZpzQI065Tt0TG7UuKUW1flYsbiS9NaXnuw+mcrBW25ZA9F5CLePHki01ZzUw+XtNmKthEb7SR30mzPoj08Dji22daYvGu82IR01wIZPoQJPCGMT6y2xC/pQPqGljAg/vUa+gaYgaMaAVYxhk/hfgMUBlOeKACBaGTmygab1Nz5KvPg==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com124urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.comphoebe.simon@scaleft.com` + +const assertionInfoOneTimeUseResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7http://www.okta.com/exk5zt0r12Edi4rD20h7FsWGCBC+t/LaVkUKUvRQpzyZTmlxUzw4R9FOzXPPJRw=hS50WgYs/cn3uxmhrza/0/0QW3H7bwdjPZ2hQmG7IeSd7awTOghBqdrjvaPfQ7tRW+UK6ewMgIBVKG6jV3qYAWeW2U70hMb7hE9qJqBKyYyimmhVWULx1HB2YmlU1wmispywoPlXQ6gj0iWaL2RFI83vUp7X50eZ6dELqoJVZpzQI065Tt0TG7UuKUW1flYsbiS9NaXnuw+mcrBW25ZA9F5CLePHki01ZzUw+XtNmKthEb7SR30mzPoj08Dji22daYvGu82IR01wIZPoQJPCGMT6y2xC/pQPqGljAg/vUa+gaYgaMaAVYxhk/hfgMUBlOeKACBaGTmygab1Nz5KvPg==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com123urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.comphoebe.simon@scaleft.com` + +const assertionInfoProxyRestrictionResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7http://www.okta.com/exk5zt0r12Edi4rD20h7FsWGCBC+t/LaVkUKUvRQpzyZTmlxUzw4R9FOzXPPJRw=hS50WgYs/cn3uxmhrza/0/0QW3H7bwdjPZ2hQmG7IeSd7awTOghBqdrjvaPfQ7tRW+UK6ewMgIBVKG6jV3qYAWeW2U70hMb7hE9qJqBKyYyimmhVWULx1HB2YmlU1wmispywoPlXQ6gj0iWaL2RFI83vUp7X50eZ6dELqoJVZpzQI065Tt0TG7UuKUW1flYsbiS9NaXnuw+mcrBW25ZA9F5CLePHki01ZzUw+XtNmKthEb7SR30mzPoj08Dji22daYvGu82IR01wIZPoQJPCGMT6y2xC/pQPqGljAg/vUa+gaYgaMaAVYxhk/hfgMUBlOeKACBaGTmygab1Nz5KvPg==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com123123urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.comphoebe.simon@scaleft.com` + +const assertionInfoProxyRestrictionNoCountResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7http://www.okta.com/exk5zt0r12Edi4rD20h7FsWGCBC+t/LaVkUKUvRQpzyZTmlxUzw4R9FOzXPPJRw=hS50WgYs/cn3uxmhrza/0/0QW3H7bwdjPZ2hQmG7IeSd7awTOghBqdrjvaPfQ7tRW+UK6ewMgIBVKG6jV3qYAWeW2U70hMb7hE9qJqBKyYyimmhVWULx1HB2YmlU1wmispywoPlXQ6gj0iWaL2RFI83vUp7X50eZ6dELqoJVZpzQI065Tt0TG7UuKUW1flYsbiS9NaXnuw+mcrBW25ZA9F5CLePHki01ZzUw+XtNmKthEb7SR30mzPoj08Dji22daYvGu82IR01wIZPoQJPCGMT6y2xC/pQPqGljAg/vUa+gaYgaMaAVYxhk/hfgMUBlOeKACBaGTmygab1Nz5KvPg==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com123123urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.comphoebe.simon@scaleft.com` + +const assertionInfoProxyRestrictionNoAudienceResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7http://www.okta.com/exk5zt0r12Edi4rD20h7FsWGCBC+t/LaVkUKUvRQpzyZTmlxUzw4R9FOzXPPJRw=hS50WgYs/cn3uxmhrza/0/0QW3H7bwdjPZ2hQmG7IeSd7awTOghBqdrjvaPfQ7tRW+UK6ewMgIBVKG6jV3qYAWeW2U70hMb7hE9qJqBKyYyimmhVWULx1HB2YmlU1wmispywoPlXQ6gj0iWaL2RFI83vUp7X50eZ6dELqoJVZpzQI065Tt0TG7UuKUW1flYsbiS9NaXnuw+mcrBW25ZA9F5CLePHki01ZzUw+XtNmKthEb7SR30mzPoj08Dji22daYvGu82IR01wIZPoQJPCGMT6y2xC/pQPqGljAg/vUa+gaYgaMaAVYxhk/hfgMUBlOeKACBaGTmygab1Nz5KvPg==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com123urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.comphoebe.simon@scaleft.com` + +const exampleBase64 = `PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cDovL2xvY2FsaG9zdDo4MDgwL3YxL19zYW1sX2NhbGxiYWNrIiBJRD0iaWQxMDM1MzI4MDQ2NDc3ODc5NzUzODEzMjUiIEluUmVzcG9uc2VUbz0iXzg2OTljNjU1LWM0ODItNDUxYS05YjdmLTYxNjY4ZjE0MGI0NyIgSXNzdWVJbnN0YW50PSIyMDE2LTAzLTE2VDAxOjAyOjU3LjY4MloiIFZlcnNpb249IjIuMCI+PHNhbWwyOklzc3VlciB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDplbnRpdHkiPmh0dHA6Ly93d3cub2t0YS5jb20vZXhrNXp0MHIxMkVkaTRyRDIwaDc8L3NhbWwyOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8+PGRzOlJlZmVyZW5jZSBVUkk9IiNpZDEwMzUzMjgwNDY0Nzc4Nzk3NTM4MTMyNSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjc2hhMjU2Ii8+PGRzOkRpZ2VzdFZhbHVlPm5wVEFsNmtyYWtzQmxDUmx1bmJ5RDZuSUNUY2ZzRGFIalBYVnhvRFBydzA9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPlNiQjAzZkkxVFZzdEo3cTFCNlh4OFlSR2tEcE5ROGFyNHpGM3AzYWlra2NxOFRUUzBlUjI4Rm9RdU4xSFg3MlBuMnJjY0U0T05pellOUzYvcnZybHlWL1NsWFhtQzltaFRMUlBlSno1bXJ4anFPNVFZRDFZM0l6bW5rZlE2S3V0dWtrY0dPSkVwYTN2WWVzZjVKS1JTKzBXR1J0ek9TNHdKRjE4b0dJWitiYThQNmd4bU1yeUE4eEIvZUpneHBmcm1VYkJqUEhMU2ZsamViaDg4RWlOSUQwODhYdVNHeWQrM0RtcFc1QjUyRFFCOGNBeXlPQlJrUlJjcUxGSWd4aWJtdnRJaWVxdVUwYTJuY29qcHUwKzRvamwrNHdEQ1dkR09FeXF0Sm9UUVhDNHNLUmFVNzlGSzVJRmZFaVlNcXZpRkQwb2F1NHNQajBnbkZDRUY1Rmw0dz09PC9kczpTaWduYXR1cmVWYWx1ZT48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlEcERDQ0FveWdBd0lCQWdJR0FWTElCaEF3TUEwR0NTcUdTSWIzRFFFQkJRVUFNSUdTTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHCkExVUVDQXdLUTJGc2FXWnZjbTVwWVRFV01CUUdBMVVFQnd3TlUyRnVJRVp5WVc1amFYTmpiekVOTUFzR0ExVUVDZ3dFVDJ0MFlURVUKTUJJR0ExVUVDd3dMVTFOUFVISnZkbWxrWlhJeEV6QVJCZ05WQkFNTUNtUmxkaTB4TVRZNE1EY3hIREFhQmdrcWhraUc5dzBCQ1FFVwpEV2x1Wm05QWIydDBZUzVqYjIwd0hoY05NVFl3TWpBNU1qRTFNakEyV2hjTk1qWXdNakE1TWpFMU16QTJXakNCa2pFTE1Ba0dBMVVFCkJoTUNWVk14RXpBUkJnTlZCQWdNQ2tOaGJHbG1iM0p1YVdFeEZqQVVCZ05WQkFjTURWTmhiaUJHY21GdVkybHpZMjh4RFRBTEJnTlYKQkFvTUJFOXJkR0V4RkRBU0JnTlZCQXNNQzFOVFQxQnliM1pwWkdWeU1STXdFUVlEVlFRRERBcGtaWFl0TVRFMk9EQTNNUnd3R2dZSgpLb1pJaHZjTkFRa0JGZzFwYm1adlFHOXJkR0V1WTI5dE1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBCm10akJPWjhNbWhVeWk4Y0drNGRVWTZGajFNRkR0L3EzRkZpYVFwTHp1My9xNWxSVlVOVUJiQXRxUVd3WTEwZHpmWmd1SE91dkE1cDUKUXlpVkR2VWhlK1hrVndOMlIyV2ZBclFKUlRQbkljT2FIcnhxUWYzbzVjQ0lHMjFadHlzRkhKU284Y2xQU09lKzBWc29SZ2NKMWFGNAoyck9Ed2dxUlJaZE85V2gzNTAyWGxKNzk5REpRMjNJQzdYYXNLRXNHS3pKcWhsUnJmZC9GeUl1WlQwc0ZIREtSejVzblNKaG05Z3BOCnVRbENtazdPTloxc1hxdHQrbkJJZldJcWVvWVF1YlBXN3BUNUdUYzd3b3VXcTRUQ2pISmlLOWsySGl5TnhXMEUzSlgwOHN3RVppMisKTFZEamdMek5jNGx3alNZSWozQU90UFpzOHM2MDZvQmRJQm5pNHdJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0SUJBUUJNeFNrSgpUeGtYeHNvS05XMGF3Sk5wV1JiVTgxUXBoZU1GZkVOSXpMYW00SXRjLzVrU1pBYVN5LzllMlFLZm80akJvL01NYkNxMnZNOVR5ZUpRCkRKcFJhaW9VVGQybEdoNFRMVXhBeEN4dFVrL3Bhc2NMKzNObjkzNkxGbVVDTHhheG5iZUd6UE9YQWhzY0N0VTFIMG5Gc1hSbkt4NWEKY1BYWVNLRlpaWmt0aWVTa3d3Mk9pOGRnMkRZYVFoR1FNU0ZNVnFnVmZ3RXU0YnZDUkJ2ZFNpTlhkV0dDWlFtRlZ6QlpaLzlyT0x6UApwdlRGVFBucGthdkptODFGTGxVaGlFL29GZ0tsQ0RMV0RrblNwWEFJMHVaR0VSY3dQY2E2eHZJTWg4NkxqUUtqYlZjaTlGWURTdFhDCnFSbnFRK1RjY1N1L0I2dU9ORnNERW5nR2NYU0tmQithPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWwycDpTdGF0dXMgeG1sbnM6c2FtbDJwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiPjxzYW1sMnA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1sMnA6U3RhdHVzPjxzYW1sMjpBc3NlcnRpb24geG1sbnM6c2FtbDI9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJpZDEwMzUzMjgwNDY1MjY1ODg5MDAwODk0MjQiIElzc3VlSW5zdGFudD0iMjAxNi0wMy0xNlQwMTowMjo1Ny42ODJaIiBWZXJzaW9uPSIyLjAiPjxzYW1sMjpJc3N1ZXIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDplbnRpdHkiIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj5odHRwOi8vd3d3Lm9rdGEuY29tL2V4azV6dDByMTJFZGk0ckQyMGg3PC9zYW1sMjpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+PGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxkc2lnLW1vcmUjcnNhLXNoYTI1NiIvPjxkczpSZWZlcmVuY2UgVVJJPSIjaWQxMDM1MzI4MDQ2NTI2NTg4OTAwMDg5NDI0Ij48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNzaGEyNTYiLz48ZHM6RGlnZXN0VmFsdWU+Tm8xVnlRbGs4WGlmNEZpSitoYVZpd0VReVNJekJhMTRsR3kwY29DbjBjOD08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+VlNWOFZ3NDdxN24vWFp3YVFPUFdRZUtJNVpBNjlmbkdaeUVGaGV4NHh1YUlmQytMT1luZmQ4cThxY1pzbTFNNmt2NDdIL2RSNllYUklNalBLWFpleVgvTUtjbUdQQ2FkcVdGVDdFV0Z2enVPL3V5L0FCL0NMNVpDUWlZOUgvYU9oRHlzTzhnbHNlMVMrWTJLMEN3dnNvUndNZkZpTzJYT1loVk9zbmdVU2tDQmRMSUI2T3E0Zitac0swcncvRTc5bjlRVWQ4b3dEcTNkVkMxOFNGWVlkY0lWRGhRcHBnbHl1QkVaZnUydEcwNmdEOWpsczdaRTh2amNNZkhtaHVIdHhsSDNvdk5MQjM1TkZPL1ZyQ05kRnFtRDc2R25FQTk4Zm9pSnhDWDh2ek5IRjRyUFVGWEFFZGlTNE9kUUF4YjdqTk5Wb0tWWXVhZHVuTHlneXNaR1NnPT08L2RzOlNpZ25hdHVyZVZhbHVlPjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSURwRENDQW95Z0F3SUJBZ0lHQVZMSUJoQXdNQTBHQ1NxR1NJYjNEUUVCQlFVQU1JR1NNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUcKQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVXTUJRR0ExVUVCd3dOVTJGdUlFWnlZVzVqYVhOamJ6RU5NQXNHQTFVRUNnd0VUMnQwWVRFVQpNQklHQTFVRUN3d0xVMU5QVUhKdmRtbGtaWEl4RXpBUkJnTlZCQU1NQ21SbGRpMHhNVFk0TURjeEhEQWFCZ2txaGtpRzl3MEJDUUVXCkRXbHVabTlBYjJ0MFlTNWpiMjB3SGhjTk1UWXdNakE1TWpFMU1qQTJXaGNOTWpZd01qQTVNakUxTXpBMldqQ0JrakVMTUFrR0ExVUUKQmhNQ1ZWTXhFekFSQmdOVkJBZ01Da05oYkdsbWIzSnVhV0V4RmpBVUJnTlZCQWNNRFZOaGJpQkdjbUZ1WTJselkyOHhEVEFMQmdOVgpCQW9NQkU5cmRHRXhGREFTQmdOVkJBc01DMU5UVDFCeWIzWnBaR1Z5TVJNd0VRWURWUVFEREFwa1pYWXRNVEUyT0RBM01Sd3dHZ1lKCktvWklodmNOQVFrQkZnMXBibVp2UUc5cmRHRXVZMjl0TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUEKbXRqQk9aOE1taFV5aThjR2s0ZFVZNkZqMU1GRHQvcTNGRmlhUXBMenUzL3E1bFJWVU5VQmJBdHFRV3dZMTBkemZaZ3VIT3V2QTVwNQpReWlWRHZVaGUrWGtWd04yUjJXZkFyUUpSVFBuSWNPYUhyeHFRZjNvNWNDSUcyMVp0eXNGSEpTbzhjbFBTT2UrMFZzb1JnY0oxYUY0CjJyT0R3Z3FSUlpkTzlXaDM1MDJYbEo3OTlESlEyM0lDN1hhc0tFc0dLekpxaGxScmZkL0Z5SXVaVDBzRkhES1J6NXNuU0pobTlncE4KdVFsQ21rN09OWjFzWHF0dCtuQklmV0lxZW9ZUXViUFc3cFQ1R1RjN3dvdVdxNFRDakhKaUs5azJIaXlOeFcwRTNKWDA4c3dFWmkyKwpMVkRqZ0x6TmM0bHdqU1lJajNBT3RQWnM4czYwNm9CZElCbmk0d0lEQVFBQk1BMEdDU3FHU0liM0RRRUJCUVVBQTRJQkFRQk14U2tKClR4a1h4c29LTlcwYXdKTnBXUmJVODFRcGhlTUZmRU5JekxhbTRJdGMvNWtTWkFhU3kvOWUyUUtmbzRqQm8vTU1iQ3Eydk05VHllSlEKREpwUmFpb1VUZDJsR2g0VExVeEF4Q3h0VWsvcGFzY0wrM05uOTM2TEZtVUNMeGF4bmJlR3pQT1hBaHNjQ3RVMUgwbkZzWFJuS3g1YQpjUFhZU0tGWlpaa3RpZVNrd3cyT2k4ZGcyRFlhUWhHUU1TRk1WcWdWZndFdTRidkNSQnZkU2lOWGRXR0NaUW1GVnpCWlovOXJPTHpQCnB2VEZUUG5wa2F2Sm04MUZMbFVoaUUvb0ZnS2xDRExXRGtuU3BYQUkwdVpHRVJjd1BjYTZ4dklNaDg2TGpRS2piVmNpOUZZRFN0WEMKcVJucVErVGNjU3UvQjZ1T05Gc0RFbmdHY1hTS2ZCK2E8L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT48c2FtbDI6U3ViamVjdCB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI+PHNhbWwyOk5hbWVJRCBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OnVuc3BlY2lmaWVkIj5ydXNzZWxsLmhhZXJpbmdAc2NhbGVmdC5jb208L3NhbWwyOk5hbWVJRD48c2FtbDI6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sMjpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBJblJlc3BvbnNlVG89Il84Njk5YzY1NS1jNDgyLTQ1MWEtOWI3Zi02MTY2OGYxNDBiNDciIE5vdE9uT3JBZnRlcj0iMjAxNi0wMy0xNlQwMTowNzo1Ny42ODJaIiBSZWNpcGllbnQ9Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC92MS9fc2FtbF9jYWxsYmFjayIvPjwvc2FtbDI6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWwyOlN1YmplY3Q+PHNhbWwyOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE2LTAzLTE2VDAwOjU3OjU3LjY4MloiIE5vdE9uT3JBZnRlcj0iMjAxNi0wMy0xNlQwMTowNzo1Ny42ODJaIiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI+PHNhbWwyOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWwyOkF1ZGllbmNlPjEyMzwvc2FtbDI6QXVkaWVuY2U+PC9zYW1sMjpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDI6Q29uZGl0aW9ucz48c2FtbDI6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE2LTAzLTE2VDAxOjAyOjU3LjY4MloiIFNlc3Npb25JbmRleD0iXzg2OTljNjU1LWM0ODItNDUxYS05YjdmLTYxNjY4ZjE0MGI0NyIgeG1sbnM6c2FtbDI9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPjxzYW1sMjpBdXRobkNvbnRleHQ+PHNhbWwyOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkUHJvdGVjdGVkVHJhbnNwb3J0PC9zYW1sMjpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWwyOkF1dGhuQ29udGV4dD48L3NhbWwyOkF1dGhuU3RhdGVtZW50Pjwvc2FtbDI6QXNzZXJ0aW9uPjwvc2FtbDJwOlJlc3BvbnNlPg==` + +const exampleBase64_2 = `PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cDovL2xvY2FsaG9zdDo4MDgwL3YxL19zYW1sX2NhbGxiYWNrIiBJRD0iaWQyMTI4MjQ4OTI5NTEwNjcwODM0NTU5MTg1IiBJblJlc3BvbnNlVG89Il9kYTIxM2RmOC1lZjk1LTQxZDAtYjliZi03MWQyNzE3MzVjZDciIElzc3VlSW5zdGFudD0iMjAxNi0wMy0yOFQxNjozODoxOC41NjVaIiBWZXJzaW9uPSIyLjAiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSI+PHNhbWwyOklzc3VlciB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDplbnRpdHkiPmh0dHA6Ly93d3cub2t0YS5jb20vZXhrNXp0MHIxMkVkaTRyRDIwaDc8L3NhbWwyOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8+PGRzOlJlZmVyZW5jZSBVUkk9IiNpZDIxMjgyNDg5Mjk1MTA2NzA4MzQ1NTkxODUiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiPjxlYzpJbmNsdXNpdmVOYW1lc3BhY2VzIHhtbG5zOmVjPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiIFByZWZpeExpc3Q9InhzIi8+PC9kczpUcmFuc2Zvcm0+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNzaGEyNTYiLz48ZHM6RGlnZXN0VmFsdWU+V3ZnVy9KZlA0bWpVKy8xd3R5WDA2RTlFR3hZTnNvQ1UrcmJTWm5Bdmoycz08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+R0ExVVJvTU9FNUVGZmtIWWltR1htN0VjcGgvbTBzMTM1VnlGOVd1dDZOU3B1WmRRMmNyTTFJc2x2S0NSamtFMDlyWmdhZ1FRTUFUaFVjT0Z1WDM1ZFpQejlKNElocHQxanVoZkd2MUFWOEk4amlPS0ZFVGo2NU1pUGFiREVpOCtQNllXZjRxTnVqQUpYSEtKSWEvTUZYQnFvS1IvaW1MUVQ4ZXUxbmhWQlFHWXFXd1plUGRkZlhPMkpZazJjZTdtdG55TVQwZFVWYitvK3RsRURZYTdyaTlmajRKTC96MVhYN3lyYlZaeG4ybWRLUEp0U1NQOHVITk9XU002ajF2cDRvSytLU0R2aUJmaVZMbFZBNThub3o1R3lGdHA2NDJoK0xWMnF1S2JuY01GZm5mQjFrZkhMSy94YXo5VWFEQnkrYkhLNG9HelNwVmhacWNPenpsaUtBPT08L2RzOlNpZ25hdHVyZVZhbHVlPjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSURwRENDQW95Z0F3SUJBZ0lHQVZMSUJoQXdNQTBHQ1NxR1NJYjNEUUVCQlFVQU1JR1NNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUcKQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVXTUJRR0ExVUVCd3dOVTJGdUlFWnlZVzVqYVhOamJ6RU5NQXNHQTFVRUNnd0VUMnQwWVRFVQpNQklHQTFVRUN3d0xVMU5QVUhKdmRtbGtaWEl4RXpBUkJnTlZCQU1NQ21SbGRpMHhNVFk0TURjeEhEQWFCZ2txaGtpRzl3MEJDUUVXCkRXbHVabTlBYjJ0MFlTNWpiMjB3SGhjTk1UWXdNakE1TWpFMU1qQTJXaGNOTWpZd01qQTVNakUxTXpBMldqQ0JrakVMTUFrR0ExVUUKQmhNQ1ZWTXhFekFSQmdOVkJBZ01Da05oYkdsbWIzSnVhV0V4RmpBVUJnTlZCQWNNRFZOaGJpQkdjbUZ1WTJselkyOHhEVEFMQmdOVgpCQW9NQkU5cmRHRXhGREFTQmdOVkJBc01DMU5UVDFCeWIzWnBaR1Z5TVJNd0VRWURWUVFEREFwa1pYWXRNVEUyT0RBM01Sd3dHZ1lKCktvWklodmNOQVFrQkZnMXBibVp2UUc5cmRHRXVZMjl0TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUEKbXRqQk9aOE1taFV5aThjR2s0ZFVZNkZqMU1GRHQvcTNGRmlhUXBMenUzL3E1bFJWVU5VQmJBdHFRV3dZMTBkemZaZ3VIT3V2QTVwNQpReWlWRHZVaGUrWGtWd04yUjJXZkFyUUpSVFBuSWNPYUhyeHFRZjNvNWNDSUcyMVp0eXNGSEpTbzhjbFBTT2UrMFZzb1JnY0oxYUY0CjJyT0R3Z3FSUlpkTzlXaDM1MDJYbEo3OTlESlEyM0lDN1hhc0tFc0dLekpxaGxScmZkL0Z5SXVaVDBzRkhES1J6NXNuU0pobTlncE4KdVFsQ21rN09OWjFzWHF0dCtuQklmV0lxZW9ZUXViUFc3cFQ1R1RjN3dvdVdxNFRDakhKaUs5azJIaXlOeFcwRTNKWDA4c3dFWmkyKwpMVkRqZ0x6TmM0bHdqU1lJajNBT3RQWnM4czYwNm9CZElCbmk0d0lEQVFBQk1BMEdDU3FHU0liM0RRRUJCUVVBQTRJQkFRQk14U2tKClR4a1h4c29LTlcwYXdKTnBXUmJVODFRcGhlTUZmRU5JekxhbTRJdGMvNWtTWkFhU3kvOWUyUUtmbzRqQm8vTU1iQ3Eydk05VHllSlEKREpwUmFpb1VUZDJsR2g0VExVeEF4Q3h0VWsvcGFzY0wrM05uOTM2TEZtVUNMeGF4bmJlR3pQT1hBaHNjQ3RVMUgwbkZzWFJuS3g1YQpjUFhZU0tGWlpaa3RpZVNrd3cyT2k4ZGcyRFlhUWhHUU1TRk1WcWdWZndFdTRidkNSQnZkU2lOWGRXR0NaUW1GVnpCWlovOXJPTHpQCnB2VEZUUG5wa2F2Sm04MUZMbFVoaUUvb0ZnS2xDRExXRGtuU3BYQUkwdVpHRVJjd1BjYTZ4dklNaDg2TGpRS2piVmNpOUZZRFN0WEMKcVJucVErVGNjU3UvQjZ1T05Gc0RFbmdHY1hTS2ZCK2E8L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT48c2FtbDJwOlN0YXR1cyB4bWxuczpzYW1sMnA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCI+PHNhbWwycDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWwycDpTdGF0dXM+PHNhbWwyOkFzc2VydGlvbiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9ImlkMjEyODI0ODkyOTU3NzY3ODIxMjY0NjgzMTkiIElzc3VlSW5zdGFudD0iMjAxNi0wMy0yOFQxNjozODoxOC41NjVaIiBWZXJzaW9uPSIyLjAiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSI+PHNhbWwyOklzc3VlciBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OmVudGl0eSIgeG1sbnM6c2FtbDI9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPmh0dHA6Ly93d3cub2t0YS5jb20vZXhrNXp0MHIxMkVkaTRyRDIwaDc8L3NhbWwyOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8+PGRzOlJlZmVyZW5jZSBVUkk9IiNpZDIxMjgyNDg5Mjk1Nzc2NzgyMTI2NDY4MzE5Ij48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIj48ZWM6SW5jbHVzaXZlTmFtZXNwYWNlcyB4bWxuczplYz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIiBQcmVmaXhMaXN0PSJ4cyIvPjwvZHM6VHJhbnNmb3JtPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjc2hhMjU2Ii8+PGRzOkRpZ2VzdFZhbHVlPkZzV0dDQkMrdC9MYVZrVUtVdlJRcHp5WlRtbHhVenc0UjlGT3pYUFBKUnc9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPmhTNTBXZ1lzL2NuM3V4bWhyemEvMC8wUVczSDdid2RqUFoyaFFtRzdJZVNkN2F3VE9naEJxZHJqdmFQZlE3dFJXK1VLNmV3TWdJQlZLRzZqVjNxWUFXZVcyVTcwaE1iN2hFOXFKcUJLeVl5aW1taFZXVUx4MUhCMlltbFUxd21pc3B5d29QbFhRNmdqMGlXYUwyUkZJODN2VXA3WDUwZVo2ZEVMcW9KVlpwelFJMDY1VHQwVEc3VXVLVVcxZmxZc2JpUzlOYVhudXcrbWNyQlcyNVpBOUY1Q0xlUEhraTAxWnpVdytYdE5tS3RoRWI3U1IzMG16UG9qMDhEamkyMmRhWXZHdTgySVIwMXdJWlBvUUpQQ0dNVDZ5MnhDL3BRUHFHbGpBZy92VWErZ2FZZ2FNYUFWWXhoay9oZmdNVUJsT2VLQUNCYUdUbXlnYWIxTno1S3ZQZz09PC9kczpTaWduYXR1cmVWYWx1ZT48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlEcERDQ0FveWdBd0lCQWdJR0FWTElCaEF3TUEwR0NTcUdTSWIzRFFFQkJRVUFNSUdTTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHCkExVUVDQXdLUTJGc2FXWnZjbTVwWVRFV01CUUdBMVVFQnd3TlUyRnVJRVp5WVc1amFYTmpiekVOTUFzR0ExVUVDZ3dFVDJ0MFlURVUKTUJJR0ExVUVDd3dMVTFOUFVISnZkbWxrWlhJeEV6QVJCZ05WQkFNTUNtUmxkaTB4TVRZNE1EY3hIREFhQmdrcWhraUc5dzBCQ1FFVwpEV2x1Wm05QWIydDBZUzVqYjIwd0hoY05NVFl3TWpBNU1qRTFNakEyV2hjTk1qWXdNakE1TWpFMU16QTJXakNCa2pFTE1Ba0dBMVVFCkJoTUNWVk14RXpBUkJnTlZCQWdNQ2tOaGJHbG1iM0p1YVdFeEZqQVVCZ05WQkFjTURWTmhiaUJHY21GdVkybHpZMjh4RFRBTEJnTlYKQkFvTUJFOXJkR0V4RkRBU0JnTlZCQXNNQzFOVFQxQnliM1pwWkdWeU1STXdFUVlEVlFRRERBcGtaWFl0TVRFMk9EQTNNUnd3R2dZSgpLb1pJaHZjTkFRa0JGZzFwYm1adlFHOXJkR0V1WTI5dE1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBCm10akJPWjhNbWhVeWk4Y0drNGRVWTZGajFNRkR0L3EzRkZpYVFwTHp1My9xNWxSVlVOVUJiQXRxUVd3WTEwZHpmWmd1SE91dkE1cDUKUXlpVkR2VWhlK1hrVndOMlIyV2ZBclFKUlRQbkljT2FIcnhxUWYzbzVjQ0lHMjFadHlzRkhKU284Y2xQU09lKzBWc29SZ2NKMWFGNAoyck9Ed2dxUlJaZE85V2gzNTAyWGxKNzk5REpRMjNJQzdYYXNLRXNHS3pKcWhsUnJmZC9GeUl1WlQwc0ZIREtSejVzblNKaG05Z3BOCnVRbENtazdPTloxc1hxdHQrbkJJZldJcWVvWVF1YlBXN3BUNUdUYzd3b3VXcTRUQ2pISmlLOWsySGl5TnhXMEUzSlgwOHN3RVppMisKTFZEamdMek5jNGx3alNZSWozQU90UFpzOHM2MDZvQmRJQm5pNHdJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0SUJBUUJNeFNrSgpUeGtYeHNvS05XMGF3Sk5wV1JiVTgxUXBoZU1GZkVOSXpMYW00SXRjLzVrU1pBYVN5LzllMlFLZm80akJvL01NYkNxMnZNOVR5ZUpRCkRKcFJhaW9VVGQybEdoNFRMVXhBeEN4dFVrL3Bhc2NMKzNObjkzNkxGbVVDTHhheG5iZUd6UE9YQWhzY0N0VTFIMG5Gc1hSbkt4NWEKY1BYWVNLRlpaWmt0aWVTa3d3Mk9pOGRnMkRZYVFoR1FNU0ZNVnFnVmZ3RXU0YnZDUkJ2ZFNpTlhkV0dDWlFtRlZ6QlpaLzlyT0x6UApwdlRGVFBucGthdkptODFGTGxVaGlFL29GZ0tsQ0RMV0RrblNwWEFJMHVaR0VSY3dQY2E2eHZJTWg4NkxqUUtqYlZjaTlGWURTdFhDCnFSbnFRK1RjY1N1L0I2dU9ORnNERW5nR2NYU0tmQithPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWwyOlN1YmplY3QgeG1sbnM6c2FtbDI9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPjxzYW1sMjpOYW1lSUQgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPnBob2ViZS5zaW1vbkBzY2FsZWZ0LmNvbTwvc2FtbDI6TmFtZUlEPjxzYW1sMjpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+PHNhbWwyOlN1YmplY3RDb25maXJtYXRpb25EYXRhIEluUmVzcG9uc2VUbz0iX2RhMjEzZGY4LWVmOTUtNDFkMC1iOWJmLTcxZDI3MTczNWNkNyIgTm90T25PckFmdGVyPSIyMDE2LTAzLTI4VDE2OjQzOjE4LjU2NVoiIFJlY2lwaWVudD0iaHR0cDovL2xvY2FsaG9zdDo4MDgwL3YxL19zYW1sX2NhbGxiYWNrIi8+PC9zYW1sMjpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDI6U3ViamVjdD48c2FtbDI6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTYtMDMtMjhUMTY6MzM6MTguNTY1WiIgTm90T25PckFmdGVyPSIyMDE2LTAzLTI4VDE2OjQzOjE4LjU2NVoiIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj48c2FtbDI6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDI6QXVkaWVuY2U+MTIzPC9zYW1sMjpBdWRpZW5jZT48L3NhbWwyOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sMjpDb25kaXRpb25zPjxzYW1sMjpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTYtMDMtMjhUMTY6Mzg6MTguNTY1WiIgU2Vzc2lvbkluZGV4PSJfZGEyMTNkZjgtZWY5NS00MWQwLWI5YmYtNzFkMjcxNzM1Y2Q3IiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI+PHNhbWwyOkF1dGhuQ29udGV4dD48c2FtbDI6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmRQcm90ZWN0ZWRUcmFuc3BvcnQ8L3NhbWwyOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDI6QXV0aG5Db250ZXh0Pjwvc2FtbDI6QXV0aG5TdGF0ZW1lbnQ+PHNhbWwyOkF0dHJpYnV0ZVN0YXRlbWVudCB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI+PHNhbWwyOkF0dHJpYnV0ZSBOYW1lPSJGaXJzdE5hbWUiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dW5zcGVjaWZpZWQiPjxzYW1sMjpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTp0eXBlPSJ4czpzdHJpbmciPlBob2ViZTwvc2FtbDI6QXR0cmlidXRlVmFsdWU+PC9zYW1sMjpBdHRyaWJ1dGU+PHNhbWwyOkF0dHJpYnV0ZSBOYW1lPSJMYXN0TmFtZSIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDp1bnNwZWNpZmllZCI+PHNhbWwyOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+U2ltb248L3NhbWwyOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDI6QXR0cmlidXRlPjxzYW1sMjpBdHRyaWJ1dGUgTmFtZT0iRW1haWwiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dW5zcGVjaWZpZWQiPjxzYW1sMjpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTp0eXBlPSJ4czpzdHJpbmciPnBob2ViZS5zaW1vbkBzY2FsZWZ0LmNvbTwvc2FtbDI6QXR0cmlidXRlVmFsdWU+PC9zYW1sMjpBdHRyaWJ1dGU+PHNhbWwyOkF0dHJpYnV0ZSBOYW1lPSJMb2dpbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDp1bnNwZWNpZmllZCI+PHNhbWwyOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+cGhvZWJlLnNpbW9uQHNjYWxlZnQuY29tPC9zYW1sMjpBdHRyaWJ1dGVWYWx1ZT48L3NhbWwyOkF0dHJpYnV0ZT48L3NhbWwyOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWwyOkFzc2VydGlvbj48L3NhbWwycDpSZXNwb25zZT4=` diff --git a/vendor/github.com/russellhaering/gosaml2/types/encrypted_assertion.go b/vendor/github.com/russellhaering/gosaml2/types/encrypted_assertion.go new file mode 100644 index 0000000000000..62da1bb9d33f0 --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/types/encrypted_assertion.go @@ -0,0 +1,72 @@ +package types + +import ( + "bytes" + "crypto/cipher" + "crypto/tls" + "encoding/base64" + "encoding/xml" + "fmt" +) + +type EncryptedAssertion struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion EncryptedAssertion"` + EncryptionMethod EncryptionMethod `xml:"EncryptedData>EncryptionMethod"` + EncryptedKey EncryptedKey `xml:"EncryptedData>KeyInfo>EncryptedKey"` + CipherValue string `xml:"EncryptedData>CipherData>CipherValue"` +} + +func (ea *EncryptedAssertion) decrypt(cert *tls.Certificate) ([]byte, error) { + data, err := base64.StdEncoding.DecodeString(ea.CipherValue) + if err != nil { + return nil, err + } + + k, err := ea.EncryptedKey.DecryptSymmetricKey(cert) + if err != nil { + return nil, fmt.Errorf("cannot decrypt, error retrieving private key: %s", err) + } + + switch ea.EncryptionMethod.Algorithm { + case MethodAES128GCM: + c, err := cipher.NewGCM(k) + if err != nil { + return nil, fmt.Errorf("cannot create AES-GCM: %s", err) + } + + nonce, data := data[:c.NonceSize()], data[c.NonceSize():] + plainText, err := c.Open(nil, nonce, data, nil) + if err != nil { + return nil, fmt.Errorf("cannot open AES-GCM: %s", err) + } + return plainText, nil + case MethodAES128CBC: + nonce, data := data[:k.BlockSize()], data[k.BlockSize():] + c := cipher.NewCBCDecrypter(k, nonce) + c.CryptBlocks(data, data) + + // Remove zero bytes + data = bytes.TrimRight(data, "\x00") + + // Calculate index to remove based on padding + padLength := data[len(data)-1] + lastGoodIndex := len(data) - int(padLength) + return data[:lastGoodIndex], nil + default: + return nil, fmt.Errorf("unknown symmetric encryption method %#v", ea.EncryptionMethod.Algorithm) + } +} + +// Decrypt decrypts and unmarshals the EncryptedAssertion. +func (ea *EncryptedAssertion) Decrypt(cert *tls.Certificate) (*Assertion, error) { + plaintext, err := ea.decrypt(cert) + + assertion := &Assertion{} + + err = xml.Unmarshal(plaintext, assertion) + if err != nil { + return nil, fmt.Errorf("Error decrypting assertion: %v", err) + } + + return assertion, nil +} diff --git a/vendor/github.com/russellhaering/gosaml2/types/encrypted_key.go b/vendor/github.com/russellhaering/gosaml2/types/encrypted_key.go new file mode 100644 index 0000000000000..40405f715108d --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/types/encrypted_key.go @@ -0,0 +1,109 @@ +package types + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "crypto/tls" + "encoding/base64" + "fmt" + "hash" +) + +//EncryptedKey contains the decryption key data from the saml2 core and xmlenc +//standards. +type EncryptedKey struct { + // EncryptionMethod string `xml:"EncryptionMethod>Algorithm"` + X509Data string `xml:"KeyInfo>X509Data>X509Certificate"` + CipherValue string `xml:"CipherData>CipherValue"` + EncryptionMethod EncryptionMethod +} + +//EncryptionMethod specifies the type of encryption that was used. +type EncryptionMethod struct { + Algorithm string `xml:",attr"` + DigestMethod DigestMethod +} + +//DigestMethod is a digest type specification +type DigestMethod struct { + Algorithm string `xml:",attr"` +} + +//Well-known public-key encryption methods +const ( + MethodRSAOAEP = "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p" + MethodRSAOAEP2 = "http://www.w3.org/2009/xmlenc11#rsa-oaep" +) + +//Well-known private key encryption methods +const ( + MethodAES128GCM = "http://www.w3.org/2009/xmlenc11#aes128-gcm" + MethodAES128CBC = "http://www.w3.org/2001/04/xmlenc#aes128-cbc" +) + +//Well-known hash methods +const ( + MethodSHA1 = "http://www.w3.org/2000/09/xmldsig#sha1" + MethodSHA256 = "http://www.w3.org/2000/09/xmldsig#sha256" + MethodSHA512 = "http://www.w3.org/2000/09/xmldsig#sha512" +) + +//DecryptSymmetricKey returns the private key contained in the EncryptedKey document +func (ek *EncryptedKey) DecryptSymmetricKey(cert *tls.Certificate) (cipher.Block, error) { + encCert, err := base64.StdEncoding.DecodeString(ek.X509Data) + if err != nil { + return nil, fmt.Errorf("error getting certificate from encryptedkey: %v", err) + } + + if len(cert.Certificate) < 1 { + return nil, fmt.Errorf("decryption tls.Certificate has no public certs attached") + } + + if !bytes.Equal(cert.Certificate[0], encCert) { + return nil, fmt.Errorf("key decryption attempted with mismatched cert: %#v != %#v", + string(cert.Certificate[0]), string(encCert)) + } + + cipherText, err := base64.StdEncoding.DecodeString(ek.CipherValue) + if err != nil { + return nil, err + } + + switch pk := cert.PrivateKey.(type) { + case *rsa.PrivateKey: + var h hash.Hash + + switch ek.EncryptionMethod.DigestMethod.Algorithm { + case MethodSHA1: + h = sha1.New() + case MethodSHA256: + h = sha256.New() + case MethodSHA512: + h = sha512.New() + } + + switch ek.EncryptionMethod.Algorithm { + case MethodRSAOAEP, MethodRSAOAEP2: + pt, err := rsa.DecryptOAEP(h, rand.Reader, pk, cipherText, nil) + if err != nil { + return nil, fmt.Errorf("rsa internal error: %v", err) + } + + b, err := aes.NewCipher(pt) + if err != nil { + return nil, err + } + + return b, nil + default: + return nil, fmt.Errorf("unsupported encryption algorithm: %s", ek.EncryptionMethod.Algorithm) + } + } + return nil, fmt.Errorf("no cipher for decoding symmetric key") +} diff --git a/vendor/github.com/russellhaering/gosaml2/types/metadata.go b/vendor/github.com/russellhaering/gosaml2/types/metadata.go new file mode 100644 index 0000000000000..6c8f4f972f640 --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/types/metadata.go @@ -0,0 +1,39 @@ +package types + +import ( + "encoding/xml" + + dsigtypes "github.com/russellhaering/goxmldsig/types" +) + +type EntityDescriptor struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata EntityDescriptor"` + // SAML 2.0 8.3.6 Entity Identifier could be used to represent issuer + EntityID string `xml:"entityID,attr"` + IDPSSODescriptor IDPSSODescriptor `xml:"IDPSSODescriptor"` +} + +type IDPSSODescriptor struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata IDPSSODescriptor"` + KeyDescriptors []KeyDescriptor `xml:"KeyDescriptor"` + NameIDFormats []NameIDFormat `xml:"NameIDFormat"` + SingleSignOnService SingleSignOnService `xml:"SingleSignOnService"` + Attributes []Attribute `xml:"Attribute"` +} + +type KeyDescriptor struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata KeyDescriptor"` + Use string `xml:"use,attr"` + KeyInfo dsigtypes.KeyInfo `xml:"KeyInfo"` +} + +type NameIDFormat struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata NameIDFormat"` + Value string `xml:",chardata"` +} + +type SingleSignOnService struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata SingleSignOnService"` + Binding string `xml:"Binding,attr"` + Location string `xml:"Location,attr"` +} diff --git a/vendor/github.com/russellhaering/gosaml2/types/response.go b/vendor/github.com/russellhaering/gosaml2/types/response.go new file mode 100644 index 0000000000000..8b5cac74e2f1a --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/types/response.go @@ -0,0 +1,118 @@ +package types + +import ( + "encoding/xml" + "time" +) + +type Response struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol Response"` + Destination string `xml:"Destination,attr"` + Version string `xml:"Version,attr"` + Status *Status `xml:"Status"` + Issuer *Issuer `xml:"Issuer"` + Assertions []Assertion `xml:"Assertion"` + EncryptedAssertions []EncryptedAssertion `xml:"EncryptedAssertion"` +} + +type Status struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol Status"` + StatusCode *StatusCode `xml:"StatusCode"` +} + +type StatusCode struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol StatusCode"` + Value string `xml:"Value,attr"` +} + +type Issuer struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Issuer"` + Value string `xml:",chardata"` +} + +type Assertion struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Assertion"` + Issuer *Issuer `xml:"Issuer"` + Subject *Subject `xml:"Subject"` + Conditions *Conditions `xml:"Conditions"` + AttributeStatement *AttributeStatement `xml:"AttributeStatement"` + AuthnStatement *AuthnStatement `xml:"AuthnStatement"` +} + +type Subject struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Subject"` + NameID *NameID `xml:"NameID"` + SubjectConfirmation *SubjectConfirmation `xml:"SubjectConfirmation"` +} + +type NameID struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion NameID"` + Value string `xml:",chardata"` +} + +type SubjectConfirmation struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion SubjectConfirmation"` + Method string `xml:"Method,attr"` + SubjectConfirmationData *SubjectConfirmationData `xml:"SubjectConfirmationData"` +} + +type SubjectConfirmationData struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion SubjectConfirmationData"` + NotOnOrAfter string `xml:"NotOnOrAfter,attr"` + Recipient string `xml:"Recipient,attr"` + InResponseTo string `xml:"InResponseTo,attr"` +} + +type Conditions struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Conditions"` + NotBefore string `xml:"NotBefore,attr"` + NotOnOrAfter string `xml:"NotOnOrAfter,attr"` + AudienceRestrictions []AudienceRestriction `xml:"AudienceRestriction"` + OneTimeUse *OneTimeUse `xml:"OneTimeUse"` + ProxyRestriction *ProxyRestriction `xml:"ProxyRestriction"` +} + +type AudienceRestriction struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion AudienceRestriction"` + Audiences []Audience `xml:"Audience"` +} + +type Audience struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Audience"` + Value string `xml:",chardata"` +} + +type OneTimeUse struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion OneTimeUse"` +} + +type ProxyRestriction struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion ProxyRestriction"` + Count int `xml:"Count,attr"` + Audience []Audience `xml:"Audience"` +} + +type AttributeStatement struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion AttributeStatement"` + Attributes []Attribute `xml:"Attribute"` +} + +type Attribute struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Attribute"` + FriendlyName string `xml:"FriendlyName,attr"` + Name string `xml:"Name,attr"` + NameFormat string `xml:"NameFormat,attr"` + Values []AttributeValue `xml:"AttributeValue"` +} + +type AttributeValue struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion AttributeValue"` + Type string `xml:"xsi:type,attr"` + Value string `xml:",chardata"` +} + +type AuthnStatement struct { + XMLName xml.Name `xml:"AuthnStatement"` + AuthnInstant *time.Time `xml:"AuthnInstant,attr,omitempty"` + SessionNotOnOrAfter *time.Time `xml:"SessionNotOnOrAfter,attr,omitempty"` +} diff --git a/vendor/github.com/russellhaering/gosaml2/validate.go b/vendor/github.com/russellhaering/gosaml2/validate.go new file mode 100644 index 0000000000000..53182406333bc --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/validate.go @@ -0,0 +1,218 @@ +package saml2 + +import ( + "fmt" + "time" + + "github.com/russellhaering/gosaml2/types" +) + +//ErrParsing indicates that the value present in an assertion could not be +//parsed. It can be inspected for the specific tag name, the contents, and the +//intended type. +type ErrParsing struct { + Tag, Value, Type string +} + +func (ep ErrParsing) Error() string { + return fmt.Sprintf("Error parsing %s tag value as type %s", ep.Tag, ep.Value) +} + +//Oft-used messages +const ( + ReasonUnsupported = "Unsupported" + ReasonExpired = "Expired" +) + +//ErrInvalidValue indicates that the expected value did not match the received +//value. +type ErrInvalidValue struct { + Key, Expected, Actual string + Reason string +} + +func (e ErrInvalidValue) Error() string { + if e.Reason == "" { + e.Reason = "Unrecognized" + } + return fmt.Sprintf("%s %s value, Expected: %s, Actual: %s", e.Reason, e.Key, e.Expected, e.Actual) +} + +//Well-known methods of subject confirmation +const ( + SubjMethodBearer = "urn:oasis:names:tc:SAML:2.0:cm:bearer" +) + +//VerifyAssertionConditions inspects an assertion element and makes sure that +//all SAML2 contracts are upheld. +func (sp *SAMLServiceProvider) VerifyAssertionConditions(assertion *types.Assertion) (*WarningInfo, error) { + warningInfo := &WarningInfo{} + now := sp.Clock.Now() + + conditions := assertion.Conditions + if conditions == nil { + return nil, ErrMissingElement{Tag: ConditionsTag} + } + + if conditions.NotBefore == "" { + return nil, ErrMissingElement{Tag: ConditionsTag, Attribute: NotBeforeAttr} + } + + notBefore, err := time.Parse(time.RFC3339, conditions.NotBefore) + if err != nil { + return nil, ErrParsing{Tag: NotBeforeAttr, Value: conditions.NotBefore, Type: "time.RFC3339"} + } + + if now.Before(notBefore) { + warningInfo.InvalidTime = true + } + + if conditions.NotOnOrAfter == "" { + return nil, ErrMissingElement{Tag: ConditionsTag, Attribute: NotOnOrAfterAttr} + } + + notOnOrAfter, err := time.Parse(time.RFC3339, conditions.NotOnOrAfter) + if err != nil { + return nil, ErrParsing{Tag: NotOnOrAfterAttr, Value: conditions.NotOnOrAfter, Type: "time.RFC3339"} + } + + if now.After(notOnOrAfter) { + warningInfo.InvalidTime = true + } + + for _, audienceRestriction := range conditions.AudienceRestrictions { + matched := false + + for _, audience := range audienceRestriction.Audiences { + if audience.Value == sp.AudienceURI { + matched = true + break + } + } + + if !matched { + warningInfo.NotInAudience = true + break + } + } + + if conditions.OneTimeUse != nil { + warningInfo.OneTimeUse = true + } + + proxyRestriction := conditions.ProxyRestriction + if proxyRestriction != nil { + proxyRestrictionInfo := &ProxyRestriction{ + Count: proxyRestriction.Count, + Audience: []string{}, + } + + for _, audience := range proxyRestriction.Audience { + proxyRestrictionInfo.Audience = append(proxyRestrictionInfo.Audience, audience.Value) + } + + warningInfo.ProxyRestriction = proxyRestrictionInfo + } + + return warningInfo, nil +} + +//Validate ensures that the assertion passed is valid for the current Service +//Provider. +func (sp *SAMLServiceProvider) Validate(response *types.Response) error { + err := sp.validateResponseAttributes(response) + if err != nil { + return err + } + + if len(response.Assertions) == 0 { + return ErrMissingAssertion + } + + issuer := response.Issuer + if issuer == nil { + return ErrMissingElement{Tag: IssuerTag} + } + + if sp.IdentityProviderIssuer != "" && response.Issuer.Value != sp.IdentityProviderIssuer { + return ErrInvalidValue{ + Key: IssuerTag, + Expected: sp.IdentityProviderIssuer, + Actual: response.Issuer.Value, + } + } + + status := response.Status + if status == nil { + return ErrMissingElement{Tag: StatusTag} + } + + statusCode := status.StatusCode + if statusCode == nil { + return ErrMissingElement{Tag: StatusCodeTag} + } + + if statusCode.Value != StatusCodeSuccess { + return ErrInvalidValue{ + Key: StatusCodeTag, + Expected: StatusCodeSuccess, + Actual: statusCode.Value, + } + } + + for _, assertion := range response.Assertions { + subject := assertion.Subject + if subject == nil { + return ErrMissingElement{Tag: SubjectTag} + } + + subjectConfirmation := subject.SubjectConfirmation + if subjectConfirmation == nil { + return ErrMissingElement{Tag: SubjectConfirmationTag} + } + + if subjectConfirmation.Method != SubjMethodBearer { + return ErrInvalidValue{ + Reason: ReasonUnsupported, + Key: SubjectConfirmationTag, + Expected: SubjMethodBearer, + Actual: subjectConfirmation.Method, + } + } + + subjectConfirmationData := subjectConfirmation.SubjectConfirmationData + if subjectConfirmationData == nil { + return ErrMissingElement{Tag: SubjectConfirmationDataTag} + } + + if subjectConfirmationData.Recipient != sp.AssertionConsumerServiceURL { + return ErrInvalidValue{ + Key: RecipientAttr, + Expected: sp.AssertionConsumerServiceURL, + Actual: subjectConfirmationData.Recipient, + } + } + + if subjectConfirmationData.NotOnOrAfter == "" { + return ErrMissingElement{Tag: SubjectConfirmationDataTag, Attribute: NotOnOrAfterAttr} + } + + notOnOrAfter, err := time.Parse(time.RFC3339, subjectConfirmationData.NotOnOrAfter) + if err != nil { + return ErrParsing{Tag: NotOnOrAfterAttr, Value: subjectConfirmationData.NotOnOrAfter, Type: "time.RFC3339"} + } + + now := sp.Clock.Now() + if now.After(notOnOrAfter) { + return ErrInvalidValue{ + Reason: ReasonExpired, + Key: NotOnOrAfterAttr, + Expected: now.Format(time.RFC3339), + Actual: subjectConfirmationData.NotOnOrAfter, + } + } + + } + + return nil +} diff --git a/vendor/github.com/russellhaering/gosaml2/xml_constants.go b/vendor/github.com/russellhaering/gosaml2/xml_constants.go new file mode 100644 index 0000000000000..64bfc3b9173be --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/xml_constants.go @@ -0,0 +1,54 @@ +package saml2 + +const ( + ResponseTag = "Response" + AssertionTag = "Assertion" + SubjectTag = "Subject" + NameIdTag = "NameID" + SubjectConfirmationTag = "SubjectConfirmation" + SubjectConfirmationDataTag = "SubjectConfirmationData" + AttributeStatementTag = "AttributeStatement" + AttributeValueTag = "AttributeValue" + ConditionsTag = "Conditions" + AudienceRestrictionTag = "AudienceRestriction" + AudienceTag = "Audience" + OneTimeUseTag = "OneTimeUse" + ProxyRestrictionTag = "ProxyRestriction" + IssuerTag = "Issuer" + StatusTag = "Status" + StatusCodeTag = "StatusCode" +) + +const ( + DestinationAttr = "Destination" + VersionAttr = "Version" + IdAttr = "ID" + MethodAttr = "Method" + RecipientAttr = "Recipient" + NameAttr = "Name" + NotBeforeAttr = "NotBefore" + NotOnOrAfterAttr = "NotOnOrAfter" + CountAttr = "Count" +) + +const ( + NameIdFormatPersistent = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" + NameIdFormatTransient = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + NameIdFormatEmailAddress = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + NameIdFormatUnspecified = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" + NameIdFormatX509SubjectName = "urn:oasis:names:tc:SAML:1.1:nameid-format:x509SubjectName" + + AuthnContextPasswordProtectedTransport = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport" + + AuthnPolicyMatchExact = "exact" + AuthnPolicyMatchMinimum = "minimum" + AuthnPolicyMatchMaximum = "maximum" + AuthnPolicyMatchBetter = "better" + + StatusCodeSuccess = "urn:oasis:names:tc:SAML:2.0:status:Success" +) + +const ( + SAMLAssertionNamespace = "urn:oasis:names:tc:SAML:2.0:assertion" + SAMLProtocolNamespace = "urn:oasis:names:tc:SAML:2.0:protocol" +) diff --git a/vendor/github.com/russellhaering/goxmldsig/.travis.yml b/vendor/github.com/russellhaering/goxmldsig/.travis.yml new file mode 100644 index 0000000000000..637f840c3a27a --- /dev/null +++ b/vendor/github.com/russellhaering/goxmldsig/.travis.yml @@ -0,0 +1,6 @@ +language: go + +go: + - 1.5 + - 1.6 + - tip \ No newline at end of file diff --git a/vendor/github.com/russellhaering/goxmldsig/LICENSE b/vendor/github.com/russellhaering/goxmldsig/LICENSE new file mode 100644 index 0000000000000..67db8588217f2 --- /dev/null +++ b/vendor/github.com/russellhaering/goxmldsig/LICENSE @@ -0,0 +1,175 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/vendor/github.com/russellhaering/goxmldsig/README.md b/vendor/github.com/russellhaering/goxmldsig/README.md new file mode 100644 index 0000000000000..5fc3bdbceb2df --- /dev/null +++ b/vendor/github.com/russellhaering/goxmldsig/README.md @@ -0,0 +1,90 @@ +# goxmldsig + +[![Build Status](https://travis-ci.org/russellhaering/goxmldsig.svg?branch=master)](https://travis-ci.org/russellhaering/goxmldsig) +[![GoDoc](https://godoc.org/github.com/russellhaering/goxmldsig?status.svg)](https://godoc.org/github.com/russellhaering/goxmldsig) + +XML Digital Signatures implemented in pure Go. + +## Installation + +Install `goxmldsig` into your `$GOPATH` using `go get`: + +``` +$ go get github.com/russellhaering/goxmldsig +``` + +## Usage + +### Signing + +```go +package main + +import ( + "github.com/beevik/etree" + "github.com/russellhaering/goxmldsig" +) + +func main() { + // Generate a key and self-signed certificate for signing + randomKeyStore := dsig.RandomKeyStoreForTest() + ctx := dsig.NewDefaultSigningContext(randomKeyStore) + elementToSign := &etree.Element{ + Tag: "ExampleElement", + } + elementToSign.CreateAttr("ID", "id1234") + + // Sign the element + signedElement, err := ctx.SignEnveloped(elementToSign) + if err != nil { + panic(err) + } + + // Serialize the signed element. It is important not to modify the element + // after it has been signed - even pretty-printing the XML will invalidate + // the signature. + doc := etree.NewDocument() + doc.SetRoot(signedElement) + str, err := doc.WriteToString() + if err != nil { + panic(err) + } + + println(str) +} +``` + +### Signature Validation + +```go +// Validate an element against a root certificate +func validate(root *x509.Certificate, el *etree.Element) { + // Construct a signing context with one or more roots of trust. + ctx := dsig.NewDefaultValidationContext(&dsig.MemoryX509CertificateStore{ + Roots: []*x509.Certificate{root}, + }) + + // It is important to only use the returned validated element. + // See: https://www.w3.org/TR/xmldsig-bestpractices/#check-what-is-signed + validated, err := ctx.Validate(el) + if err != nil { + panic(err) + } + + doc := etree.NewDocument() + doc.SetRoot(validated) + str, err := doc.WriteToString() + if err != nil { + panic(err) + } + + println(str) +} +``` + +## Limitations + +This library was created in order to [implement SAML 2.0](https://github.com/russellhaering/gosaml2) +without needing to execute a command line tool to create and validate signatures. It currently +only implements the subset of relevant standards needed to support that implementation, but +I hope to make it more complete over time. Contributions are welcome. diff --git a/vendor/github.com/russellhaering/goxmldsig/canonicalize.go b/vendor/github.com/russellhaering/goxmldsig/canonicalize.go new file mode 100644 index 0000000000000..34e1a581ffd03 --- /dev/null +++ b/vendor/github.com/russellhaering/goxmldsig/canonicalize.go @@ -0,0 +1,128 @@ +package dsig + +import ( + "sort" + + "github.com/beevik/etree" + "github.com/russellhaering/goxmldsig/etreeutils" +) + +// Canonicalizer is an implementation of a canonicalization algorithm. +type Canonicalizer interface { + Canonicalize(el *etree.Element) ([]byte, error) + Algorithm() AlgorithmID +} + +type c14N10ExclusiveCanonicalizer struct { + prefixList string +} + +// MakeC14N10ExclusiveCanonicalizerWithPrefixList constructs an exclusive Canonicalizer +// from a PrefixList in NMTOKENS format (a white space separated list). +func MakeC14N10ExclusiveCanonicalizerWithPrefixList(prefixList string) Canonicalizer { + return &c14N10ExclusiveCanonicalizer{ + prefixList: prefixList, + } +} + +// Canonicalize transforms the input Element into a serialized XML document in canonical form. +func (c *c14N10ExclusiveCanonicalizer) Canonicalize(el *etree.Element) ([]byte, error) { + err := etreeutils.TransformExcC14n(el, c.prefixList) + if err != nil { + return nil, err + } + + return canonicalSerialize(el) +} + +func (c *c14N10ExclusiveCanonicalizer) Algorithm() AlgorithmID { + return CanonicalXML10ExclusiveAlgorithmId +} + +type c14N11Canonicalizer struct{} + +// MakeC14N11Canonicalizer constructs an inclusive canonicalizer. +func MakeC14N11Canonicalizer() Canonicalizer { + return &c14N11Canonicalizer{} +} + +// Canonicalize transforms the input Element into a serialized XML document in canonical form. +func (c *c14N11Canonicalizer) Canonicalize(el *etree.Element) ([]byte, error) { + scope := make(map[string]struct{}) + return canonicalSerialize(canonicalPrep(el, scope)) +} + +func (c *c14N11Canonicalizer) Algorithm() AlgorithmID { + return CanonicalXML11AlgorithmId +} + +func composeAttr(space, key string) string { + if space != "" { + return space + ":" + key + } + + return key +} + +type c14nSpace struct { + a etree.Attr + used bool +} + +const nsSpace = "xmlns" + +// canonicalPrep accepts an *etree.Element and transforms it into one which is ready +// for serialization into inclusive canonical form. Specifically this +// entails: +// +// 1. Stripping re-declarations of namespaces +// 2. Sorting attributes into canonical order +// +// Inclusive canonicalization does not strip unused namespaces. +// +// TODO(russell_h): This is very similar to excCanonicalPrep - perhaps they should +// be unified into one parameterized function? +func canonicalPrep(el *etree.Element, seenSoFar map[string]struct{}) *etree.Element { + _seenSoFar := make(map[string]struct{}) + for k, v := range seenSoFar { + _seenSoFar[k] = v + } + + ne := el.Copy() + sort.Sort(etreeutils.SortedAttrs(ne.Attr)) + if len(ne.Attr) != 0 { + for _, attr := range ne.Attr { + if attr.Space != nsSpace { + continue + } + key := attr.Space + ":" + attr.Key + if _, seen := _seenSoFar[key]; seen { + ne.RemoveAttr(attr.Space + ":" + attr.Key) + } else { + _seenSoFar[key] = struct{}{} + } + } + } + + for i, token := range ne.Child { + childElement, ok := token.(*etree.Element) + if ok { + ne.Child[i] = canonicalPrep(childElement, _seenSoFar) + } + } + + return ne +} + +func canonicalSerialize(el *etree.Element) ([]byte, error) { + doc := etree.NewDocument() + doc.SetRoot(el.Copy()) + + doc.WriteSettings = etree.WriteSettings{ + CanonicalAttrVal: true, + CanonicalEndTags: true, + CanonicalText: true, + } + + return doc.WriteToBytes() +} diff --git a/vendor/github.com/russellhaering/goxmldsig/clock.go b/vendor/github.com/russellhaering/goxmldsig/clock.go new file mode 100644 index 0000000000000..cceaaa5460023 --- /dev/null +++ b/vendor/github.com/russellhaering/goxmldsig/clock.go @@ -0,0 +1,55 @@ +package dsig + +import ( + "time" + + "github.com/jonboulle/clockwork" +) + +// Clock wraps a clockwork.Clock (which could be real or fake) in order +// to default to a real clock when a nil *Clock is used. In other words, +// if you attempt to use a nil *Clock it will defer to the real system +// clock. This allows Clock to be easily added to structs with methods +// that currently reference the time package, without requiring every +// instantiation of that struct to be updated. +type Clock struct { + wrapped clockwork.Clock +} + +func (c *Clock) getWrapped() clockwork.Clock { + if c == nil { + return clockwork.NewRealClock() + } + + return c.wrapped +} + +func (c *Clock) After(d time.Duration) <-chan time.Time { + return c.getWrapped().After(d) +} + +func (c *Clock) Sleep(d time.Duration) { + c.getWrapped().Sleep(d) +} + +func (c *Clock) Now() time.Time { + return c.getWrapped().Now() +} + +func NewRealClock() *Clock { + return &Clock{ + wrapped: clockwork.NewRealClock(), + } +} + +func NewFakeClock(wrapped clockwork.Clock) *Clock { + return &Clock{ + wrapped: wrapped, + } +} + +func NewFakeClockAt(t time.Time) *Clock { + return &Clock{ + wrapped: clockwork.NewFakeClockAt(t), + } +} diff --git a/vendor/github.com/russellhaering/goxmldsig/etreeutils/canonicalize.go b/vendor/github.com/russellhaering/goxmldsig/etreeutils/canonicalize.go new file mode 100644 index 0000000000000..9e6df954d230f --- /dev/null +++ b/vendor/github.com/russellhaering/goxmldsig/etreeutils/canonicalize.go @@ -0,0 +1,98 @@ +package etreeutils + +import ( + "sort" + "strings" + + "github.com/beevik/etree" +) + +// TransformExcC14n transforms the passed element into xml-exc-c14n form. +func TransformExcC14n(el *etree.Element, inclusiveNamespacesPrefixList string) error { + prefixes := strings.Fields(inclusiveNamespacesPrefixList) + prefixSet := make(map[string]struct{}, len(prefixes)) + + for _, prefix := range prefixes { + prefixSet[prefix] = struct{}{} + } + + err := transformExcC14n(DefaultNSContext, EmptyNSContext, el, prefixSet) + if err != nil { + return err + } + + return nil +} + +func transformExcC14n(ctx, declared NSContext, el *etree.Element, inclusiveNamespaces map[string]struct{}) error { + scope, err := ctx.SubContext(el) + if err != nil { + return err + } + + visiblyUtilizedPrefixes := map[string]struct{}{ + el.Space: struct{}{}, + } + + filteredAttrs := []etree.Attr{} + + // Filter out all namespace declarations + for _, attr := range el.Attr { + switch { + case attr.Space == xmlnsPrefix: + if _, ok := inclusiveNamespaces[attr.Key]; ok { + visiblyUtilizedPrefixes[attr.Key] = struct{}{} + } + + case attr.Space == defaultPrefix && attr.Key == xmlnsPrefix: + if _, ok := inclusiveNamespaces[defaultPrefix]; ok { + visiblyUtilizedPrefixes[defaultPrefix] = struct{}{} + } + + default: + if attr.Space != defaultPrefix { + visiblyUtilizedPrefixes[attr.Space] = struct{}{} + } + + filteredAttrs = append(filteredAttrs, attr) + } + } + + el.Attr = filteredAttrs + + declared = declared.Copy() + + // Declare all visibly utilized prefixes that are in-scope but haven't + // been declared in the canonicalized form yet. These might have been + // declared on this element but then filtered out above, or they might + // have been declared on an ancestor (before canonicalization) which + // didn't visibly utilize and thus had them removed. + for prefix := range visiblyUtilizedPrefixes { + // Skip redundant declarations - they have to already have the same + // value. + if declaredNamespace, ok := declared.prefixes[prefix]; ok { + if value, ok := scope.prefixes[prefix]; ok && declaredNamespace == value { + continue + } + } + + namespace, err := scope.LookupPrefix(prefix) + if err != nil { + return err + } + + el.Attr = append(el.Attr, declared.declare(prefix, namespace)) + } + + sort.Sort(SortedAttrs(el.Attr)) + + // Transform child elements + for _, child := range el.ChildElements() { + err := transformExcC14n(scope, declared, child, inclusiveNamespaces) + if err != nil { + return err + } + } + + return nil +} diff --git a/vendor/github.com/russellhaering/goxmldsig/etreeutils/namespace.go b/vendor/github.com/russellhaering/goxmldsig/etreeutils/namespace.go new file mode 100644 index 0000000000000..45f3bea1647c5 --- /dev/null +++ b/vendor/github.com/russellhaering/goxmldsig/etreeutils/namespace.go @@ -0,0 +1,328 @@ +package etreeutils + +import ( + "errors" + + "fmt" + + "sort" + + "github.com/beevik/etree" +) + +const ( + defaultPrefix = "" + xmlnsPrefix = "xmlns" + xmlPrefix = "xml" + + XMLNamespace = "http://www.w3.org/XML/1998/namespace" + XMLNSNamespace = "http://www.w3.org/2000/xmlns/" +) + +var ( + DefaultNSContext = NSContext{ + prefixes: map[string]string{ + defaultPrefix: XMLNamespace, + xmlPrefix: XMLNamespace, + xmlnsPrefix: XMLNSNamespace, + }, + } + + EmptyNSContext = NSContext{} + + ErrReservedNamespace = errors.New("disallowed declaration of reserved namespace") + ErrInvalidDefaultNamespace = errors.New("invalid default namespace declaration") + ErrTraversalHalted = errors.New("traversal halted") +) + +type ErrUndeclaredNSPrefix struct { + Prefix string +} + +func (e ErrUndeclaredNSPrefix) Error() string { + return fmt.Sprintf("undeclared namespace prefix: '%s'", e.Prefix) +} + +type NSContext struct { + prefixes map[string]string +} + +func (ctx NSContext) Copy() NSContext { + prefixes := make(map[string]string, len(ctx.prefixes)+4) + for k, v := range ctx.prefixes { + prefixes[k] = v + } + + return NSContext{prefixes: prefixes} +} + +func (ctx NSContext) declare(prefix, namespace string) etree.Attr { + ctx.prefixes[prefix] = namespace + + switch prefix { + case defaultPrefix: + return etree.Attr{ + Key: xmlnsPrefix, + Value: namespace, + } + + default: + return etree.Attr{ + Space: xmlnsPrefix, + Key: prefix, + Value: namespace, + } + } +} + +func (ctx NSContext) SubContext(el *etree.Element) (NSContext, error) { + // The subcontext should inherit existing declared prefixes + newCtx := ctx.Copy() + + // Merge new namespace declarations on top of existing ones. + for _, attr := range el.Attr { + if attr.Space == xmlnsPrefix { + // This attribute is a namespace declaration of the form "xmlns:" + + // The 'xml' namespace may only be re-declared with the name 'http://www.w3.org/XML/1998/namespace' + if attr.Key == xmlPrefix && attr.Value != XMLNamespace { + return ctx, ErrReservedNamespace + } + + // The 'xmlns' namespace may not be re-declared + if attr.Key == xmlnsPrefix { + return ctx, ErrReservedNamespace + } + + newCtx.declare(attr.Key, attr.Value) + } else if attr.Space == defaultPrefix && attr.Key == xmlnsPrefix { + // This attribute is a default namespace declaration + + // The xmlns namespace value may not be declared as the default namespace + if attr.Value == XMLNSNamespace { + return ctx, ErrInvalidDefaultNamespace + } + + newCtx.declare(defaultPrefix, attr.Value) + } + } + + return newCtx, nil +} + +// Prefixes returns a copy of this context's prefix map. +func (ctx NSContext) Prefixes() map[string]string { + prefixes := make(map[string]string, len(ctx.prefixes)) + for k, v := range ctx.prefixes { + prefixes[k] = v + } + + return prefixes +} + +// LookupPrefix attempts to find a declared namespace for the specified prefix. If the prefix +// is an empty string this will be the default namespace for this context. If the prefix is +// undeclared in this context an ErrUndeclaredNSPrefix will be returned. +func (ctx NSContext) LookupPrefix(prefix string) (string, error) { + if namespace, ok := ctx.prefixes[prefix]; ok { + return namespace, nil + } + + return "", ErrUndeclaredNSPrefix{ + Prefix: prefix, + } +} + +// NSIterHandler is a function which is invoked with a element and its surrounding +// NSContext during traversals. +type NSIterHandler func(NSContext, *etree.Element) error + +// NSTraverse traverses an element tree, invoking the passed handler for each element +// in the tree. +func NSTraverse(ctx NSContext, el *etree.Element, handle NSIterHandler) error { + ctx, err := ctx.SubContext(el) + if err != nil { + return err + } + + err = handle(ctx, el) + if err != nil { + return err + } + + // Recursively traverse child elements. + for _, child := range el.ChildElements() { + err := NSTraverse(ctx, child, handle) + if err != nil { + return err + } + } + + return nil +} + +// NSDetatch makes a copy of the passed element, and declares any namespaces in +// the passed context onto the new element before returning it. +func NSDetatch(ctx NSContext, el *etree.Element) (*etree.Element, error) { + ctx, err := ctx.SubContext(el) + if err != nil { + return nil, err + } + + el = el.Copy() + + // Build a new attribute list + attrs := make([]etree.Attr, 0, len(el.Attr)) + + // First copy over anything that isn't a namespace declaration + for _, attr := range el.Attr { + if attr.Space == xmlnsPrefix { + continue + } + + if attr.Space == defaultPrefix && attr.Key == xmlnsPrefix { + continue + } + + attrs = append(attrs, attr) + } + + // Append all in-context namespace declarations + for prefix, namespace := range ctx.prefixes { + // Skip the implicit "xml" and "xmlns" prefix declarations + if prefix == xmlnsPrefix || prefix == xmlPrefix { + continue + } + + // Also skip declararing the default namespace as XMLNamespace + if prefix == defaultPrefix && namespace == XMLNamespace { + continue + } + + if prefix != defaultPrefix { + attrs = append(attrs, etree.Attr{ + Space: xmlnsPrefix, + Key: prefix, + Value: namespace, + }) + } else { + attrs = append(attrs, etree.Attr{ + Key: xmlnsPrefix, + Value: namespace, + }) + } + } + + sort.Sort(SortedAttrs(attrs)) + + el.Attr = attrs + + return el, nil +} + +// NSSelectOne behaves identically to NSSelectOneCtx, but uses DefaultNSContext as the +// surrounding context. +func NSSelectOne(el *etree.Element, namespace, tag string) (*etree.Element, error) { + return NSSelectOneCtx(DefaultNSContext, el, namespace, tag) +} + +// NSSelectOneCtx conducts a depth-first search for an element with the specified namespace +// and tag. If such an element is found, a new *etree.Element is returned which is a +// copy of the found element, but with all in-context namespace declarations attached +// to the element as attributes. +func NSSelectOneCtx(ctx NSContext, el *etree.Element, namespace, tag string) (*etree.Element, error) { + var found *etree.Element + + err := NSFindIterateCtx(ctx, el, namespace, tag, func(ctx NSContext, el *etree.Element) error { + var err error + + found, err = NSDetatch(ctx, el) + if err != nil { + return err + } + + return ErrTraversalHalted + }) + + if err != nil { + return nil, err + } + + return found, nil +} + +// NSFindIterate behaves identically to NSFindIterateCtx, but uses DefaultNSContext +// as the surrounding context. +func NSFindIterate(el *etree.Element, namespace, tag string, handle NSIterHandler) error { + return NSFindIterateCtx(DefaultNSContext, el, namespace, tag, handle) +} + +// NSFindIterateCtx conducts a depth-first traversal searching for elements with the +// specified tag in the specified namespace. It uses the passed NSContext for prefix +// lookups. For each such element, the passed handler function is invoked. If the +// handler function returns an error traversal is immediately halted. If the error +// returned by the handler is ErrTraversalHalted then nil will be returned by +// NSFindIterate. If any other error is returned by the handler, that error will be +// returned by NSFindIterate. +func NSFindIterateCtx(ctx NSContext, el *etree.Element, namespace, tag string, handle NSIterHandler) error { + err := NSTraverse(ctx, el, func(ctx NSContext, el *etree.Element) error { + currentNS, err := ctx.LookupPrefix(el.Space) + if err != nil { + return err + } + + // Base case, el is the sought after element. + if currentNS == namespace && el.Tag == tag { + return handle(ctx, el) + } + + return nil + }) + + if err != nil && err != ErrTraversalHalted { + return err + } + + return nil +} + +// NSFindOne behaves identically to NSFindOneCtx, but uses DefaultNSContext for +// context. +func NSFindOne(el *etree.Element, namespace, tag string) (*etree.Element, error) { + return NSFindOneCtx(DefaultNSContext, el, namespace, tag) +} + +// NSFindOneCtx conducts a depth-first search for the specified element. If such an element +// is found a reference to it is returned. +func NSFindOneCtx(ctx NSContext, el *etree.Element, namespace, tag string) (*etree.Element, error) { + var found *etree.Element + + err := NSFindIterateCtx(ctx, el, namespace, tag, func(ctx NSContext, el *etree.Element) error { + found = el + return ErrTraversalHalted + }) + + if err != nil { + return nil, err + } + + return found, nil +} + +// NSBuildParentContext recurses upward from an element in order to build an NSContext +// for its immediate parent. If the element has no parent DefaultNSContext +// is returned. +func NSBuildParentContext(el *etree.Element) (NSContext, error) { + parent := el.Parent() + if parent == nil { + return DefaultNSContext, nil + } + + ctx, err := NSBuildParentContext(parent) + + if err != nil { + return ctx, err + } + + return ctx.SubContext(parent) +} diff --git a/vendor/github.com/russellhaering/goxmldsig/etreeutils/sort.go b/vendor/github.com/russellhaering/goxmldsig/etreeutils/sort.go new file mode 100644 index 0000000000000..5871a3913de94 --- /dev/null +++ b/vendor/github.com/russellhaering/goxmldsig/etreeutils/sort.go @@ -0,0 +1,66 @@ +package etreeutils + +import "github.com/beevik/etree" + +// SortedAttrs provides sorting capabilities, compatible with XML C14N, on top +// of an []etree.Attr +type SortedAttrs []etree.Attr + +func (a SortedAttrs) Len() int { + return len(a) +} + +func (a SortedAttrs) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} + +func (a SortedAttrs) Less(i, j int) bool { + // This is the best reference I've found on sort order: + // http://dst.lbl.gov/~ksb/Scratch/XMLC14N.html + + // If attr j is a default namespace declaration, attr i may + // not be strictly "less" than it. + if a[j].Space == defaultPrefix && a[j].Key == xmlnsPrefix { + return false + } + + // Otherwise, if attr i is a default namespace declaration, it + // must be less than anything else. + if a[i].Space == defaultPrefix && a[i].Key == xmlnsPrefix { + return true + } + + // Next, namespace prefix declarations, sorted by prefix, come before + // anythign else. + if a[i].Space == xmlnsPrefix { + if a[j].Space == xmlnsPrefix { + return a[i].Key < a[j].Key + } + return true + } + + if a[j].Space == xmlnsPrefix { + return false + } + + // Then come unprefixed attributes, sorted by key. + if a[i].Space == defaultPrefix { + if a[j].Space == defaultPrefix { + return a[i].Key < a[j].Key + } + return true + } + + if a[j].Space == defaultPrefix { + return false + } + + // Wow. We're still going. Finally, attributes in the same namespace should be + // sorted by key. Attributes in different namespaces should be sorted by the + // actual namespace (_not_ the prefix). For now just use the prefix. + if a[i].Space == a[j].Space { + return a[i].Key < a[j].Key + } + + return a[i].Space < a[j].Space +} diff --git a/vendor/github.com/russellhaering/goxmldsig/etreeutils/unmarshal.go b/vendor/github.com/russellhaering/goxmldsig/etreeutils/unmarshal.go new file mode 100644 index 0000000000000..b1fecf85a4cdf --- /dev/null +++ b/vendor/github.com/russellhaering/goxmldsig/etreeutils/unmarshal.go @@ -0,0 +1,43 @@ +package etreeutils + +import ( + "encoding/xml" + + "github.com/beevik/etree" +) + +// NSUnmarshalElement unmarshals the passed etree Element into the value pointed to by +// v using encoding/xml in the context of the passed NSContext. If v implements +// ElementKeeper, SetUnderlyingElement will be called on v with a reference to el. +func NSUnmarshalElement(ctx NSContext, el *etree.Element, v interface{}) error { + detatched, err := NSDetatch(ctx, el) + if err != nil { + return err + } + + doc := etree.NewDocument() + doc.AddChild(detatched) + data, err := doc.WriteToBytes() + if err != nil { + return err + } + + err = xml.Unmarshal(data, v) + if err != nil { + return err + } + + switch v := v.(type) { + case ElementKeeper: + v.SetUnderlyingElement(el) + } + + return nil +} + +// ElementKeeper should be implemented by types which will be passed to +// UnmarshalElement, but wish to keep a reference +type ElementKeeper interface { + SetUnderlyingElement(*etree.Element) + UnderlyingElement() *etree.Element +} diff --git a/vendor/github.com/russellhaering/goxmldsig/keystore.go b/vendor/github.com/russellhaering/goxmldsig/keystore.go new file mode 100644 index 0000000000000..81487f080e3c3 --- /dev/null +++ b/vendor/github.com/russellhaering/goxmldsig/keystore.go @@ -0,0 +1,63 @@ +package dsig + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "math/big" + "time" +) + +type X509KeyStore interface { + GetKeyPair() (privateKey *rsa.PrivateKey, cert []byte, err error) +} + +type X509CertificateStore interface { + Certificates() (roots []*x509.Certificate, err error) +} + +type MemoryX509CertificateStore struct { + Roots []*x509.Certificate +} + +func (mX509cs *MemoryX509CertificateStore) Certificates() ([]*x509.Certificate, error) { + return mX509cs.Roots, nil +} + +type MemoryX509KeyStore struct { + privateKey *rsa.PrivateKey + cert []byte +} + +func (ks *MemoryX509KeyStore) GetKeyPair() (*rsa.PrivateKey, []byte, error) { + return ks.privateKey, ks.cert, nil +} + +func RandomKeyStoreForTest() X509KeyStore { + key, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + panic(err) + } + + now := time.Now() + + template := &x509.Certificate{ + SerialNumber: big.NewInt(0), + NotBefore: now.Add(-5 * time.Minute), + NotAfter: now.Add(365 * 24 * time.Hour), + + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{}, + BasicConstraintsValid: true, + } + + cert, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + if err != nil { + panic(err) + } + + return &MemoryX509KeyStore{ + privateKey: key, + cert: cert, + } +} diff --git a/vendor/github.com/russellhaering/goxmldsig/sign.go b/vendor/github.com/russellhaering/goxmldsig/sign.go new file mode 100644 index 0000000000000..82498098bd66a --- /dev/null +++ b/vendor/github.com/russellhaering/goxmldsig/sign.go @@ -0,0 +1,211 @@ +package dsig + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + _ "crypto/sha1" + _ "crypto/sha256" + "encoding/base64" + "errors" + "fmt" + + "github.com/beevik/etree" + "github.com/russellhaering/goxmldsig/etreeutils" +) + +type SigningContext struct { + Hash crypto.Hash + KeyStore X509KeyStore + IdAttribute string + Prefix string + Canonicalizer Canonicalizer +} + +func NewDefaultSigningContext(ks X509KeyStore) *SigningContext { + return &SigningContext{ + Hash: crypto.SHA256, + KeyStore: ks, + IdAttribute: DefaultIdAttr, + Prefix: DefaultPrefix, + Canonicalizer: MakeC14N11Canonicalizer(), + } +} + +func (ctx *SigningContext) SetSignatureMethod(algorithmID string) error { + hash, ok := signatureMethodsByIdentifier[algorithmID] + if !ok { + return fmt.Errorf("Unknown SignatureMethod: %s", algorithmID) + } + + ctx.Hash = hash + + return nil +} + +func (ctx *SigningContext) digest(el *etree.Element) ([]byte, error) { + canonical, err := ctx.Canonicalizer.Canonicalize(el) + if err != nil { + return nil, err + } + + hash := ctx.Hash.New() + _, err = hash.Write(canonical) + if err != nil { + return nil, err + } + + return hash.Sum(nil), nil +} + +func (ctx *SigningContext) constructSignedInfo(el *etree.Element, enveloped bool) (*etree.Element, error) { + digestAlgorithmIdentifier, ok := digestAlgorithmIdentifiers[ctx.Hash] + if !ok { + return nil, errors.New("unsupported hash mechanism") + } + + signatureMethodIdentifier, ok := signatureMethodIdentifiers[ctx.Hash] + if !ok { + return nil, errors.New("unsupported signature method") + } + + digest, err := ctx.digest(el) + if err != nil { + return nil, err + } + + signedInfo := &etree.Element{ + Tag: SignedInfoTag, + Space: ctx.Prefix, + } + + // /SignedInfo/CanonicalizationMethod + canonicalizationMethod := ctx.createNamespacedElement(signedInfo, CanonicalizationMethodTag) + canonicalizationMethod.CreateAttr(AlgorithmAttr, string(ctx.Canonicalizer.Algorithm())) + + // /SignedInfo/SignatureMethod + signatureMethod := ctx.createNamespacedElement(signedInfo, SignatureMethodTag) + signatureMethod.CreateAttr(AlgorithmAttr, signatureMethodIdentifier) + + // /SignedInfo/Reference + reference := ctx.createNamespacedElement(signedInfo, ReferenceTag) + + dataId := el.SelectAttrValue(ctx.IdAttribute, "") + if dataId == "" { + return nil, errors.New("Missing data ID") + } + + reference.CreateAttr(URIAttr, "#"+dataId) + + // /SignedInfo/Reference/Transforms + transforms := ctx.createNamespacedElement(reference, TransformsTag) + if enveloped { + envelopedTransform := ctx.createNamespacedElement(transforms, TransformTag) + envelopedTransform.CreateAttr(AlgorithmAttr, EnvelopedSignatureAltorithmId.String()) + } + canonicalizationAlgorithm := ctx.createNamespacedElement(transforms, TransformTag) + canonicalizationAlgorithm.CreateAttr(AlgorithmAttr, string(ctx.Canonicalizer.Algorithm())) + + // /SignedInfo/Reference/DigestMethod + digestMethod := ctx.createNamespacedElement(reference, DigestMethodTag) + digestMethod.CreateAttr(AlgorithmAttr, digestAlgorithmIdentifier) + + // /SignedInfo/Reference/DigestValue + digestValue := ctx.createNamespacedElement(reference, DigestValueTag) + digestValue.SetText(base64.StdEncoding.EncodeToString(digest)) + + return signedInfo, nil +} + +func (ctx *SigningContext) constructSignature(el *etree.Element, enveloped bool) (*etree.Element, error) { + signedInfo, err := ctx.constructSignedInfo(el, enveloped) + if err != nil { + return nil, err + } + + sig := &etree.Element{ + Tag: SignatureTag, + Space: ctx.Prefix, + } + + xmlns := "xmlns" + if ctx.Prefix != "" { + xmlns += ":" + ctx.Prefix + } + + sig.CreateAttr(xmlns, Namespace) + sig.AddChild(signedInfo) + + // When using xml-c14n11 (ie, non-exclusive canonicalization) the canonical form + // of the SignedInfo must declare all namespaces that are in scope at it's final + // enveloped location in the document. In order to do that, we're going to construct + // a series of cascading NSContexts to capture namespace declarations: + + // First get the context surrounding the element we are signing. + rootNSCtx, err := etreeutils.NSBuildParentContext(el) + if err != nil { + return nil, err + } + + // Then capture any declarations on the element itself. + elNSCtx, err := rootNSCtx.SubContext(el) + if err != nil { + return nil, err + } + + // Followed by declarations on the Signature (which we just added above) + sigNSCtx, err := elNSCtx.SubContext(sig) + if err != nil { + return nil, err + } + + // Finally detatch the SignedInfo in order to capture all of the namespace + // declarations in the scope we've constructed. + detatchedSignedInfo, err := etreeutils.NSDetatch(sigNSCtx, signedInfo) + if err != nil { + return nil, err + } + + digest, err := ctx.digest(detatchedSignedInfo) + if err != nil { + return nil, err + } + + key, cert, err := ctx.KeyStore.GetKeyPair() + if err != nil { + return nil, err + } + + rawSignature, err := rsa.SignPKCS1v15(rand.Reader, key, ctx.Hash, digest) + if err != nil { + return nil, err + } + + signatureValue := ctx.createNamespacedElement(sig, SignatureValueTag) + signatureValue.SetText(base64.StdEncoding.EncodeToString(rawSignature)) + + keyInfo := ctx.createNamespacedElement(sig, KeyInfoTag) + x509Data := ctx.createNamespacedElement(keyInfo, X509DataTag) + x509Certificate := ctx.createNamespacedElement(x509Data, X509CertificateTag) + x509Certificate.SetText(base64.StdEncoding.EncodeToString(cert)) + + return sig, nil +} + +func (ctx *SigningContext) createNamespacedElement(el *etree.Element, tag string) *etree.Element { + child := el.CreateElement(tag) + child.Space = ctx.Prefix + return child +} + +func (ctx *SigningContext) SignEnveloped(el *etree.Element) (*etree.Element, error) { + sig, err := ctx.constructSignature(el, true) + if err != nil { + return nil, err + } + + ret := el.Copy() + ret.Child = append(ret.Child, sig) + + return ret, nil +} diff --git a/vendor/github.com/russellhaering/goxmldsig/tls_keystore.go b/vendor/github.com/russellhaering/goxmldsig/tls_keystore.go new file mode 100644 index 0000000000000..c98f312cae6af --- /dev/null +++ b/vendor/github.com/russellhaering/goxmldsig/tls_keystore.go @@ -0,0 +1,34 @@ +package dsig + +import ( + "crypto/rsa" + "crypto/tls" + "fmt" +) + +//Well-known errors +var ( + ErrNonRSAKey = fmt.Errorf("Private key was not RSA") + ErrMissingCertificates = fmt.Errorf("No public certificates provided") +) + +//TLSCertKeyStore wraps the stdlib tls.Certificate to return its contained key +//and certs. +type TLSCertKeyStore tls.Certificate + +//GetKeyPair implements X509KeyStore using the underlying tls.Certificate +func (d TLSCertKeyStore) GetKeyPair() (*rsa.PrivateKey, []byte, error) { + pk, ok := d.PrivateKey.(*rsa.PrivateKey) + + if !ok { + return nil, nil, ErrNonRSAKey + } + + if len(d.Certificate) < 1 { + return nil, nil, ErrMissingCertificates + } + + crt := d.Certificate[0] + + return pk, crt, nil +} diff --git a/vendor/github.com/russellhaering/goxmldsig/types/signature.go b/vendor/github.com/russellhaering/goxmldsig/types/signature.go new file mode 100644 index 0000000000000..2c7b1632a8ebd --- /dev/null +++ b/vendor/github.com/russellhaering/goxmldsig/types/signature.go @@ -0,0 +1,93 @@ +package types + +import ( + "encoding/xml" + + "github.com/beevik/etree" +) + +type InclusiveNamespaces struct { + XMLName xml.Name `xml:"http://www.w3.org/2001/10/xml-exc-c14n# InclusiveNamespaces"` + PrefixList string `xml:"PrefixList,attr"` +} + +type Transform struct { + XMLName xml.Name `xml:"http://www.w3.org/2000/09/xmldsig# Transform"` + Algorithm string `xml:"Algorithm,attr"` + InclusiveNamespaces *InclusiveNamespaces `xml:"InclusiveNamespaces"` +} + +type Transforms struct { + XMLName xml.Name `xml:"http://www.w3.org/2000/09/xmldsig# Transforms"` + Transforms []Transform `xml:"Transform"` +} + +type DigestMethod struct { + XMLName xml.Name `xml:"http://www.w3.org/2000/09/xmldsig# DigestMethod"` + Algorithm string `xml:"Algorithm,attr"` +} + +type Reference struct { + XMLName xml.Name `xml:"http://www.w3.org/2000/09/xmldsig# Reference"` + URI string `xml:"URI,attr"` + DigestValue string `xml:"DigestValue"` + DigestAlgo DigestMethod `xml:"DigestMethod"` + Transforms Transforms `xml:"Transforms"` +} + +type CanonicalizationMethod struct { + XMLName xml.Name `xml:"http://www.w3.org/2000/09/xmldsig# CanonicalizationMethod"` + Algorithm string `xml:"Algorithm,attr"` +} + +type SignatureMethod struct { + XMLName xml.Name `xml:"http://www.w3.org/2000/09/xmldsig# SignatureMethod"` + Algorithm string `xml:"Algorithm,attr"` +} + +type SignedInfo struct { + XMLName xml.Name `xml:"http://www.w3.org/2000/09/xmldsig# SignedInfo"` + CanonicalizationMethod CanonicalizationMethod `xml:"CanonicalizationMethod"` + SignatureMethod SignatureMethod `xml:"SignatureMethod"` + References []Reference `xml:"Reference"` +} + +type SignatureValue struct { + XMLName xml.Name `xml:"http://www.w3.org/2000/09/xmldsig# SignatureValue"` + Data string `xml:",chardata"` +} + +type KeyInfo struct { + XMLName xml.Name `xml:"http://www.w3.org/2000/09/xmldsig# KeyInfo"` + X509Data X509Data `xml:"X509Data"` +} + +type X509Data struct { + XMLName xml.Name `xml:"http://www.w3.org/2000/09/xmldsig# X509Data"` + X509Certificate X509Certificate `xml:"X509Certificate"` +} + +type X509Certificate struct { + XMLName xml.Name `xml:"http://www.w3.org/2000/09/xmldsig# X509Certificate"` + Data string `xml:",chardata"` +} + +type Signature struct { + XMLName xml.Name `xml:"http://www.w3.org/2000/09/xmldsig# Signature"` + SignedInfo *SignedInfo `xml:"SignedInfo"` + SignatureValue *SignatureValue `xml:"SignatureValue"` + KeyInfo *KeyInfo `xml:"KeyInfo"` + el *etree.Element +} + +// SetUnderlyingElement will be called with a reference to the Element this Signature +// was unmarshaled from. +func (s *Signature) SetUnderlyingElement(el *etree.Element) { + s.el = el +} + +// UnderlyingElement returns a reference to the Element this signature was unmarshaled +// from, where applicable. +func (s *Signature) UnderlyingElement() *etree.Element { + return s.el +} diff --git a/vendor/github.com/russellhaering/goxmldsig/validate.go b/vendor/github.com/russellhaering/goxmldsig/validate.go new file mode 100644 index 0000000000000..7aa2d73e1472d --- /dev/null +++ b/vendor/github.com/russellhaering/goxmldsig/validate.go @@ -0,0 +1,417 @@ +package dsig + +import ( + "bytes" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "errors" + "fmt" + "regexp" + + "github.com/beevik/etree" + "github.com/russellhaering/goxmldsig/etreeutils" + "github.com/russellhaering/goxmldsig/types" +) + +var uriRegexp = regexp.MustCompile("^#[a-zA-Z_][\\w.-]*$") + +var ( + // ErrMissingSignature indicates that no enveloped signature was found referencing + // the top level element passed for signature verification. + ErrMissingSignature = errors.New("Missing signature referencing the top-level element") +) + +type ValidationContext struct { + CertificateStore X509CertificateStore + IdAttribute string + Clock *Clock +} + +func NewDefaultValidationContext(certificateStore X509CertificateStore) *ValidationContext { + return &ValidationContext{ + CertificateStore: certificateStore, + IdAttribute: DefaultIdAttr, + } +} + +// TODO(russell_h): More flexible namespace support. This might barely work. +func inNamespace(el *etree.Element, ns string) bool { + for _, attr := range el.Attr { + if attr.Value == ns { + if attr.Space == "" && attr.Key == "xmlns" { + return el.Space == "" + } else if attr.Space == "xmlns" { + return el.Space == attr.Key + } + } + } + + return false +} + +func childPath(space, tag string) string { + if space == "" { + return "./" + tag + } else { + return "./" + space + ":" + tag + } +} + +// The RemoveElement method on etree.Element isn't recursive... +func recursivelyRemoveElement(tree, el *etree.Element) bool { + if tree.RemoveChild(el) != nil { + return true + } + + for _, child := range tree.Child { + if childElement, ok := child.(*etree.Element); ok { + if recursivelyRemoveElement(childElement, el) { + return true + } + } + } + + return false +} + +// transform applies the passed set of transforms to the specified root element. +// +// The functionality of transform is currently very limited and purpose-specific. +// +// NOTE(russell_h): Ideally this wouldn't mutate the root passed to it, and would +// instead return a copy. Unfortunately copying the tree makes it difficult to +// correctly locate the signature. I'm opting, for now, to simply mutate the root +// parameter. +func (ctx *ValidationContext) transform( + el *etree.Element, + sig *types.Signature, + ref *types.Reference) (*etree.Element, Canonicalizer, error) { + transforms := ref.Transforms.Transforms + + if len(transforms) != 2 { + return nil, nil, errors.New("Expected Enveloped and C14N transforms") + } + + var canonicalizer Canonicalizer + + for _, transform := range transforms { + algo := transform.Algorithm + + switch AlgorithmID(algo) { + case EnvelopedSignatureAltorithmId: + if !recursivelyRemoveElement(el, sig.UnderlyingElement()) { + return nil, nil, errors.New("Error applying canonicalization transform: Signature not found") + } + + case CanonicalXML10ExclusiveAlgorithmId: + var prefixList string + if transform.InclusiveNamespaces != nil { + prefixList = transform.InclusiveNamespaces.PrefixList + } + + canonicalizer = MakeC14N10ExclusiveCanonicalizerWithPrefixList(prefixList) + + case CanonicalXML11AlgorithmId: + canonicalizer = MakeC14N11Canonicalizer() + + default: + return nil, nil, errors.New("Unknown Transform Algorithm: " + algo) + } + } + + if canonicalizer == nil { + return nil, nil, errors.New("Expected canonicalization transform") + } + + return el, canonicalizer, nil +} + +func (ctx *ValidationContext) digest(el *etree.Element, digestAlgorithmId string, canonicalizer Canonicalizer) ([]byte, error) { + data, err := canonicalizer.Canonicalize(el) + if err != nil { + return nil, err + } + + digestAlgorithm, ok := digestAlgorithmsByIdentifier[digestAlgorithmId] + if !ok { + return nil, errors.New("Unknown digest algorithm: " + digestAlgorithmId) + } + + hash := digestAlgorithm.New() + _, err = hash.Write(data) + if err != nil { + return nil, err + } + + return hash.Sum(nil), nil +} + +func (ctx *ValidationContext) verifySignedInfo(sig *types.Signature, canonicalizer Canonicalizer, signatureMethodId string, cert *x509.Certificate, decodedSignature []byte) error { + signatureElement := sig.UnderlyingElement() + + signedInfo := signatureElement.FindElement(childPath(signatureElement.Space, SignedInfoTag)) + if signedInfo == nil { + return errors.New("Missing SignedInfo") + } + + // Canonicalize the xml + canonical, err := canonicalSerialize(signedInfo) + if err != nil { + return err + } + + signatureAlgorithm, ok := signatureMethodsByIdentifier[signatureMethodId] + if !ok { + return errors.New("Unknown signature method: " + signatureMethodId) + } + + hash := signatureAlgorithm.New() + _, err = hash.Write(canonical) + if err != nil { + return err + } + + hashed := hash.Sum(nil) + + pubKey, ok := cert.PublicKey.(*rsa.PublicKey) + if !ok { + return errors.New("Invalid public key") + } + + // Verify that the private key matching the public key from the cert was what was used to sign the 'SignedInfo' and produce the 'SignatureValue' + err = rsa.VerifyPKCS1v15(pubKey, signatureAlgorithm, hashed[:], decodedSignature) + if err != nil { + return err + } + + return nil +} + +func (ctx *ValidationContext) validateSignature(el *etree.Element, sig *types.Signature, cert *x509.Certificate) (*etree.Element, error) { + idAttr := el.SelectAttr(DefaultIdAttr) + if idAttr == nil || idAttr.Value == "" { + return nil, errors.New("Missing ID attribute") + } + + var ref *types.Reference + + // Find the first reference which references the top-level element + for _, _ref := range sig.SignedInfo.References { + if _ref.URI == "" || _ref.URI[1:] == idAttr.Value { + ref = &_ref + } + } + + // Perform all transformations listed in the 'SignedInfo' + // Basically, this means removing the 'SignedInfo' + transformed, canonicalizer, err := ctx.transform(el, sig, ref) + if err != nil { + return nil, err + } + + digestAlgorithm := ref.DigestAlgo.Algorithm + + // Digest the transformed XML and compare it to the 'DigestValue' from the 'SignedInfo' + digest, err := ctx.digest(transformed, digestAlgorithm, canonicalizer) + if err != nil { + return nil, err + } + + decodedDigestValue, err := base64.StdEncoding.DecodeString(ref.DigestValue) + if err != nil { + return nil, err + } + + if !bytes.Equal(digest, decodedDigestValue) { + return nil, errors.New("Signature could not be verified") + } + + // Decode the 'SignatureValue' so we can compare against it + decodedSignature, err := base64.StdEncoding.DecodeString(sig.SignatureValue.Data) + if err != nil { + return nil, errors.New("Could not decode signature") + } + + // Actually verify the 'SignedInfo' was signed by a trusted source + signatureMethod := sig.SignedInfo.SignatureMethod.Algorithm + err = ctx.verifySignedInfo(sig, canonicalizer, signatureMethod, cert, decodedSignature) + if err != nil { + return nil, err + } + + return transformed, nil +} + +func contains(roots []*x509.Certificate, cert *x509.Certificate) bool { + for _, root := range roots { + if root.Equal(cert) { + return true + } + } + return false +} + +// findSignature searches for a Signature element referencing the passed root element. +func (ctx *ValidationContext) findSignature(el *etree.Element) (*types.Signature, error) { + idAttr := el.SelectAttr(DefaultIdAttr) + if idAttr == nil || idAttr.Value == "" { + return nil, errors.New("Missing ID attribute") + } + + var sig *types.Signature + + // Traverse the tree looking for a Signature element + err := etreeutils.NSFindIterate(el, Namespace, SignatureTag, func(ctx etreeutils.NSContext, el *etree.Element) error { + + found := false + err := etreeutils.NSFindIterateCtx(ctx, el, Namespace, SignedInfoTag, + func(ctx etreeutils.NSContext, signedInfo *etree.Element) error { + // Ignore any SignedInfo that isn't an immediate descendent of Signature. + if signedInfo.Parent() != el { + return nil + } + + detachedSignedInfo, err := etreeutils.NSDetatch(ctx, signedInfo) + if err != nil { + return err + } + + c14NMethod := detachedSignedInfo.FindElement(childPath(detachedSignedInfo.Space, CanonicalizationMethodTag)) + if c14NMethod == nil { + return errors.New("missing CanonicalizationMethod on Signature") + } + + c14NAlgorithm := c14NMethod.SelectAttrValue(AlgorithmAttr, "") + + var canonicalSignedInfo *etree.Element + + switch AlgorithmID(c14NAlgorithm) { + case CanonicalXML10ExclusiveAlgorithmId: + err := etreeutils.TransformExcC14n(detachedSignedInfo, "") + if err != nil { + return err + } + + // NOTE: TransformExcC14n transforms the element in-place, + // while canonicalPrep isn't meant to. Once we standardize + // this behavior we can drop this, as well as the adding and + // removing of elements below. + canonicalSignedInfo = detachedSignedInfo + + case CanonicalXML11AlgorithmId: + canonicalSignedInfo = canonicalPrep(detachedSignedInfo, map[string]struct{}{}) + + default: + return fmt.Errorf("invalid CanonicalizationMethod on Signature: %s", c14NAlgorithm) + } + + el.RemoveChild(signedInfo) + el.AddChild(canonicalSignedInfo) + + found = true + + return etreeutils.ErrTraversalHalted + }) + if err != nil { + return err + } + + if !found { + return errors.New("Missing SignedInfo") + } + + // Unmarshal the signature into a structured Signature type + _sig := &types.Signature{} + err = etreeutils.NSUnmarshalElement(ctx, el, _sig) + if err != nil { + return err + } + + // Traverse references in the signature to determine whether it has at least + // one reference to the top level element. If so, conclude the search. + for _, ref := range _sig.SignedInfo.References { + if ref.URI == "" || ref.URI[1:] == idAttr.Value { + sig = _sig + return etreeutils.ErrTraversalHalted + } + } + + return nil + }) + + if err != nil { + return nil, err + } + + if sig == nil { + return nil, ErrMissingSignature + } + + return sig, nil +} + +func (ctx *ValidationContext) verifyCertificate(sig *types.Signature) (*x509.Certificate, error) { + now := ctx.Clock.Now() + + roots, err := ctx.CertificateStore.Certificates() + if err != nil { + return nil, err + } + + var cert *x509.Certificate + + if sig.KeyInfo != nil { + // If the Signature includes KeyInfo, extract the certificate from there + if sig.KeyInfo.X509Data.X509Certificate.Data == "" { + return nil, errors.New("missing X509Certificate within KeyInfo") + } + + certData, err := base64.StdEncoding.DecodeString(sig.KeyInfo.X509Data.X509Certificate.Data) + if err != nil { + return nil, errors.New("Failed to parse certificate") + } + + cert, err = x509.ParseCertificate(certData) + if err != nil { + return nil, err + } + } else { + // If the Signature doesn't have KeyInfo, Use the root certificate if there is only one + if len(roots) == 1 { + cert = roots[0] + } else { + return nil, errors.New("Missing x509 Element") + } + } + + // Verify that the certificate is one we trust + if !contains(roots, cert) { + return nil, errors.New("Could not verify certificate against trusted certs") + } + + if now.Before(cert.NotBefore) || now.After(cert.NotAfter) { + return nil, errors.New("Cert is not valid at this time") + } + + return cert, nil +} + +// Validate verifies that the passed element contains a valid enveloped signature +// matching a currently-valid certificate in the context's CertificateStore. +func (ctx *ValidationContext) Validate(el *etree.Element) (*etree.Element, error) { + // Make a copy of the element to avoid mutating the one we were passed. + el = el.Copy() + + sig, err := ctx.findSignature(el) + if err != nil { + return nil, err + } + + cert, err := ctx.verifyCertificate(sig) + if err != nil { + return nil, err + } + + return ctx.validateSignature(el, sig, cert) +} diff --git a/vendor/github.com/russellhaering/goxmldsig/xml_constants.go b/vendor/github.com/russellhaering/goxmldsig/xml_constants.go new file mode 100644 index 0000000000000..5c9cb6937de07 --- /dev/null +++ b/vendor/github.com/russellhaering/goxmldsig/xml_constants.go @@ -0,0 +1,78 @@ +package dsig + +import "crypto" + +const ( + DefaultPrefix = "ds" + Namespace = "http://www.w3.org/2000/09/xmldsig#" +) + +// Tags +const ( + SignatureTag = "Signature" + SignedInfoTag = "SignedInfo" + CanonicalizationMethodTag = "CanonicalizationMethod" + SignatureMethodTag = "SignatureMethod" + ReferenceTag = "Reference" + TransformsTag = "Transforms" + TransformTag = "Transform" + DigestMethodTag = "DigestMethod" + DigestValueTag = "DigestValue" + SignatureValueTag = "SignatureValue" + KeyInfoTag = "KeyInfo" + X509DataTag = "X509Data" + X509CertificateTag = "X509Certificate" + InclusiveNamespacesTag = "InclusiveNamespaces" +) + +const ( + AlgorithmAttr = "Algorithm" + URIAttr = "URI" + DefaultIdAttr = "ID" + PrefixListAttr = "PrefixList" +) + +type AlgorithmID string + +func (id AlgorithmID) String() string { + return string(id) +} + +const ( + RSASHA1SignatureMethod = "http://www.w3.org/2000/09/xmldsig#rsa-sha1" + RSASHA256SignatureMethod = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" + RSASHA512SignatureMethod = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512" +) + +//Well-known signature algorithms +const ( + // Supported canonicalization algorithms + CanonicalXML10ExclusiveAlgorithmId AlgorithmID = "http://www.w3.org/2001/10/xml-exc-c14n#" + CanonicalXML11AlgorithmId AlgorithmID = "http://www.w3.org/2006/12/xml-c14n11" + + EnvelopedSignatureAltorithmId AlgorithmID = "http://www.w3.org/2000/09/xmldsig#enveloped-signature" +) + +var digestAlgorithmIdentifiers = map[crypto.Hash]string{ + crypto.SHA1: "http://www.w3.org/2000/09/xmldsig#sha1", + crypto.SHA256: "http://www.w3.org/2001/04/xmlenc#sha256", + crypto.SHA512: "http://www.w3.org/2001/04/xmlenc#sha512", +} + +var digestAlgorithmsByIdentifier = map[string]crypto.Hash{} +var signatureMethodsByIdentifier = map[string]crypto.Hash{} + +func init() { + for hash, id := range digestAlgorithmIdentifiers { + digestAlgorithmsByIdentifier[id] = hash + } + for hash, id := range signatureMethodIdentifiers { + signatureMethodsByIdentifier[id] = hash + } +} + +var signatureMethodIdentifiers = map[crypto.Hash]string{ + crypto.SHA1: RSASHA1SignatureMethod, + crypto.SHA256: RSASHA256SignatureMethod, + crypto.SHA512: RSASHA512SignatureMethod, +} diff --git a/vendor/github.com/satori/go.uuid/.travis.yml b/vendor/github.com/satori/go.uuid/.travis.yml new file mode 100644 index 0000000000000..fdf960e86b556 --- /dev/null +++ b/vendor/github.com/satori/go.uuid/.travis.yml @@ -0,0 +1,22 @@ +language: go +sudo: false +go: + - 1.2 + - 1.3 + - 1.4 + - 1.5 + - 1.6 + - 1.7 + - 1.8 + - tip +matrix: + allow_failures: + - go: tip + fast_finish: true +before_install: + - go get github.com/mattn/goveralls + - go get golang.org/x/tools/cmd/cover +script: + - $HOME/gopath/bin/goveralls -service=travis-ci +notifications: + email: false diff --git a/vendor/github.com/satori/go.uuid/LICENSE b/vendor/github.com/satori/go.uuid/LICENSE new file mode 100644 index 0000000000000..488357b8af1fe --- /dev/null +++ b/vendor/github.com/satori/go.uuid/LICENSE @@ -0,0 +1,20 @@ +Copyright (C) 2013-2016 by Maxim Bublis + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/satori/go.uuid/README.md b/vendor/github.com/satori/go.uuid/README.md new file mode 100644 index 0000000000000..b6aad1c81303b --- /dev/null +++ b/vendor/github.com/satori/go.uuid/README.md @@ -0,0 +1,65 @@ +# UUID package for Go language + +[![Build Status](https://travis-ci.org/satori/go.uuid.png?branch=master)](https://travis-ci.org/satori/go.uuid) +[![Coverage Status](https://coveralls.io/repos/github/satori/go.uuid/badge.svg?branch=master)](https://coveralls.io/github/satori/go.uuid) +[![GoDoc](http://godoc.org/github.com/satori/go.uuid?status.png)](http://godoc.org/github.com/satori/go.uuid) + +This package provides pure Go implementation of Universally Unique Identifier (UUID). Supported both creation and parsing of UUIDs. + +With 100% test coverage and benchmarks out of box. + +Supported versions: +* Version 1, based on timestamp and MAC address (RFC 4122) +* Version 2, based on timestamp, MAC address and POSIX UID/GID (DCE 1.1) +* Version 3, based on MD5 hashing (RFC 4122) +* Version 4, based on random numbers (RFC 4122) +* Version 5, based on SHA-1 hashing (RFC 4122) + +## Installation + +Use the `go` command: + + $ go get github.com/satori/go.uuid + +## Requirements + +UUID package requires Go >= 1.2. + +## Example + +```go +package main + +import ( + "fmt" + "github.com/satori/go.uuid" +) + +func main() { + // Creating UUID Version 4 + u1 := uuid.NewV4() + fmt.Printf("UUIDv4: %s\n", u1) + + // Parsing UUID from string input + u2, err := uuid.FromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8") + if err != nil { + fmt.Printf("Something gone wrong: %s", err) + } + fmt.Printf("Successfully parsed: %s", u2) +} +``` + +## Documentation + +[Documentation](http://godoc.org/github.com/satori/go.uuid) is hosted at GoDoc project. + +## Links +* [RFC 4122](http://tools.ietf.org/html/rfc4122) +* [DCE 1.1: Authentication and Security Services](http://pubs.opengroup.org/onlinepubs/9696989899/chap5.htm#tagcjh_08_02_01_01) + +## Copyright + +Copyright (C) 2013-2016 by Maxim Bublis . + +UUID package released under MIT License. +See [LICENSE](https://github.com/satori/go.uuid/blob/master/LICENSE) for details. diff --git a/vendor/github.com/satori/go.uuid/uuid.go b/vendor/github.com/satori/go.uuid/uuid.go new file mode 100644 index 0000000000000..295f3fc2c57fa --- /dev/null +++ b/vendor/github.com/satori/go.uuid/uuid.go @@ -0,0 +1,481 @@ +// Copyright (C) 2013-2015 by Maxim Bublis +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +// Package uuid provides implementation of Universally Unique Identifier (UUID). +// Supported versions are 1, 3, 4 and 5 (as specified in RFC 4122) and +// version 2 (as specified in DCE 1.1). +package uuid + +import ( + "bytes" + "crypto/md5" + "crypto/rand" + "crypto/sha1" + "database/sql/driver" + "encoding/binary" + "encoding/hex" + "fmt" + "hash" + "net" + "os" + "sync" + "time" +) + +// UUID layout variants. +const ( + VariantNCS = iota + VariantRFC4122 + VariantMicrosoft + VariantFuture +) + +// UUID DCE domains. +const ( + DomainPerson = iota + DomainGroup + DomainOrg +) + +// Difference in 100-nanosecond intervals between +// UUID epoch (October 15, 1582) and Unix epoch (January 1, 1970). +const epochStart = 122192928000000000 + +// Used in string method conversion +const dash byte = '-' + +// UUID v1/v2 storage. +var ( + storageMutex sync.Mutex + storageOnce sync.Once + epochFunc = unixTimeFunc + clockSequence uint16 + lastTime uint64 + hardwareAddr [6]byte + posixUID = uint32(os.Getuid()) + posixGID = uint32(os.Getgid()) +) + +// String parse helpers. +var ( + urnPrefix = []byte("urn:uuid:") + byteGroups = []int{8, 4, 4, 4, 12} +) + +func initClockSequence() { + buf := make([]byte, 2) + safeRandom(buf) + clockSequence = binary.BigEndian.Uint16(buf) +} + +func initHardwareAddr() { + interfaces, err := net.Interfaces() + if err == nil { + for _, iface := range interfaces { + if len(iface.HardwareAddr) >= 6 { + copy(hardwareAddr[:], iface.HardwareAddr) + return + } + } + } + + // Initialize hardwareAddr randomly in case + // of real network interfaces absence + safeRandom(hardwareAddr[:]) + + // Set multicast bit as recommended in RFC 4122 + hardwareAddr[0] |= 0x01 +} + +func initStorage() { + initClockSequence() + initHardwareAddr() +} + +func safeRandom(dest []byte) { + if _, err := rand.Read(dest); err != nil { + panic(err) + } +} + +// Returns difference in 100-nanosecond intervals between +// UUID epoch (October 15, 1582) and current time. +// This is default epoch calculation function. +func unixTimeFunc() uint64 { + return epochStart + uint64(time.Now().UnixNano()/100) +} + +// UUID representation compliant with specification +// described in RFC 4122. +type UUID [16]byte + +// NullUUID can be used with the standard sql package to represent a +// UUID value that can be NULL in the database +type NullUUID struct { + UUID UUID + Valid bool +} + +// The nil UUID is special form of UUID that is specified to have all +// 128 bits set to zero. +var Nil = UUID{} + +// Predefined namespace UUIDs. +var ( + NamespaceDNS, _ = FromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8") + NamespaceURL, _ = FromString("6ba7b811-9dad-11d1-80b4-00c04fd430c8") + NamespaceOID, _ = FromString("6ba7b812-9dad-11d1-80b4-00c04fd430c8") + NamespaceX500, _ = FromString("6ba7b814-9dad-11d1-80b4-00c04fd430c8") +) + +// And returns result of binary AND of two UUIDs. +func And(u1 UUID, u2 UUID) UUID { + u := UUID{} + for i := 0; i < 16; i++ { + u[i] = u1[i] & u2[i] + } + return u +} + +// Or returns result of binary OR of two UUIDs. +func Or(u1 UUID, u2 UUID) UUID { + u := UUID{} + for i := 0; i < 16; i++ { + u[i] = u1[i] | u2[i] + } + return u +} + +// Equal returns true if u1 and u2 equals, otherwise returns false. +func Equal(u1 UUID, u2 UUID) bool { + return bytes.Equal(u1[:], u2[:]) +} + +// Version returns algorithm version used to generate UUID. +func (u UUID) Version() uint { + return uint(u[6] >> 4) +} + +// Variant returns UUID layout variant. +func (u UUID) Variant() uint { + switch { + case (u[8] & 0x80) == 0x00: + return VariantNCS + case (u[8]&0xc0)|0x80 == 0x80: + return VariantRFC4122 + case (u[8]&0xe0)|0xc0 == 0xc0: + return VariantMicrosoft + } + return VariantFuture +} + +// Bytes returns bytes slice representation of UUID. +func (u UUID) Bytes() []byte { + return u[:] +} + +// Returns canonical string representation of UUID: +// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. +func (u UUID) String() string { + buf := make([]byte, 36) + + hex.Encode(buf[0:8], u[0:4]) + buf[8] = dash + hex.Encode(buf[9:13], u[4:6]) + buf[13] = dash + hex.Encode(buf[14:18], u[6:8]) + buf[18] = dash + hex.Encode(buf[19:23], u[8:10]) + buf[23] = dash + hex.Encode(buf[24:], u[10:]) + + return string(buf) +} + +// SetVersion sets version bits. +func (u *UUID) SetVersion(v byte) { + u[6] = (u[6] & 0x0f) | (v << 4) +} + +// SetVariant sets variant bits as described in RFC 4122. +func (u *UUID) SetVariant() { + u[8] = (u[8] & 0xbf) | 0x80 +} + +// MarshalText implements the encoding.TextMarshaler interface. +// The encoding is the same as returned by String. +func (u UUID) MarshalText() (text []byte, err error) { + text = []byte(u.String()) + return +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface. +// Following formats are supported: +// "6ba7b810-9dad-11d1-80b4-00c04fd430c8", +// "{6ba7b810-9dad-11d1-80b4-00c04fd430c8}", +// "urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8" +func (u *UUID) UnmarshalText(text []byte) (err error) { + if len(text) < 32 { + err = fmt.Errorf("uuid: UUID string too short: %s", text) + return + } + + t := text[:] + braced := false + + if bytes.Equal(t[:9], urnPrefix) { + t = t[9:] + } else if t[0] == '{' { + braced = true + t = t[1:] + } + + b := u[:] + + for i, byteGroup := range byteGroups { + if i > 0 { + if t[0] != '-' { + err = fmt.Errorf("uuid: invalid string format") + return + } + t = t[1:] + } + + if len(t) < byteGroup { + err = fmt.Errorf("uuid: UUID string too short: %s", text) + return + } + + if i == 4 && len(t) > byteGroup && + ((braced && t[byteGroup] != '}') || len(t[byteGroup:]) > 1 || !braced) { + err = fmt.Errorf("uuid: UUID string too long: %s", text) + return + } + + _, err = hex.Decode(b[:byteGroup/2], t[:byteGroup]) + if err != nil { + return + } + + t = t[byteGroup:] + b = b[byteGroup/2:] + } + + return +} + +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (u UUID) MarshalBinary() (data []byte, err error) { + data = u.Bytes() + return +} + +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +// It will return error if the slice isn't 16 bytes long. +func (u *UUID) UnmarshalBinary(data []byte) (err error) { + if len(data) != 16 { + err = fmt.Errorf("uuid: UUID must be exactly 16 bytes long, got %d bytes", len(data)) + return + } + copy(u[:], data) + + return +} + +// Value implements the driver.Valuer interface. +func (u UUID) Value() (driver.Value, error) { + return u.String(), nil +} + +// Scan implements the sql.Scanner interface. +// A 16-byte slice is handled by UnmarshalBinary, while +// a longer byte slice or a string is handled by UnmarshalText. +func (u *UUID) Scan(src interface{}) error { + switch src := src.(type) { + case []byte: + if len(src) == 16 { + return u.UnmarshalBinary(src) + } + return u.UnmarshalText(src) + + case string: + return u.UnmarshalText([]byte(src)) + } + + return fmt.Errorf("uuid: cannot convert %T to UUID", src) +} + +// Value implements the driver.Valuer interface. +func (u NullUUID) Value() (driver.Value, error) { + if !u.Valid { + return nil, nil + } + // Delegate to UUID Value function + return u.UUID.Value() +} + +// Scan implements the sql.Scanner interface. +func (u *NullUUID) Scan(src interface{}) error { + if src == nil { + u.UUID, u.Valid = Nil, false + return nil + } + + // Delegate to UUID Scan function + u.Valid = true + return u.UUID.Scan(src) +} + +// FromBytes returns UUID converted from raw byte slice input. +// It will return error if the slice isn't 16 bytes long. +func FromBytes(input []byte) (u UUID, err error) { + err = u.UnmarshalBinary(input) + return +} + +// FromBytesOrNil returns UUID converted from raw byte slice input. +// Same behavior as FromBytes, but returns a Nil UUID on error. +func FromBytesOrNil(input []byte) UUID { + uuid, err := FromBytes(input) + if err != nil { + return Nil + } + return uuid +} + +// FromString returns UUID parsed from string input. +// Input is expected in a form accepted by UnmarshalText. +func FromString(input string) (u UUID, err error) { + err = u.UnmarshalText([]byte(input)) + return +} + +// FromStringOrNil returns UUID parsed from string input. +// Same behavior as FromString, but returns a Nil UUID on error. +func FromStringOrNil(input string) UUID { + uuid, err := FromString(input) + if err != nil { + return Nil + } + return uuid +} + +// Returns UUID v1/v2 storage state. +// Returns epoch timestamp, clock sequence, and hardware address. +func getStorage() (uint64, uint16, []byte) { + storageOnce.Do(initStorage) + + storageMutex.Lock() + defer storageMutex.Unlock() + + timeNow := epochFunc() + // Clock changed backwards since last UUID generation. + // Should increase clock sequence. + if timeNow <= lastTime { + clockSequence++ + } + lastTime = timeNow + + return timeNow, clockSequence, hardwareAddr[:] +} + +// NewV1 returns UUID based on current timestamp and MAC address. +func NewV1() UUID { + u := UUID{} + + timeNow, clockSeq, hardwareAddr := getStorage() + + binary.BigEndian.PutUint32(u[0:], uint32(timeNow)) + binary.BigEndian.PutUint16(u[4:], uint16(timeNow>>32)) + binary.BigEndian.PutUint16(u[6:], uint16(timeNow>>48)) + binary.BigEndian.PutUint16(u[8:], clockSeq) + + copy(u[10:], hardwareAddr) + + u.SetVersion(1) + u.SetVariant() + + return u +} + +// NewV2 returns DCE Security UUID based on POSIX UID/GID. +func NewV2(domain byte) UUID { + u := UUID{} + + timeNow, clockSeq, hardwareAddr := getStorage() + + switch domain { + case DomainPerson: + binary.BigEndian.PutUint32(u[0:], posixUID) + case DomainGroup: + binary.BigEndian.PutUint32(u[0:], posixGID) + } + + binary.BigEndian.PutUint16(u[4:], uint16(timeNow>>32)) + binary.BigEndian.PutUint16(u[6:], uint16(timeNow>>48)) + binary.BigEndian.PutUint16(u[8:], clockSeq) + u[9] = domain + + copy(u[10:], hardwareAddr) + + u.SetVersion(2) + u.SetVariant() + + return u +} + +// NewV3 returns UUID based on MD5 hash of namespace UUID and name. +func NewV3(ns UUID, name string) UUID { + u := newFromHash(md5.New(), ns, name) + u.SetVersion(3) + u.SetVariant() + + return u +} + +// NewV4 returns random generated UUID. +func NewV4() UUID { + u := UUID{} + safeRandom(u[:]) + u.SetVersion(4) + u.SetVariant() + + return u +} + +// NewV5 returns UUID based on SHA-1 hash of namespace UUID and name. +func NewV5(ns UUID, name string) UUID { + u := newFromHash(sha1.New(), ns, name) + u.SetVersion(5) + u.SetVariant() + + return u +} + +// Returns UUID based on hashing of namespace UUID and name. +func newFromHash(h hash.Hash, ns UUID, name string) UUID { + u := UUID{} + h.Write(ns[:]) + h.Write([]byte(name)) + copy(u[:], h.Sum(nil)) + + return u +}