diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml deleted file mode 100644 index 1512e3ec..00000000 --- a/.github/workflows/go.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Presubmit -on: - push: - branches: [main] - pull_request: - branches: [main] -jobs: - check: - name: Presubmit checks - runs-on: ubuntu-latest - steps: - - name: Set up Go 1.x - uses: actions/setup-go@v2 - with: - go-version: ^1.13 - - name: Check out code into the Go module directory - uses: actions/checkout@v2 - - name: Get dependencies - run: | - curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(go env GOPATH)/bin v1.24.0 - - name: Lint - run: golangci-lint run - - name: Test - run: go test -v ./... diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..f1640505 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,18 @@ +name: lint + +on: + push: + branches: [ 'main' ] + pull_request: + branches: [ 'main' ] + +jobs: + golangci: + name: Run golangci-lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: golangci-lint + uses: golangci/golangci-lint-action@v2 + with: + version: v1.52.2 diff --git a/.github/workflows/maint.yml b/.github/workflows/maint.yml deleted file mode 100644 index 1081e7a4..00000000 --- a/.github/workflows/maint.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Maintainer -on: - workflow_dispatch: - schedule: - - cron: "0 12 * * 0" -jobs: - upgrade_go: - name: Upgrade go.mod - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-go@v2 - with: - go-version: "^1.15.6" - - name: Install goupdate - run: | - ( - cd $(mktemp -d) - go get github.com/crewjam/goupdate - ) - git config --global user.email noreply@github.com - git config --global user.name "Github Actions" - - name: Update go.mod - run: | - go version - go env - $(go env GOPATH)/bin/goupdate -test 'go test ./...' --commit -v - - name: Create Pull Request - uses: peter-evans/create-pull-request@v3 - with: - commit-message: "Update go.mod" - branch: auto/update-go - title: "Update go.mod" - body: "" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..60f3f8f0 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,26 @@ +name: test + +on: + push: + branches: [ 'main' ] + pull_request: + branches: [ 'main' ] +jobs: + tests: + name: Run tests + runs-on: ubuntu-latest + strategy: + matrix: + go: [ '1.17.x', '1.18.x', '1.19.x'] + steps: + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + - name: Set up Go ${{ matrix.go }} + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go }} + - name: Go version + run: go version + - name: Run Go tests + run: | + go test -v ./... diff --git a/.golangci.yml b/.golangci.yml index 1392c902..f93ef23b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -7,41 +7,35 @@ linters: enable: - - gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification [fast: true, auto-fix: true] - - goimports # Goimports does everything that gofmt does. Additionally it checks unused imports [fast: true, auto-fix: true] - - gosec # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: true, auto-fix: false] - - misspell # Finds commonly misspelled English words in comments [fast: true, auto-fix: true] - - deadcode # Finds unused code [fast: true, auto-fix: false] - - golint # Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes [fast: true, auto-fix: false] - - unconvert # Remove unnecessary type conversions [fast: true, auto-fix: false] - - disable: - # TODO(ross): fix errors reported by these checkers and enable them - bodyclose # checks whether HTTP response body is closed successfully [fast: false, auto-fix: false] - depguard # Go linter that checks if package imports are in a list of acceptable packages [fast: true, auto-fix: false] - - dupl # Tool for code clone detection [fast: true, auto-fix: false] - errcheck # Inspects source code for security problems [fast: true, auto-fix: false] - - gochecknoglobals # Checks that no globals are present in Go code [fast: true, auto-fix: false] - - gochecknoinits # Checks that no init functions are present in Go code [fast: true, auto-fix: false] - - goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false] - gocritic # The most opinionated Go source code linter [fast: true, auto-fix: false] - gocyclo # Computes and checks the cyclomatic complexity of functions [fast: true, auto-fix: false] + - gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification [fast: true, auto-fix: true] + - goimports # Goimports does everything that gofmt does. Additionally it checks unused imports [fast: true, auto-fix: true] + - gosec # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: true, auto-fix: false] - gosimple # Linter for Go source code that specializes in simplifying a code [fast: false, auto-fix: false] - govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: false, auto-fix: false] - ineffassign # Detects when assignments to existing variables are not used [fast: true, auto-fix: false] - - interfacer # Linter that suggests narrower interface types [fast: false, auto-fix: false] - - lll # Reports long lines [fast: true, auto-fix: false] - - maligned # Tool to detect Go structs that would take less memory if their fields were sorted [fast: true, auto-fix: false] + - misspell # Finds commonly misspelled English words in comments [fast: true, auto-fix: true] - nakedret # Finds naked returns in functions greater than a specified function length [fast: true, auto-fix: false] - prealloc # Finds slice declarations that could potentially be preallocated [fast: true, auto-fix: false] - - scopelint # Scopelint checks for unpinned variables in go programs [fast: true, auto-fix: false] + - revive # Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes [fast: true, auto-fix: false] - staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: false, auto-fix: false] - - structcheck # Finds unused struct fields [fast: true, auto-fix: false] - stylecheck # Stylecheck is a replacement for golint [fast: false, auto-fix: false] - typecheck # Like the front-end of a Go compiler, parses and type-checks Go code [fast: true, auto-fix: false] + - unconvert # Remove unnecessary type conversions [fast: true, auto-fix: false] - unparam # Reports unused function parameters [fast: false, auto-fix: false] - unused # Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false] - - varcheck # Finds unused global variables and constants [fast: true, auto-fix: false] + + disable: + # TODO(ross): fix errors reported by these checkers and enable them + - dupl # Tool for code clone detection [fast: true, auto-fix: false] + - gochecknoglobals # Checks that no globals are present in Go code [fast: true, auto-fix: false] + - gochecknoinits # Checks that no init functions are present in Go code [fast: true, auto-fix: false] + - goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false] + - lll # Reports long lines [fast: true, auto-fix: false] linters-settings: goimports: local-prefixes: github.com/crewjam/saml diff --git a/README.md b/README.md index 71f24786..c0b98058 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ import ( ) func hello(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "Hello, %s!", samlsp.AttributeFromContext(r.Context(), "cn")) + fmt.Fprintf(w, "Hello, %s!", samlsp.AttributeFromContext(r.Context(), "displayName")) } func main() { diff --git a/example/idp/idp.go b/example/idp/idp.go index 6069d379..4e47a56a 100644 --- a/example/idp/idp.go +++ b/example/idp/idp.go @@ -1,3 +1,4 @@ +// Package main contains an example identity provider implementation. package main import ( diff --git a/example/service.go b/example/service.go index c153b65f..5b6ddb27 100644 --- a/example/service.go +++ b/example/service.go @@ -32,7 +32,7 @@ type Link struct { } // CreateLink handles requests to create links -func CreateLink(c web.C, w http.ResponseWriter, r *http.Request) { +func CreateLink(_ web.C, w http.ResponseWriter, r *http.Request) { account := r.Header.Get("X-Remote-User") l := Link{ ShortLink: uniuri.New(), @@ -42,22 +42,20 @@ func CreateLink(c web.C, w http.ResponseWriter, r *http.Request) { links[l.ShortLink] = l fmt.Fprintf(w, "%s\n", l.ShortLink) - return } // ServeLink handles requests to redirect to a link -func ServeLink(c web.C, w http.ResponseWriter, r *http.Request) { +func ServeLink(_ web.C, w http.ResponseWriter, r *http.Request) { l, ok := links[strings.TrimPrefix(r.URL.Path, "/")] if !ok { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } http.Redirect(w, r, l.Target, http.StatusFound) - return } // ListLinks returns a list of the current user's links -func ListLinks(c web.C, w http.ResponseWriter, r *http.Request) { +func ListLinks(_ web.C, w http.ResponseWriter, r *http.Request) { account := r.Header.Get("X-Remote-User") for _, l := range links { if l.Owner == account { @@ -145,14 +143,24 @@ func main() { spURL := *idpMetadataURL spURL.Path = "/services/sp" - http.Post(spURL.String(), "text/xml", bytes.NewReader(spMetadataBuf)) + resp, err := http.Post(spURL.String(), "text/xml", bytes.NewReader(spMetadataBuf)) + + if err != nil { + panic(err) + } + + if err := resp.Body.Close(); err != nil { + panic(err) + } goji.Handle("/saml/*", samlSP) authMux := web.New() authMux.Use(samlSP.RequireAccount) authMux.Get("/whoami", func(w http.ResponseWriter, r *http.Request) { - pretty.Fprintf(w, "%# v", r) + if _, err := pretty.Fprintf(w, "%# v", r); err != nil { + panic(err) + } }) authMux.Post("/", CreateLink) authMux.Get("/", ListLinks) diff --git a/example/trivial/trivial.go b/example/trivial/trivial.go index e8be7cb9..45f46080 100644 --- a/example/trivial/trivial.go +++ b/example/trivial/trivial.go @@ -1,3 +1,4 @@ +// Package main contains an example service provider implementation. package main import ( @@ -6,14 +7,34 @@ import ( "crypto/tls" "crypto/x509" "fmt" + "log" "net/http" "net/url" + "time" "github.com/crewjam/saml/samlsp" ) +var samlMiddleware *samlsp.Middleware + func hello(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "Hello, %s!", samlsp.AttributeFromContext(r.Context(), "cn")) + fmt.Fprintf(w, "Hello, %s!", samlsp.AttributeFromContext(r.Context(), "displayName")) +} + +func logout(w http.ResponseWriter, r *http.Request) { + nameID := samlsp.AttributeFromContext(r.Context(), "urn:oasis:names:tc:SAML:attribute:subject-id") + url, err := samlMiddleware.ServiceProvider.MakeRedirectLogoutRequest(nameID, "") + if err != nil { + panic(err) // TODO handle error + } + + err = samlMiddleware.Session.DeleteSession(w, r) + if err != nil { + panic(err) // TODO handle error + } + + w.Header().Add("Location", url.String()) + w.WriteHeader(http.StatusFound) } func main() { @@ -26,30 +47,38 @@ func main() { panic(err) // TODO handle error } - rootURL, _ := url.Parse("http://localhost:8000") - idpMetadataURL, _ := url.Parse("https://samltest.id/saml/idp") - - idpMetadata, err := samlsp.FetchMetadata( - context.Background(), - http.DefaultClient, + idpMetadataURL, err := url.Parse("https://samltest.id/saml/idp") + if err != nil { + panic(err) // TODO handle error + } + idpMetadata, err := samlsp.FetchMetadata(context.Background(), http.DefaultClient, *idpMetadataURL) if err != nil { panic(err) // TODO handle error } - samlSP, err := samlsp.New(samlsp.Options{ - URL: *rootURL, - IDPMetadata: idpMetadata, - Key: keyPair.PrivateKey.(*rsa.PrivateKey), - Certificate: keyPair.Leaf, - SignRequest: true, - }) + rootURL, err := url.Parse("http://localhost:8000") if err != nil { panic(err) // TODO handle error } + samlMiddleware, _ = samlsp.New(samlsp.Options{ + URL: *rootURL, + Key: keyPair.PrivateKey.(*rsa.PrivateKey), + Certificate: keyPair.Leaf, + IDPMetadata: idpMetadata, + SignRequest: true, // some IdP require the SLO request to be signed + }) app := http.HandlerFunc(hello) - http.Handle("/hello", samlSP.RequireAccount(app)) - http.Handle("/saml/", samlSP) - http.ListenAndServe(":8000", nil) + slo := http.HandlerFunc(logout) + + http.Handle("/hello", samlMiddleware.RequireAccount(app)) + http.Handle("/saml/", samlMiddleware) + http.Handle("/logout", slo) + + server := &http.Server{ + Addr: ":8080", + ReadHeaderTimeout: 5 * time.Second, + } + log.Fatal(server.ListenAndServe()) } diff --git a/flate.go b/flate.go new file mode 100644 index 00000000..4d14e780 --- /dev/null +++ b/flate.go @@ -0,0 +1,31 @@ +package saml + +import ( + "compress/flate" + "fmt" + "io" +) + +const flateUncompressLimit = 10 * 1024 * 1024 // 10MB + +func newSaferFlateReader(r io.Reader) io.ReadCloser { + return &saferFlateReader{r: flate.NewReader(r)} +} + +type saferFlateReader struct { + r io.ReadCloser + count int +} + +func (r *saferFlateReader) Read(p []byte) (n int, err error) { + if r.count+len(p) > flateUncompressLimit { + return 0, fmt.Errorf("flate: uncompress limit exceeded (%d bytes)", flateUncompressLimit) + } + n, err = r.r.Read(p) + r.count += n + return n, err +} + +func (r *saferFlateReader) Close() error { + return r.r.Close() +} diff --git a/go.mod b/go.mod index 687d29a5..745c5c2c 100644 --- a/go.mod +++ b/go.mod @@ -1,22 +1,19 @@ module github.com/crewjam/saml -go 1.13 +go 1.16 require ( github.com/beevik/etree v1.1.0 github.com/crewjam/httperr v0.2.0 - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 - github.com/form3tech-oss/jwt-go v3.2.2+incompatible - github.com/google/go-cmp v0.5.5 - github.com/jonboulle/clockwork v0.2.2 // indirect - github.com/kr/pretty v0.3.0 - github.com/kr/text v0.2.0 // indirect + github.com/dchest/uniuri v1.2.0 + github.com/golang-jwt/jwt/v4 v4.4.3 + github.com/google/go-cmp v0.5.9 + github.com/kr/pretty v0.3.1 github.com/mattermost/xml-roundtrip-validator v0.1.0 github.com/pkg/errors v0.9.1 // indirect - github.com/russellhaering/goxmldsig v1.1.1 + github.com/russellhaering/goxmldsig v1.3.0 + github.com/stretchr/testify v1.8.1 github.com/zenazn/goji v1.0.1 - golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed gotest.tools v2.2.0+incompatible ) diff --git a/go.sum b/go.sum index 80810704..7ab71ea2 100644 --- a/go.sum +++ b/go.sum @@ -6,20 +6,19 @@ github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3p github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 h1:RAV05c0xOkJ3dZGS0JFybxFKZ2WMLabgx3uXnd7rpGs= -github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4= -github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= -github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/jonboulle/clockwork v0.2.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g= +github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY= +github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU= +github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -33,37 +32,40 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/russellhaering/goxmldsig v1.1.0 h1:lK/zeJie2sqG52ZAlPNn1oBBqsIsEKypUUBGpYYF6lk= -github.com/russellhaering/goxmldsig v1.1.0/go.mod h1:QK8GhXPB3+AfuCrfo0oRISa9NfzeCpWmxeGnqEpDF9o= -github.com/russellhaering/goxmldsig v1.1.1 h1:vI0r2osGF1A9PLvsGdPUAGwEIrKa4Pj5sesSBsebIxM= -github.com/russellhaering/goxmldsig v1.1.1/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russellhaering/goxmldsig v1.3.0 h1:DllIWUgMy0cRUMfGiASiYEa35nsieyD3cigIwLonTPM= +github.com/russellhaering/goxmldsig v1.3.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8= github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed h1:YoWVYYAfvQ4ddHv3OKmIvX7NCAhFGTj62VP2l2kfBbA= +golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= diff --git a/identity_provider.go b/identity_provider.go index 20258daf..b03dc89b 100644 --- a/identity_provider.go +++ b/identity_provider.go @@ -2,7 +2,6 @@ package saml import ( "bytes" - "compress/flate" "crypto" "crypto/tls" "crypto/x509" @@ -36,7 +35,10 @@ type Session struct { ExpireTime time.Time Index string - NameID string + NameID string + NameIDFormat string + SubjectID string + Groups []string UserName string UserEmail string @@ -94,6 +96,7 @@ type AssertionMaker interface { // and password). type IdentityProvider struct { Key crypto.PrivateKey + Signer crypto.Signer Logger logger.Interface Certificate *x509.Certificate Intermediates []*x509.Certificate @@ -131,13 +134,21 @@ func (idp *IdentityProvider) Metadata() *EntityDescriptor { { Use: "signing", KeyInfo: KeyInfo{ - Certificate: certStr, + X509Data: X509Data{ + X509Certificates: []X509Certificate{ + {Data: certStr}, + }, + }, }, }, { Use: "encryption", KeyInfo: KeyInfo{ - Certificate: certStr, + X509Data: X509Data{ + X509Certificates: []X509Certificate{ + {Data: certStr}, + }, + }, }, EncryptionMethods: []EncryptionMethod{ {Algorithm: "http://www.w3.org/2001/04/xmlenc#aes128-cbc"}, @@ -186,10 +197,13 @@ func (idp *IdentityProvider) Handler() http.Handler { } // ServeMetadata is an http.HandlerFunc that serves the IDP metadata -func (idp *IdentityProvider) ServeMetadata(w http.ResponseWriter, r *http.Request) { +func (idp *IdentityProvider) ServeMetadata(w http.ResponseWriter, _ *http.Request) { buf, _ := xml.MarshalIndent(idp.Metadata(), "", " ") w.Header().Set("Content-Type", "application/samlmetadata+xml") - w.Write(buf) + if _, err := w.Write(buf); err != nil { + idp.Logger.Printf("ERROR: %s", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } } // ServeSSO handles SAML auth requests. @@ -352,7 +366,7 @@ func NewIdpAuthnRequest(idp *IdentityProvider, r *http.Request) (*IdpAuthnReques if err != nil { return nil, fmt.Errorf("cannot decode request: %s", err) } - req.RequestBuffer, err = ioutil.ReadAll(flate.NewReader(bytes.NewReader(compressedRequest))) + req.RequestBuffer, err = ioutil.ReadAll(newSaferFlateReader(bytes.NewReader(compressedRequest))) if err != nil { return nil, fmt.Errorf("cannot decompress request: %s", err) } @@ -706,9 +720,7 @@ func (DefaultAssertionMaker) MakeAssertion(req *IdpAuthnRequest, session *Sessio }) } - for _, ca := range session.CustomAttributes { - attributes = append(attributes, ca) - } + attributes = append(attributes, session.CustomAttributes...) if len(session.Groups) != 0 { groupMemberAttributeValues := []AttributeValue{} @@ -726,6 +738,19 @@ func (DefaultAssertionMaker) MakeAssertion(req *IdpAuthnRequest, session *Sessio }) } + if session.SubjectID != "" { + attributes = append(attributes, Attribute{ + Name: "urn:oasis:names:tc:SAML:attribute:subject-id", + NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", + Values: []AttributeValue{ + { + Type: "xs:string", + Value: session.SubjectID, + }, + }, + }) + } + // allow for some clock skew in the validity period using the // issuer's apparent clock. notBefore := req.Now.Add(-1 * MaxClockSkew) @@ -735,6 +760,12 @@ func (DefaultAssertionMaker) MakeAssertion(req *IdpAuthnRequest, session *Sessio notOnOrAfterAfter = notBefore.Add(MaxIssueDelay) } + nameIDFormat := "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + + if session.NameIDFormat != "" { + nameIDFormat = session.NameIDFormat + } + req.Assertion = &Assertion{ ID: fmt.Sprintf("id-%x", randomBytes(20)), IssueInstant: TimeNow(), @@ -745,7 +776,7 @@ func (DefaultAssertionMaker) MakeAssertion(req *IdpAuthnRequest, session *Sessio }, Subject: &Subject{ NameID: &NameID{ - Format: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient", + Format: nameIDFormat, NameQualifier: req.IDP.Metadata().EntityID, SPNameQualifier: req.ServiceProviderMetadata.EntityID, Value: session.NameID, @@ -801,24 +832,8 @@ const canonicalizerPrefixList = "" // MakeAssertionEl sets `AssertionEl` to a signed, possibly encrypted, version of `Assertion`. func (req *IdpAuthnRequest) MakeAssertionEl() error { - keyPair := tls.Certificate{ - Certificate: [][]byte{req.IDP.Certificate.Raw}, - PrivateKey: req.IDP.Key, - Leaf: req.IDP.Certificate, - } - for _, cert := range req.IDP.Intermediates { - keyPair.Certificate = append(keyPair.Certificate, cert.Raw) - } - keyStore := dsig.TLSCertKeyStore(keyPair) - - signatureMethod := req.IDP.SignatureMethod - if signatureMethod == "" { - signatureMethod = dsig.RSASHA1SignatureMethod - } - - signingContext := dsig.NewDefaultSigningContext(keyStore) - signingContext.Canonicalizer = dsig.MakeC14N10ExclusiveCanonicalizerWithPrefixList(canonicalizerPrefixList) - if err := signingContext.SetSignatureMethod(signatureMethod); err != nil { + signingContext, err := req.signingContext() + if err != nil { return err } @@ -854,7 +869,7 @@ func (req *IdpAuthnRequest) MakeAssertionEl() error { encryptor := xmlenc.OAEP() encryptor.BlockCipher = xmlenc.AES128CBC encryptor.DigestMethod = &xmlenc.SHA1 - encryptedDataEl, err := encryptor.Encrypt(certBuf, signedAssertionBuf) + encryptedDataEl, err := encryptor.Encrypt(certBuf, signedAssertionBuf, nil) if err != nil { return err } @@ -867,12 +882,23 @@ func (req *IdpAuthnRequest) MakeAssertionEl() error { return nil } -// WriteResponse writes the `Response` to the http.ResponseWriter. If -// `Response` is not already set, it calls MakeResponse to produce it. -func (req *IdpAuthnRequest) WriteResponse(w http.ResponseWriter) error { +// IdpAuthnRequestForm contans HTML form information to be submitted to the +// SAML HTTP POST binding ACS. +type IdpAuthnRequestForm struct { + URL string + SAMLResponse string + RelayState string +} + +// PostBinding creates the HTTP POST form information for this +// `IdpAuthnRequest`. If `Response` is not already set, it calls MakeResponse +// to produce it. +func (req *IdpAuthnRequest) PostBinding() (IdpAuthnRequestForm, error) { + var form IdpAuthnRequestForm + if req.ResponseEl == nil { if err := req.MakeResponse(); err != nil { - return err + return form, err } } @@ -880,45 +906,48 @@ func (req *IdpAuthnRequest) WriteResponse(w http.ResponseWriter) error { doc.SetRoot(req.ResponseEl) responseBuf, err := doc.WriteToBytes() if err != nil { - return err + return form, err } - // the only supported binding is the HTTP-POST binding - switch req.ACSEndpoint.Binding { - case HTTPPostBinding: - tmpl := template.Must(template.New("saml-post-form").Parse(`` + - `
` + - `` + - `` + - `` + - `
` + - `` + - `` + - ``)) - data := struct { - URL string - SAMLResponse string - RelayState string - }{ - URL: req.ACSEndpoint.Location, - SAMLResponse: base64.StdEncoding.EncodeToString(responseBuf), - RelayState: req.RelayState, - } - - buf := bytes.NewBuffer(nil) - if err := tmpl.Execute(buf, data); err != nil { - return err - } - if _, err := io.Copy(w, buf); err != nil { - return err - } - return nil - - default: - return fmt.Errorf("%s: unsupported binding %s", + if req.ACSEndpoint.Binding != HTTPPostBinding { + return form, fmt.Errorf("%s: unsupported binding %s", req.ServiceProviderMetadata.EntityID, req.ACSEndpoint.Binding) } + + form.URL = req.ACSEndpoint.Location + form.SAMLResponse = base64.StdEncoding.EncodeToString(responseBuf) + form.RelayState = req.RelayState + + return form, nil +} + +// WriteResponse writes the `Response` to the http.ResponseWriter. If +// `Response` is not already set, it calls MakeResponse to produce it. +func (req *IdpAuthnRequest) WriteResponse(w http.ResponseWriter) error { + form, err := req.PostBinding() + if err != nil { + return err + } + + tmpl := template.Must(template.New("saml-post-form").Parse(`` + + `
` + + `` + + `` + + `` + + `
` + + `` + + `` + + ``)) + + buf := bytes.NewBuffer(nil) + if err := tmpl.Execute(buf, form); err != nil { + return err + } + if _, err := io.Copy(w, buf); err != nil { + return err + } + return nil } // getSPEncryptionCert returns the certificate which we can use to encrypt things @@ -927,7 +956,7 @@ func (req *IdpAuthnRequest) getSPEncryptionCert() (*x509.Certificate, error) { certStr := "" for _, keyDescriptor := range req.SPSSODescriptor.KeyDescriptors { if keyDescriptor.Use == "encryption" { - certStr = keyDescriptor.KeyInfo.Certificate + certStr = keyDescriptor.KeyInfo.X509Data.X509Certificates[0].Data break } } @@ -936,8 +965,8 @@ func (req *IdpAuthnRequest) getSPEncryptionCert() (*x509.Certificate, error) { // non-empty cert we find. if certStr == "" { for _, keyDescriptor := range req.SPSSODescriptor.KeyDescriptors { - if keyDescriptor.Use == "" && keyDescriptor.KeyInfo.Certificate != "" { - certStr = keyDescriptor.KeyInfo.Certificate + if keyDescriptor.Use == "" && len(keyDescriptor.KeyInfo.X509Data.X509Certificates) != 0 && keyDescriptor.KeyInfo.X509Data.X509Certificates[0].Data != "" { + certStr = keyDescriptor.KeyInfo.X509Data.X509Certificates[0].Data break } } @@ -1005,24 +1034,8 @@ func (req *IdpAuthnRequest) MakeResponse() error { // Sign the response element (we've already signed the Assertion element) { - keyPair := tls.Certificate{ - Certificate: [][]byte{req.IDP.Certificate.Raw}, - PrivateKey: req.IDP.Key, - Leaf: req.IDP.Certificate, - } - for _, cert := range req.IDP.Intermediates { - keyPair.Certificate = append(keyPair.Certificate, cert.Raw) - } - keyStore := dsig.TLSCertKeyStore(keyPair) - - signatureMethod := req.IDP.SignatureMethod - if signatureMethod == "" { - signatureMethod = dsig.RSASHA1SignatureMethod - } - - signingContext := dsig.NewDefaultSigningContext(keyStore) - signingContext.Canonicalizer = dsig.MakeC14N10ExclusiveCanonicalizerWithPrefixList(canonicalizerPrefixList) - if err := signingContext.SetSignatureMethod(signatureMethod); err != nil { + signingContext, err := req.signingContext() + if err != nil { return err } @@ -1040,3 +1053,44 @@ func (req *IdpAuthnRequest) MakeResponse() error { req.ResponseEl = responseEl return nil } + +// signingContext will create a signing context for the request. +func (req *IdpAuthnRequest) signingContext() (*dsig.SigningContext, error) { + // Create a cert chain based off of the IDP cert and its intermediates. + certificates := [][]byte{req.IDP.Certificate.Raw} + for _, cert := range req.IDP.Intermediates { + certificates = append(certificates, cert.Raw) + } + + var signingContext *dsig.SigningContext + var err error + // If signer is set, use it instead of the private key. + if req.IDP.Signer != nil { + signingContext, err = dsig.NewSigningContext(req.IDP.Signer, certificates) + if err != nil { + return nil, err + } + } else { + keyPair := tls.Certificate{ + Certificate: certificates, + PrivateKey: req.IDP.Key, + Leaf: req.IDP.Certificate, + } + keyStore := dsig.TLSCertKeyStore(keyPair) + + signingContext = dsig.NewDefaultSigningContext(keyStore) + } + + // Default to using SHA1 if the signature method isn't set. + signatureMethod := req.IDP.SignatureMethod + if signatureMethod == "" { + signatureMethod = dsig.RSASHA1SignatureMethod + } + + signingContext.Canonicalizer = dsig.MakeC14N10ExclusiveCanonicalizerWithPrefixList(canonicalizerPrefixList) + if err := signingContext.SetSignatureMethod(signatureMethod); err != nil { + return nil, err + } + + return signingContext, nil +} diff --git a/identity_provider_go116_test.go b/identity_provider_go116_test.go new file mode 100644 index 00000000..6d4a0a53 --- /dev/null +++ b/identity_provider_go116_test.go @@ -0,0 +1,57 @@ +//go:build !go1.17 +// +build !go1.17 + +package saml + +import ( + "bytes" + "compress/flate" + "encoding/base64" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "gotest.tools/assert" + is "gotest.tools/assert/cmp" +) + +func TestIDPHTTPCanHandleSSORequest(t *testing.T) { + test := NewIdentityProviderTest(t, applyKey) + w := httptest.NewRecorder() + + const validRequest = `lJJBayoxFIX%2FypC9JhnU5wszAz7lgWCLaNtFd5fMbQ1MkmnunVb%2FfUfbUqEgdhs%2BTr5zkmLW8S5s8KVD4mzvm0Cl6FIwEciRCeCRDFuznd2sTD5Upk2Ro42NyGZEmNjFMI%2BBOo9pi%2BnVWbzfrEqxY27JSEntEPfg2waHNnpJ4JtcgiWRLfoLXYBjwDfu6p%2B8JIoiWy5K4eqBUipXIzVRUwXKKtRK53qkJ3qqQVuNPUjU4TIQQ%2BBS5EqPBzofKH2ntBn%2FMervo8jWnyX%2BuVC78FwKkT1gopNKX1JUxSklXTMIfM0gsv8xeeDL%2BPGk7%2FF0Qg0GdnwQ1cW5PDLUwFDID6uquO1Dlot1bJw9%2FPLRmia%2BzRMCYyk4dSiq6205QSDXOxfy3KAq5Pkvqt4DAAD%2F%2Fw%3D%3D` + + r, _ := http.NewRequest("GET", "https://idp.example.com/saml/sso?RelayState=ThisIsTheRelayState&"+ + "SAMLRequest="+validRequest, nil) + test.IDP.Handler().ServeHTTP(w, r) + assert.Check(t, is.Equal(http.StatusOK, w.Code)) + + // rejects requests that are invalid + w = httptest.NewRecorder() + r, _ = http.NewRequest("GET", "https://idp.example.com/saml/sso?RelayState=ThisIsTheRelayState&"+ + "SAMLRequest=PEF1dGhuUmVxdWVzdA%3D%3D", nil) + test.IDP.Handler().ServeHTTP(w, r) + assert.Check(t, is.Equal(http.StatusBadRequest, w.Code)) + + // rejects requests that contain malformed XML + { + a, _ := url.QueryUnescape(validRequest) + b, _ := base64.StdEncoding.DecodeString(a) + c, _ := ioutil.ReadAll(flate.NewReader(bytes.NewReader(b))) + d := bytes.Replace(c, []byte("]]"), 1) + f := bytes.Buffer{} + e, _ := flate.NewWriter(&f, flate.DefaultCompression) + _, err := e.Write(d) + assert.Check(t, err) + err = e.Close() + assert.Check(t, err) + g := base64.StdEncoding.EncodeToString(f.Bytes()) + invalidRequest := url.QueryEscape(g) + + w = httptest.NewRecorder() + r, _ = http.NewRequest("GET", "https://idp.example.com/saml/sso?RelayState=ThisIsTheRelayState&"+ + "SAMLRequest="+invalidRequest, nil) + test.IDP.Handler().ServeHTTP(w, r) + assert.Check(t, is.Equal(http.StatusBadRequest, w.Code)) + } +} diff --git a/identity_provider_test.go b/identity_provider_test.go index 69840f2a..6ad81e2c 100644 --- a/identity_provider_test.go +++ b/identity_provider_test.go @@ -10,7 +10,6 @@ import ( "encoding/pem" "encoding/xml" "fmt" - "io/ioutil" "math/rand" "net/http" "net/http/httptest" @@ -25,7 +24,8 @@ import ( "gotest.tools/golden" "github.com/beevik/etree" - "github.com/form3tech-oss/jwt-go" + "github.com/golang-jwt/jwt/v4" + dsig "github.com/russellhaering/goxmldsig" "github.com/crewjam/saml/logger" "github.com/crewjam/saml/testsaml" @@ -38,6 +38,7 @@ type IdentityProviderTest struct { SP ServiceProvider Key crypto.PrivateKey + Signer crypto.Signer Certificate *x509.Certificate SessionProvider SessionProvider IDP IdentityProvider @@ -51,7 +52,7 @@ func mustParseURL(s string) url.URL { return *rv } -func mustParsePrivateKey(pemStr []byte) crypto.PrivateKey { +func mustParsePrivateKey(pemStr []byte) crypto.Signer { b, _ := pem.Decode(pemStr) if b == nil { panic("cannot parse PEM") @@ -75,7 +76,28 @@ func mustParseCertificate(pemStr []byte) *x509.Certificate { return cert } -func NewIdentifyProviderTest(t *testing.T) *IdentityProviderTest { +// idpTestOpts are options that can be applied to the identity provider. +type idpTestOpts struct { + apply func(*testing.T, *IdentityProviderTest) +} + +// applyKey will set the private key for the identity provider. +var applyKey = idpTestOpts{ + apply: func(t *testing.T, test *IdentityProviderTest) { + test.Key = mustParsePrivateKey(golden.Get(t, "idp_key.pem")) + (&test.IDP).Key = test.Key + }, +} + +// applySigner will set the signer for the identity provider. +var applySigner = idpTestOpts{ + apply: func(t *testing.T, test *IdentityProviderTest) { + test.Signer = mustParsePrivateKey(golden.Get(t, "idp_key.pem")) + (&test.IDP).Signer = test.Signer + }, +} + +func NewIdentityProviderTest(t *testing.T, opts ...idpTestOpts) *IdentityProviderTest { test := IdentityProviderTest{} TimeNow = func() time.Time { rv, _ := time.Parse("Mon Jan 2 15:04:05 MST 2006", "Mon Dec 1 01:57:09 UTC 2015") @@ -95,11 +117,9 @@ func NewIdentifyProviderTest(t *testing.T) *IdentityProviderTest { IDPMetadata: &EntityDescriptor{}, } - test.Key = mustParsePrivateKey(golden.Get(t, "idp_key.pem")) test.Certificate = mustParseCertificate(golden.Get(t, "idp_cert.pem")) test.IDP = IdentityProvider{ - Key: test.Key, Certificate: test.Certificate, Logger: logger.DefaultLogger, MetadataURL: mustParseURL("https://idp.example.com/saml/metadata"), @@ -119,6 +139,11 @@ func NewIdentifyProviderTest(t *testing.T) *IdentityProviderTest { }, } + // apply the test options + for _, opt := range opts { + opt.apply(t, &test) + } + // bind the service provider and the IDP test.SP.IDPMetadata = test.IDP.Metadata() return &test @@ -141,7 +166,7 @@ func (mspp *mockServiceProviderProvider) GetServiceProvider(r *http.Request, ser } func TestIDPCanProduceMetadata(t *testing.T) { - test := NewIdentifyProviderTest(t) + test := NewIdentityProviderTest(t, applyKey) expected := &EntityDescriptor{ ValidUntil: TimeNow().Add(DefaultValidDuration), CacheDuration: DefaultValidDuration, @@ -155,16 +180,24 @@ func TestIDPCanProduceMetadata(t *testing.T) { { Use: "signing", KeyInfo: KeyInfo{ - XMLName: xml.Name{}, - Certificate: "MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ==", + XMLName: xml.Name{}, + X509Data: X509Data{ + X509Certificates: []X509Certificate{ + {Data: "MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ=="}, + }, + }, }, EncryptionMethods: nil, }, { Use: "encryption", KeyInfo: KeyInfo{ - XMLName: xml.Name{}, - Certificate: "MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ==", + XMLName: xml.Name{}, + X509Data: X509Data{ + X509Certificates: []X509Certificate{ + {Data: "MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ=="}, + }, + }, }, EncryptionMethods: []EncryptionMethod{ {Algorithm: "http://www.w3.org/2001/04/xmlenc#aes128-cbc"}, @@ -194,58 +227,18 @@ func TestIDPCanProduceMetadata(t *testing.T) { } func TestIDPHTTPCanHandleMetadataRequest(t *testing.T) { - test := NewIdentifyProviderTest(t) + test := NewIdentityProviderTest(t, applyKey) w := httptest.NewRecorder() r, _ := http.NewRequest("GET", "https://idp.example.com/saml/metadata", nil) test.IDP.Handler().ServeHTTP(w, r) assert.Check(t, is.Equal(http.StatusOK, w.Code)) assert.Check(t, is.Equal("application/samlmetadata+xml", w.Header().Get("Content-type"))) - assert.Check(t, strings.HasPrefix(string(w.Body.Bytes()), "X509Certificate"` + XMLName xml.Name `xml:"http://www.w3.org/2000/09/xmldsig# KeyInfo"` + X509Data X509Data `xml:"X509Data"` +} + +// X509Data represents the XMLSEC object of the same name +type X509Data struct { + XMLName xml.Name `xml:"http://www.w3.org/2000/09/xmldsig# X509Data"` + X509Certificates []X509Certificate `xml:"X509Certificate"` +} + +// X509Certificate represents the XMLSEC object of the same name +type X509Certificate struct { + XMLName xml.Name `xml:"http://www.w3.org/2000/09/xmldsig# X509Certificate"` + Data string `xml:",chardata"` } // Endpoint represents the SAML EndpointType object. @@ -203,6 +219,7 @@ type IDPSSODescriptor struct { WantAuthnRequestsSigned *bool `xml:",attr"` SingleSignOnServices []Endpoint `xml:"SingleSignOnService"` + ArtifactResolutionServices []Endpoint `xml:"ArtifactResolutionService"` NameIDMappingServices []Endpoint `xml:"NameIDMappingService"` AssertionIDRequestServices []Endpoint `xml:"AssertionIDRequestService"` AttributeProfiles []string `xml:"AttributeProfile"` diff --git a/metadata_test.go b/metadata_test.go index 8ac1091b..cf1a4659 100644 --- a/metadata_test.go +++ b/metadata_test.go @@ -88,7 +88,7 @@ func TestCanParseMetadata(t *testing.T) { } func TestCanProduceSPMetadata(t *testing.T) { - validUntil, _ := time.Parse("2006-02-01T15:04:05.000000", "2013-10-03T00:32:19.104000") + validUntil, _ := time.Parse("2006-01-02T15:04:05.000000", "2013-03-10T00:32:19.104000") AuthnRequestsSigned := true WantAssertionsSigned := true metadata := EntityDescriptor{ @@ -106,7 +106,10 @@ func TestCanProduceSPMetadata(t *testing.T) { { Use: "encryption", KeyInfo: KeyInfo{ - Certificate: `MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UE + X509Data: X509Data{ + X509Certificates: []X509Certificate{ + { + Data: `MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UE CAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoX DTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28x EjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308 @@ -115,12 +118,18 @@ SPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gf nqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90Dv TLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+ cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ==`, + }, + }, + }, }, }, { Use: "signing", KeyInfo: KeyInfo{ - Certificate: `MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UE + X509Data: X509Data{ + X509Certificates: []X509Certificate{ + { + Data: `MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UE CAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoX DTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28x EjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308 @@ -129,6 +138,9 @@ SPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gf nqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90Dv TLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+ cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ==`, + }, + }, + }, }, }, }, diff --git a/saml.go b/saml.go index e559182e..b171e56d 100644 --- a/saml.go +++ b/saml.go @@ -1,13 +1,13 @@ // Package saml contains a partial implementation of the SAML standard in golang. // SAML is a standard for identity federation, i.e. either allowing a third party to authenticate your users or allowing third parties to rely on us to authenticate their users. // -// Introduction +// # Introduction // // In SAML parlance an Identity Provider (IDP) is a service that knows how to authenticate users. A Service Provider (SP) is a service that delegates authentication to an IDP. If you are building a service where users log in with someone else's credentials, then you are a Service Provider. This package supports implementing both service providers and identity providers. // // The core package contains the implementation of SAML. The package samlsp provides helper middleware suitable for use in Service Provider applications. The package samlidp provides a rudimentary IDP service that is useful for testing or as a starting point for other integrations. // -// Breaking Changes +// # Breaking Changes // // Version 0.4.0 introduces a few breaking changes to the _samlsp_ package in order to make the package more extensible, and to clean up the interfaces a bit. The default behavior remains the same, but you can now provide interface implementations of _RequestTracker_ (which tracks pending requests), _Session_ (which handles maintaining a session) and _OnError_ which handles reporting errors. // @@ -32,7 +32,7 @@ // - `CookieDomain` - Instead assign a custom CookieRequestTracker or CookieSessionProvider // - `CookieDomain` - Instead assign a custom CookieRequestTracker or CookieSessionProvider // -// Getting Started as a Service Provider +// # Getting Started as a Service Provider // // Let us assume we have a simple web application to protect. We'll modify this application so it uses SAML to authenticate users. // @@ -40,24 +40,27 @@ // package main // // import ( -// "fmt" -// "net/http" +// +// "fmt" +// "net/http" +// // ) // -// func hello(w http.ResponseWriter, r *http.Request) { -// fmt.Fprintf(w, "Hello, World!") -// } +// func hello(w http.ResponseWriter, r *http.Request) { +// fmt.Fprintf(w, "Hello, World!") +// } +// +// func main() { +// app := http.HandlerFunc(hello) +// http.Handle("/hello", app) +// http.ListenAndServe(":8000", nil) +// } // -// func main() { -// app := http.HandlerFunc(hello) -// http.Handle("/hello", app) -// http.ListenAndServe(":8000", nil) -// } // ``` // // Each service provider must have an self-signed X.509 key pair established. You can generate your own with something like this: // -// openssl req -x509 -newkey rsa:2048 -keyout myservice.key -out myservice.cert -days 365 -nodes -subj "/CN=myservice.example.com" +// openssl req -x509 -newkey rsa:2048 -keyout myservice.key -out myservice.cert -days 365 -nodes -subj "/CN=myservice.example.com" // // We will use `samlsp.Middleware` to wrap the endpoint we want to protect. Middleware provides both an `http.Handler` to serve the SAML specific URLs and a set of wrappers to require the user to be logged in. We also provide the URL where the service provider can fetch the metadata from the IDP at startup. In our case, we'll use [samltest.id](https://samltest.id/), an identity provider designed for testing. // @@ -65,57 +68,60 @@ // package main // // import ( -// "crypto/rsa" -// "crypto/tls" -// "crypto/x509" -// "fmt" -// "net/http" -// "net/url" -// -// "github.com/crewjam/saml/samlsp" +// +// "crypto/rsa" +// "crypto/tls" +// "crypto/x509" +// "fmt" +// "net/http" +// "net/url" +// +// "github.com/crewjam/saml/samlsp" +// // ) // -// func hello(w http.ResponseWriter, r *http.Request) { -// fmt.Fprintf(w, "Hello, %s!", samlsp.Token(r.Context()).Attributes.Get("cn")) -// } -// -// func main() { -// keyPair, err := tls.LoadX509KeyPair("myservice.cert", "myservice.key") -// if err != nil { -// panic(err) // TODO handle error -// } -// keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0]) -// if err != nil { -// panic(err) // TODO handle error -// } -// -// idpMetadataURL, err := url.Parse("https://samltest.id/saml/idp") -// if err != nil { -// panic(err) // TODO handle error -// } -// -// rootURL, err := url.Parse("http://localhost:8000") -// if err != nil { -// panic(err) // TODO handle error -// } -// -// samlSP, _ := samlsp.New(samlsp.Options{ -// URL: *rootURL, -// Key: keyPair.PrivateKey.(*rsa.PrivateKey), -// Certificate: keyPair.Leaf, -// IDPMetadataURL: idpMetadataURL, -// }) -// app := http.HandlerFunc(hello) -// http.Handle("/hello", samlSP.RequireAccount(app)) -// http.Handle("/saml/", samlSP) -// http.ListenAndServe(":8000", nil) -// } +// func hello(w http.ResponseWriter, r *http.Request) { +// fmt.Fprintf(w, "Hello, %s!", samlsp.Token(r.Context()).Attributes.Get("cn")) +// } +// +// func main() { +// keyPair, err := tls.LoadX509KeyPair("myservice.cert", "myservice.key") +// if err != nil { +// panic(err) // TODO handle error +// } +// keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0]) +// if err != nil { +// panic(err) // TODO handle error +// } +// +// idpMetadataURL, err := url.Parse("https://samltest.id/saml/idp") +// if err != nil { +// panic(err) // TODO handle error +// } +// +// rootURL, err := url.Parse("http://localhost:8000") +// if err != nil { +// panic(err) // TODO handle error +// } +// +// samlSP, _ := samlsp.New(samlsp.Options{ +// URL: *rootURL, +// Key: keyPair.PrivateKey.(*rsa.PrivateKey), +// Certificate: keyPair.Leaf, +// IDPMetadataURL: idpMetadataURL, +// }) +// app := http.HandlerFunc(hello) +// http.Handle("/hello", samlSP.RequireAccount(app)) +// http.Handle("/saml/", samlSP) +// http.ListenAndServe(":8000", nil) +// } +// // ``` // // Next we'll have to register our service provider with the identity provider to establish trust from the service provider to the IDP. For [samltest.id](https://samltest.id/), you can do something like: // -// mdpath=saml-test-$USER-$HOST.xml -// curl localhost:8000/saml/metadata > $mdpath +// mdpath=saml-test-$USER-$HOST.xml +// curl localhost:8000/saml/metadata > $mdpath // // Navigate to https://samltest.id/upload.php and upload the file you fetched. // @@ -133,11 +139,11 @@ // // 1. This time when `localhost:8000/hello` is requested there is a valid session and so the main content is served. // -// Getting Started as an Identity Provider +// # Getting Started as an Identity Provider // // Please see `example/idp/` for a substantially complete example of how to use the library and helpers to be an identity provider. // -// Support +// # Support // // The SAML standard is huge and complex with many dark corners and strange, unused features. This package implements the most commonly used subset of these features required to provide a single sign on experience. The package supports at least the subset of SAML known as [interoperable SAML](http://saml2int.org). // @@ -145,13 +151,13 @@ // // The package can produce signed SAML assertions, and can validate both signed and encrypted SAML assertions. It does not support signed or encrypted requests. // -// RelayState +// # RelayState // // The _RelayState_ parameter allows you to pass user state information across the authentication flow. The most common use for this is to allow a user to request a deep link into your site, be redirected through the SAML login flow, and upon successful completion, be directed to the originally requested link, rather than the root. // // Unfortunately, _RelayState_ is less useful than it could be. Firstly, it is not authenticated, so anything you supply must be signed to avoid XSS or CSRF. Secondly, it is limited to 80 bytes in length, which precludes signing. (See section 3.6.3.1 of SAMLProfiles.) // -// References +// # References // // The SAML specification is a collection of PDFs (sadly): // @@ -165,7 +171,7 @@ // // [SAMLtest](https://samltest.id/) is a testing ground for SAML service and identity providers. // -// Security Issues +// # Security Issues // // Please do not report security issues in the issue tracker. Rather, please contact me directly at ross@kndr.org ([PGP Key `78B6038B3B9DFB88`](https://keybase.io/crewjam)). package saml diff --git a/samlidp/samlidp.go b/samlidp/samlidp.go index f22ca27c..13ca10b9 100644 --- a/samlidp/samlidp.go +++ b/samlidp/samlidp.go @@ -7,6 +7,7 @@ import ( "crypto/x509" "net/http" "net/url" + "regexp" "sync" "github.com/zenazn/goji/web" @@ -19,6 +20,7 @@ import ( type Options struct { URL url.URL Key crypto.PrivateKey + Signer crypto.Signer Logger logger.Interface Certificate *x509.Certificate Store Store @@ -26,14 +28,14 @@ type Options struct { // Server represents an IDP server. The server provides the following URLs: // -// /metadata - the SAML metadata -// /sso - the SAML endpoint to initiate an authentication flow -// /login - prompt for a username and password if no session established -// /login/:shortcut - kick off an IDP-initiated authentication flow -// /services - RESTful interface to Service objects -// /users - RESTful interface to User objects -// /sessions - RESTful interface to Session objects -// /shortcuts - RESTful interface to Shortcut objects +// /metadata - the SAML metadata +// /sso - the SAML endpoint to initiate an authentication flow +// /login - prompt for a username and password if no session established +// /login/:shortcut - kick off an IDP-initiated authentication flow +// /services - RESTful interface to Service objects +// /users - RESTful interface to User objects +// /sessions - RESTful interface to Session objects +// /shortcuts - RESTful interface to Shortcut objects type Server struct { http.Handler idpConfigMu sync.RWMutex // protects calls into the IDP @@ -46,9 +48,9 @@ type Server struct { // New returns a new Server func New(opts Options) (*Server, error) { metadataURL := opts.URL - metadataURL.Path = metadataURL.Path + "/metadata" + metadataURL.Path += "/metadata" ssoURL := opts.URL - ssoURL.Path = ssoURL.Path + "/sso" + ssoURL.Path += "/sso" logr := opts.Logger if logr == nil { logr = logger.DefaultLogger @@ -58,6 +60,7 @@ func New(opts Options) (*Server, error) { serviceProviders: map[string]*saml.EntityDescriptor{}, IDP: saml.IdentityProvider{ Key: opts.Key, + Signer: opts.Signer, Logger: logr, Certificate: opts.Certificate, MetadataURL: metadataURL, @@ -110,9 +113,10 @@ func (s *Server) InitializeHTTP() { mux.Put("/users/:id", s.HandlePutUser) mux.Delete("/users/:id", s.HandleDeleteUser) + sessionPath := regexp.MustCompile("/sessions/(?P.*)") mux.Get("/sessions/", s.HandleListSessions) - mux.Get("/sessions/:id", s.HandleGetSession) - mux.Delete("/sessions/:id", s.HandleDeleteSession) + mux.Get(sessionPath, s.HandleGetSession) + mux.Delete(sessionPath, s.HandleDeleteSession) mux.Get("/shortcuts/", s.HandleListShortcuts) mux.Get("/shortcuts/:id", s.HandleGetShortcut) diff --git a/samlidp/samlidp_test.go b/samlidp/samlidp_test.go index 8100a8c7..e5b2dafb 100644 --- a/samlidp/samlidp_test.go +++ b/samlidp/samlidp_test.go @@ -16,7 +16,7 @@ import ( is "gotest.tools/assert/cmp" "gotest.tools/golden" - "github.com/form3tech-oss/jwt-go" + "github.com/golang-jwt/jwt/v4" "github.com/crewjam/saml" "github.com/crewjam/saml/logger" @@ -124,8 +124,8 @@ func TestHTTPCanHandleMetadataRequest(t *testing.T) { test.Server.ServeHTTP(w, r) assert.Check(t, is.Equal(http.StatusOK, w.Code)) assert.Check(t, - strings.HasPrefix(string(w.Body.Bytes()), "

"), - string(w.Body.Bytes())) + strings.HasPrefix(w.Body.String(), "

"), + w.Body.String()) golden.Assert(t, w.Body.String(), "http_sso_response.html") } diff --git a/samlidp/service.go b/samlidp/service.go index 5c2cc659..0b62cd3b 100644 --- a/samlidp/service.go +++ b/samlidp/service.go @@ -25,7 +25,7 @@ type Service struct { // service provider ID, which is typically the service provider's // metadata URL. If an appropriate service provider cannot be found then // the returned error must be os.ErrNotExist. -func (s *Server) GetServiceProvider(r *http.Request, serviceProviderID string) (*saml.EntityDescriptor, error) { +func (s *Server) GetServiceProvider(_ *http.Request, serviceProviderID string) (*saml.EntityDescriptor, error) { s.idpConfigMu.RLock() defer s.idpConfigMu.RUnlock() rv, ok := s.serviceProviders[serviceProviderID] @@ -37,7 +37,7 @@ func (s *Server) GetServiceProvider(r *http.Request, serviceProviderID string) ( // HandleListServices handles the `GET /services/` request and responds with a JSON formatted list // of service names. -func (s *Server) HandleListServices(c web.C, w http.ResponseWriter, r *http.Request) { +func (s *Server) HandleListServices(_ web.C, w http.ResponseWriter, _ *http.Request) { services, err := s.Store.List("/services/") if err != nil { s.logger.Printf("ERROR: %s", err) @@ -45,14 +45,18 @@ func (s *Server) HandleListServices(c web.C, w http.ResponseWriter, r *http.Requ return } - json.NewEncoder(w).Encode(struct { + err = json.NewEncoder(w).Encode(struct { Services []string `json:"services"` }{Services: services}) + if err != nil { + s.logger.Printf("ERROR: %s", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } } // HandleGetService handles the `GET /services/:id` request and responds with the service // metadata in XML format. -func (s *Server) HandleGetService(c web.C, w http.ResponseWriter, r *http.Request) { +func (s *Server) HandleGetService(c web.C, w http.ResponseWriter, _ *http.Request) { service := Service{} err := s.Store.Get(fmt.Sprintf("/services/%s", c.URLParams["id"]), &service) if err != nil { @@ -60,7 +64,11 @@ func (s *Server) HandleGetService(c web.C, w http.ResponseWriter, r *http.Reques http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - xml.NewEncoder(w).Encode(service.Metadata) + err = xml.NewEncoder(w).Encode(service.Metadata) + if err != nil { + s.logger.Printf("ERROR: %s", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } } // HandlePutService handles the `PUT /shortcuts/:id` request. It accepts the XML-formatted @@ -92,7 +100,7 @@ func (s *Server) HandlePutService(c web.C, w http.ResponseWriter, r *http.Reques } // HandleDeleteService handles the `DELETE /services/:id` request. -func (s *Server) HandleDeleteService(c web.C, w http.ResponseWriter, r *http.Request) { +func (s *Server) HandleDeleteService(c web.C, w http.ResponseWriter, _ *http.Request) { service := Service{} err := s.Store.Get(fmt.Sprintf("/services/%s", c.URLParams["id"]), &service) if err != nil { diff --git a/samlidp/service_test.go b/samlidp/service_test.go index 57e7c4d4..7ee4df83 100644 --- a/samlidp/service_test.go +++ b/samlidp/service_test.go @@ -18,7 +18,7 @@ func TestServicesCrud(t *testing.T) { r, _ := http.NewRequest("GET", "https://idp.example.com/services/", nil) test.Server.ServeHTTP(w, r) assert.Check(t, is.Equal(http.StatusOK, w.Code)) - assert.Check(t, is.Equal("{\"services\":[]}\n", string(w.Body.Bytes()))) + assert.Check(t, is.Equal("{\"services\":[]}\n", w.Body.String())) w = httptest.NewRecorder() r, _ = http.NewRequest("PUT", "https://idp.example.com/services/sp", @@ -36,7 +36,7 @@ func TestServicesCrud(t *testing.T) { r, _ = http.NewRequest("GET", "https://idp.example.com/services/", nil) test.Server.ServeHTTP(w, r) assert.Check(t, is.Equal(http.StatusOK, w.Code)) - assert.Check(t, is.Equal("{\"services\":[\"sp\"]}\n", string(w.Body.Bytes()))) + assert.Check(t, is.Equal("{\"services\":[\"sp\"]}\n", w.Body.String())) assert.Check(t, is.Len(test.Server.serviceProviders, 2)) @@ -49,6 +49,6 @@ func TestServicesCrud(t *testing.T) { r, _ = http.NewRequest("GET", "https://idp.example.com/services/", nil) test.Server.ServeHTTP(w, r) assert.Check(t, is.Equal(http.StatusOK, w.Code)) - assert.Check(t, is.Equal("{\"services\":[]}\n", string(w.Body.Bytes()))) + assert.Check(t, is.Equal("{\"services\":[]}\n", w.Body.String())) assert.Check(t, is.Len(test.Server.serviceProviders, 1)) } diff --git a/samlidp/session.go b/samlidp/session.go index ba3bd65b..8ffae2ba 100644 --- a/samlidp/session.go +++ b/samlidp/session.go @@ -48,12 +48,13 @@ func (s *Server) GetSession(w http.ResponseWriter, r *http.Request, req *saml.Id } session := &saml.Session{ - ID: base64.StdEncoding.EncodeToString(randomBytes(32)), - NameID: user.Email, - CreateTime: saml.TimeNow(), - ExpireTime: saml.TimeNow().Add(sessionMaxAge), - Index: hex.EncodeToString(randomBytes(32)), - UserName: user.Name, + ID: base64.StdEncoding.EncodeToString(randomBytes(32)), + NameID: user.Email, + CreateTime: saml.TimeNow(), + ExpireTime: saml.TimeNow().Add(sessionMaxAge), + Index: hex.EncodeToString(randomBytes(32)), + UserName: user.Name, + // nolint:gocritic // Groups should be a slice here. Groups: user.Groups[:], UserEmail: user.Email, UserCommonName: user.CommonName, @@ -102,7 +103,7 @@ func (s *Server) GetSession(w http.ResponseWriter, r *http.Request, req *saml.Id // sendLoginForm produces a form which requests a username and password and directs the user // back to the IDP authorize URL to restart the SAML login flow, this time establishing a // session based on the credentials that were provided. -func (s *Server) sendLoginForm(w http.ResponseWriter, r *http.Request, req *saml.IdpAuthnRequest, toast string) { +func (s *Server) sendLoginForm(w http.ResponseWriter, _ *http.Request, req *saml.IdpAuthnRequest, toast string) { tmpl := template.Must(template.New("saml-post-form").Parse(`` + `` + `

{{.Toast}}

` + @@ -135,7 +136,7 @@ func (s *Server) sendLoginForm(w http.ResponseWriter, r *http.Request, req *saml // in the request body, then they are validated. For valid credentials, the response is a // 200 OK and the JSON session object. For invalid credentials, the HTML login prompt form // is sent. -func (s *Server) HandleLogin(c web.C, w http.ResponseWriter, r *http.Request) { +func (s *Server) HandleLogin(_ web.C, w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return @@ -144,38 +145,48 @@ func (s *Server) HandleLogin(c web.C, w http.ResponseWriter, r *http.Request) { if session == nil { return } - json.NewEncoder(w).Encode(session) + if err := json.NewEncoder(w).Encode(session); err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } } // HandleListSessions handles the `GET /sessions/` request and responds with a JSON formatted list // of session names. -func (s *Server) HandleListSessions(c web.C, w http.ResponseWriter, r *http.Request) { +func (s *Server) HandleListSessions(_ web.C, w http.ResponseWriter, _ *http.Request) { sessions, err := s.Store.List("/sessions/") if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - json.NewEncoder(w).Encode(struct { + err = json.NewEncoder(w).Encode(struct { Sessions []string `json:"sessions"` }{Sessions: sessions}) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } } // HandleGetSession handles the `GET /sessions/:id` request and responds with the session // object in JSON format. -func (s *Server) HandleGetSession(c web.C, w http.ResponseWriter, r *http.Request) { +func (s *Server) HandleGetSession(c web.C, w http.ResponseWriter, _ *http.Request) { session := saml.Session{} err := s.Store.Get(fmt.Sprintf("/sessions/%s", c.URLParams["id"]), &session) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - json.NewEncoder(w).Encode(session) + if err := json.NewEncoder(w).Encode(session); err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } } // HandleDeleteSession handles the `DELETE /sessions/:id` request. It invalidates the // specified session. -func (s *Server) HandleDeleteSession(c web.C, w http.ResponseWriter, r *http.Request) { +func (s *Server) HandleDeleteSession(c web.C, w http.ResponseWriter, _ *http.Request) { err := s.Store.Delete(fmt.Sprintf("/sessions/%s", c.URLParams["id"])) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) diff --git a/samlidp/session_test.go b/samlidp/session_test.go index d24684f3..cb3d5c3d 100644 --- a/samlidp/session_test.go +++ b/samlidp/session_test.go @@ -17,7 +17,7 @@ func TestSessionsCrud(t *testing.T) { test.Server.ServeHTTP(w, r) assert.Check(t, is.Equal(http.StatusOK, w.Code)) assert.Check(t, is.Equal("{\"sessions\":[]}\n", - string(w.Body.Bytes()))) + w.Body.String())) w = httptest.NewRecorder() r, _ = http.NewRequest("PUT", "https://idp.example.com/users/alice", @@ -33,23 +33,23 @@ func TestSessionsCrud(t *testing.T) { assert.Check(t, is.Equal(http.StatusOK, w.Code)) assert.Check(t, is.Equal("session=AAIEBggKDA4QEhQWGBocHiAiJCYoKiwuMDI0Njg6PD4=; Path=/; Max-Age=3600; HttpOnly; Secure", w.Header().Get("Set-Cookie"))) - assert.Check(t, is.Equal("{\"ID\":\"AAIEBggKDA4QEhQWGBocHiAiJCYoKiwuMDI0Njg6PD4=\",\"CreateTime\":\"2015-12-01T01:57:09Z\",\"ExpireTime\":\"2015-12-01T02:57:09Z\",\"Index\":\"40424446484a4c4e50525456585a5c5e60626466686a6c6e70727476787a7c7e\",\"NameID\":\"\",\"Groups\":null,\"UserName\":\"alice\",\"UserEmail\":\"\",\"UserCommonName\":\"\",\"UserSurname\":\"\",\"UserGivenName\":\"\",\"UserScopedAffiliation\":\"\",\"CustomAttributes\":null}\n", - string(w.Body.Bytes()))) + assert.Check(t, is.Equal("{\"ID\":\"AAIEBggKDA4QEhQWGBocHiAiJCYoKiwuMDI0Njg6PD4=\",\"CreateTime\":\"2015-12-01T01:57:09Z\",\"ExpireTime\":\"2015-12-01T02:57:09Z\",\"Index\":\"40424446484a4c4e50525456585a5c5e60626466686a6c6e70727476787a7c7e\",\"NameID\":\"\",\"NameIDFormat\":\"\",\"SubjectID\":\"\",\"Groups\":null,\"UserName\":\"alice\",\"UserEmail\":\"\",\"UserCommonName\":\"\",\"UserSurname\":\"\",\"UserGivenName\":\"\",\"UserScopedAffiliation\":\"\",\"CustomAttributes\":null}\n", + w.Body.String())) w = httptest.NewRecorder() r, _ = http.NewRequest("GET", "https://idp.example.com/login", nil) r.Header.Set("Cookie", "session=AAIEBggKDA4QEhQWGBocHiAiJCYoKiwuMDI0Njg6PD4=") test.Server.ServeHTTP(w, r) assert.Check(t, is.Equal(http.StatusOK, w.Code)) - assert.Check(t, is.Equal("{\"ID\":\"AAIEBggKDA4QEhQWGBocHiAiJCYoKiwuMDI0Njg6PD4=\",\"CreateTime\":\"2015-12-01T01:57:09Z\",\"ExpireTime\":\"2015-12-01T02:57:09Z\",\"Index\":\"40424446484a4c4e50525456585a5c5e60626466686a6c6e70727476787a7c7e\",\"NameID\":\"\",\"Groups\":null,\"UserName\":\"alice\",\"UserEmail\":\"\",\"UserCommonName\":\"\",\"UserSurname\":\"\",\"UserGivenName\":\"\",\"UserScopedAffiliation\":\"\",\"CustomAttributes\":null}\n", - string(w.Body.Bytes()))) + assert.Check(t, is.Equal("{\"ID\":\"AAIEBggKDA4QEhQWGBocHiAiJCYoKiwuMDI0Njg6PD4=\",\"CreateTime\":\"2015-12-01T01:57:09Z\",\"ExpireTime\":\"2015-12-01T02:57:09Z\",\"Index\":\"40424446484a4c4e50525456585a5c5e60626466686a6c6e70727476787a7c7e\",\"NameID\":\"\",\"NameIDFormat\":\"\",\"SubjectID\":\"\",\"Groups\":null,\"UserName\":\"alice\",\"UserEmail\":\"\",\"UserCommonName\":\"\",\"UserSurname\":\"\",\"UserGivenName\":\"\",\"UserScopedAffiliation\":\"\",\"CustomAttributes\":null}\n", + w.Body.String())) w = httptest.NewRecorder() r, _ = http.NewRequest("GET", "https://idp.example.com/sessions/AAIEBggKDA4QEhQWGBocHiAiJCYoKiwuMDI0Njg6PD4=", nil) test.Server.ServeHTTP(w, r) assert.Check(t, is.Equal(http.StatusOK, w.Code)) - assert.Check(t, is.Equal("{\"ID\":\"AAIEBggKDA4QEhQWGBocHiAiJCYoKiwuMDI0Njg6PD4=\",\"CreateTime\":\"2015-12-01T01:57:09Z\",\"ExpireTime\":\"2015-12-01T02:57:09Z\",\"Index\":\"40424446484a4c4e50525456585a5c5e60626466686a6c6e70727476787a7c7e\",\"NameID\":\"\",\"Groups\":null,\"UserName\":\"alice\",\"UserEmail\":\"\",\"UserCommonName\":\"\",\"UserSurname\":\"\",\"UserGivenName\":\"\",\"UserScopedAffiliation\":\"\",\"CustomAttributes\":null}\n", - string(w.Body.Bytes()))) + assert.Check(t, is.Equal("{\"ID\":\"AAIEBggKDA4QEhQWGBocHiAiJCYoKiwuMDI0Njg6PD4=\",\"CreateTime\":\"2015-12-01T01:57:09Z\",\"ExpireTime\":\"2015-12-01T02:57:09Z\",\"Index\":\"40424446484a4c4e50525456585a5c5e60626466686a6c6e70727476787a7c7e\",\"NameID\":\"\",\"NameIDFormat\":\"\",\"SubjectID\":\"\",\"Groups\":null,\"UserName\":\"alice\",\"UserEmail\":\"\",\"UserCommonName\":\"\",\"UserSurname\":\"\",\"UserGivenName\":\"\",\"UserScopedAffiliation\":\"\",\"CustomAttributes\":null}\n", + w.Body.String())) w = httptest.NewRecorder() r, _ = http.NewRequest("DELETE", "https://idp.example.com/sessions/AAIEBggKDA4QEhQWGBocHiAiJCYoKiwuMDI0Njg6PD4=", nil) @@ -61,6 +61,6 @@ func TestSessionsCrud(t *testing.T) { test.Server.ServeHTTP(w, r) assert.Check(t, is.Equal(http.StatusOK, w.Code)) assert.Check(t, is.Equal("{\"sessions\":[]}\n", - string(w.Body.Bytes()))) + w.Body.String())) } diff --git a/samlidp/shortcut.go b/samlidp/shortcut.go index 151a84ea..4c5b8650 100644 --- a/samlidp/shortcut.go +++ b/samlidp/shortcut.go @@ -31,28 +31,35 @@ type Shortcut struct { // HandleListShortcuts handles the `GET /shortcuts/` request and responds with a JSON formatted list // of shortcut names. -func (s *Server) HandleListShortcuts(c web.C, w http.ResponseWriter, r *http.Request) { +func (s *Server) HandleListShortcuts(_ web.C, w http.ResponseWriter, _ *http.Request) { shortcuts, err := s.Store.List("/shortcuts/") if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - json.NewEncoder(w).Encode(struct { + err = json.NewEncoder(w).Encode(struct { Shortcuts []string `json:"shortcuts"` }{Shortcuts: shortcuts}) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } } // HandleGetShortcut handles the `GET /shortcuts/:id` request and responds with the shortcut // object in JSON format. -func (s *Server) HandleGetShortcut(c web.C, w http.ResponseWriter, r *http.Request) { +func (s *Server) HandleGetShortcut(c web.C, w http.ResponseWriter, _ *http.Request) { shortcut := Shortcut{} err := s.Store.Get(fmt.Sprintf("/shortcuts/%s", c.URLParams["id"]), &shortcut) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - json.NewEncoder(w).Encode(shortcut) + if err := json.NewEncoder(w).Encode(shortcut); err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } } // HandlePutShortcut handles the `PUT /shortcuts/:id` request. It accepts a JSON formatted @@ -74,7 +81,7 @@ func (s *Server) HandlePutShortcut(c web.C, w http.ResponseWriter, r *http.Reque } // HandleDeleteShortcut handles the `DELETE /shortcuts/:id` request. -func (s *Server) HandleDeleteShortcut(c web.C, w http.ResponseWriter, r *http.Request) { +func (s *Server) HandleDeleteShortcut(c web.C, w http.ResponseWriter, _ *http.Request) { err := s.Store.Delete(fmt.Sprintf("/shortcuts/%s", c.URLParams["id"])) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -100,7 +107,7 @@ func (s *Server) HandleIDPInitiated(c web.C, w http.ResponseWriter, r *http.Requ case shortcut.RelayState != nil: relayState = *shortcut.RelayState case shortcut.URISuffixAsRelayState: - relayState, _ = c.URLParams["*"] + relayState = c.URLParams["*"] } s.idpConfigMu.RLock() diff --git a/samlidp/shortcut_test.go b/samlidp/shortcut_test.go index 7a15f148..e74f34bf 100644 --- a/samlidp/shortcut_test.go +++ b/samlidp/shortcut_test.go @@ -17,7 +17,7 @@ func TestShortcutsCrud(t *testing.T) { test.Server.ServeHTTP(w, r) assert.Check(t, is.Equal(http.StatusOK, w.Code)) assert.Check(t, is.Equal("{\"shortcuts\":[]}\n", - string(w.Body.Bytes()))) + w.Body.String())) w = httptest.NewRecorder() r, _ = http.NewRequest("PUT", "https://idp.example.com/shortcuts/bob", @@ -30,14 +30,14 @@ func TestShortcutsCrud(t *testing.T) { test.Server.ServeHTTP(w, r) assert.Check(t, is.Equal(http.StatusOK, w.Code)) assert.Check(t, is.Equal("{\"name\":\"bob\",\"service_provider\":\"https://example.com/saml2/metadata\",\"url_suffix_as_relay_state\":true}\n", - string(w.Body.Bytes()))) + w.Body.String())) w = httptest.NewRecorder() r, _ = http.NewRequest("GET", "https://idp.example.com/shortcuts/", nil) test.Server.ServeHTTP(w, r) assert.Check(t, is.Equal(http.StatusOK, w.Code)) assert.Check(t, is.Equal("{\"shortcuts\":[\"bob\"]}\n", - string(w.Body.Bytes()))) + w.Body.String())) w = httptest.NewRecorder() r, _ = http.NewRequest("DELETE", "https://idp.example.com/shortcuts/bob", nil) @@ -49,7 +49,7 @@ func TestShortcutsCrud(t *testing.T) { test.Server.ServeHTTP(w, r) assert.Check(t, is.Equal(http.StatusOK, w.Code)) assert.Check(t, is.Equal("{\"shortcuts\":[]}\n", - string(w.Body.Bytes()))) + w.Body.String())) } func TestShortcut(t *testing.T) { @@ -78,7 +78,7 @@ func TestShortcut(t *testing.T) { r.Header.Set("Cookie", "session=AAIEBggKDA4QEhQWGBocHiAiJCYoKiwuMDI0Njg6PD4=") test.Server.ServeHTTP(w, r) assert.Check(t, is.Equal(http.StatusOK, w.Code)) - body := string(w.Body.Bytes()) + body := w.Body.String() assert.Check(t, strings.Contains(body, ""), diff --git a/samlidp/testdata/http_metadata_response.html b/samlidp/testdata/http_metadata_response.html index e204fd3e..a6836ddd 100644 --- a/samlidp/testdata/http_metadata_response.html +++ b/samlidp/testdata/http_metadata_response.html @@ -2,15 +2,15 @@ - - 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 diff --git a/samlidp/testdata/sp_metadata.xml b/samlidp/testdata/sp_metadata.xml index f5cef423..6e7610ed 100644 --- a/samlidp/testdata/sp_metadata.xml +++ b/samlidp/testdata/sp_metadata.xml @@ -1 +1 @@ -MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ==MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ== \ No newline at end of file +MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ==MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ== \ No newline at end of file diff --git a/samlidp/user.go b/samlidp/user.go index 46d1a964..c8c412cb 100644 --- a/samlidp/user.go +++ b/samlidp/user.go @@ -25,7 +25,7 @@ type User struct { // HandleListUsers handles the `GET /users/` request and responds with a JSON formatted list // of user names. -func (s *Server) HandleListUsers(c web.C, w http.ResponseWriter, r *http.Request) { +func (s *Server) HandleListUsers(_ web.C, w http.ResponseWriter, _ *http.Request) { users, err := s.Store.List("/users/") if err != nil { s.logger.Printf("ERROR: %s", err) @@ -33,14 +33,19 @@ func (s *Server) HandleListUsers(c web.C, w http.ResponseWriter, r *http.Request return } - json.NewEncoder(w).Encode(struct { + err = json.NewEncoder(w).Encode(struct { Users []string `json:"users"` }{Users: users}) + if err != nil { + s.logger.Printf("ERROR: %s", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } } // HandleGetUser handles the `GET /users/:id` request and responds with the user object in JSON // format. The HashedPassword field is excluded. -func (s *Server) HandleGetUser(c web.C, w http.ResponseWriter, r *http.Request) { +func (s *Server) HandleGetUser(c web.C, w http.ResponseWriter, _ *http.Request) { user := User{} err := s.Store.Get(fmt.Sprintf("/users/%s", c.URLParams["id"]), &user) if err != nil { @@ -49,7 +54,11 @@ func (s *Server) HandleGetUser(c web.C, w http.ResponseWriter, r *http.Request) return } user.HashedPassword = nil - json.NewEncoder(w).Encode(user) + if err := json.NewEncoder(w).Encode(user); err != nil { + s.logger.Printf("ERROR: %s", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } } // HandlePutUser handles the `PUT /users/:id` request. It accepts a JSON formatted user object in @@ -99,7 +108,7 @@ func (s *Server) HandlePutUser(c web.C, w http.ResponseWriter, r *http.Request) } // HandleDeleteUser handles the `DELETE /users/:id` request. -func (s *Server) HandleDeleteUser(c web.C, w http.ResponseWriter, r *http.Request) { +func (s *Server) HandleDeleteUser(c web.C, w http.ResponseWriter, _ *http.Request) { err := s.Store.Delete(fmt.Sprintf("/users/%s", c.URLParams["id"])) if err != nil { s.logger.Printf("ERROR: %s", err) diff --git a/samlidp/user_test.go b/samlidp/user_test.go index ecac3459..30a98a6b 100644 --- a/samlidp/user_test.go +++ b/samlidp/user_test.go @@ -16,7 +16,7 @@ func TestUsersCrud(t *testing.T) { r, _ := http.NewRequest("GET", "https://idp.example.com/users/", nil) test.Server.ServeHTTP(w, r) assert.Check(t, is.Equal(http.StatusOK, w.Code)) - assert.Check(t, is.Equal("{\"users\":[]}\n", string(w.Body.Bytes()))) + assert.Check(t, is.Equal("{\"users\":[]}\n", w.Body.String())) w = httptest.NewRecorder() r, _ = http.NewRequest("PUT", "https://idp.example.com/users/alice", @@ -28,13 +28,13 @@ func TestUsersCrud(t *testing.T) { r, _ = http.NewRequest("GET", "https://idp.example.com/users/alice", nil) test.Server.ServeHTTP(w, r) assert.Check(t, is.Equal(http.StatusOK, w.Code)) - assert.Check(t, is.Equal("{\"name\":\"alice\"}\n", string(w.Body.Bytes()))) + assert.Check(t, is.Equal("{\"name\":\"alice\"}\n", w.Body.String())) w = httptest.NewRecorder() r, _ = http.NewRequest("GET", "https://idp.example.com/users/", nil) test.Server.ServeHTTP(w, r) assert.Check(t, is.Equal(http.StatusOK, w.Code)) - assert.Check(t, is.Equal("{\"users\":[\"alice\"]}\n", string(w.Body.Bytes()))) + assert.Check(t, is.Equal("{\"users\":[\"alice\"]}\n", w.Body.String())) w = httptest.NewRecorder() r, _ = http.NewRequest("DELETE", "https://idp.example.com/users/alice", nil) @@ -45,5 +45,5 @@ func TestUsersCrud(t *testing.T) { r, _ = http.NewRequest("GET", "https://idp.example.com/users/", nil) test.Server.ServeHTTP(w, r) assert.Check(t, is.Equal(http.StatusOK, w.Code)) - assert.Check(t, is.Equal("{\"users\":[]}\n", string(w.Body.Bytes()))) + assert.Check(t, is.Equal("{\"users\":[]}\n", w.Body.String())) } diff --git a/samlidp/util_test.go b/samlidp/util_go116_test.go similarity index 97% rename from samlidp/util_test.go rename to samlidp/util_go116_test.go index 4bc25a8a..16f54740 100644 --- a/samlidp/util_test.go +++ b/samlidp/util_go116_test.go @@ -1,3 +1,6 @@ +//go:build !go1.17 +// +build !go1.17 + package samlidp import ( diff --git a/samlidp/util_go117_test.go b/samlidp/util_go117_test.go new file mode 100644 index 00000000..2c273bc1 --- /dev/null +++ b/samlidp/util_go117_test.go @@ -0,0 +1,26 @@ +//go:build go1.17 +// +build go1.17 + +package samlidp + +import ( + "strings" + "testing" + + "gotest.tools/assert" + is "gotest.tools/assert/cmp" +) + +func TestGetSPMetadata(t *testing.T) { + good := "" + + "\n" + + "" + _, err := getSPMetadata(strings.NewReader(good)) + assert.Check(t, err) + + bad := "" + + "]]>\n" + + "" + _, err = getSPMetadata(strings.NewReader(bad)) + assert.Check(t, is.Error(err, "XML syntax error on line 1: unescaped ]]> not in CDATA section")) +} diff --git a/samlsp/error.go b/samlsp/error.go index 662bce74..496faccf 100644 --- a/samlsp/error.go +++ b/samlsp/error.go @@ -14,7 +14,7 @@ type ErrorFunction func(w http.ResponseWriter, r *http.Request, err error) // DefaultOnError is the default ErrorFunction implementation. It prints // an message via the standard log package and returns a simple text // "Forbidden" message to the user. -func DefaultOnError(w http.ResponseWriter, r *http.Request, err error) { +func DefaultOnError(w http.ResponseWriter, _ *http.Request, err error) { if parseErr, ok := err.(*saml.InvalidResponseError); ok { log.Printf("WARNING: received invalid saml response: %s (now: %s) %s", parseErr.Response, parseErr.Now, parseErr.PrivateErr) diff --git a/samlsp/fetch_metadata.go b/samlsp/fetch_metadata.go index 1ef521ac..4d92503e 100644 --- a/samlsp/fetch_metadata.go +++ b/samlsp/fetch_metadata.go @@ -12,6 +12,8 @@ import ( "github.com/crewjam/httperr" xrv "github.com/mattermost/xml-roundtrip-validator" + "github.com/crewjam/saml/logger" + "github.com/crewjam/saml" ) @@ -61,7 +63,11 @@ func FetchMetadata(ctx context.Context, httpClient *http.Client, metadataURL url if err != nil { return nil, err } - defer resp.Body.Close() + defer func() { + if err := resp.Body.Close(); err != nil { + logger.DefaultLogger.Printf("Error while closing response body during fetch metadata: %v", err) + } + }() if resp.StatusCode >= 400 { return nil, httperr.Response(*resp) } diff --git a/samlsp/fetch_metadata_go116_test.go b/samlsp/fetch_metadata_go116_test.go new file mode 100644 index 00000000..91c3aa69 --- /dev/null +++ b/samlsp/fetch_metadata_go116_test.go @@ -0,0 +1,34 @@ +//go:build !go1.17 +// +build !go1.17 + +package samlsp + +import ( + "bytes" + "context" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "gotest.tools/assert" + is "gotest.tools/assert/cmp" +) + +func TestFetchMetadataRejectsInvalid(t *testing.T) { + test := NewMiddlewareTest(t) + test.IDPMetadata = bytes.Replace(test.IDPMetadata, + []byte("]]")) + + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Check(t, is.Equal("/metadata", r.URL.String())) + _, err := w.Write(test.IDPMetadata) + assert.Check(t, err) + })) + + fmt.Println(testServer.URL + "/metadata") + u, _ := url.Parse(testServer.URL + "/metadata") + md, err := FetchMetadata(context.Background(), testServer.Client(), *u) + assert.Check(t, is.Error(err, "expected element in name space urn:oasis:names:tc:SAML:2.0:metadata but have no name space")) + assert.Check(t, is.Nil(md)) +} diff --git a/samlsp/fetch_metadata_test.go b/samlsp/fetch_metadata_test.go index 2bc2caf1..bd90dd8a 100644 --- a/samlsp/fetch_metadata_test.go +++ b/samlsp/fetch_metadata_test.go @@ -1,7 +1,6 @@ package samlsp import ( - "bytes" "context" "fmt" "net/http" @@ -18,7 +17,8 @@ func TestFetchMetadata(t *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Check(t, is.Equal("/metadata", r.URL.String())) - w.Write(test.IDPMetadata) + _, err := w.Write(test.IDPMetadata) + assert.Check(t, err) })) fmt.Println(testServer.URL + "/metadata") @@ -27,20 +27,3 @@ func TestFetchMetadata(t *testing.T) { assert.Check(t, err) assert.Check(t, is.Equal("https://idp.testshib.org/idp/shibboleth", md.EntityID)) } - -func TestFetchMetadataRejectsInvalid(t *testing.T) { - test := NewMiddlewareTest(t) - test.IDPMetadata = bytes.Replace(test.IDPMetadata, - []byte("`, + string(x))) +} + +func TestArtifactResolveElement(t *testing.T) { + issueInstant := time.Date(2020, 7, 21, 12, 30, 45, 0, time.UTC) + expected := ArtifactResolve{ + ID: "index", + Version: "version", + IssueInstant: issueInstant, + // Signature *etree.Element + } + + doc := etree.NewDocument() + doc.SetRoot(expected.Element()) + x, err := doc.WriteToBytes() + assert.Check(t, err) + assert.Check(t, is.Equal(``, + string(x))) + + var actual ArtifactResolve + err = xml.Unmarshal(x, &actual) + assert.Check(t, err) + assert.Check(t, is.DeepEqual(expected, actual)) + + x, err = xml.Marshal(expected) + assert.Check(t, err) + assert.Check(t, is.Equal(``, + string(x))) +} + +func TestArtifactResolveSoapRequest(t *testing.T) { + issueInstant := time.Date(2020, 7, 21, 12, 30, 45, 0, time.UTC) + expected := ArtifactResolve{ + ID: "index", + Version: "version", + IssueInstant: issueInstant, + // Signature *etree.Element + } + + doc := etree.NewDocument() + doc.SetRoot(expected.SoapRequest()) + x, err := doc.WriteToBytes() + assert.Check(t, err) + assert.Check(t, is.Equal(``, + string(x))) +} + +func TestArtifactResponseElement(t *testing.T) { + issueInstant := time.Date(2020, 7, 21, 12, 30, 45, 0, time.UTC) + status := Status{ + XMLName: xml.Name{ + Space: "urn:oasis:names:tc:SAML:2.0:protocol", + Local: "Status", + }, + StatusCode: StatusCode{ + XMLName: xml.Name{ + Space: "urn:oasis:names:tc:SAML:2.0:protocol", + Local: "StatusCode", + }, + Value: "value", + }, + } + expected := ArtifactResponse{ + ID: "index", + InResponseTo: "ID", + Version: "version", + IssueInstant: issueInstant, + Status: status, + Response: Response{ + ID: "index", + InResponseTo: "ID", + Version: "version", + Destination: "destination", + Consent: "consent", + Status: status, + IssueInstant: issueInstant, + }, + // Signature *etree.Element + } + + doc := etree.NewDocument() + doc.SetRoot(expected.Element()) + x, err := doc.WriteToBytes() + assert.Check(t, err) + assert.Check(t, is.Equal(``, + string(x))) + + var actual ArtifactResponse + err = xml.Unmarshal(x, &actual) + assert.Check(t, err) + assert.Check(t, is.DeepEqual(expected, actual)) + + x, err = xml.Marshal(expected) + assert.Check(t, err) + assert.Check(t, is.Equal(``, + string(x))) +} + +func TestLogoutRequestXMLRoundTrip(t *testing.T) { + issueInstant := time.Date(2021, 10, 8, 12, 30, 0, 0, time.UTC) + notOnOrAfter := time.Date(2021, 10, 8, 12, 35, 0, 0, time.UTC) + expected := LogoutRequest{ + ID: "request-id", + Version: "2.0", + IssueInstant: issueInstant, + NotOnOrAfter: ¬OnOrAfter, + Issuer: &Issuer{ + XMLName: xml.Name{ + Space: "urn:oasis:names:tc:SAML:2.0:assertion", + Local: "Issuer", + }, + Value: "uri:issuer", + }, + NameID: &NameID{ + Value: "name-id", + }, + SessionIndex: &SessionIndex{ + Value: "index", + }, + } + + doc := etree.NewDocument() + doc.SetRoot(expected.Element()) + x, err := doc.WriteToBytes() + assert.Check(t, err) + assert.Check(t, is.Equal(`uri:issuername-idindex`, + string(x))) + + var actual LogoutRequest + err = xml.Unmarshal(x, &actual) + assert.Check(t, err) + assert.Check(t, is.DeepEqual(expected, actual)) + + x, err = xml.Marshal(expected) + assert.Check(t, err) + assert.Check(t, is.Equal(`uri:issuername-idindex`, + string(x))) +} + +func TestLogoutRequestMarshalWithoutNotOnOrAfter(t *testing.T) { + issueInstant := time.Date(2021, 10, 8, 12, 30, 0, 0, time.UTC) + expected := LogoutRequest{ + ID: "request-id", + Version: "2.0", + IssueInstant: issueInstant, + Issuer: &Issuer{ + XMLName: xml.Name{ + Space: "urn:oasis:names:tc:SAML:2.0:assertion", + Local: "Issuer", + }, + Value: "uri:issuer", + }, + NameID: &NameID{ + Value: "name-id", + }, + SessionIndex: &SessionIndex{ + Value: "index", + }, + } + + doc := etree.NewDocument() + doc.SetRoot(expected.Element()) + x, err := doc.WriteToBytes() + assert.Check(t, err) + assert.Check(t, is.Equal(`uri:issuername-idindex`, + string(x))) + + var actual LogoutRequest + err = xml.Unmarshal(x, &actual) + assert.Check(t, err) + assert.Check(t, is.DeepEqual(expected, actual)) +} diff --git a/service_provider.go b/service_provider.go index 3c7b94f4..927ad6e4 100644 --- a/service_provider.go +++ b/service_provider.go @@ -3,6 +3,7 @@ package saml import ( "bytes" "compress/flate" + "context" "crypto/rsa" "crypto/tls" "crypto/x509" @@ -17,12 +18,12 @@ import ( "regexp" "time" - xrv "github.com/mattermost/xml-roundtrip-validator" - "github.com/beevik/etree" + xrv "github.com/mattermost/xml-roundtrip-validator" dsig "github.com/russellhaering/goxmldsig" "github.com/russellhaering/goxmldsig/etreeutils" + "github.com/crewjam/saml/logger" "github.com/crewjam/saml/xmlenc" ) @@ -72,6 +73,9 @@ type ServiceProvider struct { Certificate *x509.Certificate Intermediates []*x509.Certificate + // HTTPClient to use during SAML artifact resolution + HTTPClient *http.Client + // MetadataURL is the full URL to the metadata endpoint on this host, // i.e. https://example.com/saml/metadata MetadataURL url.URL @@ -99,15 +103,26 @@ type ServiceProvider struct { // has a SSO session at the IdP. ForceAuthn *bool + // RequestedAuthnContext allow you to specify the requested authentication + // context in authentication requests + RequestedAuthnContext *RequestedAuthnContext + // AllowIdpInitiated AllowIDPInitiated bool + // DefaultRedirectURI where untracked requests (as of IDPInitiated) are redirected to + DefaultRedirectURI string + // SignatureVerifier, if non-nil, allows you to implement an alternative way // to verify signatures. SignatureVerifier SignatureVerifier // SignatureMethod, if non-empty, authentication requests will be signed SignatureMethod string + + // LogoutBindings specify the bindings available for SLO endpoint. If empty, + // HTTP-POST binding is used. + LogoutBindings []string } // MaxIssueDelay is the longest allowed time between when a SAML assertion is @@ -147,7 +162,11 @@ func (sp *ServiceProvider) Metadata() *EntityDescriptor { { Use: "encryption", KeyInfo: KeyInfo{ - Certificate: base64.StdEncoding.EncodeToString(certBytes), + X509Data: X509Data{ + X509Certificates: []X509Certificate{ + {Data: base64.StdEncoding.EncodeToString(certBytes)}, + }, + }, }, EncryptionMethods: []EncryptionMethod{ {Algorithm: "http://www.w3.org/2001/04/xmlenc#aes128-cbc"}, @@ -161,12 +180,25 @@ func (sp *ServiceProvider) Metadata() *EntityDescriptor { keyDescriptors = append(keyDescriptors, KeyDescriptor{ Use: "signing", KeyInfo: KeyInfo{ - Certificate: base64.StdEncoding.EncodeToString(certBytes), + X509Data: X509Data{ + X509Certificates: []X509Certificate{ + {Data: base64.StdEncoding.EncodeToString(certBytes)}, + }, + }, }, }) } } + sloEndpoints := make([]Endpoint, len(sp.LogoutBindings)) + for i, binding := range sp.LogoutBindings { + sloEndpoints[i] = Endpoint{ + Binding: binding, + Location: sp.SloURL.String(), + ResponseLocation: sp.SloURL.String(), + } + } + return &EntityDescriptor{ EntityID: firstSet(sp.EntityID, sp.MetadataURL.String()), ValidUntil: validUntil, @@ -179,13 +211,8 @@ func (sp *ServiceProvider) Metadata() *EntityDescriptor { KeyDescriptors: keyDescriptors, ValidUntil: &validUntil, }, - SingleLogoutServices: []Endpoint{ - { - Binding: HTTPPostBinding, - Location: sp.SloURL.String(), - ResponseLocation: sp.SloURL.String(), - }, - }, + SingleLogoutServices: sloEndpoints, + NameIDFormats: []NameIDFormat{sp.AuthnNameIDFormat}, }, AuthnRequestsSigned: &authnRequestsSigned, WantAssertionsSigned: &wantAssertionsSigned, @@ -196,6 +223,11 @@ func (sp *ServiceProvider) Metadata() *EntityDescriptor { Location: sp.AcsURL.String(), Index: 1, }, + { + Binding: HTTPArtifactBinding, + Location: sp.AcsURL.String(), + Index: 2, + }, }, }, }, @@ -206,7 +238,7 @@ func (sp *ServiceProvider) Metadata() *EntityDescriptor { // the HTTP-Redirect binding. It returns a URL that we will redirect the user to // in order to start the auth process. func (sp *ServiceProvider) MakeRedirectAuthenticationRequest(relayState string) (*url.URL, error) { - req, err := sp.MakeAuthenticationRequest(sp.GetSSOBindingLocation(HTTPRedirectBinding), HTTPRedirectBinding) + req, err := sp.MakeAuthenticationRequest(sp.GetSSOBindingLocation(HTTPRedirectBinding), HTTPRedirectBinding, HTTPPostBinding) if err != nil { return nil, err } @@ -214,25 +246,29 @@ func (sp *ServiceProvider) MakeRedirectAuthenticationRequest(relayState string) } // Redirect returns a URL suitable for using the redirect binding with the request -func (req *AuthnRequest) Redirect(relayState string, sp *ServiceProvider) (*url.URL, error) { +func (r *AuthnRequest) Redirect(relayState string, sp *ServiceProvider) (*url.URL, error) { w := &bytes.Buffer{} w1 := base64.NewEncoder(base64.StdEncoding, w) w2, _ := flate.NewWriter(w1, 9) doc := etree.NewDocument() - doc.SetRoot(req.Element()) + doc.SetRoot(r.Element()) if _, err := doc.WriteTo(w2); err != nil { panic(err) } - w2.Close() - w1.Close() + if err := w2.Close(); err != nil { + panic(err) + } + if err := w1.Close(); err != nil { + panic(err) + } - rv, _ := url.Parse(req.Destination) + rv, _ := url.Parse(r.Destination) // We can't depend on Query().set() as order matters for signing query := rv.RawQuery if len(query) > 0 { - query += "&SAMLRequest=" + url.QueryEscape(string(w.Bytes())) + query += "&SAMLRequest=" + url.QueryEscape(w.String()) } else { - query += "SAMLRequest=" + url.QueryEscape(string(w.Bytes())) + query += "SAMLRequest=" + url.QueryEscape(w.String()) } if relayState != "" { @@ -271,6 +307,19 @@ func (sp *ServiceProvider) GetSSOBindingLocation(binding string) string { return "" } +// GetArtifactBindingLocation returns URL for the IDP's Artifact binding of the +// specified type +func (sp *ServiceProvider) GetArtifactBindingLocation(binding string) string { + for _, idpSSODescriptor := range sp.IDPMetadata.IDPSSODescriptors { + for _, artifactResolutionService := range idpSSODescriptor.ArtifactResolutionServices { + if artifactResolutionService.Binding == binding { + return artifactResolutionService.Location + } + } + } + return "" +} + // GetSLOBindingLocation returns URL for the IDP's Single Log Out Service binding // of the specified type (HTTPRedirectBinding or HTTPPostBinding) func (sp *ServiceProvider) GetSLOBindingLocation(binding string) string { @@ -293,10 +342,12 @@ func (sp *ServiceProvider) getIDPSigningCerts() ([]*x509.Certificate, error) { // either set to "signing" or is missing for _, idpSSODescriptor := range sp.IDPMetadata.IDPSSODescriptors { for _, keyDescriptor := range idpSSODescriptor.KeyDescriptors { - if keyDescriptor.KeyInfo.Certificate != "" { + if len(keyDescriptor.KeyInfo.X509Data.X509Certificates) != 0 { switch keyDescriptor.Use { case "", "signing": - certStrs = append(certStrs, keyDescriptor.KeyInfo.Certificate) + for _, certificate := range keyDescriptor.KeyInfo.X509Data.X509Certificates { + certStrs = append(certStrs, certificate.Data) + } } } } @@ -306,11 +357,11 @@ func (sp *ServiceProvider) getIDPSigningCerts() ([]*x509.Certificate, error) { return nil, errors.New("cannot find any signing certificate in the IDP SSO descriptor") } - var certs []*x509.Certificate + certs := make([]*x509.Certificate, len(certStrs)) // cleanup whitespace regex := regexp.MustCompile(`\s+`) - for _, certStr := range certStrs { + for i, certStr := range certStrs { certStr = regex.ReplaceAllString(certStr, "") certBytes, err := base64.StdEncoding.DecodeString(certStr) if err != nil { @@ -321,22 +372,44 @@ func (sp *ServiceProvider) getIDPSigningCerts() ([]*x509.Certificate, error) { if err != nil { return nil, err } - certs = append(certs, parsedCert) + certs[i] = parsedCert } return certs, nil } +// MakeArtifactResolveRequest produces a new ArtifactResolve object to send to the idp's Artifact resolver +func (sp *ServiceProvider) MakeArtifactResolveRequest(artifactID string) (*ArtifactResolve, error) { + req := ArtifactResolve{ + ID: fmt.Sprintf("id-%x", randomBytes(20)), + IssueInstant: TimeNow(), + Version: "2.0", + Issuer: &Issuer{ + Format: "urn:oasis:names:tc:SAML:2.0:nameid-format:entity", + Value: firstSet(sp.EntityID, sp.MetadataURL.String()), + }, + Artifact: artifactID, + } + + if len(sp.SignatureMethod) > 0 { + if err := sp.SignArtifactResolve(&req); err != nil { + return nil, err + } + } + + return &req, nil +} + // MakeAuthenticationRequest produces a new AuthnRequest object to send to the idpURL // that uses the specified binding (HTTPRedirectBinding or HTTPPostBinding) -func (sp *ServiceProvider) MakeAuthenticationRequest(idpURL string, binding string) (*AuthnRequest, error) { +func (sp *ServiceProvider) MakeAuthenticationRequest(idpURL string, binding string, resultBinding string) (*AuthnRequest, error) { allowCreate := true nameIDFormat := sp.nameIDFormat() req := AuthnRequest{ AssertionConsumerServiceURL: sp.AcsURL.String(), Destination: idpURL, - ProtocolBinding: HTTPPostBinding, // default binding for the response + ProtocolBinding: resultBinding, // default binding for the response ID: fmt.Sprintf("id-%x", randomBytes(20)), IssueInstant: TimeNow(), Version: "2.0", @@ -351,7 +424,8 @@ func (sp *ServiceProvider) MakeAuthenticationRequest(idpURL string, binding stri // urn:oasis:names:tc:SAML:2.0:nameid-format:transient Format: &nameIDFormat, }, - ForceAuthn: sp.ForceAuthn, + ForceAuthn: sp.ForceAuthn, + RequestedAuthnContext: sp.RequestedAuthnContext, } // We don't need to sign the XML document if the IDP uses HTTP-Redirect binding if len(sp.SignatureMethod) > 0 && binding == HTTPPostBinding { @@ -370,9 +444,9 @@ func GetSigningContext(sp *ServiceProvider) (*dsig.SigningContext, error) { Leaf: sp.Certificate, } // TODO: add intermediates for SP - //for _, cert := range sp.Intermediates { - // keyPair.Certificate = append(keyPair.Certificate, cert.Raw) - //} + // for _, cert := range sp.Intermediates { + // keyPair.Certificate = append(keyPair.Certificate, cert.Raw) + // } keyStore := dsig.TLSCertKeyStore(keyPair) if sp.SignatureMethod != dsig.RSASHA1SignatureMethod && @@ -390,6 +464,24 @@ func GetSigningContext(sp *ServiceProvider) (*dsig.SigningContext, error) { return signingContext, nil } +// SignArtifactResolve adds the `Signature` element to the `ArtifactResolve`. +func (sp *ServiceProvider) SignArtifactResolve(req *ArtifactResolve) error { + signingContext, err := GetSigningContext(sp) + if err != nil { + return err + } + assertionEl := req.Element() + + signedRequestEl, err := signingContext.SignEnveloped(assertionEl) + if err != nil { + return err + } + + sigEl := signedRequestEl.Child[len(signedRequestEl.Child)-1] + req.Signature = sigEl.(*etree.Element) + return nil +} + // SignAuthnRequest adds the `Signature` element to the `AuthnRequest`. func (sp *ServiceProvider) SignAuthnRequest(req *AuthnRequest) error { @@ -413,7 +505,7 @@ func (sp *ServiceProvider) SignAuthnRequest(req *AuthnRequest) error { // the HTTP-POST binding. It returns HTML text representing an HTML form that // can be sent presented to a browser to initiate the login process. func (sp *ServiceProvider) MakePostAuthenticationRequest(relayState string) ([]byte, error) { - req, err := sp.MakeAuthenticationRequest(sp.GetSSOBindingLocation(HTTPPostBinding), HTTPPostBinding) + req, err := sp.MakeAuthenticationRequest(sp.GetSSOBindingLocation(HTTPPostBinding), HTTPPostBinding, HTTPPostBinding) if err != nil { return nil, err } @@ -421,9 +513,9 @@ func (sp *ServiceProvider) MakePostAuthenticationRequest(relayState string) ([]b } // Post returns an HTML form suitable for using the HTTP-POST binding with the request -func (req *AuthnRequest) Post(relayState string) []byte { +func (r *AuthnRequest) Post(relayState string) []byte { doc := etree.NewDocument() - doc.SetRoot(req.Element()) + doc.SetRoot(r.Element()) reqBuf, err := doc.WriteToBytes() if err != nil { panic(err) @@ -443,7 +535,7 @@ func (req *AuthnRequest) Post(relayState string) []byte { SAMLRequest string RelayState string }{ - URL: req.Destination, + URL: r.Destination, SAMLRequest: encodedReqBuf, RelayState: relayState, } @@ -492,7 +584,7 @@ type InvalidResponseError struct { } func (ivr *InvalidResponseError) Error() string { - return fmt.Sprintf("Authentication failed") + return "Authentication failed" } // ErrBadStatus is returned when the assertion provided is valid but the @@ -505,46 +597,71 @@ func (e ErrBadStatus) Error() string { return e.Status } -func responseIsSigned(response *etree.Document) (bool, error) { - signatureElement, err := findChild(response.Root(), "http://www.w3.org/2000/09/xmldsig#", "Signature") - if err != nil { - return false, err +// ParseResponse extracts the SAML IDP response received in req, resolves +// artifacts when necessary, validates it, and returns the verified assertion. +func (sp *ServiceProvider) ParseResponse(req *http.Request, possibleRequestIDs []string) (*Assertion, error) { + if artifactID := req.Form.Get("SAMLart"); artifactID != "" { + return sp.handleArtifactRequest(req.Context(), artifactID, possibleRequestIDs) } - return signatureElement != nil, nil + return sp.parseResponseHTTP(req, possibleRequestIDs) } -// validateDestination validates the Destination attribute. -// If the response is signed, the Destination is required to be present. -func (sp *ServiceProvider) validateDestination(response []byte, responseDom *Response) error { - responseXML := etree.NewDocument() - err := responseXML.ReadFromBytes(response) +func (sp *ServiceProvider) handleArtifactRequest(ctx context.Context, artifactID string, possibleRequestIDs []string) (*Assertion, error) { + retErr := &InvalidResponseError{Now: TimeNow()} + + artifactResolveRequest, err := sp.MakeArtifactResolveRequest(artifactID) if err != nil { - return err + retErr.PrivateErr = fmt.Errorf("cannot generate artifact resolution request: %s", err) + return nil, retErr } - signed, err := responseIsSigned(responseXML) + requestBody, err := elementToBytes(artifactResolveRequest.SoapRequest()) if err != nil { - return err + retErr.PrivateErr = err + return nil, retErr } - // Compare if the response is signed OR the Destination is provided. - // (Even if the response is not signed, if the Destination is set it must match.) - if signed || responseDom.Destination != "" { - if responseDom.Destination != sp.AcsURL.String() { - return fmt.Errorf("`Destination` does not match AcsURL (expected %q, actual %q)", sp.AcsURL.String(), responseDom.Destination) - } + req, err := http.NewRequestWithContext(ctx, "POST", sp.GetArtifactBindingLocation(SOAPBinding), + bytes.NewReader(requestBody)) + if err != nil { + retErr.PrivateErr = err + return nil, retErr } + req.Header.Set("Content-Type", "text/xml") - return nil + httpClient := sp.HTTPClient + if httpClient == nil { + httpClient = http.DefaultClient + } + response, err := httpClient.Do(req) + if err != nil { + retErr.PrivateErr = fmt.Errorf("cannot resolve artifact: %s", err) + return nil, retErr + } + defer func() { + if err := response.Body.Close(); err != nil { + logger.DefaultLogger.Printf("Error while closing response body during artifact resolution: %v", err) + } + }() + if response.StatusCode != 200 { + retErr.PrivateErr = fmt.Errorf("Error during artifact resolution: HTTP status %d (%s)", response.StatusCode, response.Status) + return nil, retErr + } + responseBody, err := ioutil.ReadAll(response.Body) + if err != nil { + retErr.PrivateErr = fmt.Errorf("Error during artifact resolution: %s", err) + return nil, retErr + } + assertion, err := sp.ParseXMLArtifactResponse(responseBody, possibleRequestIDs, artifactResolveRequest.ID) + if err != nil { + return nil, err + } + return assertion, nil } -// ParseResponse extracts the SAML IDP response received in req, validates -// it, and returns the verified assertion. -func (sp *ServiceProvider) ParseResponse(req *http.Request, possibleRequestIDs []string) (*Assertion, error) { - now := TimeNow() +func (sp *ServiceProvider) parseResponseHTTP(req *http.Request, possibleRequestIDs []string) (*Assertion, error) { retErr := &InvalidResponseError{ - Now: now, - Response: req.PostForm.Get("SAMLResponse"), + Now: TimeNow(), } rawResponseBuf, err := base64.StdEncoding.DecodeString(req.PostForm.Get("SAMLResponse")) @@ -552,181 +669,355 @@ func (sp *ServiceProvider) ParseResponse(req *http.Request, possibleRequestIDs [ retErr.PrivateErr = fmt.Errorf("cannot parse base64: %s", err) return nil, retErr } - retErr.Response = string(rawResponseBuf) + assertion, err := sp.ParseXMLResponse(rawResponseBuf, possibleRequestIDs) if err != nil { return nil, err } - return assertion, nil - } -// ParseXMLResponse validates the SAML IDP response and -// returns the verified assertion. +// ParseXMLArtifactResponse validates the SAML Artifact resolver response +// and returns the verified assertion. // -// This function handles decrypting the message, verifying the digital -// signature on the assertion, and verifying that the specified conditions -// and properties are met. +// This function handles verifying the digital signature, and verifying +// that the specified conditions and properties are met. // // If the function fails it will return an InvalidResponseError whose // properties are useful in describing which part of the parsing process // failed. However, to discourage inadvertent disclosure the diagnostic // information, the Error() method returns a static string. -func (sp *ServiceProvider) ParseXMLResponse(decodedResponseXML []byte, possibleRequestIDs []string) (*Assertion, error) { +func (sp *ServiceProvider) ParseXMLArtifactResponse(soapResponseXML []byte, possibleRequestIDs []string, artifactRequestID string) (*Assertion, error) { now := TimeNow() - var err error retErr := &InvalidResponseError{ + Response: string(soapResponseXML), Now: now, - Response: string(decodedResponseXML), } - // ensure that the response XML is well formed before we parse it - if err := xrv.Validate(bytes.NewReader(decodedResponseXML)); err != nil { + // ensure that the response XML is well-formed before we parse it + if err := xrv.Validate(bytes.NewReader(soapResponseXML)); err != nil { retErr.PrivateErr = fmt.Errorf("invalid xml: %s", err) return nil, retErr } - // do some validation first before we decrypt - resp := Response{} - if err := xml.Unmarshal(decodedResponseXML, &resp); err != nil { + doc := etree.NewDocument() + if err := doc.ReadFromBytes(soapResponseXML); err != nil { retErr.PrivateErr = fmt.Errorf("cannot unmarshal response: %s", err) return nil, retErr } + if doc.Root() == nil { + retErr.PrivateErr = errors.New("invalid xml: no root") + return nil, retErr + } + if doc.Root().NamespaceURI() != "http://schemas.xmlsoap.org/soap/envelope/" || + doc.Root().Tag != "Envelope" { + retErr.PrivateErr = fmt.Errorf("expected a SOAP Envelope") + return nil, retErr + } - if err := sp.validateDestination(decodedResponseXML, &resp); err != nil { + soapBodyEl, err := findOneChild(doc.Root(), "http://schemas.xmlsoap.org/soap/envelope/", "Body") + if err != nil { retErr.PrivateErr = err return nil, retErr } - requestIDvalid := false + artifactResponseEl, err := findOneChild(soapBodyEl, "urn:oasis:names:tc:SAML:2.0:protocol", "ArtifactResponse") + if err != nil { + retErr.PrivateErr = err + return nil, retErr + } - if sp.AllowIDPInitiated { - requestIDvalid = true - } else { - for _, possibleRequestID := range possibleRequestIDs { - if resp.InResponseTo == possibleRequestID { - requestIDvalid = true - } + return sp.parseArtifactResponse(artifactResponseEl, possibleRequestIDs, artifactRequestID, now) +} + +func (sp *ServiceProvider) parseArtifactResponse(artifactResponseEl *etree.Element, possibleRequestIDs []string, artifactRequestID string, now time.Time) (*Assertion, error) { + retErr := &InvalidResponseError{ + Now: now, + Response: elementToString(artifactResponseEl), + } + + { + var artifactResponse ArtifactResponse + if err := unmarshalElement(artifactResponseEl, &artifactResponse); err != nil { + retErr.PrivateErr = err + return nil, retErr + } + if artifactResponse.InResponseTo != artifactRequestID { + retErr.PrivateErr = fmt.Errorf("`InResponseTo` does not match the artifact request ID (expected %s)", artifactRequestID) + return nil, retErr + } + if artifactResponse.IssueInstant.Add(MaxIssueDelay).Before(now) { + retErr.PrivateErr = fmt.Errorf("response IssueInstant expired at %s", artifactResponse.IssueInstant.Add(MaxIssueDelay)) + return nil, retErr + } + if artifactResponse.Issuer != nil && artifactResponse.Issuer.Value != sp.IDPMetadata.EntityID { + retErr.PrivateErr = fmt.Errorf("response Issuer does not match the IDP metadata (expected %q)", sp.IDPMetadata.EntityID) + return nil, retErr + } + if artifactResponse.Status.StatusCode.Value != StatusSuccess { + retErr.PrivateErr = ErrBadStatus{Status: artifactResponse.Status.StatusCode.Value} + return nil, retErr } } - if !requestIDvalid { - retErr.PrivateErr = fmt.Errorf("`InResponseTo` does not match any of the possible request IDs (expected %v)", possibleRequestIDs) + var signatureRequirement signatureRequirement + sigErr := sp.validateSignature(artifactResponseEl) + switch sigErr { + case nil: + signatureRequirement = signatureNotRequired + case errSignatureElementNotPresent: + signatureRequirement = signatureRequired + default: + retErr.PrivateErr = sigErr return nil, retErr } - if resp.IssueInstant.Add(MaxIssueDelay).Before(now) { - retErr.PrivateErr = fmt.Errorf("response IssueInstant expired at %s", resp.IssueInstant.Add(MaxIssueDelay)) + responseEl, err := findOneChild(artifactResponseEl, "urn:oasis:names:tc:SAML:2.0:protocol", "Response") + if err != nil { + retErr.PrivateErr = err return nil, retErr } - if resp.Issuer != nil && resp.Issuer.Value != sp.IDPMetadata.EntityID { - retErr.PrivateErr = fmt.Errorf("response Issuer does not match the IDP metadata (expected %q)", sp.IDPMetadata.EntityID) + + assertion, err := sp.parseResponse(responseEl, possibleRequestIDs, now, signatureRequirement) + if err != nil { + retErr.PrivateErr = err return nil, retErr } - if resp.Status.StatusCode.Value != StatusSuccess { - retErr.PrivateErr = ErrBadStatus{Status: resp.Status.StatusCode.Value} + + return assertion, nil +} + +// ParseXMLResponse parses and validates the SAML IDP response and +// returns the verified assertion. +// +// This function handles decrypting the message, verifying the digital +// signature on the assertion, and verifying that the specified conditions +// and properties are met. +// +// If the function fails it will return an InvalidResponseError whose +// properties are useful in describing which part of the parsing process +// failed. However, to discourage inadvertent disclosure the diagnostic +// information, the Error() method returns a static string. +func (sp *ServiceProvider) ParseXMLResponse(decodedResponseXML []byte, possibleRequestIDs []string) (*Assertion, error) { + now := TimeNow() + var err error + retErr := &InvalidResponseError{ + Now: now, + Response: string(decodedResponseXML), + } + + // ensure that the response XML is well-formed before we parse it + if err := xrv.Validate(bytes.NewReader(decodedResponseXML)); err != nil { + retErr.PrivateErr = fmt.Errorf("invalid xml: %s", err) + return nil, retErr + } + + doc := etree.NewDocument() + if err := doc.ReadFromBytes(decodedResponseXML); err != nil { + retErr.PrivateErr = err + return nil, retErr + } + if doc.Root() == nil { + retErr.PrivateErr = errors.New("invalid xml: no root") return nil, retErr } - var assertion *Assertion - if resp.EncryptedAssertion == nil { + assertion, err := sp.parseResponse(doc.Root(), possibleRequestIDs, now, signatureRequired) + if err != nil { + retErr.PrivateErr = err + return nil, retErr + } - doc := etree.NewDocument() - if err := doc.ReadFromBytes(decodedResponseXML); err != nil { - retErr.PrivateErr = err - return nil, retErr + return assertion, nil +} + +type signatureRequirement int + +const ( + signatureRequired signatureRequirement = iota + signatureNotRequired +) + +// validateXMLResponse validates the SAML IDP response and returns +// the verified assertion. +// +// This function handles decrypting the message, verifying the digital +// signature on the assertion, and verifying that the specified conditions +// and properties are met. +func (sp *ServiceProvider) parseResponse(responseEl *etree.Element, possibleRequestIDs []string, now time.Time, signatureRequirement signatureRequirement) (*Assertion, error) { + var responseSignatureErr error + var responseHasSignature bool + if signatureRequirement == signatureRequired { + responseSignatureErr = sp.validateSignature(responseEl) + if responseSignatureErr != errSignatureElementNotPresent { + responseHasSignature = true } - // TODO(ross): verify that the namespace is urn:oasis:names:tc:SAML:2.0:protocol - responseEl := doc.Root() - if responseEl.Tag != "Response" { - retErr.PrivateErr = fmt.Errorf("expected to find a response object, not %s", doc.Root().Tag) - return nil, retErr + // Note: we're deferring taking action on the signature validation until after we've + // processed the request attributes, because certain test cases seem to require this mis-feature. + // TODO(ross): adjust the test cases so that we can abort here if the Response signature is invalid. + } + + // validate request attributes + { + var response Response + if err := unmarshalElement(responseEl, &response); err != nil { + return nil, fmt.Errorf("cannot unmarshal response: %v", err) } - if err = sp.validateSigned(responseEl); err != nil { - retErr.PrivateErr = err - return nil, retErr + // If the response is *not* signed, the Destination may be omitted. + if responseHasSignature || response.Destination != "" { + if response.Destination != sp.AcsURL.String() { + return nil, fmt.Errorf("`Destination` does not match AcsURL (expected %q, actual %q)", sp.AcsURL.String(), response.Destination) + } } - assertion = resp.Assertion + requestIDvalid := false + if sp.AllowIDPInitiated { + requestIDvalid = true + } else { + for _, possibleRequestID := range possibleRequestIDs { + if response.InResponseTo == possibleRequestID { + requestIDvalid = true + } + } + } + if !requestIDvalid { + return nil, fmt.Errorf("`InResponseTo` does not match any of the possible request IDs (expected %v)", possibleRequestIDs) + } + + if response.IssueInstant.Add(MaxIssueDelay).Before(now) { + return nil, fmt.Errorf("response IssueInstant expired at %s", response.IssueInstant.Add(MaxIssueDelay)) + } + if response.Issuer != nil && response.Issuer.Value != sp.IDPMetadata.EntityID { + return nil, fmt.Errorf("response Issuer does not match the IDP metadata (expected %q)", sp.IDPMetadata.EntityID) + } + if response.Status.StatusCode.Value != StatusSuccess { + return nil, ErrBadStatus{Status: response.Status.StatusCode.Value} + } } - // decrypt the response - if resp.EncryptedAssertion != nil { - doc := etree.NewDocument() - if err := doc.ReadFromBytes(decodedResponseXML); err != nil { - retErr.PrivateErr = err - return nil, retErr + if signatureRequirement == signatureRequired { + switch responseSignatureErr { + case nil: + // since the request has a signature, none of the Assertions need one + signatureRequirement = signatureNotRequired + case errSignatureElementNotPresent: + // the request has no signature, so assertions must be signed + signatureRequirement = signatureRequired // nop + default: + return nil, responseSignatureErr } + } - // encrypted assertions are part of the signature - // before decrypting the response verify that - responseSigned, err := responseIsSigned(doc) + var errs []error + var assertions []Assertion + + // look for encrypted assertions + { + encryptedAssertionEls, err := findChildren(responseEl, "urn:oasis:names:tc:SAML:2.0:assertion", "EncryptedAssertion") if err != nil { - retErr.PrivateErr = err - return nil, retErr + return nil, err } - if responseSigned { - if err := sp.validateSigned(doc.Root()); err != nil { - retErr.PrivateErr = err - return nil, retErr + for _, encryptedAssertionEl := range encryptedAssertionEls { + assertion, err := sp.parseEncryptedAssertion(encryptedAssertionEl, possibleRequestIDs, now, signatureRequirement) + if err != nil { + errs = append(errs, err) + continue } + assertions = append(assertions, *assertion) } + } - var key interface{} = sp.Key - keyEl := doc.FindElement("//EncryptedAssertion/EncryptedKey") - if keyEl != nil { - key, err = xmlenc.Decrypt(sp.Key, keyEl) + // look for plaintext assertions + { + assertionEls, err := findChildren(responseEl, "urn:oasis:names:tc:SAML:2.0:assertion", "Assertion") + if err != nil { + return nil, err + } + for _, assertionEl := range assertionEls { + assertion, err := sp.parseAssertion(assertionEl, possibleRequestIDs, now, signatureRequirement) if err != nil { - retErr.PrivateErr = fmt.Errorf("failed to decrypt key from response: %s", err) - return nil, retErr + errs = append(errs, err) + continue } + assertions = append(assertions, *assertion) } + } - el := doc.FindElement("//EncryptedAssertion/EncryptedData") - plaintextAssertion, err := xmlenc.Decrypt(key, el) - if err != nil { - retErr.PrivateErr = fmt.Errorf("failed to decrypt response: %s", err) - return nil, retErr + if len(assertions) == 0 { + if len(errs) > 0 { + return nil, errs[0] } - retErr.Response = string(plaintextAssertion) + return nil, fmt.Errorf("expected at least one valid Assertion, none found") + } - // TODO(ross): add test case for this - if err := xrv.Validate(bytes.NewReader(plaintextAssertion)); err != nil { - retErr.PrivateErr = fmt.Errorf("plaintext response contains invalid XML: %s", err) - return nil, retErr - } + // if we have at least one assertion, return the first one. It is almost universally true that valid responses + // contain only one assertion. This is less that fully correct, but we didn't realize that there could be more + // than one assertion at the time of establishing the public interface of ParseXMLResponse(), so for compatibility + // we return the first one. + return &assertions[0], nil +} - doc = etree.NewDocument() - if err := doc.ReadFromBytes(plaintextAssertion); err != nil { - retErr.PrivateErr = fmt.Errorf("cannot parse plaintext response %v", err) - return nil, retErr - } +func (sp *ServiceProvider) parseEncryptedAssertion(encryptedAssertionEl *etree.Element, possibleRequestIDs []string, now time.Time, signatureRequirement signatureRequirement) (*Assertion, error) { + assertionEl, err := sp.decryptElement(encryptedAssertionEl) + if err != nil { + return nil, fmt.Errorf("failed to decrypt EncryptedAssertion: %v", err) + } + return sp.parseAssertion(assertionEl, possibleRequestIDs, now, signatureRequirement) +} - // the decrypted assertion may be signed too - // otherwise, a signed response is sufficient - if err := sp.validateSigned(doc.Root()); err != nil && !responseSigned { - retErr.PrivateErr = err - return nil, retErr +func (sp *ServiceProvider) decryptElement(encryptedEl *etree.Element) (*etree.Element, error) { + encryptedDataEl, err := findOneChild(encryptedEl, "http://www.w3.org/2001/04/xmlenc#", "EncryptedData") + if err != nil { + return nil, err + } + + var key interface{} = sp.Key + keyEl := encryptedEl.FindElement("./EncryptedKey") + if keyEl != nil { + var err error + key, err = xmlenc.Decrypt(sp.Key, keyEl) + if err != nil { + return nil, fmt.Errorf("failed to decrypt key from response: %s", err) } + } - assertion = &Assertion{} - // Note: plaintextAssertion is known to be safe to parse because - // plaintextAssertion is unmodified from when xrv.Validate() was called above. - if err := xml.Unmarshal(plaintextAssertion, assertion); err != nil { - retErr.PrivateErr = err - return nil, retErr + plaintextEl, err := xmlenc.Decrypt(key, encryptedDataEl) + if err != nil { + return nil, err + } + + if err := xrv.Validate(bytes.NewReader(plaintextEl)); err != nil { + return nil, fmt.Errorf("plaintext response contains invalid XML: %s", err) + } + + doc := etree.NewDocument() + if err := doc.ReadFromBytes(plaintextEl); err != nil { + return nil, fmt.Errorf("cannot parse plaintext response %v", err) + } + return doc.Root(), nil +} + +func (sp *ServiceProvider) parseAssertion(assertionEl *etree.Element, possibleRequestIDs []string, now time.Time, signatureRequirement signatureRequirement) (*Assertion, error) { + if signatureRequirement == signatureRequired { + sigErr := sp.validateSignature(assertionEl) + if sigErr != nil { + return nil, sigErr } } - if err := sp.validateAssertion(assertion, possibleRequestIDs, now); err != nil { - retErr.PrivateErr = fmt.Errorf("assertion invalid: %s", err) - return nil, retErr + // parse the assertion we just validated + var assertion Assertion + if err := unmarshalElement(assertionEl, &assertion); err != nil { + return nil, err } - return assertion, nil + if err := sp.validateAssertion(&assertion, possibleRequestIDs, now); err != nil { + return nil, err + } + + return &assertion, nil } // validateAssertion checks that the conditions specified in assertion match @@ -798,81 +1089,21 @@ func (sp *ServiceProvider) validateAssertion(assertion *Assertion, possibleReque return nil } -func findChild(parentEl *etree.Element, childNS string, childTag string) (*etree.Element, error) { - for _, childEl := range parentEl.ChildElements() { - if childEl.Tag != childTag { - continue - } - - ctx, err := etreeutils.NSBuildParentContext(childEl) - if err != nil { - return nil, err - } - ctx, err = ctx.SubContext(childEl) - if err != nil { - return nil, err - } - - ns, err := ctx.LookupPrefix(childEl.Space) - if err != nil { - return nil, fmt.Errorf("[%s]:%s cannot find prefix %s: %v", childNS, childTag, childEl.Space, err) - } - if ns != childNS { - continue - } - - return childEl, nil - } - return nil, nil -} - -// validateSigned returns a nil error iff each of the signatures on the Response and Assertion elements -// are valid and there is at least one signature. -func (sp *ServiceProvider) validateSigned(responseEl *etree.Element) error { - haveSignature := false - - // Some SAML responses have the signature on the Response object, and some on the Assertion - // object, and some on both. We will require that at least one signature be present and that - // all signatures be valid - sigEl, err := findChild(responseEl, "http://www.w3.org/2000/09/xmldsig#", "Signature") - if err != nil { - return err - } - if sigEl != nil { - if err = sp.validateSignature(responseEl); err != nil { - return fmt.Errorf("cannot validate signature on Response: %v", err) - } - haveSignature = true - } +var errSignatureElementNotPresent = errors.New("signature element not present") - assertionEl, err := findChild(responseEl, "urn:oasis:names:tc:SAML:2.0:assertion", "Assertion") +// validateSignature returns nil iff the Signature embedded in the element is valid +func (sp *ServiceProvider) validateSignature(el *etree.Element) error { + sigEl, err := findChild(el, "http://www.w3.org/2000/09/xmldsig#", "Signature") if err != nil { return err } - if assertionEl != nil { - sigEl, err := findChild(assertionEl, "http://www.w3.org/2000/09/xmldsig#", "Signature") - if err != nil { - return err - } - if sigEl != nil { - if err = sp.validateSignature(assertionEl); err != nil { - return fmt.Errorf("cannot validate signature on Response: %v", err) - } - haveSignature = true - } - } - - if !haveSignature { - return errors.New("either the Response or Assertion must be signed") + if sigEl == nil { + return errSignatureElementNotPresent } - return nil -} -// validateSignature returns nill iff the Signature embedded in the element is valid -func (sp *ServiceProvider) validateSignature(el *etree.Element) error { certs, err := sp.getIDPSigningCerts() if err != nil { - return err + return fmt.Errorf("cannot validate signature on %s: %v", el.Tag, err) } certificateStore := dsig.MemoryX509CertificateStore{ @@ -904,23 +1135,26 @@ func (sp *ServiceProvider) validateSignature(el *etree.Element) error { ctx, err := etreeutils.NSBuildParentContext(el) if err != nil { - return err + return fmt.Errorf("cannot validate signature on %s: %v", el.Tag, err) } ctx, err = ctx.SubContext(el) if err != nil { - return err + return fmt.Errorf("cannot validate signature on %s: %v", el.Tag, err) } el, err = etreeutils.NSDetatch(ctx, el) if err != nil { - return err + return fmt.Errorf("cannot validate signature on %s: %v", el.Tag, err) } if sp.SignatureVerifier != nil { return sp.SignatureVerifier.VerifySignature(validationContext, el) } - _, err = validationContext.Validate(el) - return err + if _, err := validationContext.Validate(el); err != nil { + return fmt.Errorf("cannot validate signature on %s: %v", el.Tag, err) + } + + return nil } // SignLogoutRequest adds the `Signature` element to the `LogoutRequest`. @@ -931,9 +1165,9 @@ func (sp *ServiceProvider) SignLogoutRequest(req *LogoutRequest) error { Leaf: sp.Certificate, } // TODO: add intermediates for SP - //for _, cert := range sp.Intermediates { - // keyPair.Certificate = append(keyPair.Certificate, cert.Raw) - //} + // for _, cert := range sp.Intermediates { + // keyPair.Certificate = append(keyPair.Certificate, cert.Raw) + // } keyStore := dsig.TLSCertKeyStore(keyPair) if sp.SignatureMethod != dsig.RSASHA1SignatureMethod && @@ -999,22 +1233,26 @@ func (sp *ServiceProvider) MakeRedirectLogoutRequest(nameID, relayState string) } // Redirect returns a URL suitable for using the redirect binding with the request -func (req *LogoutRequest) Redirect(relayState string) *url.URL { +func (r *LogoutRequest) Redirect(relayState string) *url.URL { w := &bytes.Buffer{} w1 := base64.NewEncoder(base64.StdEncoding, w) w2, _ := flate.NewWriter(w1, 9) doc := etree.NewDocument() - doc.SetRoot(req.Element()) + doc.SetRoot(r.Element()) if _, err := doc.WriteTo(w2); err != nil { panic(err) } - w2.Close() - w1.Close() + if err := w2.Close(); err != nil { + panic(err) + } + if err := w1.Close(); err != nil { + panic(err) + } - rv, _ := url.Parse(req.Destination) + rv, _ := url.Parse(r.Destination) query := rv.Query() - query.Set("SAMLRequest", string(w.Bytes())) + query.Set("SAMLRequest", w.String()) if relayState != "" { query.Set("RelayState", relayState) } @@ -1035,9 +1273,9 @@ func (sp *ServiceProvider) MakePostLogoutRequest(nameID, relayState string) ([]b } // Post returns an HTML form suitable for using the HTTP-POST binding with the request -func (req *LogoutRequest) Post(relayState string) []byte { +func (r *LogoutRequest) Post(relayState string) []byte { doc := etree.NewDocument() - doc.SetRoot(req.Element()) + doc.SetRoot(r.Element()) reqBuf, err := doc.WriteToBytes() if err != nil { panic(err) @@ -1057,7 +1295,7 @@ func (req *LogoutRequest) Post(relayState string) []byte { SAMLRequest string RelayState string }{ - URL: req.Destination, + URL: r.Destination, SAMLRequest: encodedReqBuf, RelayState: relayState, } @@ -1109,22 +1347,26 @@ func (sp *ServiceProvider) MakeRedirectLogoutResponse(logoutRequestID, relayStat } // Redirect returns a URL suitable for using the redirect binding with the LogoutResponse. -func (resp *LogoutResponse) Redirect(relayState string) *url.URL { +func (r *LogoutResponse) Redirect(relayState string) *url.URL { w := &bytes.Buffer{} w1 := base64.NewEncoder(base64.StdEncoding, w) w2, _ := flate.NewWriter(w1, 9) doc := etree.NewDocument() - doc.SetRoot(resp.Element()) + doc.SetRoot(r.Element()) if _, err := doc.WriteTo(w2); err != nil { panic(err) } - w2.Close() - w1.Close() + if err := w2.Close(); err != nil { + panic(err) + } + if err := w1.Close(); err != nil { + panic(err) + } - rv, _ := url.Parse(resp.Destination) + rv, _ := url.Parse(r.Destination) query := rv.Query() - query.Set("SAMLResponse", string(w.Bytes())) + query.Set("SAMLResponse", w.String()) if relayState != "" { query.Set("RelayState", relayState) } @@ -1145,9 +1387,9 @@ func (sp *ServiceProvider) MakePostLogoutResponse(logoutRequestID, relayState st } // Post returns an HTML form suitable for using the HTTP-POST binding with the LogoutResponse. -func (resp *LogoutResponse) Post(relayState string) []byte { +func (r *LogoutResponse) Post(relayState string) []byte { doc := etree.NewDocument() - doc.SetRoot(resp.Element()) + doc.SetRoot(r.Element()) reqBuf, err := doc.WriteToBytes() if err != nil { panic(err) @@ -1167,7 +1409,7 @@ func (resp *LogoutResponse) Post(relayState string) []byte { SAMLResponse string RelayState string }{ - URL: resp.Destination, + URL: r.Destination, SAMLResponse: encodedReqBuf, RelayState: relayState, } @@ -1188,9 +1430,9 @@ func (sp *ServiceProvider) SignLogoutResponse(resp *LogoutResponse) error { Leaf: sp.Certificate, } // TODO: add intermediates for SP - //for _, cert := range sp.Intermediates { - // keyPair.Certificate = append(keyPair.Certificate, cert.Raw) - //} + // for _, cert := range sp.Intermediates { + // keyPair.Certificate = append(keyPair.Certificate, cert.Raw) + // } keyStore := dsig.TLSCertKeyStore(keyPair) if sp.SignatureMethod != dsig.RSASHA1SignatureMethod && @@ -1247,36 +1489,39 @@ func (sp *ServiceProvider) ValidateLogoutResponseRequest(req *http.Request) erro // ValidateLogoutResponseForm returns a nil error if the logout response is valid. func (sp *ServiceProvider) ValidateLogoutResponseForm(postFormData string) error { + retErr := &InvalidResponseError{ + Now: TimeNow(), + } + rawResponseBuf, err := base64.StdEncoding.DecodeString(postFormData) if err != nil { - return fmt.Errorf("unable to parse base64: %s", err) + retErr.PrivateErr = fmt.Errorf("unable to parse base64: %s", err) + return retErr } + retErr.Response = string(rawResponseBuf) // TODO(ross): add test case for this (SLO does not have tests right now) if err := xrv.Validate(bytes.NewReader(rawResponseBuf)); err != nil { return fmt.Errorf("response contains invalid XML: %s", err) } - var resp LogoutResponse - if err := xml.Unmarshal(rawResponseBuf, &resp); err != nil { - return fmt.Errorf("cannot unmarshal response: %s", err) - } - - if err := sp.validateLogoutResponse(&resp); err != nil { - return err - } - doc := etree.NewDocument() if err := doc.ReadFromBytes(rawResponseBuf); err != nil { - return err + retErr.PrivateErr = err + return retErr } - responseEl := doc.Root() - if err = sp.validateSigned(responseEl); err != nil { - return err + if err := sp.validateSignature(doc.Root()); err != nil { + retErr.PrivateErr = err + return retErr } - return nil + var resp LogoutResponse + if err := unmarshalElement(doc.Root(), &resp); err != nil { + retErr.PrivateErr = err + return retErr + } + return sp.validateLogoutResponse(&resp) } // ValidateLogoutResponseRedirect returns a nil error if the logout response is valid. @@ -1284,44 +1529,44 @@ func (sp *ServiceProvider) ValidateLogoutResponseForm(postFormData string) error // URL Binding appears to be gzip / flate encoded // See https://www.oasis-open.org/committees/download.php/20645/sstc-saml-tech-overview-2%200-draft-10.pdf 6.6 func (sp *ServiceProvider) ValidateLogoutResponseRedirect(queryParameterData string) error { - rawResponseBuf, err := base64.StdEncoding.DecodeString(queryParameterData) - if err != nil { - return fmt.Errorf("unable to parse base64: %s", err) + retErr := &InvalidResponseError{ + Now: TimeNow(), } - gr, err := ioutil.ReadAll(flate.NewReader(bytes.NewBuffer(rawResponseBuf))) + rawResponseBuf, err := base64.StdEncoding.DecodeString(queryParameterData) if err != nil { - return err - } - - if err := xrv.Validate(bytes.NewReader(gr)); err != nil { - return err + retErr.PrivateErr = fmt.Errorf("unable to parse base64: %s", err) + return retErr } + retErr.Response = string(rawResponseBuf) - decoder := xml.NewDecoder(bytes.NewReader(gr)) - - var resp LogoutResponse - - err = decoder.Decode(&resp) + gr, err := ioutil.ReadAll(newSaferFlateReader(bytes.NewBuffer(rawResponseBuf))) if err != nil { - return fmt.Errorf("unable to flate decode: %s", err) + retErr.PrivateErr = err + return retErr } - if err := sp.validateLogoutResponse(&resp); err != nil { + if err := xrv.Validate(bytes.NewReader(gr)); err != nil { return err } doc := etree.NewDocument() - if _, err := doc.ReadFrom(bytes.NewReader(gr)); err != nil { - return err + if err := doc.ReadFromBytes(rawResponseBuf); err != nil { + retErr.PrivateErr = err + return retErr } - responseEl := doc.Root() - if err = sp.validateSigned(responseEl); err != nil { - return err + if err := sp.validateSignature(doc.Root()); err != nil { + retErr.PrivateErr = err + return retErr } - return nil + var resp LogoutResponse + if err := unmarshalElement(doc.Root(), &resp); err != nil { + retErr.PrivateErr = err + return retErr + } + return sp.validateLogoutResponse(&resp) } // validateLogoutResponse validates the LogoutResponse fields. Returns a nil error if the LogoutResponse is valid. @@ -1350,3 +1595,102 @@ func firstSet(a, b string) string { } return a } + +// findChildren returns all the elements matching childNS/childTag that are direct children of parentEl. +func findChildren(parentEl *etree.Element, childNS string, childTag string) ([]*etree.Element, error) { + //nolint:prealloc // We don't know how many child elements we'll actually put into this array. + var rv []*etree.Element + for _, childEl := range parentEl.ChildElements() { + if childEl.Tag != childTag { + continue + } + + ctx, err := etreeutils.NSBuildParentContext(childEl) + if err != nil { + return nil, err + } + ctx, err = ctx.SubContext(childEl) + if err != nil { + return nil, err + } + + ns, err := ctx.LookupPrefix(childEl.Space) + if err != nil { + return nil, fmt.Errorf("[%s]:%s cannot find prefix %s: %v", childNS, childTag, childEl.Space, err) + } + if ns != childNS { + continue + } + + rv = append(rv, childEl) + } + + return rv, nil +} + +// findOneChild finds the specified child element. Returns an error if the element doesn't exist. +func findOneChild(parentEl *etree.Element, childNS string, childTag string) (*etree.Element, error) { + children, err := findChildren(parentEl, childNS, childTag) + if err != nil { + return nil, err + } + switch len(children) { + case 0: + return nil, fmt.Errorf("cannot find %s:%s element", childNS, childTag) + case 1: + return children[0], nil + default: + return nil, fmt.Errorf("expected exactly one %s:%s element", childNS, childTag) + } +} + +// findChild finds the specified child element. Returns (nil, nil) of the element doesn't exist. +func findChild(parentEl *etree.Element, childNS string, childTag string) (*etree.Element, error) { + children, err := findChildren(parentEl, childNS, childTag) + if err != nil { + return nil, err + } + switch len(children) { + case 0: + return nil, nil + case 1: + return children[0], nil + default: + return nil, fmt.Errorf("expected at most one %s:%s element", childNS, childTag) + } +} + +func elementToBytes(el *etree.Element) ([]byte, error) { + namespaces := map[string]string{} + for _, childEl := range el.FindElements("//*") { + ns := childEl.NamespaceURI() + if ns != "" { + namespaces[childEl.Space] = ns + } + } + + doc := etree.NewDocument() + doc.SetRoot(el.Copy()) + for space, uri := range namespaces { + doc.Root().CreateAttr("xmlns:"+space, uri) + } + + return doc.WriteToBytes() +} + +// unmarshalElement serializes el into v by serializing el and then parsing it with xml.Unmarshal. +func unmarshalElement(el *etree.Element, v interface{}) error { + buf, err := elementToBytes(el) + if err != nil { + return err + } + return xml.Unmarshal(buf, v) +} + +func elementToString(el *etree.Element) string { + buf, err := elementToBytes(el) + if err != nil { + return "" + } + return string(buf) +} diff --git a/service_provider_go116_test.go b/service_provider_go116_test.go new file mode 100644 index 00000000..77395e01 --- /dev/null +++ b/service_provider_go116_test.go @@ -0,0 +1,136 @@ +//go:build !go1.17 +// +build !go1.17 + +package saml + +import ( + "encoding/base64" + "encoding/xml" + "net/http" + "net/url" + "strings" + "testing" + "time" + + dsig "github.com/russellhaering/goxmldsig" + "gotest.tools/assert" + is "gotest.tools/assert/cmp" + "gotest.tools/golden" +) + +func TestSPRejectsMalformedResponse(t *testing.T) { + test := NewServiceProviderTest(t) + // An actual response from google + TimeNow = func() time.Time { + rv, _ := time.Parse("Mon Jan 2 15:04:05 UTC 2006", "Tue Jan 5 16:55:39 UTC 2016") + return rv + } + Clock = dsig.NewFakeClockAt(TimeNow()) + SamlResponse := golden.Get(t, "TestSPRejectsMalformedResponse_response") + test.IDPMetadata = golden.Get(t, "TestSPRejectsMalformedResponse_IDPMetadata") + + s := ServiceProvider{ + Key: test.Key, + Certificate: test.Certificate, + MetadataURL: mustParseURL("https://29ee6d2e.ngrok.io/saml/metadata"), + AcsURL: mustParseURL("https://29ee6d2e.ngrok.io/saml/acs"), + IDPMetadata: &EntityDescriptor{}, + } + err := xml.Unmarshal(test.IDPMetadata, &s.IDPMetadata) + assert.Check(t, err) + + // this is a valid response + { + req := http.Request{PostForm: url.Values{}} + req.PostForm.Set("SAMLResponse", string(SamlResponse)) + assertion, err := s.ParseResponse(&req, []string{"id-fd419a5ab0472645427f8e07d87a3a5dd0b2e9a6"}) + assert.Check(t, err) + assert.Check(t, is.Equal("ross@octolabs.io", assertion.Subject.NameID.Value)) + } + + // this is a valid response but with a comment injected + { + x, _ := base64.StdEncoding.DecodeString(string(SamlResponse)) + y := strings.Replace(string(x), "World!"))) + _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, + "cannot unmarshal response: expected element type but have ")) + + req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) + _, err = s.ParseResponse(&req, []string{"wrongRequestID"}) + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, + "`InResponseTo` does not match any of the possible request IDs (expected [wrongRequestID])")) + + TimeNow = func() time.Time { + rv, _ := time.Parse("Mon Jan 2 15:04:05 MST 2006", "Mon Nov 30 20:57:09 UTC 2016") + return rv + } + Clock = dsig.NewFakeClockAt(TimeNow()) + req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) + _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, + "response IssueInstant expired at 2015-12-01 01:57:51.375 +0000 UTC")) + TimeNow = func() time.Time { + rv, _ := time.Parse("Mon Jan 2 15:04:05 MST 2006", "Mon Dec 1 01:57:09 UTC 2015") + return rv + } + Clock = dsig.NewFakeClockAt(TimeNow()) + + s.IDPMetadata.EntityID = "http://snakeoil.com" + req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) + _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, + "response Issuer does not match the IDP metadata (expected \"http://snakeoil.com\")")) + s.IDPMetadata.EntityID = "https://idp.testshib.org/idp/shibboleth" + + oldSpStatusSuccess := StatusSuccess + StatusSuccess = "not:the:success:value" + req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) + _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, + "urn:oasis:names:tc:SAML:2.0:status:Success")) + StatusSuccess = oldSpStatusSuccess + + s.IDPMetadata.IDPSSODescriptors[0].KeyDescriptors[0].KeyInfo.X509Data.X509Certificates[0].Data = "invalid" + req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) + _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, + "cannot validate signature on Response: cannot parse certificate: illegal base64 data at input byte 4")) + + s.IDPMetadata.IDPSSODescriptors[0].KeyDescriptors[0].KeyInfo.X509Data.X509Certificates[0].Data = "aW52YWxpZA==" + req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) + _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) + + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, + "cannot validate signature on Response: asn1: structure error: tags don't match (16 vs {class:1 tag:9 length:110 isCompound:true}) {optional:false explicit:false application:false private:false defaultValue: tag: stringType:0 timeType:0 set:false omitEmpty:false} certificate @2")) +} diff --git a/service_provider_go117_test.go b/service_provider_go117_test.go new file mode 100644 index 00000000..3d4a5835 --- /dev/null +++ b/service_provider_go117_test.go @@ -0,0 +1,136 @@ +//go:build go1.17 +// +build go1.17 + +package saml + +import ( + "encoding/base64" + "encoding/xml" + "net/http" + "net/url" + "strings" + "testing" + "time" + + dsig "github.com/russellhaering/goxmldsig" + "gotest.tools/assert" + is "gotest.tools/assert/cmp" + "gotest.tools/golden" +) + +func TestSPRejectsMalformedResponse(t *testing.T) { + test := NewServiceProviderTest(t) + // An actual response from google + TimeNow = func() time.Time { + rv, _ := time.Parse("Mon Jan 2 15:04:05 UTC 2006", "Tue Jan 5 16:55:39 UTC 2016") + return rv + } + Clock = dsig.NewFakeClockAt(TimeNow()) + SamlResponse := golden.Get(t, "TestSPRejectsMalformedResponse_response") + test.IDPMetadata = golden.Get(t, "TestSPRejectsMalformedResponse_IDPMetadata") + + s := ServiceProvider{ + Key: test.Key, + Certificate: test.Certificate, + MetadataURL: mustParseURL("https://29ee6d2e.ngrok.io/saml/metadata"), + AcsURL: mustParseURL("https://29ee6d2e.ngrok.io/saml/acs"), + IDPMetadata: &EntityDescriptor{}, + } + err := xml.Unmarshal(test.IDPMetadata, &s.IDPMetadata) + assert.Check(t, err) + + // this is a valid response + { + req := http.Request{PostForm: url.Values{}} + req.PostForm.Set("SAMLResponse", string(SamlResponse)) + assertion, err := s.ParseResponse(&req, []string{"id-fd419a5ab0472645427f8e07d87a3a5dd0b2e9a6"}) + assert.Check(t, err) + assert.Check(t, is.Equal("ross@octolabs.io", assertion.Subject.NameID.Value)) + } + + // this is a valid response but with a comment injected + { + x, _ := base64.StdEncoding.DecodeString(string(SamlResponse)) + y := strings.Replace(string(x), "World!"))) + _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, + "cannot unmarshal response: expected element type but have ")) + + req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) + _, err = s.ParseResponse(&req, []string{"wrongRequestID"}) + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, + "`InResponseTo` does not match any of the possible request IDs (expected [wrongRequestID])")) + + TimeNow = func() time.Time { + rv, _ := time.Parse("Mon Jan 2 15:04:05 MST 2006", "Mon Nov 30 20:57:09 UTC 2016") + return rv + } + Clock = dsig.NewFakeClockAt(TimeNow()) + req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) + _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, + "response IssueInstant expired at 2015-12-01 01:57:51.375 +0000 UTC")) + TimeNow = func() time.Time { + rv, _ := time.Parse("Mon Jan 2 15:04:05 MST 2006", "Mon Dec 1 01:57:09 UTC 2015") + return rv + } + Clock = dsig.NewFakeClockAt(TimeNow()) + + s.IDPMetadata.EntityID = "http://snakeoil.com" + req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) + _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, + "response Issuer does not match the IDP metadata (expected \"http://snakeoil.com\")")) + s.IDPMetadata.EntityID = "https://idp.testshib.org/idp/shibboleth" + + oldSpStatusSuccess := StatusSuccess + StatusSuccess = "not:the:success:value" + req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) + _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, + "urn:oasis:names:tc:SAML:2.0:status:Success")) + StatusSuccess = oldSpStatusSuccess + + s.IDPMetadata.IDPSSODescriptors[0].KeyDescriptors[0].KeyInfo.X509Data.X509Certificates[0].Data = "invalid" + req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) + _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, + "cannot validate signature on Assertion: cannot parse certificate: illegal base64 data at input byte 4")) + + s.IDPMetadata.IDPSSODescriptors[0].KeyDescriptors[0].KeyInfo.X509Data.X509Certificates[0].Data = "aW52YWxpZA==" + req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) + _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) + + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, + "cannot validate signature on Assertion: x509: malformed certificate")) +} diff --git a/service_provider_test.go b/service_provider_test.go index 72d95070..67c2fbd9 100644 --- a/service_provider_test.go +++ b/service_provider_test.go @@ -81,25 +81,25 @@ func TestSPCanSetAuthenticationNameIDFormat(t *testing.T) { } // defaults to "transient" - req, err := s.MakeAuthenticationRequest("", HTTPRedirectBinding) + req, err := s.MakeAuthenticationRequest("", HTTPRedirectBinding, HTTPPostBinding) assert.Check(t, err) assert.Check(t, is.Equal(string(TransientNameIDFormat), *req.NameIDPolicy.Format)) // explicitly set to "transient" s.AuthnNameIDFormat = TransientNameIDFormat - req, err = s.MakeAuthenticationRequest("", HTTPRedirectBinding) + req, err = s.MakeAuthenticationRequest("", HTTPRedirectBinding, HTTPPostBinding) assert.Check(t, err) assert.Check(t, is.Equal(string(TransientNameIDFormat), *req.NameIDPolicy.Format)) // explicitly set to "unspecified" s.AuthnNameIDFormat = UnspecifiedNameIDFormat - req, err = s.MakeAuthenticationRequest("", HTTPRedirectBinding) + req, err = s.MakeAuthenticationRequest("", HTTPRedirectBinding, HTTPPostBinding) assert.Check(t, err) assert.Check(t, is.Equal("", *req.NameIDPolicy.Format)) // explicitly set to "emailAddress" s.AuthnNameIDFormat = EmailAddressNameIDFormat - req, err = s.MakeAuthenticationRequest("", HTTPRedirectBinding) + req, err = s.MakeAuthenticationRequest("", HTTPRedirectBinding, HTTPPostBinding) assert.Check(t, err) assert.Check(t, is.Equal(string(EmailAddressNameIDFormat), *req.NameIDPolicy.Format)) } @@ -107,12 +107,13 @@ func TestSPCanSetAuthenticationNameIDFormat(t *testing.T) { func TestSPCanProduceMetadataWithEncryptionCert(t *testing.T) { test := NewServiceProviderTest(t) s := ServiceProvider{ - Key: test.Key, - Certificate: test.Certificate, - MetadataURL: mustParseURL("https://example.com/saml2/metadata"), - AcsURL: mustParseURL("https://example.com/saml2/acs"), - SloURL: mustParseURL("https://example.com/saml2/slo"), - IDPMetadata: &EntityDescriptor{}, + Key: test.Key, + Certificate: test.Certificate, + MetadataURL: mustParseURL("https://example.com/saml2/metadata"), + AcsURL: mustParseURL("https://example.com/saml2/acs"), + SloURL: mustParseURL("https://example.com/saml2/slo"), + IDPMetadata: &EntityDescriptor{}, + LogoutBindings: []string{HTTPPostBinding}, } err := xml.Unmarshal(test.IDPMetadata, &s.IDPMetadata) assert.Check(t, err) @@ -125,13 +126,15 @@ func TestSPCanProduceMetadataWithEncryptionCert(t *testing.T) { func TestSPCanProduceMetadataWithBothCerts(t *testing.T) { test := NewServiceProviderTest(t) s := ServiceProvider{ - Key: test.Key, - Certificate: test.Certificate, - MetadataURL: mustParseURL("https://example.com/saml2/metadata"), - AcsURL: mustParseURL("https://example.com/saml2/acs"), - SloURL: mustParseURL("https://example.com/saml2/slo"), - IDPMetadata: &EntityDescriptor{}, - SignatureMethod: "not-empty", + Key: test.Key, + Certificate: test.Certificate, + MetadataURL: mustParseURL("https://example.com/saml2/metadata"), + AcsURL: mustParseURL("https://example.com/saml2/acs"), + SloURL: mustParseURL("https://example.com/saml2/slo"), + IDPMetadata: &EntityDescriptor{}, + AuthnNameIDFormat: TransientNameIDFormat, + LogoutBindings: []string{HTTPPostBinding}, + SignatureMethod: "not-empty", } err := xml.Unmarshal(test.IDPMetadata, &s.IDPMetadata) assert.Check(t, err) @@ -145,9 +148,11 @@ func TestSPCanProduceMetadataWithBothCerts(t *testing.T) { func TestCanProduceMetadataNoCerts(t *testing.T) { test := NewServiceProviderTest(t) s := ServiceProvider{ - MetadataURL: mustParseURL("https://example.com/saml2/metadata"), - AcsURL: mustParseURL("https://example.com/saml2/acs"), - IDPMetadata: &EntityDescriptor{}, + MetadataURL: mustParseURL("https://example.com/saml2/metadata"), + AcsURL: mustParseURL("https://example.com/saml2/acs"), + IDPMetadata: &EntityDescriptor{}, + AuthnNameIDFormat: TransientNameIDFormat, + LogoutBindings: []string{HTTPPostBinding}, } err := xml.Unmarshal(test.IDPMetadata, &s.IDPMetadata) assert.Check(t, err) @@ -160,9 +165,28 @@ func TestCanProduceMetadataNoCerts(t *testing.T) { func TestCanProduceMetadataEntityID(t *testing.T) { test := NewServiceProviderTest(t) s := ServiceProvider{ - EntityID: "spn:11111111-2222-3333-4444-555555555555", + EntityID: "spn:11111111-2222-3333-4444-555555555555", + MetadataURL: mustParseURL("https://example.com/saml2/metadata"), + AcsURL: mustParseURL("https://example.com/saml2/acs"), + IDPMetadata: &EntityDescriptor{}, + LogoutBindings: []string{HTTPPostBinding}, + } + err := xml.Unmarshal(test.IDPMetadata, &s.IDPMetadata) + assert.Check(t, err) + + spMetadata, err := xml.MarshalIndent(s.Metadata(), "", " ") + assert.Check(t, err) + golden.Assert(t, string(spMetadata), t.Name()+"_metadata") +} + +func TestSPCanProduceMetadataWithNoLougoutBindings(t *testing.T) { + test := NewServiceProviderTest(t) + s := ServiceProvider{ + Key: test.Key, + Certificate: test.Certificate, MetadataURL: mustParseURL("https://example.com/saml2/metadata"), AcsURL: mustParseURL("https://example.com/saml2/acs"), + SloURL: mustParseURL("https://example.com/saml2/slo"), IDPMetadata: &EntityDescriptor{}, } err := xml.Unmarshal(test.IDPMetadata, &s.IDPMetadata) @@ -173,6 +197,25 @@ func TestCanProduceMetadataEntityID(t *testing.T) { golden.Assert(t, string(spMetadata), t.Name()+"_metadata") } +func TestSPCanProduceMetadataWithBothLougoutBindings(t *testing.T) { + test := NewServiceProviderTest(t) + s := ServiceProvider{ + Key: test.Key, + Certificate: test.Certificate, + MetadataURL: mustParseURL("https://example.com/saml2/metadata"), + AcsURL: mustParseURL("https://example.com/saml2/acs"), + SloURL: mustParseURL("https://example.com/saml2/slo"), + IDPMetadata: &EntityDescriptor{}, + LogoutBindings: []string{HTTPPostBinding, HTTPRedirectBinding}, + } + err := xml.Unmarshal(test.IDPMetadata, &s.IDPMetadata) + assert.Check(t, err) + + spMetadata, err := xml.MarshalIndent(s.Metadata(), "", " ") + assert.Check(t, err) + golden.Assert(t, string(spMetadata), t.Name()+"_metadata") +} + func TestSPCanProduceRedirectRequest(t *testing.T) { test := NewServiceProviderTest(t) TimeNow = func() time.Time { @@ -307,7 +350,7 @@ func TestSPFailToProduceSignedRequestWithBogusSignatureMethod(t *testing.T) { assert.Check(t, err) _, err = s.MakeRedirectAuthenticationRequest("relayState") - assert.Check(t, is.ErrorContains(err, ""), "invalid signing method bogus") + assert.Check(t, is.ErrorContains(err, "invalid signing method bogus")) } func TestSPCanProducePostLogoutRequest(t *testing.T) { @@ -728,7 +771,7 @@ func TestSPRejectsInjectedComment(t *testing.T) { // it *MUST NOT* validate { x, _ := base64.StdEncoding.DecodeString(string(SamlResponse)) - y := strings.Replace(string(x), "ross@octolabs.io", "ross@octolabs.io.example.com", 1) + y := strings.Replace(string(x), "ross@octolabs.io", "ross@octolabs.io.example.com", 1) SamlResponse = []byte(base64.StdEncoding.EncodeToString([]byte(y))) req := http.Request{PostForm: url.Values{}} @@ -742,51 +785,6 @@ func TestSPRejectsInjectedComment(t *testing.T) { } } -func TestSPRejectsMalformedResponse(t *testing.T) { - test := NewServiceProviderTest(t) - // An actual response from google - TimeNow = func() time.Time { - rv, _ := time.Parse("Mon Jan 2 15:04:05 UTC 2006", "Tue Jan 5 16:55:39 UTC 2016") - return rv - } - Clock = dsig.NewFakeClockAt(TimeNow()) - SamlResponse := golden.Get(t, "TestSPRejectsMalformedResponse_response") - test.IDPMetadata = golden.Get(t, "TestSPRejectsMalformedResponse_IDPMetadata") - - s := ServiceProvider{ - Key: test.Key, - Certificate: test.Certificate, - MetadataURL: mustParseURL("https://29ee6d2e.ngrok.io/saml/metadata"), - AcsURL: mustParseURL("https://29ee6d2e.ngrok.io/saml/acs"), - IDPMetadata: &EntityDescriptor{}, - } - err := xml.Unmarshal(test.IDPMetadata, &s.IDPMetadata) - assert.Check(t, err) - - // this is a valid response - { - req := http.Request{PostForm: url.Values{}} - req.PostForm.Set("SAMLResponse", string(SamlResponse)) - assertion, err := s.ParseResponse(&req, []string{"id-fd419a5ab0472645427f8e07d87a3a5dd0b2e9a6"}) - assert.Check(t, err) - assert.Check(t, is.Equal("ross@octolabs.io", assertion.Subject.NameID.Value)) - } - - // this is a valid response but with a comment injected - { - x, _ := base64.StdEncoding.DecodeString(string(SamlResponse)) - y := strings.Replace(string(x), "World!"))) - _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) - assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, - "cannot unmarshal response: expected element type but have ")) - - req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) - _, err = s.ParseResponse(&req, []string{"wrongRequestID"}) - assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, - "`InResponseTo` does not match any of the possible request IDs (expected [wrongRequestID])")) - - TimeNow = func() time.Time { - rv, _ := time.Parse("Mon Jan 2 15:04:05 MST 2006", "Mon Nov 30 20:57:09 UTC 2016") - return rv - } - Clock = dsig.NewFakeClockAt(TimeNow()) - req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) - _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) - assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, - "response IssueInstant expired at 2015-12-01 01:57:51.375 +0000 UTC")) - TimeNow = func() time.Time { - rv, _ := time.Parse("Mon Jan 2 15:04:05 MST 2006", "Mon Dec 1 01:57:09 UTC 2015") - return rv - } - Clock = dsig.NewFakeClockAt(TimeNow()) - - s.IDPMetadata.EntityID = "http://snakeoil.com" - req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) - _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) - assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, - "response Issuer does not match the IDP metadata (expected \"http://snakeoil.com\")")) - s.IDPMetadata.EntityID = "https://idp.testshib.org/idp/shibboleth" - - oldSpStatusSuccess := StatusSuccess - StatusSuccess = "not:the:success:value" - req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) - _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) - assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, - "urn:oasis:names:tc:SAML:2.0:status:Success")) - StatusSuccess = oldSpStatusSuccess - - s.IDPMetadata.IDPSSODescriptors[0].KeyDescriptors[0].KeyInfo.Certificate = "invalid" - req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) - _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) - assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, - "cannot validate signature on Response: cannot parse certificate: illegal base64 data at input byte 4")) - - s.IDPMetadata.IDPSSODescriptors[0].KeyDescriptors[0].KeyInfo.Certificate = "aW52YWxpZA==" - req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) - _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) - - assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, - "cannot validate signature on Response: asn1: structure error: tags don't match (16 vs {class:1 tag:9 length:110 isCompound:true}) {optional:false explicit:false application:false private:false defaultValue: tag: stringType:0 timeType:0 set:false omitEmpty:false} certificate @2")) -} + // HACK: decrypt response without verifying assertions + var assertionBuf []byte + { + doc := etree.NewDocument() + assert.Check(t, doc.ReadFromBytes(test.SamlResponse)) + encryptedEL := doc.Root().FindElement("//EncryptedAssertion") + assertionEl, err := s.decryptElement(encryptedEL) + assert.Check(t, err) -func TestSPInvalidAssertions(t *testing.T) { - test := NewServiceProviderTest(t) - s := ServiceProvider{ - Key: test.Key, - Certificate: test.Certificate, - MetadataURL: mustParseURL("https://15661444.ngrok.io/saml2/metadata"), - AcsURL: mustParseURL("https://15661444.ngrok.io/saml2/acs"), - IDPMetadata: &EntityDescriptor{}, + doc = etree.NewDocument() + doc.SetRoot(assertionEl) + assertionBuf, err = doc.WriteToBytes() + assert.Check(t, err) } - err := xml.Unmarshal(test.IDPMetadata, &s.IDPMetadata) - assert.Check(t, err) - - req := http.Request{PostForm: url.Values{}} - req.PostForm.Set("SAMLResponse", base64.StdEncoding.EncodeToString(test.SamlResponse)) - s.IDPMetadata.IDPSSODescriptors[0].KeyDescriptors[0].KeyInfo.Certificate = "invalid" - _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) - assertionBuf := []byte(err.(*InvalidResponseError).Response) assertion := Assertion{} err = xml.Unmarshal(assertionBuf, &assertion) @@ -1178,19 +1114,19 @@ func TestSPInvalidAssertions(t *testing.T) { err = s.validateAssertion(&assertion, []string{"id-9e61753d64e928af5a7a341a97f420c9"}, TimeNow()) assert.Check(t, is.Error(err, "issuer is not \"https://idp.testshib.org/idp/shibboleth\"")) assertion = Assertion{} - xml.Unmarshal(assertionBuf, &assertion) + assert.Check(t, xml.Unmarshal(assertionBuf, &assertion)) assertion.Subject.NameID.NameQualifier = "bob" err = s.validateAssertion(&assertion, []string{"id-9e61753d64e928af5a7a341a97f420c9"}, TimeNow()) assert.Check(t, err) // not verified assertion = Assertion{} - xml.Unmarshal(assertionBuf, &assertion) + assert.Check(t, xml.Unmarshal(assertionBuf, &assertion)) assertion.Subject.NameID.SPNameQualifier = "bob" err = s.validateAssertion(&assertion, []string{"id-9e61753d64e928af5a7a341a97f420c9"}, TimeNow()) assert.Check(t, err) // not verified assertion = Assertion{} - xml.Unmarshal(assertionBuf, &assertion) + assert.Check(t, xml.Unmarshal(assertionBuf, &assertion)) err = s.validateAssertion(&assertion, []string{"any request id"}, TimeNow()) assert.Check(t, is.Error(err, "assertion SubjectConfirmation one of the possible request IDs ([any request id])")) @@ -1199,31 +1135,31 @@ func TestSPInvalidAssertions(t *testing.T) { err = s.validateAssertion(&assertion, []string{"id-9e61753d64e928af5a7a341a97f420c9"}, TimeNow()) assert.Check(t, is.Error(err, "assertion SubjectConfirmation Recipient is not https://15661444.ngrok.io/saml2/acs")) assertion = Assertion{} - xml.Unmarshal(assertionBuf, &assertion) + assert.Check(t, xml.Unmarshal(assertionBuf, &assertion)) assertion.Subject.SubjectConfirmations[0].SubjectConfirmationData.NotOnOrAfter = TimeNow().Add(-1 * time.Hour) err = s.validateAssertion(&assertion, []string{"id-9e61753d64e928af5a7a341a97f420c9"}, TimeNow()) assert.Check(t, is.Error(err, "assertion SubjectConfirmationData is expired")) assertion = Assertion{} - xml.Unmarshal(assertionBuf, &assertion) + assert.Check(t, xml.Unmarshal(assertionBuf, &assertion)) assertion.Conditions.NotBefore = TimeNow().Add(time.Hour) err = s.validateAssertion(&assertion, []string{"id-9e61753d64e928af5a7a341a97f420c9"}, TimeNow()) assert.Check(t, is.Error(err, "assertion Conditions is not yet valid")) assertion = Assertion{} - xml.Unmarshal(assertionBuf, &assertion) + assert.Check(t, xml.Unmarshal(assertionBuf, &assertion)) assertion.Conditions.NotOnOrAfter = TimeNow().Add(-1 * time.Hour) err = s.validateAssertion(&assertion, []string{"id-9e61753d64e928af5a7a341a97f420c9"}, TimeNow()) assert.Check(t, is.Error(err, "assertion Conditions is expired")) assertion = Assertion{} - xml.Unmarshal(assertionBuf, &assertion) + assert.Check(t, xml.Unmarshal(assertionBuf, &assertion)) assertion.Conditions.AudienceRestrictions[0].Audience.Value = "not/our/metadata/url" err = s.validateAssertion(&assertion, []string{"id-9e61753d64e928af5a7a341a97f420c9"}, TimeNow()) assert.Check(t, is.Error(err, "assertion Conditions AudienceRestriction does not contain \"https://15661444.ngrok.io/saml2/metadata\"")) assertion = Assertion{} - xml.Unmarshal(assertionBuf, &assertion) + assert.Check(t, xml.Unmarshal(assertionBuf, &assertion)) // Not having an audience is not an error assertion.Conditions.AudienceRestrictions = []AudienceRestriction{} @@ -1308,9 +1244,13 @@ func TestXswPermutationThreeIsRejected(t *testing.T) { req := http.Request{PostForm: url.Values{}} req.PostForm.Set("SAMLResponse", string(respStr)) _, err = s.ParseResponse(&req, []string{"ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"}) - // Because this permutation contains an unsigned assertion as child of the response - assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, - "either the Response or Assertion must be signed")) + + // This response contains two assertions. The first is missing a Signature element. The second is + // signed by a certificate that is not yet valid at the time of issue. + // + // When no assertions are valid, we return the first error encountered, which in this case is that + // there is no Signature on the element. + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, "signature element not present")) } func TestXswPermutationFourIsRejected(t *testing.T) { @@ -1336,9 +1276,11 @@ func TestXswPermutationFourIsRejected(t *testing.T) { req := http.Request{PostForm: url.Values{}} req.PostForm.Set("SAMLResponse", string(respStr)) _, err = s.ParseResponse(&req, []string{"ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"}) - // Because this permutation contains an unsigned assertion as child of the response - assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, - "either the Response or Assertion must be signed")) + + // This permutation contains a signed assertion embedded within an unsigned assertion. + // I'm pretty sure this is just not allowed, so we properly decide that there are no + // signed assertions at all. + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, "signature element not present")) } func TestXswPermutationFiveIsRejected(t *testing.T) { @@ -1365,7 +1307,7 @@ func TestXswPermutationFiveIsRejected(t *testing.T) { req.PostForm.Set("SAMLResponse", string(respStr)) _, err = s.ParseResponse(&req, []string{"ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"}) assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, - "cannot validate signature on Response: Missing signature referencing the top-level element")) + "cannot validate signature on Assertion: Missing signature referencing the top-level element")) } func TestXswPermutationSixIsRejected(t *testing.T) { @@ -1392,7 +1334,7 @@ func TestXswPermutationSixIsRejected(t *testing.T) { req.PostForm.Set("SAMLResponse", string(respStr)) _, err = s.ParseResponse(&req, []string{"ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"}) assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, - "cannot validate signature on Response: Missing signature referencing the top-level element")) + "cannot validate signature on Assertion: Missing signature referencing the top-level element")) } func TestXswPermutationSevenIsRejected(t *testing.T) { @@ -1421,9 +1363,9 @@ func TestXswPermutationSevenIsRejected(t *testing.T) { req := http.Request{PostForm: url.Values{}} req.PostForm.Set("SAMLResponse", string(respStr)) _, err = s.ParseResponse(&req, []string{"ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"}) - //It's the assertion signature that can't be verified. The error message is generic and always mentions Response + // It's the assertion signature that can't be verified. The error message is generic and always mentions Response assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, - "cannot validate signature on Response: Signature could not be verified")) + "cannot validate signature on Assertion: Signature could not be verified")) } func TestXswPermutationEightIsRejected(t *testing.T) { @@ -1452,9 +1394,9 @@ func TestXswPermutationEightIsRejected(t *testing.T) { req := http.Request{PostForm: url.Values{}} req.PostForm.Set("SAMLResponse", string(respStr)) _, err = s.ParseResponse(&req, []string{"ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"}) - //It's the assertion signature that can't be verified. The error message is generic and always mentions Response + // It's the assertion signature that can't be verified. The error message is generic and always mentions Response assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, - "cannot validate signature on Response: Signature could not be verified")) + "cannot validate signature on Assertion: Signature could not be verified")) } func TestXswPermutationNineIsRejected(t *testing.T) { @@ -1483,9 +1425,9 @@ func TestXswPermutationNineIsRejected(t *testing.T) { req := http.Request{PostForm: url.Values{}} req.PostForm.Set("SAMLResponse", string(respStr)) _, err = s.ParseResponse(&req, []string{"ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"}) - //It's the assertion signature that can't be verified. The error message is generic and always mentions Response + // It's the assertion signature that can't be verified. The error message is generic and always mentions Response assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, - "cannot validate signature on Response: Missing signature referencing the top-level element")) + "cannot validate signature on Assertion: Missing signature referencing the top-level element")) } func TestSPRealWorldKeyInfoHasRSAPublicKeyNotX509Cert(t *testing.T) { @@ -1648,3 +1590,265 @@ func TestSPResponseWithNoIssuer(t *testing.T) { _, err = s.ParseResponse(&req, []string{"id-9e61753d64e928af5a7a341a97f420c9"}) assert.Check(t, err) } + +func TestGetArtifactBindingLocation(t *testing.T) { + test := NewServiceProviderTest(t) + test.IDPMetadata = golden.Get(t, "TestGetArtifactBindingLocation_IDPMetadata") + + sp := ServiceProvider{ + Key: test.Key, + Certificate: test.Certificate, + MetadataURL: mustParseURL("https://example.com/saml2/metadata"), + AcsURL: mustParseURL("https://example.com/saml2/acs"), + IDPMetadata: &EntityDescriptor{}, + } + + location := sp.GetArtifactBindingLocation(SOAPBinding) + assert.Check(t, is.Equal(location, "")) + + err := xml.Unmarshal(test.IDPMetadata, &sp.IDPMetadata) + assert.Check(t, err) + + location = sp.GetArtifactBindingLocation(SOAPBinding) + assert.Check(t, is.Equal(location, "https://samltest.id/idp/profile/SAML2/SOAP/ArtifactResolution")) +} + +func TestMakeArtifactResolveRequest(t *testing.T) { + test := NewServiceProviderTest(t) + + sp := ServiceProvider{ + Key: test.Key, + Certificate: test.Certificate, + MetadataURL: mustParseURL("https://example.com/saml2/metadata"), + AcsURL: mustParseURL("https://example.com/saml2/acs"), + IDPMetadata: &EntityDescriptor{}, + } + + req, err := sp.MakeArtifactResolveRequest("artifactId") + assert.Check(t, err) + + x, err := xml.Marshal(req) + assert.Check(t, err) + golden.Assert(t, string(x), t.Name()) +} + +func TestMakeSignedArtifactResolveRequest(t *testing.T) { + test := NewServiceProviderTest(t) + + sp := ServiceProvider{ + Key: test.Key, + Certificate: test.Certificate, + MetadataURL: mustParseURL("https://example.com/saml2/metadata"), + AcsURL: mustParseURL("https://example.com/saml2/acs"), + IDPMetadata: &EntityDescriptor{}, + SignatureMethod: dsig.RSASHA1SignatureMethod, + } + + req, err := sp.MakeArtifactResolveRequest("artifactId") + assert.Check(t, err) + + x, err := xml.Marshal(req) + assert.Check(t, err) + golden.Assert(t, string(x), t.Name()) +} + +func TestMakeSignedArtifactResolveRequestWithBogusSignatureMethod(t *testing.T) { + test := NewServiceProviderTest(t) + + sp := ServiceProvider{ + Key: test.Key, + Certificate: test.Certificate, + MetadataURL: mustParseURL("https://example.com/saml2/metadata"), + AcsURL: mustParseURL("https://example.com/saml2/acs"), + IDPMetadata: &EntityDescriptor{}, + SignatureMethod: "bogus", + } + + _, err := sp.MakeArtifactResolveRequest("artifactId") + assert.Check(t, is.ErrorContains(err, "invalid signing method bogus")) + +} + +func TestParseXMLArtifactResponse(t *testing.T) { + test := NewServiceProviderTest(t) + TimeNow = func() time.Time { + rv, _ := time.Parse(timeFormat, "2021-08-17T10:26:57Z") + return rv + } + Clock = dsig.NewFakeClockAt(TimeNow()) + + // an actual response from samltest.id + samlResponse := golden.Get(t, "TestParseXMLArtifactResponse_response") + test.IDPMetadata = golden.Get(t, "TestGetArtifactBindingLocation_IDPMetadata") + + sp := ServiceProvider{ + Key: test.Key, + Certificate: test.Certificate, + MetadataURL: mustParseURL("http://localhost:8000/saml/metadata"), + AcsURL: mustParseURL("http://localhost:8000/saml/acs"), + IDPMetadata: &EntityDescriptor{}, + } + + err := xml.Unmarshal(test.IDPMetadata, &sp.IDPMetadata) + assert.Check(t, err) + + possibleReqIDs := []string{"id-f3c7bc7d626a4ededa6028b718e5252c6e770b94"} + reqID := "id-218eb155248f7db7c85fe4e2709a3f17a70d09c7" + + assertion, err := sp.ParseXMLArtifactResponse(samlResponse, possibleReqIDs, reqID) + assert.Check(t, err) + + x, err := xml.Marshal(assertion) + assert.Check(t, err) + + golden.Assert(t, string(x), t.Name()+"_assertion") +} + +func TestParseBadXMLArtifactResponse(t *testing.T) { + test := NewServiceProviderTest(t) + TimeNow = func() time.Time { + rv, _ := time.Parse(timeFormat, "2021-08-17T10:26:57Z") + return rv + } + Clock = dsig.NewFakeClockAt(TimeNow()) + + // an actual response from samltest.id + samlResponse := golden.Get(t, "TestParseXMLArtifactResponse_response") + test.IDPMetadata = golden.Get(t, "TestGetArtifactBindingLocation_IDPMetadata") + + possibleReqIDs := []string{"id-f3c7bc7d626a4ededa6028b718e5252c6e770b94"} + reqID := "id-218eb155248f7db7c85fe4e2709a3f17a70d09c7" + + sp := ServiceProvider{ + Key: test.Key, + Certificate: test.Certificate, + MetadataURL: mustParseURL("http://localhost:8000/saml/metadata"), + AcsURL: mustParseURL("https://example.com/saml2/acs"), + IDPMetadata: &EntityDescriptor{}, + } + + assertion, err := sp.ParseXMLArtifactResponse(samlResponse, possibleReqIDs, reqID) + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, + "response Issuer does not match the IDP metadata (expected \"\")")) + assert.Check(t, is.Nil(assertion)) + + err = xml.Unmarshal(test.IDPMetadata, &sp.IDPMetadata) + assert.Check(t, err) + + assertion, err = sp.ParseXMLArtifactResponse(samlResponse, possibleReqIDs, reqID) + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, + "`Destination` does not match AcsURL (expected \"https://example.com/saml2/acs\", actual \"http://localhost:8000/saml/acs\")")) + assert.Check(t, is.Nil(assertion)) + + sp.AcsURL = mustParseURL("http://localhost:8000/saml/acs") + + // TimeNow is used to verify the response time + TimeNow = func() time.Time { + rv, _ := time.Parse(timeFormat, "2022-08-17T10:26:57Z") + return rv + } + + assertion, err = sp.ParseXMLArtifactResponse(samlResponse, possibleReqIDs, reqID) + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, + "response IssueInstant expired at 2021-08-17 10:28:50.146 +0000 UTC")) + assert.Check(t, is.Nil(assertion)) + + // Clock is used to verify the certificate + Clock = dsig.NewFakeClockAt(func() time.Time { + rv, _ := time.Parse(timeFormat, "2039-08-17T10:26:57Z") + return rv + }()) + TimeNow = func() time.Time { + rv, _ := time.Parse(timeFormat, "2021-08-17T10:26:57Z") + return rv + } + + assertion, err = sp.ParseXMLArtifactResponse(samlResponse, possibleReqIDs, reqID) + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, + "cannot validate signature on ArtifactResponse: Cert is not valid at this time")) + assert.Check(t, is.Nil(assertion)) + Clock = dsig.NewFakeClockAt(TimeNow()) + + wrongReqID := "id-218eb155248f7db7c85fe4e2709a3f17a70d09c8" + assertion, err = sp.ParseXMLArtifactResponse(samlResponse, possibleReqIDs, wrongReqID) + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, + "`InResponseTo` does not match the artifact request ID (expected id-218eb155248f7db7c85fe4e2709a3f17a70d09c8)")) + assert.Check(t, is.Nil(assertion)) + + wrongPossibleReqIDs := []string{"id-f3c7bc7d626a4ededa6028b718e5252c6e770b95"} + assertion, err = sp.ParseXMLArtifactResponse(samlResponse, wrongPossibleReqIDs, reqID) + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, + "`InResponseTo` does not match any of the possible request IDs (expected [id-f3c7bc7d626a4ededa6028b718e5252c6e770b95])")) + assert.Check(t, is.Nil(assertion)) + + // random other key + sp.Key = mustParsePrivateKey(golden.Get(t, "key_2017.pem")).(*rsa.PrivateKey) + assertion, err = sp.ParseXMLArtifactResponse(samlResponse, possibleReqIDs, reqID) + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, + "failed to decrypt EncryptedAssertion: certificate does not match provided key")) + assert.Check(t, is.Nil(assertion)) + + // no input + assertion, err = sp.ParseXMLArtifactResponse([]byte(""), possibleReqIDs, reqID) + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, + "invalid xml: no root")) + assert.Check(t, is.Nil(assertion)) + + assertion, err = sp.ParseXMLArtifactResponse([]byte(""), []string{}) + assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, + "invalid xml: no root")) + assert.Check(t, is.Nil(assertion)) + + assertion, err = sp.ParseXMLResponse([]byte(" + + \ No newline at end of file diff --git a/testdata/TestCanProduceMetadataNoCerts_metadata b/testdata/TestCanProduceMetadataNoCerts_metadata index 3e802f77..9b475d98 100644 --- a/testdata/TestCanProduceMetadataNoCerts_metadata +++ b/testdata/TestCanProduceMetadataNoCerts_metadata @@ -1,6 +1,8 @@ + urn:oasis:names:tc:SAML:2.0:nameid-format:transient + \ No newline at end of file diff --git a/testdata/TestCanProduceSPMetadata_expected b/testdata/TestCanProduceSPMetadata_expected index 9250ba1a..79158e1e 100644 --- a/testdata/TestCanProduceSPMetadata_expected +++ b/testdata/TestCanProduceSPMetadata_expected @@ -2,15 +2,15 @@ - - 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 diff --git a/testdata/TestGetArtifactBindingLocation_IDPMetadata b/testdata/TestGetArtifactBindingLocation_IDPMetadata new file mode 100644 index 00000000..f0f8b54c --- /dev/null +++ b/testdata/TestGetArtifactBindingLocation_IDPMetadata @@ -0,0 +1,122 @@ + + + + + + + + + samltest.id + + + + SAMLtest IdP + A free and basic IdP for testing SAML deployments + https://samltest.id/saml/logo.png + + + + + + + +MIIDETCCAfmgAwIBAgIUZRpDhkNKl5eWtJqk0Bu1BgTTargwDQYJKoZIhvcNAQEL +BQAwFjEUMBIGA1UEAwwLc2FtbHRlc3QuaWQwHhcNMTgwODI0MjExNDEwWhcNMzgw +ODI0MjExNDEwWjAWMRQwEgYDVQQDDAtzYW1sdGVzdC5pZDCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAJrh9/PcDsiv3UeL8Iv9rf4WfLPxuOm9W6aCntEA +8l6c1LQ1Zyrz+Xa/40ZgP29ENf3oKKbPCzDcc6zooHMji2fBmgXp6Li3fQUzu7yd ++nIC2teejijVtrNLjn1WUTwmqjLtuzrKC/ePoZyIRjpoUxyEMJopAd4dJmAcCq/K +k2eYX9GYRlqvIjLFoGNgy2R4dWwAKwljyh6pdnPUgyO/WjRDrqUBRFrLQJorR2kD +c4seZUbmpZZfp4MjmWMDgyGM1ZnR0XvNLtYeWAyt0KkSvFoOMjZUeVK/4xR74F8e +8ToPqLmZEg9ZUx+4z2KjVK00LpdRkH9Uxhh03RQ0FabHW6UCAwEAAaNXMFUwHQYD +VR0OBBYEFJDbe6uSmYQScxpVJhmt7PsCG4IeMDQGA1UdEQQtMCuCC3NhbWx0ZXN0 +LmlkhhxodHRwczovL3NhbWx0ZXN0LmlkL3NhbWwvaWRwMA0GCSqGSIb3DQEBCwUA +A4IBAQBNcF3zkw/g51q26uxgyuy4gQwnSr01Mhvix3Dj/Gak4tc4XwvxUdLQq+jC +cxr2Pie96klWhY/v/JiHDU2FJo9/VWxmc/YOk83whvNd7mWaNMUsX3xGv6AlZtCO +L3JhCpHjiN+kBcMgS5jrtGgV1Lz3/1zpGxykdvS0B4sPnFOcaCwHe2B9SOCWbDAN +JXpTjz1DmJO4ImyWPJpN1xsYKtm67Pefxmn0ax0uE2uuzq25h0xbTkqIQgJzyoE/ +DPkBFK1vDkMfAW11dQ0BXatEnW7Gtkc0lh2/PIbHWj4AzxYMyBf5Gy6HSVOftwjC +voQR2qr2xJBixsg+MIORKtmKHLfU + + + + + + + + + +MIIDEjCCAfqgAwIBAgIVAMECQ1tjghafm5OxWDh9hwZfxthWMA0GCSqGSIb3DQEB +CwUAMBYxFDASBgNVBAMMC3NhbWx0ZXN0LmlkMB4XDTE4MDgyNDIxMTQwOVoXDTM4 +MDgyNDIxMTQwOVowFjEUMBIGA1UEAwwLc2FtbHRlc3QuaWQwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQC0Z4QX1NFKs71ufbQwoQoW7qkNAJRIANGA4iM0 +ThYghul3pC+FwrGv37aTxWXfA1UG9njKbbDreiDAZKngCgyjxj0uJ4lArgkr4AOE +jj5zXA81uGHARfUBctvQcsZpBIxDOvUUImAl+3NqLgMGF2fktxMG7kX3GEVNc1kl +bN3dfYsaw5dUrw25DheL9np7G/+28GwHPvLb4aptOiONbCaVvh9UMHEA9F7c0zfF +/cL5fOpdVa54wTI0u12CsFKt78h6lEGG5jUs/qX9clZncJM7EFkN3imPPy+0HC8n +spXiH/MZW8o2cqWRkrw3MzBZW3Ojk5nQj40V6NUbjb7kfejzAgMBAAGjVzBVMB0G +A1UdDgQWBBQT6Y9J3Tw/hOGc8PNV7JEE4k2ZNTA0BgNVHREELTArggtzYW1sdGVz +dC5pZIYcaHR0cHM6Ly9zYW1sdGVzdC5pZC9zYW1sL2lkcDANBgkqhkiG9w0BAQsF +AAOCAQEASk3guKfTkVhEaIVvxEPNR2w3vWt3fwmwJCccW98XXLWgNbu3YaMb2RSn +7Th4p3h+mfyk2don6au7Uyzc1Jd39RNv80TG5iQoxfCgphy1FYmmdaSfO8wvDtHT +TNiLArAxOYtzfYbzb5QrNNH/gQEN8RJaEf/g/1GTw9x/103dSMK0RXtl+fRs2nbl +D1JJKSQ3AdhxK/weP3aUPtLxVVJ9wMOQOfcy02l+hHMb6uAjsPOpOVKqi3M8XmcU +ZOpx4swtgGdeoSpeRyrtMvRwdcciNBp9UZome44qZAYH1iqrpmmjsfI9pJItsgWu +3kXPjhSfj1AJGR1l9JGvJrHki1iHTA== + + + + + + + + + +MIIDEjCCAfqgAwIBAgIVAPVbodo8Su7/BaHXUHykx0Pi5CFaMA0GCSqGSIb3DQEB +CwUAMBYxFDASBgNVBAMMC3NhbWx0ZXN0LmlkMB4XDTE4MDgyNDIxMTQwOVoXDTM4 +MDgyNDIxMTQwOVowFjEUMBIGA1UEAwwLc2FtbHRlc3QuaWQwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCQb+1a7uDdTTBBFfwOUun3IQ9nEuKM98SmJDWa +MwM877elswKUTIBVh5gB2RIXAPZt7J/KGqypmgw9UNXFnoslpeZbA9fcAqqu28Z4 +sSb2YSajV1ZgEYPUKvXwQEmLWN6aDhkn8HnEZNrmeXihTFdyr7wjsLj0JpQ+VUlc +4/J+hNuU7rGYZ1rKY8AA34qDVd4DiJ+DXW2PESfOu8lJSOteEaNtbmnvH8KlwkDs +1NvPTsI0W/m4SK0UdXo6LLaV8saIpJfnkVC/FwpBolBrRC/Em64UlBsRZm2T89ca +uzDee2yPUvbBd5kLErw+sC7i4xXa2rGmsQLYcBPhsRwnmBmlAgMBAAGjVzBVMB0G +A1UdDgQWBBRZ3exEu6rCwRe5C7f5QrPcAKRPUjA0BgNVHREELTArggtzYW1sdGVz +dC5pZIYcaHR0cHM6Ly9zYW1sdGVzdC5pZC9zYW1sL2lkcDANBgkqhkiG9w0BAQsF +AAOCAQEABZDFRNtcbvIRmblnZItoWCFhVUlq81ceSQddLYs8DqK340//hWNAbYdj +WcP85HhIZnrw6NGCO4bUipxZXhiqTA/A9d1BUll0vYB8qckYDEdPDduYCOYemKkD +dmnHMQWs9Y6zWiYuNKEJ9mf3+1N8knN/PK0TYVjVjXAf2CnOETDbLtlj6Nqb8La3 +sQkYmU+aUdopbjd5JFFwbZRaj6KiHXHtnIRgu8sUXNPrgipUgZUOVhP0C0N5OfE4 +JW8ZBrKgQC/6vJ2rSa9TlzI6JAa5Ww7gMXMP9M+cJUNQklcq+SBnTK8G+uBHgPKR +zBDsMIEzRtQZm4GIoHJae4zmnCekkQ== + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/testdata/TestMakeArtifactResolveRequest b/testdata/TestMakeArtifactResolveRequest new file mode 100644 index 00000000..fcc54fcd --- /dev/null +++ b/testdata/TestMakeArtifactResolveRequest @@ -0,0 +1 @@ +https://example.com/saml2/metadataartifactId \ No newline at end of file diff --git a/testdata/TestMakeSignedArtifactResolveRequest b/testdata/TestMakeSignedArtifactResolveRequest new file mode 100644 index 00000000..8c1b614e --- /dev/null +++ b/testdata/TestMakeSignedArtifactResolveRequest @@ -0,0 +1 @@ +https://example.com/saml2/metadatadsSignaturexmlnsdshttp://www.w3.org/2000/09/xmldsig#dsSignedInfodsCanonicalizationMethodAlgorithmhttp://www.w3.org/2001/10/xml-exc-c14n#dsSignatureMethodAlgorithmhttp://www.w3.org/2000/09/xmldsig#rsa-sha1dsReferenceURI#id-00020406080a0c0e10121416181a1c1e20222426dsTransformsdsTransformAlgorithmhttp://www.w3.org/2000/09/xmldsig#enveloped-signaturedsTransformAlgorithmhttp://www.w3.org/2001/10/xml-exc-c14n#dsDigestMethodAlgorithmhttp://www.w3.org/2000/09/xmldsig#sha1dsDigestValueOZX5MUcmjNTL4/ULK2e2UgRiTxw=dsSignatureValuewWSt0RdNbeUfNXo6dWRO9Jdt4qyy2NXVxntlvyOi8mcgm8mHPqPC86cHCggx/DM/WKlTGOP3bvYgJYSj14GSrfAB3WIVsECOHI00juy5kRnMpFWRcsqMGij+gVX5a6WhAVoJWZox5N1avqJbw0T5bi+2rMG8pzHwfNtfSAQ8OkI=dsKeyInfodsX509DatadsX509CertificateMIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ==artifactId \ No newline at end of file diff --git a/testdata/TestParseXMLArtifactResponse_assertion b/testdata/TestParseXMLArtifactResponse_assertion new file mode 100644 index 00000000..f7b0ac21 --- /dev/null +++ b/testdata/TestParseXMLArtifactResponse_assertion @@ -0,0 +1 @@ +https://samltest.id/saml/idpAAdzZWNyZXQxmdGfdGiCl5GfFqdXr4fFg22uNwB1bPW0DpwXVsA8ZTM9Mm4WZbdwL2HGSb16cikyIjqUeddVshrsVM6DMD/iVagheXMVMIp8Y1JOQsPr6eLL4K7B9u5BIoE6FduV3W60g77uBwM2http://localhost:8000/saml/metadataurn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransporturn:mace:dir:entitlement:common-lib-termsrickrsanchez@samltest.idmanager@samltest.id+1-555-555-5515rsanchez@samltest.idSanchezRick SanchezRick \ No newline at end of file diff --git a/testdata/TestParseXMLArtifactResponse_response b/testdata/TestParseXMLArtifactResponse_response new file mode 100644 index 00000000..71f01dca --- /dev/null +++ b/testdata/TestParseXMLArtifactResponse_response @@ -0,0 +1,36 @@ + +https://samltest.id/saml/idplFfEmKnbMD3hy2rIOa9KSlQWiRbNd5mn8MelKB2TPgo=jl6a7xNieATxehNBKJpKIWvjbp4LjYd2i/9nCk5pKKKyvdUchaz74nILrg6en0iR3HKZl0sGaXmIEzkmpFNRqiam5I0lX1OMzi8HQU99UcbyDtoArLM65uHIExrd5n/W0hnWXcqAjeucNdtWVygx3ptXl36ivh9i2QXM/xi/x4QwBcNZJcbguGYy/0pSmzVcDrmvYvLq2/Sg3M9pDXh1LR5hW5ui/heJFcnDbS2O6Iu0lTbFpVDoeRZz2PTURQqjP2WnsQOfxS0822H2ldFKdXVQdQ10MIGTpq6ckhMFahwbAYophLwLyZCsrl8SWjgovYk+LvumrG/TtQlqz51Iwg==MIIDEjCCAfqgAwIBAgIVAMECQ1tjghafm5OxWDh9hwZfxthWMA0GCSqGSIb3DQEBCwUAMBYxFDAS +BgNVBAMMC3NhbWx0ZXN0LmlkMB4XDTE4MDgyNDIxMTQwOVoXDTM4MDgyNDIxMTQwOVowFjEUMBIG +A1UEAwwLc2FtbHRlc3QuaWQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC0Z4QX1NFK +s71ufbQwoQoW7qkNAJRIANGA4iM0ThYghul3pC+FwrGv37aTxWXfA1UG9njKbbDreiDAZKngCgyj +xj0uJ4lArgkr4AOEjj5zXA81uGHARfUBctvQcsZpBIxDOvUUImAl+3NqLgMGF2fktxMG7kX3GEVN +c1klbN3dfYsaw5dUrw25DheL9np7G/+28GwHPvLb4aptOiONbCaVvh9UMHEA9F7c0zfF/cL5fOpd +Va54wTI0u12CsFKt78h6lEGG5jUs/qX9clZncJM7EFkN3imPPy+0HC8nspXiH/MZW8o2cqWRkrw3 +MzBZW3Ojk5nQj40V6NUbjb7kfejzAgMBAAGjVzBVMB0GA1UdDgQWBBQT6Y9J3Tw/hOGc8PNV7JEE +4k2ZNTA0BgNVHREELTArggtzYW1sdGVzdC5pZIYcaHR0cHM6Ly9zYW1sdGVzdC5pZC9zYW1sL2lk +cDANBgkqhkiG9w0BAQsFAAOCAQEASk3guKfTkVhEaIVvxEPNR2w3vWt3fwmwJCccW98XXLWgNbu3 +YaMb2RSn7Th4p3h+mfyk2don6au7Uyzc1Jd39RNv80TG5iQoxfCgphy1FYmmdaSfO8wvDtHTTNiL +ArAxOYtzfYbzb5QrNNH/gQEN8RJaEf/g/1GTw9x/103dSMK0RXtl+fRs2nblD1JJKSQ3AdhxK/we +P3aUPtLxVVJ9wMOQOfcy02l+hHMb6uAjsPOpOVKqi3M8XmcUZOpx4swtgGdeoSpeRyrtMvRwdcci +NBp9UZome44qZAYH1iqrpmmjsfI9pJItsgWu3kXPjhSfj1AJGR1l9JGvJrHki1iHTA==https://samltest.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 \ No newline at end of file diff --git a/testdata/TestSPCanHandleOneloginResponse_IDPMetadata b/testdata/TestSPCanHandleOneloginResponse_IDPMetadata index 6203a80a..3becbd96 100644 --- a/testdata/TestSPCanHandleOneloginResponse_IDPMetadata +++ b/testdata/TestSPCanHandleOneloginResponse_IDPMetadata @@ -38,4 +38,4 @@ uzZ1y9sNHH6kH8GFnvS2MqyHiNz0h0Sq/q6n+w== Support support@onelogin.com - + \ No newline at end of file diff --git a/testdata/TestSPCanProduceMetadataWithBothCerts_metadata b/testdata/TestSPCanProduceMetadataWithBothCerts_metadata index 428cc13b..3d4792ca 100644 --- a/testdata/TestSPCanProduceMetadataWithBothCerts_metadata +++ b/testdata/TestSPCanProduceMetadataWithBothCerts_metadata @@ -2,8 +2,8 @@ - - MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ== + + MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ== @@ -13,12 +13,14 @@ - - MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ== + + MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ== + urn:oasis:names:tc:SAML:2.0:nameid-format:transient + \ No newline at end of file diff --git a/testdata/TestSPCanProduceMetadataWithBothLougoutBindings_metadata b/testdata/TestSPCanProduceMetadataWithBothLougoutBindings_metadata new file mode 100644 index 00000000..4a1d293f --- /dev/null +++ b/testdata/TestSPCanProduceMetadataWithBothLougoutBindings_metadata @@ -0,0 +1,20 @@ + + + + + + MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ== + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testdata/TestSPCanProduceMetadataWithEncryptionCert_metadata b/testdata/TestSPCanProduceMetadataWithEncryptionCert_metadata index 02d7aeea..e0acc153 100644 --- a/testdata/TestSPCanProduceMetadataWithEncryptionCert_metadata +++ b/testdata/TestSPCanProduceMetadataWithEncryptionCert_metadata @@ -2,8 +2,8 @@ - - MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ== + + MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ== @@ -12,6 +12,8 @@ + + \ No newline at end of file diff --git a/testdata/TestSPCanProduceMetadataWithNoLougoutBindings_metadata b/testdata/TestSPCanProduceMetadataWithNoLougoutBindings_metadata new file mode 100644 index 00000000..71d91b4d --- /dev/null +++ b/testdata/TestSPCanProduceMetadataWithNoLougoutBindings_metadata @@ -0,0 +1,18 @@ + + + + + + MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ== + + + + + + + + + + + + \ No newline at end of file diff --git a/testdata/TestSPMultipleAssertions b/testdata/TestSPMultipleAssertions new file mode 100644 index 00000000..bfa5cb34 --- /dev/null +++ b/testdata/TestSPMultipleAssertions @@ -0,0 +1,8 @@ +https://idp.secureworks.com/SAML2 +Authentication success.https://idp.secureworks.com/SAML2BMN0lUblP0gYGcw2PCyhwFZzkxY=F/2aaOQ3J/S6ULUd+gAuIclVueHEC2UfmtO2eR2oYb/YXub9E22yZe7eQgj2wdhYOvacVXN28QJJJG+K3Njwvi6b7mqf+T8N1YwaJW1fYAm28ayg4dEOTjHnjbRMZ6L+3cZPmPcFyE+edhCHEMnTLSqSvBnSyc1cwGdO9PmfWmt6PzUwf2nr2P5577Yc1FEQ9OtTx7ugWN3iPmjtLeTcpZfIDQX9+gSsh0KT+t61uWaYz+PJhtKnZQFeyr3uIxBTxv4wQ90FnmE4PiDvMksin5CDMfiMwd7pn7rNbk4EVHiDgSMkY6P4h8eWQwiqglOrQSZZr4BJgCoUbcNfZCq/7A==zZlTNJ+QcTp2yGH1ECXO3ry4GHhcs1CW3I6GPiPvtO+P6lyWxYdQd2RK/Hk9Kap6qpm/qom0rTwb +FU2I67Y2JdQ3T5QBJjGHbGHU1uMxVWkhJluoa0Lpm381zNCJTZp8PetoB8dnIGua9y1aL75v04CG +TzJ14I9/sW+apTkWj7xVQXutvVKETdn4kAy+L33HpriZjQNlcuAbqQj6OWsN4tGkLvNFZT40jQzp +/8/tOQE6n2+zn3I8hUePwjPQROUmCeK86CkF0yVCPQ/vOTsC00Uaeu/SPOUu5ot+/75NPyE8w5Ry +DgefdDXhYNmeuQtwGtcu/FI66atQMNTDoChXJQ==AQABrkinder@secureworks.comhttps://preview.docrocket-ross.test.octolabs.io/saml/metadataurn:oasis:names:tc:SAML:2.0:ac:classes:unspecified +https://idp.secureworks.com/SAML2 +admin@evil.comhttps://preview.docrocket-ross.test.octolabs.io/saml/metadataurn:oasis:names:tc:SAML:2.0:ac:classes:unspecified diff --git a/testdata/TestServiceProviderCanHandleSignedAssertionsResponse_IDPMetadata b/testdata/TestServiceProviderCanHandleSignedAssertionsResponse_IDPMetadata index 2bd8b35e..f2b298fc 100644 --- a/testdata/TestServiceProviderCanHandleSignedAssertionsResponse_IDPMetadata +++ b/testdata/TestServiceProviderCanHandleSignedAssertionsResponse_IDPMetadata @@ -17,4 +17,4 @@ Support support@onelogin.com - + \ No newline at end of file diff --git a/testsaml/parse.go b/testsaml/parse.go index 6c64398a..63e3dbc5 100644 --- a/testsaml/parse.go +++ b/testsaml/parse.go @@ -1,3 +1,4 @@ +// Package testsaml contains functions for use in testing SAML requests and responses. package testsaml import ( diff --git a/util.go b/util.go index c9731b1b..eda053ee 100644 --- a/util.go +++ b/util.go @@ -21,6 +21,7 @@ var Clock *dsig.Clock // rand.Reader, but it can be replaced for testing. var RandReader = rand.Reader +//nolint:unparam // This always receives 20, but we want the option to do more or less if needed. func randomBytes(n int) []byte { rv := make([]byte, n) diff --git a/xmlenc/cbc.go b/xmlenc/cbc.go index 11ee210d..991ba1eb 100644 --- a/xmlenc/cbc.go +++ b/xmlenc/cbc.go @@ -31,7 +31,7 @@ func (e CBC) Algorithm() string { // Encrypt encrypts plaintext with key, which should be a []byte of length KeySize(). // It returns an xenc:EncryptedData element. -func (e CBC) Encrypt(key interface{}, plaintext []byte) (*etree.Element, error) { +func (e CBC) Encrypt(key interface{}, plaintext []byte, _ []byte) (*etree.Element, error) { keyBuf, ok := key.([]byte) if !ok { return nil, ErrIncorrectKeyType("[]byte") diff --git a/xmlenc/corpus/encrypt-content-aes192-cbc-dh-sha512.xml b/xmlenc/corpus/encrypt-content-aes192-cbc-dh-sha512.xml index d1242784..2c8069d3 100644 --- a/xmlenc/corpus/encrypt-content-aes192-cbc-dh-sha512.xml +++ b/xmlenc/corpus/encrypt-content-aes192-cbc-dh-sha512.xml @@ -44,7 +44,7 @@ - + MIIDvjCCA36gAwIBAgIGAOxN39MIMAkGByqGSM44BAMwbjELMAkGA1UEBhMCSUUx DzANBgNVBAgTBkR1YmxpbjEkMCIGA1UEChMbQmFsdGltb3JlIFRlY2hub2xvZ2ll cyBMdGQuMREwDwYDVQQLEwhYL1NlY3VyZTEVMBMGA1UEAxMMVHJhbnNpZW50IENB @@ -71,7 +71,7 @@ - + MIIDvjCCA36gAwIBAgIGAOxN3+EMMAkGByqGSM44BAMwbjELMAkGA1UEBhMCSUUx DzANBgNVBAgTBkR1YmxpbjEkMCIGA1UEChMbQmFsdGltb3JlIFRlY2hub2xvZ2ll cyBMdGQuMREwDwYDVQQLEwhYL1NlY3VyZTEVMBMGA1UEAxMMVHJhbnNpZW50IENB diff --git a/xmlenc/corpus/encrypt-data-tripledes-cbc-rsa-oaep-mgf1p-sha256.xml b/xmlenc/corpus/encrypt-data-tripledes-cbc-rsa-oaep-mgf1p-sha256.xml index c9c30e09..562b8f87 100644 --- a/xmlenc/corpus/encrypt-data-tripledes-cbc-rsa-oaep-mgf1p-sha256.xml +++ b/xmlenc/corpus/encrypt-data-tripledes-cbc-rsa-oaep-mgf1p-sha256.xml @@ -10,8 +10,8 @@ - - + + MIICkjCCAfugAwIBAgIGAOxN32E+MA0GCSqGSIb3DQEBBQUAMG4xCzAJBgNVBAYT AklFMQ8wDQYDVQQIEwZEdWJsaW4xJDAiBgNVBAoTG0JhbHRpbW9yZSBUZWNobm9s b2dpZXMgTHRkLjERMA8GA1UECxMIWC9TZWN1cmUxFTATBgNVBAMTDFRyYW5zaWVu diff --git a/xmlenc/corpus/encrypt-data-tripledes-cbc-rsa-oaep-mgf1p.xml b/xmlenc/corpus/encrypt-data-tripledes-cbc-rsa-oaep-mgf1p.xml index 29daa4ea..fef209eb 100644 --- a/xmlenc/corpus/encrypt-data-tripledes-cbc-rsa-oaep-mgf1p.xml +++ b/xmlenc/corpus/encrypt-data-tripledes-cbc-rsa-oaep-mgf1p.xml @@ -7,8 +7,8 @@ - - + + MIICkjCCAfugAwIBAgIGAOxN32E+MA0GCSqGSIb3DQEBBQUAMG4xCzAJBgNVBAYT AklFMQ8wDQYDVQQIEwZEdWJsaW4xJDAiBgNVBAoTG0JhbHRpbW9yZSBUZWNobm9s b2dpZXMgTHRkLjERMA8GA1UECxMIWC9TZWN1cmUxFTATBgNVBAMTDFRyYW5zaWVu diff --git a/xmlenc/corpus/encrypt-element-aes128-cbc-rsa-1_5.xml b/xmlenc/corpus/encrypt-element-aes128-cbc-rsa-1_5.xml index 9d74e16c..eebd0b59 100644 --- a/xmlenc/corpus/encrypt-element-aes128-cbc-rsa-1_5.xml +++ b/xmlenc/corpus/encrypt-element-aes128-cbc-rsa-1_5.xml @@ -20,8 +20,8 @@ - - + + MIICkjCCAfugAwIBAgIGAOxN32E+MA0GCSqGSIb3DQEBBQUAMG4xCzAJBgNVBAYT AklFMQ8wDQYDVQQIEwZEdWJsaW4xJDAiBgNVBAoTG0JhbHRpbW9yZSBUZWNobm9s b2dpZXMgTHRkLjERMA8GA1UECxMIWC9TZWN1cmUxFTATBgNVBAMTDFRyYW5zaWVu diff --git a/xmlenc/corpus/encrypt-element-aes256-cbc-kw-aes256-dh-ripemd160.xml b/xmlenc/corpus/encrypt-element-aes256-cbc-kw-aes256-dh-ripemd160.xml index 5fb336ac..71b77885 100644 --- a/xmlenc/corpus/encrypt-element-aes256-cbc-kw-aes256-dh-ripemd160.xml +++ b/xmlenc/corpus/encrypt-element-aes256-cbc-kw-aes256-dh-ripemd160.xml @@ -46,7 +46,7 @@ - + MIIDvjCCA36gAwIBAgIGAOxN39MIMAkGByqGSM44BAMwbjELMAkGA1UEBhMCSUUx DzANBgNVBAgTBkR1YmxpbjEkMCIGA1UEChMbQmFsdGltb3JlIFRlY2hub2xvZ2ll cyBMdGQuMREwDwYDVQQLEwhYL1NlY3VyZTEVMBMGA1UEAxMMVHJhbnNpZW50IENB @@ -73,7 +73,7 @@ - + MIIDvjCCA36gAwIBAgIGAOxN3+EMMAkGByqGSM44BAMwbjELMAkGA1UEBhMCSUUx DzANBgNVBAgTBkR1YmxpbjEkMCIGA1UEChMbQmFsdGltb3JlIFRlY2hub2xvZ2ll cyBMdGQuMREwDwYDVQQLEwhYL1NlY3VyZTEVMBMGA1UEAxMMVHJhbnNpZW50IENB diff --git a/xmlenc/corpus/encsig-hmac-sha256-dh.xml b/xmlenc/corpus/encsig-hmac-sha256-dh.xml index a69d9361..46af3fb2 100644 --- a/xmlenc/corpus/encsig-hmac-sha256-dh.xml +++ b/xmlenc/corpus/encsig-hmac-sha256-dh.xml @@ -41,7 +41,7 @@ - + MIIDvjCCA36gAwIBAgIGAOxN39MIMAkGByqGSM44BAMwbjELMAkGA1UEBhMCSUUx DzANBgNVBAgTBkR1YmxpbjEkMCIGA1UEChMbQmFsdGltb3JlIFRlY2hub2xvZ2ll cyBMdGQuMREwDwYDVQQLEwhYL1NlY3VyZTEVMBMGA1UEAxMMVHJhbnNpZW50IENB @@ -68,7 +68,7 @@ - + MIIDvjCCA36gAwIBAgIGAOxN3+EMMAkGByqGSM44BAMwbjELMAkGA1UEBhMCSUUx DzANBgNVBAgTBkR1YmxpbjEkMCIGA1UEChMbQmFsdGltb3JlIFRlY2hub2xvZ2ll cyBMdGQuMREwDwYDVQQLEwhYL1NlY3VyZTEVMBMGA1UEAxMMVHJhbnNpZW50IENB diff --git a/xmlenc/corpus/encsig-hmac-sha256-kw-tripledes-dh.xml b/xmlenc/corpus/encsig-hmac-sha256-kw-tripledes-dh.xml index 79ef3f12..f6cf2ac7 100644 --- a/xmlenc/corpus/encsig-hmac-sha256-kw-tripledes-dh.xml +++ b/xmlenc/corpus/encsig-hmac-sha256-kw-tripledes-dh.xml @@ -44,7 +44,7 @@ - + MIIDvjCCA36gAwIBAgIGAOxN39MIMAkGByqGSM44BAMwbjELMAkGA1UEBhMCSUUx DzANBgNVBAgTBkR1YmxpbjEkMCIGA1UEChMbQmFsdGltb3JlIFRlY2hub2xvZ2ll cyBMdGQuMREwDwYDVQQLEwhYL1NlY3VyZTEVMBMGA1UEAxMMVHJhbnNpZW50IENB @@ -71,7 +71,7 @@ - + MIIDvjCCA36gAwIBAgIGAOxN3+EMMAkGByqGSM44BAMwbjELMAkGA1UEBhMCSUUx DzANBgNVBAgTBkR1YmxpbjEkMCIGA1UEChMbQmFsdGltb3JlIFRlY2hub2xvZ2ll cyBMdGQuMREwDwYDVQQLEwhYL1NlY3VyZTEVMBMGA1UEAxMMVHJhbnNpZW50IENB diff --git a/xmlenc/corpus/encsig-hmac-sha256-rsa-1_5.xml b/xmlenc/corpus/encsig-hmac-sha256-rsa-1_5.xml index ecc29878..948cad30 100644 --- a/xmlenc/corpus/encsig-hmac-sha256-rsa-1_5.xml +++ b/xmlenc/corpus/encsig-hmac-sha256-rsa-1_5.xml @@ -15,8 +15,8 @@ - - + + MIICkjCCAfugAwIBAgIGAOxN32E+MA0GCSqGSIb3DQEBBQUAMG4xCzAJBgNVBAYT AklFMQ8wDQYDVQQIEwZEdWJsaW4xJDAiBgNVBAoTG0JhbHRpbW9yZSBUZWNobm9s b2dpZXMgTHRkLjERMA8GA1UECxMIWC9TZWN1cmUxFTATBgNVBAMTDFRyYW5zaWVu diff --git a/xmlenc/corpus/encsig-hmac-sha256-rsa-oaep-mgf1p.xml b/xmlenc/corpus/encsig-hmac-sha256-rsa-oaep-mgf1p.xml index 1779093a..e088df0f 100644 --- a/xmlenc/corpus/encsig-hmac-sha256-rsa-oaep-mgf1p.xml +++ b/xmlenc/corpus/encsig-hmac-sha256-rsa-oaep-mgf1p.xml @@ -20,8 +20,8 @@ - - + + MIICkjCCAfugAwIBAgIGAOxN32E+MA0GCSqGSIb3DQEBBQUAMG4xCzAJBgNVBAYT AklFMQ8wDQYDVQQIEwZEdWJsaW4xJDAiBgNVBAoTG0JhbHRpbW9yZSBUZWNobm9s b2dpZXMgTHRkLjERMA8GA1UECxMIWC9TZWN1cmUxFTATBgNVBAMTDFRyYW5zaWVu diff --git a/xmlenc/crashers/255c4741516851ba13e8151340541d1d247a1d5f b/xmlenc/crashers/255c4741516851ba13e8151340541d1d247a1d5f index 69762d9c..c32edc58 100644 --- a/xmlenc/crashers/255c4741516851ba13e8151340541d1d247a1d5f +++ b/xmlenc/crashers/255c4741516851ba13e8151340541d1d247a1d5f @@ -1 +1 @@ -0 \ No newline at end of file +0 diff --git a/xmlenc/crashers/4d7686c884eb4d4e41d69446018d7c3122c9bc11 b/xmlenc/crashers/4d7686c884eb4d4e41d69446018d7c3122c9bc11 index fd608dc1..99b5da31 100644 --- a/xmlenc/crashers/4d7686c884eb4d4e41d69446018d7c3122c9bc11 +++ b/xmlenc/crashers/4d7686c884eb4d4e41d69446018d7c3122c9bc11 @@ -1 +1 @@ -0 \ No newline at end of file +0 diff --git a/xmlenc/crashers/5837247e7ed40387b811a2a1be52cd91b873d349 b/xmlenc/crashers/5837247e7ed40387b811a2a1be52cd91b873d349 index bda37610..3b8110e6 100644 --- a/xmlenc/crashers/5837247e7ed40387b811a2a1be52cd91b873d349 +++ b/xmlenc/crashers/5837247e7ed40387b811a2a1be52cd91b873d349 @@ -1 +1 @@ -MIICkjCCAfugAwIBAgIGAOxN32E+MA0GCSqGSIb3DQEBBQUAMG4xCzAJBgNVBAYTAklFMQ8wDQYDVQQIEwZEdWJsaW4xJDAiBgNVBAoTG0JhbHRpbW9yZSBUZWNobm9sb2dpZXMgTHRkLjERMA8GA1UECxMIWC9TZWN1cmUxFTATBgNVBAMTDFRyYW5zaWVudCBDQTAeFw0wMjAyMjgxNzUyNDZaFw0wMzAyMjgxNzUyNDBaMG8xCzAJBgNVBAYTAklFMQ8wDQYDVQQIEwZEdWJsaW4xJDAiBgNVBAoTG0JhbHRpbW9yZSBUZWNobm9sb2dpZXMgTHRkLjERMA8GA1UECxMIWC9TZWN1cmUxFjAUBgNVBAMTDU1lcmxpbiBIdWdoZXMwgZ8wDQYGKoZIhvcNAQEBBQADgY0AMIGJAoGBAORdNSxbNFWlQeNsOlYJ9gN9eZD+rguRqKhmhOm7i63VDd5ALm2APXhqAmGBPzLN5jlL9g2XALK5WSO4XKjJMcVfYg4+nPuOeHgqdD4HUgf19j/6SaTMcmDFJQMmx1Qw+Aakq3mGcSfvOJcBZctza50VucfCGL1NdfBEcaL3BnhjAgMBAAGjOjA4MA4GA1UdDwEB/wQEAwIFoDARBgNVHQ4ECgQIjFG0ZGNyvNswEwYDVR0jBAwwCoAIhJXVlhr6O4wwDQYJKoZIhvcNAQEFBQADgYEAXzG7x5aCJYRusTbmuZqhidGM5iiA9+RmZ4JTPDEgbeiTiJROxpr+ZjnATmsDKrCpqNUiHWjmsKEArYQp8R/KjdKl/pVe3jUvTxb0YZ+li/7k0GQ5LyRT/K4c2SgyLlyBPhpMq+z3g4P2egVRaZbxsLuKQILf7MIV/X5iAEBzu1w= \ No newline at end of file +MIICkjCCAfugAwIBAgIGAOxN32E+MA0GCSqGSIb3DQEBBQUAMG4xCzAJBgNVBAYTAklFMQ8wDQYDVQQIEwZEdWJsaW4xJDAiBgNVBAoTG0JhbHRpbW9yZSBUZWNobm9sb2dpZXMgTHRkLjERMA8GA1UECxMIWC9TZWN1cmUxFTATBgNVBAMTDFRyYW5zaWVudCBDQTAeFw0wMjAyMjgxNzUyNDZaFw0wMzAyMjgxNzUyNDBaMG8xCzAJBgNVBAYTAklFMQ8wDQYDVQQIEwZEdWJsaW4xJDAiBgNVBAoTG0JhbHRpbW9yZSBUZWNobm9sb2dpZXMgTHRkLjERMA8GA1UECxMIWC9TZWN1cmUxFjAUBgNVBAMTDU1lcmxpbiBIdWdoZXMwgZ8wDQYGKoZIhvcNAQEBBQADgY0AMIGJAoGBAORdNSxbNFWlQeNsOlYJ9gN9eZD+rguRqKhmhOm7i63VDd5ALm2APXhqAmGBPzLN5jlL9g2XALK5WSO4XKjJMcVfYg4+nPuOeHgqdD4HUgf19j/6SaTMcmDFJQMmx1Qw+Aakq3mGcSfvOJcBZctza50VucfCGL1NdfBEcaL3BnhjAgMBAAGjOjA4MA4GA1UdDwEB/wQEAwIFoDARBgNVHQ4ECgQIjFG0ZGNyvNswEwYDVR0jBAwwCoAIhJXVlhr6O4wwDQYJKoZIhvcNAQEFBQADgYEAXzG7x5aCJYRusTbmuZqhidGM5iiA9+RmZ4JTPDEgbeiTiJROxpr+ZjnATmsDKrCpqNUiHWjmsKEArYQp8R/KjdKl/pVe3jUvTxb0YZ+li/7k0GQ5LyRT/K4c2SgyLlyBPhpMq+z3g4P2egVRaZbxsLuKQILf7MIV/X5iAEBzu1w= diff --git a/xmlenc/crashers/655d116d6e8b8d9c1de179459ee71293a6151792 b/xmlenc/crashers/655d116d6e8b8d9c1de179459ee71293a6151792 index 35ac2fc8..b290dff3 100644 --- a/xmlenc/crashers/655d116d6e8b8d9c1de179459ee71293a6151792 +++ b/xmlenc/crashers/655d116d6e8b8d9c1de179459ee71293a6151792 @@ -1 +1 @@ -MIICkjCCAfugAwIBAgIGAOxN32E+MA0GCSqGEIb3DQEBBQUAMG4xCzAJBgNVBAYTAklFMQ8wDQYDVQQIEwZEdWJsaW4xJDAiBgNVBAoTG0JhbHRpbW9yZSBUZWNobm9sb2dpZXMgTHRkLjERMA8GA1UECxMIWC9TZWN1cmUxFTATBgNVBAMTDFRyYW5zaWVudCBDQTAeFw0wMjAyMjgxNzUyNDZaFw0wMzAyMjgxNzUyNDBaMG8xCzAJBgNVBAYTAklFMQ8wDQYDVQQIEwZEdWJsaW4xJDAiBgNVBAoTG0JhbHRpbW9yZSBUZWNobm9sb2dpZXMgTHRkLjERMA8GA1UECxMIWC9TZWN1cmUxFjAUBgNVBAMTDU1lcmxpbiBIdWdoZXMwgZ8wDQYJKoZIBvcNAQEBBQADgY0ACIGJAoGBAORdNSxbNFWlQeNsOlYJ9gN9eZD+rguRqKhmhOm7i63VDd5ALm2APXhqAmGBPzLN5jlL9g2XALK5WSO4XKjJMcVfYg4+nPuOeHgqdD4HUgf19j/6SaTMcmDFJQMmx1Qw+Aakq3mGcSfvOJcBZctza50VucfCGL1NdfBEcaL3BnhjAgMBAAGjOjA4MA4GA1UdDwEB/wQEAwIFoDARBgNVHQ4ECgQIjFG0ZGNyvNswEwYDVR0jBAwwCoAIhJXVlhr6O4wwDQYJKoZIhvcNAQEFBQADgYEAXzG7x5aCJYRusTbmuZqhidGM5iiA9+RmZ4JTPDEgbeiTiJROxpr+ZjnATmsDKrCpqNUiHWjmsKEArYQp8R/KjdKl/pVe3jUvTxb0YZ+li/7k0GQ5LyRT/K4c2SgyLlyhPhpMq+z3g4P2egVRaZbxsLuKQILf7MIV/X5iAEBzu1w= \ No newline at end of file +MIICkjCCAfugAwIBAgIGAOxN32E+MA0GCSqGEIb3DQEBBQUAMG4xCzAJBgNVBAYTAklFMQ8wDQYDVQQIEwZEdWJsaW4xJDAiBgNVBAoTG0JhbHRpbW9yZSBUZWNobm9sb2dpZXMgTHRkLjERMA8GA1UECxMIWC9TZWN1cmUxFTATBgNVBAMTDFRyYW5zaWVudCBDQTAeFw0wMjAyMjgxNzUyNDZaFw0wMzAyMjgxNzUyNDBaMG8xCzAJBgNVBAYTAklFMQ8wDQYDVQQIEwZEdWJsaW4xJDAiBgNVBAoTG0JhbHRpbW9yZSBUZWNobm9sb2dpZXMgTHRkLjERMA8GA1UECxMIWC9TZWN1cmUxFjAUBgNVBAMTDU1lcmxpbiBIdWdoZXMwgZ8wDQYJKoZIBvcNAQEBBQADgY0ACIGJAoGBAORdNSxbNFWlQeNsOlYJ9gN9eZD+rguRqKhmhOm7i63VDd5ALm2APXhqAmGBPzLN5jlL9g2XALK5WSO4XKjJMcVfYg4+nPuOeHgqdD4HUgf19j/6SaTMcmDFJQMmx1Qw+Aakq3mGcSfvOJcBZctza50VucfCGL1NdfBEcaL3BnhjAgMBAAGjOjA4MA4GA1UdDwEB/wQEAwIFoDARBgNVHQ4ECgQIjFG0ZGNyvNswEwYDVR0jBAwwCoAIhJXVlhr6O4wwDQYJKoZIhvcNAQEFBQADgYEAXzG7x5aCJYRusTbmuZqhidGM5iiA9+RmZ4JTPDEgbeiTiJROxpr+ZjnATmsDKrCpqNUiHWjmsKEArYQp8R/KjdKl/pVe3jUvTxb0YZ+li/7k0GQ5LyRT/K4c2SgyLlyhPhpMq+z3g4P2egVRaZbxsLuKQILf7MIV/X5iAEBzu1w= diff --git a/xmlenc/decrypt.go b/xmlenc/decrypt.go index 93991f9f..98a575da 100644 --- a/xmlenc/decrypt.go +++ b/xmlenc/decrypt.go @@ -90,6 +90,7 @@ func validateRSAKeyIfPresent(key interface{}, encryptedKey *etree.Element) (*rsa // if the key will work, or let the service provider know which key // to use to decrypt the message. Either way, verification is not // security-critical. + //nolint:revive,staticcheck // Keep the later empty branch so that we know to address this at a later date. if el := encryptedKey.FindElement("./KeyInfo/X509Data/X509Certificate"); el != nil { certPEMbuf := el.Text() certPEMbuf = "-----BEGIN CERTIFICATE-----\n" + certPEMbuf + "\n-----END CERTIFICATE-----\n" diff --git a/xmlenc/decrypt_test.go b/xmlenc/decrypt_test.go index ecf46dda..a8872f22 100644 --- a/xmlenc/decrypt_test.go +++ b/xmlenc/decrypt_test.go @@ -13,47 +13,92 @@ import ( ) func TestCanDecrypt(t *testing.T) { - doc := etree.NewDocument() - err := doc.ReadFromBytes(golden.Get(t, "input.xml")) - assert.Check(t, err) - - keyPEM := "-----BEGIN RSA PRIVATE KEY-----\nMIICXgIBAAKBgQDU8wdiaFmPfTyRYuFlVPi866WrH/2JubkHzp89bBQopDaLXYxi\n3PTu3O6Q/KaKxMOFBqrInwqpv/omOGZ4ycQ51O9I+Yc7ybVlW94lTo2gpGf+Y/8E\nPsVbnZaFutRctJ4dVIp9aQ2TpLiGT0xX1OzBO/JEgq9GzDRf+B+eqSuglwIDAQAB\nAoGBAMuy1eN6cgFiCOgBsB3gVDdTKpww87Qk5ivjqEt28SmXO13A1KNVPS6oQ8SJ\nCT5Azc6X/BIAoJCURVL+LHdqebogKljhH/3yIel1kH19vr4E2kTM/tYH+qj8afUS\nJEmArUzsmmK8ccuNqBcllqdwCZjxL4CHDUmyRudFcHVX9oyhAkEA/OV1OkjM3CLU\nN3sqELdMmHq5QZCUihBmk3/N5OvGdqAFGBlEeewlepEVxkh7JnaNXAXrKHRVu/f/\nfbCQxH+qrwJBANeQERF97b9Sibp9xgolb749UWNlAdqmEpmlvmS202TdcaaT1msU\n4rRLiQN3X9O9mq4LZMSVethrQAdX1whawpkCQQDk1yGf7xZpMJ8F4U5sN+F4rLyM\nRq8Sy8p2OBTwzCUXXK+fYeXjybsUUMr6VMYTRP2fQr/LKJIX+E5ZxvcIyFmDAkEA\nyfjNVUNVaIbQTzEbRlRvT6MqR+PTCefC072NF9aJWR93JimspGZMR7viY6IM4lrr\nvBkm0F5yXKaYtoiiDMzlOQJADqmEwXl0D72ZG/2KDg8b4QZEmC9i5gidpQwJXUc6\nhU+IVQoLxRq0fBib/36K9tcrrO5Ba4iEvDcNY+D8yGbUtA==\n-----END RSA PRIVATE KEY-----\n" - b, _ := pem.Decode([]byte(keyPEM)) - key, err := x509.ParsePKCS1PrivateKey(b.Bytes) - assert.Check(t, err) - - el := doc.Root().FindElement("//EncryptedKey") - buf, err := Decrypt(key, el) - assert.Check(t, err) - assert.Check(t, is.DeepEqual([]byte{0xc, 0x70, 0xa2, 0xc8, 0x15, 0x74, 0x89, 0x3f, 0x36, 0xd2, 0x7c, 0x14, 0x2a, 0x9b, 0xaa, 0xd9}, - buf)) - - el = doc.Root().FindElement("//EncryptedData") - buf, err = Decrypt(key, el) - assert.Check(t, err) - golden.Assert(t, string(buf), "plaintext.xml") + t.Run("CBC", func(t *testing.T) { + doc := etree.NewDocument() + err := doc.ReadFromBytes(golden.Get(t, "input.xml")) + assert.Check(t, err) + + keyPEM := "-----BEGIN RSA PRIVATE KEY-----\nMIICXgIBAAKBgQDU8wdiaFmPfTyRYuFlVPi866WrH/2JubkHzp89bBQopDaLXYxi\n3PTu3O6Q/KaKxMOFBqrInwqpv/omOGZ4ycQ51O9I+Yc7ybVlW94lTo2gpGf+Y/8E\nPsVbnZaFutRctJ4dVIp9aQ2TpLiGT0xX1OzBO/JEgq9GzDRf+B+eqSuglwIDAQAB\nAoGBAMuy1eN6cgFiCOgBsB3gVDdTKpww87Qk5ivjqEt28SmXO13A1KNVPS6oQ8SJ\nCT5Azc6X/BIAoJCURVL+LHdqebogKljhH/3yIel1kH19vr4E2kTM/tYH+qj8afUS\nJEmArUzsmmK8ccuNqBcllqdwCZjxL4CHDUmyRudFcHVX9oyhAkEA/OV1OkjM3CLU\nN3sqELdMmHq5QZCUihBmk3/N5OvGdqAFGBlEeewlepEVxkh7JnaNXAXrKHRVu/f/\nfbCQxH+qrwJBANeQERF97b9Sibp9xgolb749UWNlAdqmEpmlvmS202TdcaaT1msU\n4rRLiQN3X9O9mq4LZMSVethrQAdX1whawpkCQQDk1yGf7xZpMJ8F4U5sN+F4rLyM\nRq8Sy8p2OBTwzCUXXK+fYeXjybsUUMr6VMYTRP2fQr/LKJIX+E5ZxvcIyFmDAkEA\nyfjNVUNVaIbQTzEbRlRvT6MqR+PTCefC072NF9aJWR93JimspGZMR7viY6IM4lrr\nvBkm0F5yXKaYtoiiDMzlOQJADqmEwXl0D72ZG/2KDg8b4QZEmC9i5gidpQwJXUc6\nhU+IVQoLxRq0fBib/36K9tcrrO5Ba4iEvDcNY+D8yGbUtA==\n-----END RSA PRIVATE KEY-----\n" + b, _ := pem.Decode([]byte(keyPEM)) + key, err := x509.ParsePKCS1PrivateKey(b.Bytes) + assert.Check(t, err) + + el := doc.Root().FindElement("//EncryptedKey") + buf, err := Decrypt(key, el) + assert.Check(t, err) + assert.Check(t, is.DeepEqual([]byte{0xc, 0x70, 0xa2, 0xc8, 0x15, 0x74, 0x89, 0x3f, 0x36, 0xd2, 0x7c, 0x14, 0x2a, 0x9b, 0xaa, 0xd9}, + buf)) + + el = doc.Root().FindElement("//EncryptedData") + buf, err = Decrypt(key, el) + assert.Check(t, err) + golden.Assert(t, string(buf), "plaintext.xml") + }) + + t.Run("GCM", func(t *testing.T) { + doc := etree.NewDocument() + err := doc.ReadFromBytes(golden.Get(t, "input_gcm.xml")) + assert.Check(t, err) + + keyPEM := golden.Get(t, "cert.key") + b, _ := pem.Decode(keyPEM) + key, err := x509.ParsePKCS8PrivateKey(b.Bytes) + assert.Check(t, err) + + el := doc.Root().FindElement("//EncryptedKey") + _, err = Decrypt(key, el) + assert.Check(t, err) + + el = doc.Root().FindElement("//EncryptedData") + _, err = Decrypt(key, el) + assert.Check(t, err) + }) } func TestCanDecryptWithoutCertificate(t *testing.T) { - doc := etree.NewDocument() - err := doc.ReadFromBytes(golden.Get(t, "input.xml")) - assert.Check(t, err) - - el := doc.FindElement("//ds:X509Certificate") - el.Parent().RemoveChild(el) - - keyPEM := golden.Get(t, "key.pem") - b, _ := pem.Decode(keyPEM) - key, err := x509.ParsePKCS1PrivateKey(b.Bytes) - assert.Check(t, err) - - el = doc.Root().FindElement("//EncryptedKey") - buf, err := Decrypt(key, el) - assert.Check(t, err) - assert.Check(t, is.DeepEqual([]byte{0xc, 0x70, 0xa2, 0xc8, 0x15, 0x74, 0x89, 0x3f, 0x36, 0xd2, 0x7c, 0x14, 0x2a, 0x9b, 0xaa, 0xd9}, buf)) - - el = doc.Root().FindElement("//EncryptedData") - buf, err = Decrypt(key, el) - assert.Check(t, err) - golden.Assert(t, string(buf), "plaintext.xml") + t.Run("CBC", func(t *testing.T) { + doc := etree.NewDocument() + err := doc.ReadFromBytes(golden.Get(t, "input.xml")) + assert.Check(t, err) + + el := doc.FindElement("//ds:X509Certificate") + el.Parent().RemoveChild(el) + + keyPEM := golden.Get(t, "key.pem") + b, _ := pem.Decode(keyPEM) + key, err := x509.ParsePKCS1PrivateKey(b.Bytes) + assert.Check(t, err) + + el = doc.Root().FindElement("//EncryptedKey") + buf, err := Decrypt(key, el) + assert.Check(t, err) + assert.Check(t, is.DeepEqual([]byte{0xc, 0x70, 0xa2, 0xc8, 0x15, 0x74, 0x89, 0x3f, 0x36, 0xd2, 0x7c, 0x14, 0x2a, 0x9b, 0xaa, 0xd9}, buf)) + + el = doc.Root().FindElement("//EncryptedData") + buf, err = Decrypt(key, el) + assert.Check(t, err) + golden.Assert(t, string(buf), "plaintext.xml") + }) + + t.Run("GCM", func(t *testing.T) { + doc := etree.NewDocument() + err := doc.ReadFromBytes(golden.Get(t, "input_gcm.xml")) + assert.Check(t, err) + + el := doc.FindElement("//ds:X509Certificate") + el.Parent().RemoveChild(el) + + keyPEM := golden.Get(t, "cert.key") + b, _ := pem.Decode(keyPEM) + key, err := x509.ParsePKCS8PrivateKey(b.Bytes) + assert.Check(t, err) + + el = doc.Root().FindElement("//EncryptedKey") + _, err = Decrypt(key, el) + assert.Check(t, err) + + el = doc.Root().FindElement("//EncryptedData") + _, err = Decrypt(key, el) + assert.Check(t, err) + }) } diff --git a/xmlenc/digest.go b/xmlenc/digest.go index 801347f2..3eaaf7bc 100644 --- a/xmlenc/digest.go +++ b/xmlenc/digest.go @@ -6,6 +6,7 @@ import ( "crypto/sha512" "hash" + //nolint:staticcheck // We should support this for legacy reasons. "golang.org/x/crypto/ripemd160" ) diff --git a/xmlenc/encrypt_test.go b/xmlenc/encrypt_test.go index cca38604..2cfa86c5 100644 --- a/xmlenc/encrypt_test.go +++ b/xmlenc/encrypt_test.go @@ -6,30 +6,54 @@ import ( "math/rand" "testing" + "github.com/beevik/etree" "gotest.tools/assert" "gotest.tools/golden" - - "github.com/beevik/etree" ) func TestCanEncryptOAEP(t *testing.T) { - RandReader = rand.New(rand.NewSource(0)) //nolint:gosec // deterministic random numbers for tests + t.Run("CBC", func(t *testing.T) { + + RandReader = rand.New(rand.NewSource(0)) //nolint:gosec // deterministic random numbers for tests + + pemBlock, _ := pem.Decode(golden.Get(t, "cert.pem")) + certificate, err := x509.ParseCertificate(pemBlock.Bytes) + assert.Check(t, err) + + e := OAEP() + e.BlockCipher = AES128CBC + e.DigestMethod = &SHA1 + + el, err := e.Encrypt(certificate, golden.Get(t, "plaintext.xml"), nil) + assert.Check(t, err) + + doc := etree.NewDocument() + doc.SetRoot(el) + doc.IndentTabs() + ciphertext, _ := doc.WriteToString() + + golden.Assert(t, ciphertext, "ciphertext.xml") + }) - pemBlock, _ := pem.Decode(golden.Get(t, "cert.pem")) - certificate, err := x509.ParseCertificate(pemBlock.Bytes) - assert.Check(t, err) + t.Run("GCM", func(t *testing.T) { + RandReader = rand.New(rand.NewSource(0)) //nolint:gosec // deterministic random numbers for tests - e := OAEP() - e.BlockCipher = AES128CBC - e.DigestMethod = &SHA1 + cert := golden.Get(t, "cert.cert") + b, _ := pem.Decode(cert) + certificate, err := x509.ParseCertificate(b.Bytes) + assert.Check(t, err) - el, err := e.Encrypt(certificate, golden.Get(t, "plaintext.xml")) - assert.Check(t, err) + e := OAEP() + e.BlockCipher = AES128GCM + e.DigestMethod = &SHA1 - doc := etree.NewDocument() - doc.SetRoot(el) - doc.IndentTabs() - ciphertext, _ := doc.WriteToString() + el, err := e.Encrypt(certificate, golden.Get(t, "plaintext_gcm.xml"), []byte("1234567890AZ")) + assert.Check(t, err) - golden.Assert(t, ciphertext, "ciphertext.xml") + doc := etree.NewDocument() + doc.SetRoot(el) + doc.Indent(4) + ciphertext, _ := doc.WriteToString() + golden.Assert(t, ciphertext, "ciphertext_gcm.xml") + }) } diff --git a/xmlenc/fuzz_test.go b/xmlenc/fuzz_test.go index 07a40443..2d83e49c 100644 --- a/xmlenc/fuzz_test.go +++ b/xmlenc/fuzz_test.go @@ -1,3 +1,4 @@ +//go:build gofuzz // +build gofuzz package xmlenc diff --git a/xmlenc/gcm.go b/xmlenc/gcm.go new file mode 100644 index 00000000..91911319 --- /dev/null +++ b/xmlenc/gcm.go @@ -0,0 +1,143 @@ +package xmlenc + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "fmt" + "io" + + "github.com/beevik/etree" +) + +// GCM implements Decrypter and Encrypter for block ciphers in struct mode +type GCM struct { + keySize int + algorithm string + cipher func([]byte) (cipher.Block, error) +} + +// KeySize returns the length of the key required. +func (e GCM) KeySize() int { + return e.keySize +} + +// Algorithm returns the name of the algorithm, as will be found +// in an xenc:EncryptionMethod element. +func (e GCM) Algorithm() string { + return e.algorithm +} + +// Encrypt encrypts plaintext with key and nonce +func (e GCM) Encrypt(key interface{}, plaintext []byte, nonce []byte) (*etree.Element, error) { + keyBuf, ok := key.([]byte) + if !ok { + return nil, ErrIncorrectKeyType("[]byte") + } + if len(keyBuf) != e.keySize { + return nil, ErrIncorrectKeyLength(e.keySize) + } + + block, err := e.cipher(keyBuf) + if err != nil { + return nil, err + } + + encryptedDataEl := etree.NewElement("xenc:EncryptedData") + encryptedDataEl.CreateAttr("xmlns:xenc", "http://www.w3.org/2001/04/xmlenc#") + { + randBuf := make([]byte, 16) + if _, err := RandReader.Read(randBuf); err != nil { + return nil, err + } + encryptedDataEl.CreateAttr("Id", fmt.Sprintf("_%x", randBuf)) + } + + em := encryptedDataEl.CreateElement("xenc:EncryptionMethod") + em.CreateAttr("Algorithm", e.algorithm) + em.CreateAttr("xmlns:xenc", "http://www.w3.org/2001/04/xmlenc#") + + plaintext = appendPadding(plaintext, block.BlockSize()) + + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + if nonce == nil { + // generate random nonce when it's nil + nonce := make([]byte, aesgcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + panic(err.Error()) + } + } + + ciphertext := make([]byte, len(plaintext)) + text := aesgcm.Seal(nil, nonce, ciphertext, nil) + + cd := encryptedDataEl.CreateElement("xenc:CipherData") + cd.CreateAttr("xmlns:xenc", "http://www.w3.org/2001/04/xmlenc#") + cd.CreateElement("xenc:CipherValue").SetText(base64.StdEncoding.EncodeToString(text)) + return encryptedDataEl, nil +} + +// Decrypt decrypts an encrypted element with key. If the ciphertext contains an +// EncryptedKey element, then the type of `key` is determined by the registered +// Decryptor for the EncryptedKey element. Otherwise, `key` must be a []byte of +// length KeySize(). +func (e GCM) Decrypt(key interface{}, ciphertextEl *etree.Element) ([]byte, error) { + if encryptedKeyEl := ciphertextEl.FindElement("./KeyInfo/EncryptedKey"); encryptedKeyEl != nil { + var err error + key, err = Decrypt(key, encryptedKeyEl) + if err != nil { + return nil, err + } + } + + keyBuf, ok := key.([]byte) + + if !ok { + return nil, ErrIncorrectKeyType("[]byte") + } + if len(keyBuf) != e.KeySize() { + return nil, ErrIncorrectKeyLength(e.KeySize()) + } + + block, err := e.cipher(keyBuf) + if err != nil { + return nil, err + } + + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + ciphertext, err := getCiphertext(ciphertextEl) + if err != nil { + return nil, err + } + + nonce := ciphertext[:aesgcm.NonceSize()] + text := ciphertext[aesgcm.NonceSize():] + + plainText, err := aesgcm.Open(nil, nonce, text, nil) + if err != nil { + return nil, err + } + return plainText, nil +} + +var ( + // AES128GCM implements AES128-GCM mode for encryption and decryption + AES128GCM BlockCipher = GCM{ + keySize: 16, + algorithm: "http://www.w3.org/2009/xmlenc11#aes128-gcm", + cipher: aes.NewCipher, + } +) + +func init() { + RegisterDecrypter(AES128GCM) +} diff --git a/xmlenc/pubkey.go b/xmlenc/pubkey.go index 72286863..13d4d9e7 100644 --- a/xmlenc/pubkey.go +++ b/xmlenc/pubkey.go @@ -29,7 +29,7 @@ func (e RSA) Algorithm() string { // Encrypt implements encrypter. certificate must be a []byte containing the ASN.1 bytes // of certificate containing an RSA public key. -func (e RSA) Encrypt(certificate interface{}, plaintext []byte) (*etree.Element, error) { +func (e RSA) Encrypt(certificate interface{}, plaintext []byte, nonce []byte) (*etree.Element, error) { cert, ok := certificate.(*x509.Certificate) if !ok { return nil, ErrIncorrectKeyType("*x.509 certificate") @@ -83,11 +83,11 @@ func (e RSA) Encrypt(certificate interface{}, plaintext []byte) (*etree.Element, cd := encryptedKey.CreateElement("xenc:CipherData") cd.CreateAttr("xmlns:xenc", "http://www.w3.org/2001/04/xmlenc#") cd.CreateElement("xenc:CipherValue").SetText(base64.StdEncoding.EncodeToString(buf)) - encryptedDataEl, err := e.BlockCipher.Encrypt(key, plaintext) + encryptedDataEl, err := e.BlockCipher.Encrypt(key, plaintext, nonce) if err != nil { return nil, err } - encryptedDataEl.InsertChild(encryptedDataEl.FindElement("./CipherData"), keyInfoEl) + encryptedDataEl.InsertChildAt(encryptedDataEl.FindElement("./CipherData").Index(), keyInfoEl) return encryptedDataEl, nil } diff --git a/xmlenc/testdata/cert.cert b/xmlenc/testdata/cert.cert new file mode 100644 index 00000000..fea41ca9 --- /dev/null +++ b/xmlenc/testdata/cert.cert @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIE0DCCArgCCQDQ3vxsffYA7DANBgkqhkiG9w0BAQsFADAqMSgwJgYDVQQDDB9t +YWluLmF1dGgtc3NvLmRlLnFhLm1lZGljdWphLmRlMB4XDTIxMDcxOTEzMzkxOFoX +DTIyMDcxOTEzMzkxOFowKjEoMCYGA1UEAwwfbWFpbi5hdXRoLXNzby5kZS5xYS5t +ZWRpY3VqYS5kZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJe50Ka+ +qNwgmKeoLEDYTmIbLRAyVX6lgO2RKzVzDzf6LyrWtZL1SKbbA6VdmzPlo4kzA3di +f4/XgjS6hWeXXKaqXV8g/wKSSIhFTkp/WIrZY7KHF9mTYXdOrPGkR9IENiMevsVg +RfcBnnHTGgczeiAFZm1xnkHBmVPZZjfuoi3nbjrMCh8uR3EqDsLzIHJ9PB2F4KvW +KDuDaqdNOocI3fxEZuy/Ahwpr5FORJYZDk24ZusTPniy/+xFREneHPZaB+tFCxtj +3PdmCpIF54tj9dVLOMkQzbtPiTCGx2own9yjwa7NYYV8y8fBUcpuexbk+576J/rL +MIFvFusf8PaL0QfMnkCJGL1T5O8wTEdpZ6h17rVzJtaK4lLNsO9+7vtW2gYFgLGj +nr/Zg6ZKfUfZLm6h3bfjtKRKnAzrqI4E6f1nvwyEglqE5BHEXriI2+tbsrMwHQoy +XVray8KY1WVrCnFFQFtb22BPSQLHu/n4GIOz7k34h7mgWOPnlW2p625UXiuFbWJD +4sdNw7encs3gkILStbaRX1WZHwb2QjfFDvMS1k1KhdktELxmZKR8bDXRzymMYwls +KInPC2s92u+L7wesuqQI2GKeMnH6jEdFdqPwXo8ObgI6GPOxzVS6Q3BnI4SXTxeC +4wQq6hQpqZS4rge0dfxHttwRVywdzjSKVw5ZAgMBAAEwDQYJKoZIhvcNAQELBQAD +ggIBABVWyAQh+880dOqd/LEX7DR4mwVnaRnOn+Xl6gbdoKJK0KyoQWEigW+Rj4Rt +uBdi56NvxbBQk8UIamO0zFJzCpJ936OhoMwE3ZZETto0YAVtGqPlZRBJxLQo56Wz +Rtmpy3bHOpQsYZRTYOXjoFxG+pHhiOjJ5W1djRqyOPa3I8H3tsHixUOZcycHIIo6 +gLRBvsVuDgrI4YDtX8mw695nmbFzwDbpkf0kPAb18eihrFqKlI4JNegn1RE/zkHe +4kC3xpaqt/XlsU39evgUb78S8ZsVgtyt+S5ywChcRunyyv5fyQm79ZRtVPVS7oPJ +OHWG4EpzuU1XAqDu7mGfZW7Aks3j8OrGzmzt72CfcBuRaAomgdllMalO6iUkDVdh +q2RmjBKaVbh+9aq4JB+cf88TfcrzuB4lBWeOdwOgQuhn8zB0qiuHJ9TgTUAuVeIX +HI+/I5s0FFbqSCfOraFAwbfxecSs9rEaZJvLMQ5mB9g7LW3tRSYGNT2ujOQQygKI ++RDNMDNLS5wO/M2DdSDJcyrrxZXiZp2/ob6ovRERmIIlxIoaQkON2pJ0eLnt0Qz5 +wrqOhgbJSPtNUdMMD3GtGm3eZn+F0Gm0WzpMD3VVHpJYxcnvFe2pgQrS6DhRCjkr +paYGxi0gsWxwWE+H5dFPayliO0EoLQIojSKTdRVIogvy62S9 +-----END CERTIFICATE----- diff --git a/xmlenc/testdata/cert.key b/xmlenc/testdata/cert.key new file mode 100644 index 00000000..c9bdc52e --- /dev/null +++ b/xmlenc/testdata/cert.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +xESNSIqhp4ubdwD87Ec0oeEcIqHzwRM= +-----END PRIVATE KEY----- diff --git a/xmlenc/testdata/ciphertext_gcm.xml b/xmlenc/testdata/ciphertext_gcm.xml new file mode 100644 index 00000000..f722e6fc --- /dev/null +++ b/xmlenc/testdata/ciphertext_gcm.xml @@ -0,0 +1,21 @@ + + + + + + + + + + MIIE0DCCArgCCQDQ3vxsffYA7DANBgkqhkiG9w0BAQsFADAqMSgwJgYDVQQDDB9tYWluLmF1dGgtc3NvLmRlLnFhLm1lZGljdWphLmRlMB4XDTIxMDcxOTEzMzkxOFoXDTIyMDcxOTEzMzkxOFowKjEoMCYGA1UEAwwfbWFpbi5hdXRoLXNzby5kZS5xYS5tZWRpY3VqYS5kZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJe50Ka+qNwgmKeoLEDYTmIbLRAyVX6lgO2RKzVzDzf6LyrWtZL1SKbbA6VdmzPlo4kzA3dif4/XgjS6hWeXXKaqXV8g/wKSSIhFTkp/WIrZY7KHF9mTYXdOrPGkR9IENiMevsVgRfcBnnHTGgczeiAFZm1xnkHBmVPZZjfuoi3nbjrMCh8uR3EqDsLzIHJ9PB2F4KvWKDuDaqdNOocI3fxEZuy/Ahwpr5FORJYZDk24ZusTPniy/+xFREneHPZaB+tFCxtj3PdmCpIF54tj9dVLOMkQzbtPiTCGx2own9yjwa7NYYV8y8fBUcpuexbk+576J/rLMIFvFusf8PaL0QfMnkCJGL1T5O8wTEdpZ6h17rVzJtaK4lLNsO9+7vtW2gYFgLGjnr/Zg6ZKfUfZLm6h3bfjtKRKnAzrqI4E6f1nvwyEglqE5BHEXriI2+tbsrMwHQoyXVray8KY1WVrCnFFQFtb22BPSQLHu/n4GIOz7k34h7mgWOPnlW2p625UXiuFbWJD4sdNw7encs3gkILStbaRX1WZHwb2QjfFDvMS1k1KhdktELxmZKR8bDXRzymMYwlsKInPC2s92u+L7wesuqQI2GKeMnH6jEdFdqPwXo8ObgI6GPOxzVS6Q3BnI4SXTxeC4wQq6hQpqZS4rge0dfxHttwRVywdzjSKVw5ZAgMBAAEwDQYJKoZIhvcNAQELBQADggIBABVWyAQh+880dOqd/LEX7DR4mwVnaRnOn+Xl6gbdoKJK0KyoQWEigW+Rj4RtuBdi56NvxbBQk8UIamO0zFJzCpJ936OhoMwE3ZZETto0YAVtGqPlZRBJxLQo56WzRtmpy3bHOpQsYZRTYOXjoFxG+pHhiOjJ5W1djRqyOPa3I8H3tsHixUOZcycHIIo6gLRBvsVuDgrI4YDtX8mw695nmbFzwDbpkf0kPAb18eihrFqKlI4JNegn1RE/zkHe4kC3xpaqt/XlsU39evgUb78S8ZsVgtyt+S5ywChcRunyyv5fyQm79ZRtVPVS7oPJOHWG4EpzuU1XAqDu7mGfZW7Aks3j8OrGzmzt72CfcBuRaAomgdllMalO6iUkDVdhq2RmjBKaVbh+9aq4JB+cf88TfcrzuB4lBWeOdwOgQuhn8zB0qiuHJ9TgTUAuVeIXHI+/I5s0FFbqSCfOraFAwbfxecSs9rEaZJvLMQ5mB9g7LW3tRSYGNT2ujOQQygKI+RDNMDNLS5wO/M2DdSDJcyrrxZXiZp2/ob6ovRERmIIlxIoaQkON2pJ0eLnt0Qz5wrqOhgbJSPtNUdMMD3GtGm3eZn+F0Gm0WzpMD3VVHpJYxcnvFe2pgQrS6DhRCjkrpaYGxi0gsWxwWE+H5dFPayliO0EoLQIojSKTdRVIogvy62S9 + + + + DkEXwHS0QDVKFx1KweyD4/VQKD5iITl/6WpaZA6QtJSDSuV7TI6yCT5gylhSAhe2eaoFsiT57XNCD4VVn6Z1QDIUiW+4QM+Y4Ur/ULCMXpWvFYm5jsXC2FW2amqCldE00D7U4tzRNjy9kC+f1QJKlXQ59VY3N+Ee4EekTQSiS1LC+ChZLWf+G1zbBAZF5J163a/z3xtU863x3hIEaNdiA3IYXKQP/6vIw96UKH84zrRbJA43xbP5G2L31dixDPxlkfxkjxQMaqzc17bDjZlyfy5cfdgD75YsgGeQUqec/d06AcSGVVKb5itt6hZ0vNrkeaGfus0L3kTn7nQQfD4pSprmHDhrGvdxhgZywYtDdJ0HFapdvlwxUIOv6Cpss8/nDVgT+yoPvk5QfYn1GNqBEY725yFyQ0vVjZtFYg/r23LzhnX/z2++tlcSs6CL+0uv8dAiWww9Fg/LgLFYUMKR/+hW+msfatri1GUXa2Wv2VDaYUT40o43uaPReqnMNkw69ksmYC45KetWI48x3wwSDlUSuuPTOIMsX1LxxyOD5znbrHhcm3S1AMUbb/ithtxGfWRZnThqjJ28r4QBjlNHlby/vae0Kp268/JRDflFNm0UyGjGNKe8cLT2S21Gcf2S+gybVFsz0eJJOU7g3el/hP9zgV+e5ykXR4v2vclhXfk= + + + + + JEFV/mtWQpyZiWO7++64JyqTJyT/ODvBLztsFOSxeu9I4hDIGhNHTYEKnFT+sLii604tpBYUoItc9urYCpLNewTEHvUMBqFYhBbchV+v17Qa+jibh9NziQ7gYHiLeYABPCy6iEIVI0/VdfwwTX718wV27IT9MuzEOUPMDSDHukQzgf9rXTw9FeONDUHmp0arygfyGHoYHobJEpTYmAD7DiWgPNzk0mNp2WcAND+NjC48tUP7O7Z5wLCevugr4kJ/X8WgbZgyLSq4v+KiqjJxjnqDD7GtNg6o9QEtH0HEuSg46S/IcPgPtMyAEzuky48tchPYT5YuImXH7nEw292XsiFl49nXAHsS1d1DDlkfWTLqlytWW/wyMp3WiYx41moY1ykY6/A4lm14RAY8rmIg3cJWQjkTeHVm+zoDaFM89r0Xw7uJA7XRLekcfHBd/u238IDKUC3tzC3hwPFnktkYZQrc9qqeAEM34TX/lb9ROFBkMRhUT8EID3s5BUATtzN8+kDrAemfh+Q+qQq54ZSDM1tjgYKn+Wl57JMWsJEZ9yfmQ/K7TV1ueO+RmonnEnpAnegUzdKPPfyWoGgXvWl6LPVfUcd8cdHhP+mCoxAw9j5NXQYWBGPLDp0jeoLRr1msjC+aL8weQCApwtcZUHr5hnt3Ou/zMTMSeV1Mtc5REKrXr74eQqzeQg8hVey5HEw8Bk7+6WYSMtHg6Oz/tnE3o0qOLrNXK3pvT6L1Q4arF2IGXfVGLCmjE5E0vjqGM7GCo2k23h/7WSntA+M3Bh//j+m+xAS/0xToz1PyxQpOMRAnbMs2wH3xrCuKYzmmN61HKJdNlWW+BGl8G9OI/Ja9NH/9uU8mnTYukoPWIcAe9Ix3hTAV/wC5XuxOFAbWVCNQ/GST0o5VUdnYQZ4IHYEZ/sLVd/xldjsN0dr20BWy77TbyY0eNh5M7yILJjy6fSfjGLKSxQa9sdcJTBMh3ftqK7WkcqK63GLrRNvzpz71g+3HhODejZJqTl2DuJUteAWnwjMRMSywtizT5F1cnj5EM73/V410Epaf/qSyFbS7IgpkkVp0okrYnqa7Nrm9u2oFpXRxf+NJTkm9/Zosr7x3n32abdec5ydiXWX/na5qFX3QmxzmRMT/21Pdemp5IeurkxQaksTwuX4HRmAQn6XiT1LFaCe2jUZUKX0820ipj2Qoe/ssEPpAbn+eZD95gHNBb54DkE0bZ0ZBsCTfkZ/+4FIYnIfb4/O1nSKAoDHUTehJSTVNfo7huLtOIw3dsBOFUGdR/bRDvBjRHFr2Bglftuo2N9/ejXWNzS3vGC+qKl0oz+9yrbZ7DYOnqmDmZ6nEYEjb8uIRyKRbOdWeZFPwvsIrnAwNiJ0OPJMn6z5KZhazSdEMN5faZqQSqMcThy7IGzaJgQL5eSsV9p9Y9P13xEZCKRSDIigADdOh8bg+GhvbleyvoriDdY1PV6r1VgxRYZJCm4atj3MZg2BtrbQMJBhVQztyQfoeITltUt8CMtd2SmQNy9Ss23ohCZeOJvMtBuAnYIE9NkrwK88QxWB524x3zuXiKJEcoHvZaJmFW5yjgoFK2zEPNyYa54qeZdPsC0e81viaK5yVf8TlBjnxyzoLGZI19j0hSnIUi93o51igFkU6M0PdHWj3/tD5kafoI2wwVVaPEnNfwklFypuohAqYAnw+KAZh0K9zADKtRj2BYCScor2jJ/aTrRtI/taamTlMaOMaQ8pax4xjuNJQONqM+AbNOspj8L2SEuAnar0NfYb9o63mwzD8CVz4Goqj6eysqqcRPm8iebCVICQ6KF1cajnhQn7/uRhcktDkdPdxEHk9WK2J3v2lqMoZzWDemuLRmAzEGT1/cdJjGOVWfi1LoJjU8Eaa4tHgxXeTiu96C0aQMHeAxwn4sjg0uUPK8/EEhSISfGOpocKGvWtU+dEvFXOfTXpGFyUIftySMNRWkAtFHUxcRTk16z0vCk/jgNfZf6HYW8WyiLQFJSux7YAihwPuLKcjUKCgjUN94j2k5ml9WBq7QPx3Qz55BkLxuWvRUkiqZm1xd2bX8Y0i1B/j3gyLjA5T8E5Wv5Uyh0OtdAECqpIirFbk5IC4vNSqffRoIMLfH1vdCKMGu5aRKIV0R68kmAH6XyII4pVhAKqem8dSIVgblX7RL1QdOtImOEyT506OeTbCvl9lx+uusyANC8K60+LcfJcHonJANUqnV5wz4xNnN9lUxCFk9d7ibfl3W1yB0uq9tFa4kqXyx5F3DazPM9VhrtqgrPFfhUwer+MTBzOQVe8uBpWLjVE3930J8fdU/xJctX2AdYX1OmdWSlBKByur1/6Bkb34bK7MGWabfsMAhKGtAKKZMyxtHBRK/zlWCeVQutb28u1apkNNJANxGlOwPxVlrkkJ1VBvPOzssWG7H+6GClrVqxihPXAsEHt23xCK2uwRILiF/dG/XARy+7+d8ozSbN5aH4FLCkAgQ6HFu8/Mk2y+zEF41VJ/J7i0CWFBGmyT9R4fdE6ZitqjyABukLzKYZlDp+SKsfD/QahTDFbffQAKAQyiR0bsSg3yOozQZeey7UWNerM4drYzMs6w9om3l4n1C/fpwNIqBIaDY1kwkHrhZsxfZbL0T8hcRqZaL3BxVbpm4PY76QnCDtMHLUZYb2y+FWes1IGAJn+QXvGkY+NP+tsrxYeHu+70kgC9JK+jQbz5d5CxWo8uBQwjiBBjSiTfJtJEc+2VQ+D7pKBMwUDWejX69hHtq+zOZkMyqUs8qEbDYhOmO/empI3L887H9h6F2seisEuRc/mxKGa0pwEYGIt1vS/gh9AczfxxPKgt9tHBcF3Ic9bW1xOjCXye51Ad247SXcrk/byJvspDeUlBBsREzw7v/pdSToPADrohRKGih2o563/gHzk6WxF/2KM+CUg6MaBe/6DF2vs3vUxNS3rZU02Iw/Jcsg2qH4zQ1lwvb+adbHl3MLlXEsg2GwsWw6A7Byl/SI+tTqi5RTiGceR9hyGZB8FKJGRsmZlm1XKTZ+sH5mcV3JxziXT4NzXmjyLJr/zyNEQ5k95vtrfykLKFzBEExpl/ILmKhLDxJNa3tOOr35sndgyJajOEZRPMEG0Wx0/xI7D1gxgzc+UuGbTzWHSXZxHIOQgtBLBCs5I2qImnUj3wCt68NjAZtTxlJ6hHE7lDKN/xEtQ5bapg+Fy++m3EZa/m4LgeJtId5rLJhqqg+XMl1PSaVLR2YdM/mDtopZjvp1Ob0tucNrRPj7I0GyhHLeOrPR3Y5q/byFw+NTp6jn/E+1aUHrsYc/ODKUZ6bXf+mONzmUi00zr/M8wtu6T77Yu62pb+thUzsDRTe12sJvkqCnHIaGDhTWv/3tyf0PTRhLnok2g8FdgaPxzL0vBxBcLhKzfkyPq70Yz0g9oETt+JKGbdS5kIhbiKZizmEG++er22/RaYsc9D7T57yo5cA5NhuKGh/8DUIIQyNwPuvJDx/l6r6yKw0rB3vet5d+EGVa7ucPNHCxoNyrz3pJ1G1PSEkCr0SBV8q2ug/YZwHNFiuc31RnVSBvQ1OFeye/ny8fcObtFPhiKnu4a1CGcBT9dx6umyJINLG1K4PdS0UsuLuiRdbn0w7Dt3ULyD54fmjfSoQoO68ozo2NgdJUjGanP6vaMnO9XDfSEN+7vGiS9li0ugpH+RSdrPdzc9sEEv7ZT/tac650viSwUWuMb4I6E/B5REseVLK+/WDYGvORh1rn1EeZFae5mZYVQHxAZmekm8LP8WvQdWCBTDai78t/EFnzcgENn6ighm+b65ZfRcsHd5Qq0yfvqGnbybiqFu0emeP7W5Zazq20EjPcGdnQ04a5isoEHp/+P3Z8+Xnygw0AfAtkzaYpKdgKqJbOkHxvPVVFJtAfv/+M/e0ax+ZqjGZwB1gONaNnJYLmC2C8+HJZzUbHTDWe7V3H31os1xgaVn2MpojQ/N8Yawq3S8OcOQf+7phAR5EQ8ut5/CJKKnlPglyqZmzw1fCt8IWIEpx865f7TA0KVY7X/OTAqEe1dt6cRm4OcxoqtcWQ5xtYiQYy6J1OoGcWSYW+LA7cXTrPBDtx3UMS3my1DDx9B99or/9dTawLFlxXI9fNkAta/U668s3rw4zLYnggJtCdoCOtGwnZdIiZllQCMdcbSPEtXOeGwugILuTM3JwMhETz7331CgrXoHCT9B9p+rwA8iJArdms3QtLWtz2tZgtzXRW8aPFBwWniiv/JFY0vdjzqcfxAelbzveetAXpPxlL49Zw7Z3gXknVkyFdWmswmjp4uLW02I9qeLE0eK3GU6enVDobJgiW1xG3Ijl6PlfNS+QGJXq44VX2PpEndWuL7KDgEbKZqKICrh5ibcE30+F6L1qRXJw80+ahxD0haatalSSmZ7pY005V0LyHyXGqlK3qWPo3ktmhGjOYt9AVRg5EEZyifSSMVSHfUVAA9751rCfUPugT8gjISNr6MjGzb/eCCvznaj0Hzz+o8gjuCepg97QjsnIvhmQ2fElCttxr+vLNrTsqJz6WDYq7BuwQXH1UTVmBGfhaQxadaJffhVmGmaOg4S4mPbRXHhTH5Upl87xH3ahSIsAs3r6AACDLwbbJIJEGW36BRYAZEZsfO/CaLZmcPcF2ZK0rIz6SBgfRghcgK4ZSJuZ9CXT/T0UgnqUDdE+XDHcgVevWwziCSBhTOSj5yK8fb95UrhhULqpXeDMlTBeNH0e+FS3xNPd1eFig5Gp38u6/1q5UINyFZlVHw7G4pOz2utALP6CXB1UKgnF1kRfdX+ivxtboBLTPhRPvQlMphYecWYaF3emkTUGswdyLDXmI1rIlj9JYflkC6CX8E0Yq/9BKqjKzxtv9rY+2xY5QIZSM9lu6akni9ed5/e/uV0LcN30sBD9srExJRlAsP39mhMNo903fwkGQKu/UX/wOA531tiEMLIIprFcTQ+3O00dXjULMP2kLrmL9myi/ofaeQ33+Twr9v1JG5i0YE1J92ZX/+TM1ErIUXV/k07uFQOk2T6mez2JskXX6a063+TH0eziflv2CNpW1ScKBgb0WKTq7QIDg/QpLKebKyvwuPujfVFbd7sf0e7hwZ7ofOhjkdCNHpf+/ZotIEIBW5fCRnoEpacnh3o4SnoVDF5tuuNuh0UhUFg1nxWSRE85jynZFlj/kN28sWDSQjLRPw17gqYg3MeUCPAMDAPk2veXG7RQ4PTYlq/sAALsGb/6PJKHr8OiX08Ff/+i8x/u7qyCyvzGyRYl6I6UvVI/B/XHaAcIQEW3lMZoDdfw4Y89HIEfn9CqiZwEIwC8zYava+QwvYRrLR4//k3GkmCZam3g45fDZNpay10I0Yku5Mzd+G4yZg+kwccx7eOL7K7QrovEmBQV+QoJmYVXKXktMHSTm4wNPKoBEycjql2OHIVzTylivraFGbxI+DhsiDGj8hmk/FZ2yxD5Z2pcleX1RTKyygtjeWRagObgz9XEOMit9He3qRnB6HQMObf7TkCrl6PdvHqtmgCUaqAGWJX2AgAqUOUAkTvIPiJAy/vYxFkWvZYzs75TuCiqCE+S2Qa2/ywjiwI/uta5F1Fv4jOqUu6TZbgO9Ocsu5rhDh1fyc1E3HNDyabi6h84plS3t1miGP80z8gwq5BC4dPjDk8oGQdTCByjS+uDNYiWG0SItNzPx5xZbGJgLGJTNW7HhNFWWdmVaAI2PuQ2sCFaBfxXmf1iBwJn0JMhUbRz1hiQ08LrShRzZNeNZnUeKOyjsz7Roeb3JbOIvl5L0JB19zRAO1+wRA73jJK4cfR4Vu1ChpGomkCwyQhXh+vOcKy264/qsdmq3Opq/z8I99Yl6ZOMSu+Ri+WFHBMbdud4i0v7F2MiPVaU7f76aEI66WaBRdLQ1N5bLs8qveZedu4CxrX7xIHwY5zX2MxoRGvwOYdBoCseehNH9xGd+mMHmDtFBFewQpWOMqb0Db8r8IrWuU/+SgvRJiFWoEDAEv86kkFlvoFSqLnq5uApyIDozgLou3mUOje0bU2q+FXVA5EgvfMEzuD9/PQ6MSltixeiafAYr6mx/2S5EdvT4kXw8qxwJbg++e7xsbsyU65CtQyW9BcNOjg40L27TpHWRYgSXoajgynN/dA7xgYnWms+AihaMCjqljQFRPbvGbqUIGeNMR88gYim4PDjrlIRDlhvTSXTGxhlpBWc86KNUx3o2Y4EwxDRs05SeuhZ5wTQcIdYtu047DcHdtnMh2vYYjpfX1UqiTOwcPbkwZbFavUcI8a8Y3sYZylpegHMDiL22Di9hjpS4RXmmybCpRBmAaKGFx9LPDybI5HW0d2u458bqs2pLt+0lGOPchipFjK92i3ieSB+DC3meZZk3JkpRhLtWe+UMjww20HwessU6+6+KIJ6WryqKU3bzXPKEE6hv9DV2FC4Npsi9T6Z/Ybke8MQHRYhaZFQM+MbvFp0TZPgjg5sTNWRKKiZm+0wiVyDsms9L5pO9q3xgIf2CdXwBoJC5s+AilkP5d3WmtITcgx01EfAHvlhZnGmmkbBUrWeZJQQ7P6x9drbGMaMWzvQNjm4lDWAbEk1SbI2Gqbf7Ky7fPh6USips1wtW0GZ8RTZMA6+2aOzg6JCm24lbizaB4swSHHJQgWm2HkOth3fmKyGB+uYr0DSaxWLcJsZno8gUxSuE3jPLsUOI+f0YnMNuoMGJXELvFFWHUqSLInaG9QP5lECP4MdbZzSjaeoiXmpelsFwohY/UMgPb1T4KMj27AWQAHPOUMwwYxF4P/KJ9C0vG5LY/p6TPZupKad7rfyomyMii3IdPeeYRhHqNnxbKioOpYGuZs61UiVbKjTeL+AftDyUCTlUIe3YVorJVxNYby1+d9y7gEpCN78sBwSpo6HJ7T0Pu0XtfuMvgHHBgF1VbDeRmHP89SADvXtEhjsJbQ/gQ3kuTKf/yBGX2WybWnjtd20LXuqEqIrydhUC/rUZ2c/thezD0Pk+H4AFr3lw9+C5G2IcdeeBqU4/w5PjgFAscsE+hLQab1eTpLvxN55kUxErisqJILFtLvDZVYZg+v6bCUOGpp1BxC6RaD+TlqPl0WTpQcDt7m/mMVAmeoEbQHEgyIau3XjWS6w+9kkCjHjV46+qyjunY42QnDzVNmhLldXYgSL10JdhKwihMWN9HsfkhYOTzK4gBDjz71p9K62mmCj3WOUFSvUcl2riDcNqp27ImVt1SEWoiBfFVXdbkJtY8eTmLqJcTHzygbZzkxdlCYY99ONsrdw4YdgrWPtW6JtVG7vO5qeKYRrbQ+TqO+jcr/Lwm3oNpf/aFS0E8jb1LpG89sjZjFnJA2unai5gts3O1O/rJEWY4UFVuSGDUm0wrpjqXylrmYj4CLN7WRJup0S2VapQrkv0Bk1wXURVJ897Hb9NMiAd2DU5WREiv60htR12hifbQm1eqAdhSBBT6qlUrUZw8YwtbSGhDuH43V+TEi2Ph/z+JIvIU8qYckqoPLmcDtLaxEwlALAydkv0GtVoimg9tZr0/xMmIg6aK/X6FxzjuAWap7uoyiZli9qqbwb2KRIwPCcQ+ufEudQv+4DkSzkZxJgPI22/ACjvsiH7VPlXNmFskv8l/cDhzE+pUicZ2swF0yr0WiwF/viBEN2STDK6ggTenBKWu6rJRdsaPQ33IvSQRakLYw2oQ/gUcKC5yXC4zdeAMZtWQSyXVtsWaoFDIXw0OuAH0lRHInbdjS9BGLl+ZPdavXL44LEWPhGA6AboHQxcwrM4Y/Pomtdsl1gqhRI/k4x9P7uJbjr0COr+tcdhoryqRDWB2r7fLxaAdDzrjGB5QduohpEV0ma32HkWOngfrLyfpKBOmeMFWOqWUmG9sSPsfYxNy9TetUbZ8VoQyK+244uJB61pyn6qexTP7yRdyNO1dA0aimoTuAZJ4/g47+ILPIyk/X4VaFFSP+HkZ/6PyvwTX/97aiBpJdF/uFqusZg7ENNKH9QyIEU65oWPoGXi3L+29PAKTsiGwpZFFQ6hNf2lUFEiBx37bkVIUU80w3ZIY5SoumLUDlxwV1QJpNzU99TefSRDOfdkk4qS+FpShH4BNdqQwqACyWs4Kwqs6hxAo9aRXf36A+jUlFk2sVpC5Hel9w01zeY8Crga9tXvVpWjFt8umc8gLCbkw0i9X4fcLLF3y2Y8gGYgc/pOBc3gIFS1J+GD8lpMnKhUy5vwARLJSBM5yNPFCBZ3QKP//H5Wi7ICasR3WhBl2QhBIFGXjJ23pu0UP8De6UHHjQb++tnnxmZoNqUtqK3FtgQHgJFF+4AYrpdzg9RhrkRCyH91Bvg9vawHHZErNBuqfK3E9x3wWbawcjm80EWd4M1b6uGfzpgU97/xDwfPzR2BUnde3qZUnTdnRSJXjayMaLt72rWQ7AVPk6CpE0oaxK6tv+jnR7QVcg3j2akXL0hZs9NLRBfzVPbj+E6GolNjM5EfzxX7xIzqlarBK78QLUDRqE3HJL+GbOi8HgUERKKAHwE/SdR5dnGSRl/8BOZvnbEyGaAXxVzNCr6imOz3RbNkpDj5Vd6mGDBNr8Q+u7OfJFYvISisX37jkr/K6BJKmYrq2zKqQUb0/hXhjrvq/NaH6VykPYi8t1lVvuGXMP0te/nyC5jDKOUrZF5Y0wCP2yziYsTIPW5ZYg4q9IjJjyJiF9nSvcr+gpYDl+7fiGyllofq65/g7dgUrkQuunbZPXj14pkPemGBKNfpvgitDYM2wfT+tU7y+mhVuxeW1A6Kpn11Mua3KLGP519JE+yGoomPSuA9yevhLQAktgsmbGkpwwA3Ae32VqRrDVTlnL4mbetN86chmob4s2iDdxtVgH0veg0Wgiwdmoy8w8b9nkmVAUwEI6Hgn4IlNh+UqE8GrGJmL+0TvOf6D7L05tSyVfrdvAdWeLZy80Wt55gZo7rFN/qINLQ40provGs1+RA13fmt6N5Jhe4hBecJhGj3ioarpIwJnCEvvmiDkOuK8LamMdEQqkwtJNydkasji2HukT10FH0Q+avdQVG3cw9R2SLIXQNz1i6qSk9yiimqEK3gvpUGZq3NGJCNGBC2jRy5QshN/c0YECiWueOIKxww94QLCN7xx1Z8xE4tgBMJescwC3xUVYoA19KhBOudOThJn/+lIvalTysHWhy4elVzZVWahbhONy5Tr2gG+xEyfTjuZC3yjf7T4h3HIcAzZTn4KbAN8AICpwinde4i6AyrR7Yki/yDavso1DAiM1Vj1i5WIUKcuK0Jp+BhDvcytAVmhBPoXd+uY6f1+ppY0aEAoGKkI1/z+f2qCF4Nb5CPtHsrCffigoUf51ueSePmU1dLgN9RMr4+/NsLP+7O5YqbisSXYNTV5Ho4xAVLi/BH9VjrDBy/bIAxBMyX0U2z/a17daWNeQfihvrf4/gBNBBoA2pkp6p172QHLZNQUzExIie2r5X7+fi9ag7svYHV57VmuKi8P7+dxXf5yOEoY2aasAjVkuimhNdz/Dx2A53l+/bk3WrxrDIjesjIqhP48bI+cMaQELcTg51MMcaLqCyUj04THdG5KKkFebRgtf5ccxZotzc/y4TNbO3kyD2idnQmvNdgigllK29CR0X5IEc8WTi9j/duZntD1shou+FwfTUVnaOqSQoJHXsDI3g+Wm2JXMo15cawyBMSUe+XUje0ez0woOw1TUVNG2JRs90J+5bp//vQ8wSKnsjYHw9nYsKsIoVFT9SJOkwyfARU9n8nSa0MYt08tMP/cZ9A94NP+HenGkUsxWc7uDnjSm0UiL7CvUMeR1K4fveDNAfzBGUO04/SIW7ZX2KinnhvQ207br/Z2QKMKavQOTVSnoADOD13Ikf2l4cq3BjXmtLnMBR8dgbBqVNbz7k1mWbiSdAqpwFHnmAVdjWQc/paGXgOWt+K9hklXhVZlJD6SjwYxdtBglCqQ616DLHucVg0PxTVZriz8fUafR/mKEXCQTmoWfN8o+87B//Kr7DKGx9EzEEIrEozHgNaZZoHeUm1FZf1VKjxV81Bo1aa2XH3U1c5rqSO2s+gPW68GJ4wnkq/0PecjFmAzsmv4cM/6aBDQkr+ToSfSBsEY/POjLiujrsXMkbPBF438YPcYAp6MQIyHJO3gDoiBnQKQjc2TtHqHJU4jyUxAwU/fFCo2v/W5awCJwphCgS4YIxXamcn5L/Z5eofYh+C9v9kL8jUdtmI4nvGabb1mjaJkPnf2PMs6KX1UhCmoqJ67B0KezTS5kjIpP7U80U+xhqJzc6UL5aL2F7EF+Hovlhrz7GiGM0YJSwzxl5vw/AzTtWqNKIAjlv7QR4GolQfdzWT+PwW0oT6bclTq6tdhmqDD5ypqvTkky1tsa20yndWGGJfMRsO+z8Efi5kvGCBbkT4W9C9PQtpioDswNGlfjt7/4e0Lqbd7UPaTtNhMk9pP7LMXeDT+qd8er8iaIoqlfzwHK7xRn0RBXxSd5KPyLfRCqknrhiEq7aFU9ScxIcUBJMNR+RXcto8Rh4KChKu4V9vxr/5qtnxPgr3Q7KSdtYZrhL1pEud4sdoRQG+C7vR8pnVIxBJ6Lo+p1Q1jQjlkbubRFcOVTsWNf7+lkpT0CkHVLiym9miqQgaMBO09pFR8J1ck3NIk59TF0CDRXyacWSvjaj1JLNrMCUa3zdGycKQRlsaiym5iUmqwGI7U/uICHyPEzxGTqDRF3n3miWwUTGclYf3ULFWX3E4zQAsVBEiSgPHFiEY7CuYAYUi+vzgSL0HumBlyeSx8fpmrLpylMbUDHjIsTv0Wm38VWVmMpUIkGRMUXOvApeaM7GgWQSe1bT3Fwn0kfyuXFJ2JlIv7gn5zbAuNw8ruKYArz0/rXwqW//SwszE3Jfr7IGdNuHr8/whssrvGPlK1F7Fd/EqkBk0+gChhVYkpeN5FfDzMoGZjyn5aHmGfMj5g1OvuA8UstdCOi8s03T2wEGTRhZVTXNfIgn0vpMPbZxKULGjJx6U8YX002+8M/Ya4B9Txtj4dVAmL4Mf2a79VJVPCTqS0vxSwMMb8ZgCD/8hyW3yPK18CszlJef28DFqmjq4Zk9jBnBh8YdmBybT1KBs9BfbVTEX2FcVLxLD67jIFzDkLSWabRrY5cmgzywouAr/zxXanprAd31UUmVhPZQuf+VlXjt8Oxay49tPAAjFYa5iW7Wtl/8DADkGwOnzE1autou4uZhnk7HUZdOJwczjwLSF+2PDZy06lw9PTSn1A1/ABdTVo8eqHKQrKYuIWDObyKtcL093ZxVc3px/Lpg0ckx+RN5ZTn4NnBIqCTY8jKfbUdYvOh/1JvUgZmhFi8khuAbRNzpYimuqcf7e5zkrInit2YZyiPRxYUuBUZMJ29TmLGGkGmqNxJR7fgAMGTITdN+G3XRCbCn/L3Im76OQOt9ig5/zT7xaDTipgsyn+JwiYxVjE9u7hyxLJhDK2GpvJmf/T5X/2ryM7c0+eoMPOJwNqtdZ4d1q/XMLkW2437p4+MY0WmpPvgkuxvcDz7/wFw9WS5AMhLEGxCpq/ZxJG5O+fZkC5fcA== + + diff --git a/xmlenc/testdata/input_gcm.xml b/xmlenc/testdata/input_gcm.xml new file mode 100644 index 00000000..71e0dc9c --- /dev/null +++ b/xmlenc/testdata/input_gcm.xml @@ -0,0 +1,131 @@ + + + https://testidp2.aai.dfn.de/idp/shibboleth + + + + + + + + + + + + 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 + bC1nMi9wdWIvY3JsL2NhY3JsLmNybDA/oD2gO4Y5aHR0cDovL2NkcDIucGNhLmRmbi5kZS9kZm4t + Y2EtZ2xvYmFsLWcyL3B1Yi9jcmwvY2FjcmwuY3JsMIHbBggrBgEFBQcBAQSBzjCByzAzBggrBgEF + BQcwAYYnaHR0cDovL29jc3AucGNhLmRmbi5kZS9PQ1NQLVNlcnZlci9PQ1NQMEkGCCsGAQUFBzAC + hj1odHRwOi8vY2RwMS5wY2EuZGZuLmRlL2Rmbi1jYS1nbG9iYWwtZzIvcHViL2NhY2VydC9jYWNl + cnQuY3J0MEkGCCsGAQUFBzAChj1odHRwOi8vY2RwMi5wY2EuZGZuLmRlL2Rmbi1jYS1nbG9iYWwt + ZzIvcHViL2NhY2VydC9jYWNlcnQuY3J0MIIB+AYKKwYBBAHWeQIEAgSCAegEggHkAeIAdgBGpVXr + dfqRIDC1oolp9PN9ESxBdL79SbiFq/L8cP5tRwAAAXrs2cfNAAAEAwBHMEUCIQDNfyPxXrQl7gIc + Lw7wEH537JUD41i06NNZUTxBdn4iHwIgK990g8JF36529aiweqqQC59H8/T03I9yHi2N/lMthY8A + dgApeb7wnjk5IfBWc59jpXflvld9nGAK+PlNXSZcJV3HhAAAAXrs2cz2AAAEAwBHMEUCIQCLlz4B + upCeqi8KyO7T7jp8+GRlxRyWyO2C8vqbeiFD1gIgHanhzYpnfD5JwyATOH5/iCc6vqR9vJIW8ttj + DADOqSkAdwBvU3asMfAxGdiZAKRRFf93FRwR2QLBACkGjbIImjfZEwAAAXrs2cgeAAAEAwBIMEYC + IQD/h0+qUXYOK8sj+F+qoypjQ+uCHFu1b+wFJpnvQ00D/gIhAJFNPtbfAFl1m0m11u7kAuM2bPk3 + LCx6471dRixZvrLpAHcAVYHUwhaQNgFK6gubVzxT8MDkOHhwJQgXL6OqHQcT0wwAAAF67NnJKgAA + BAMASDBGAiEA/+o2fEeFg73eCZ2UawSnZcZIXycHs+9CXNRntbfRmIUCIQDPSvvsmphFvYPeQy7B + QDG+3EyrvyKqichkwKLNjgIc9TANBgkqhkiG9w0BAQsFAAOCAQEAVg7v+aFqn5443l88dXR1JGeP + 6qzL0jDB6EYREhWvxeb2JEl1kn7jvLPMF+LKatADykBWxV3L2IHxEcmtP9hDnv39t7P92FN9zssn + hHs49LZPwl3gsoErdbB1jMCkVC+0qTA0JoeEbkixlZXwarUf6UF/17jBKSLdlA3CkTv51Td7dqsl + FBihFzLxzTLpkuFYxtN8Ax5BfqbCPnNQ+XAlTenClyrgB7wzZ3qgoCS+saW7rn1MbdBcuOmUS8+A + jQnr+mBWWZJPXpZnlR7FIo/krCmxhEWpwsBf5taIguDbZ3oE92oQOtYsJ561ATAtDpxZMr91ljmk + hVoyt2aEjDtCgg== + + + + + + + + + + + + + + + + + + + MIIE0DCCArgCCQDQ3vxsffYA7DANBgkqhkiG9w0BAQsFADAqMSgwJgYDVQQDDB9tYWluLmF1dGgt + c3NvLmRlLnFhLm1lZGljdWphLmRlMB4XDTIxMDcxOTEzMzkxOFoXDTIyMDcxOTEzMzkxOFowKjEo + MCYGA1UEAwwfbWFpbi5hdXRoLXNzby5kZS5xYS5tZWRpY3VqYS5kZTCCAiIwDQYJKoZIhvcNAQEB + BQADggIPADCCAgoCggIBAJe50Ka+qNwgmKeoLEDYTmIbLRAyVX6lgO2RKzVzDzf6LyrWtZL1SKbb + A6VdmzPlo4kzA3dif4/XgjS6hWeXXKaqXV8g/wKSSIhFTkp/WIrZY7KHF9mTYXdOrPGkR9IENiMe + vsVgRfcBnnHTGgczeiAFZm1xnkHBmVPZZjfuoi3nbjrMCh8uR3EqDsLzIHJ9PB2F4KvWKDuDaqdN + OocI3fxEZuy/Ahwpr5FORJYZDk24ZusTPniy/+xFREneHPZaB+tFCxtj3PdmCpIF54tj9dVLOMkQ + zbtPiTCGx2own9yjwa7NYYV8y8fBUcpuexbk+576J/rLMIFvFusf8PaL0QfMnkCJGL1T5O8wTEdp + Z6h17rVzJtaK4lLNsO9+7vtW2gYFgLGjnr/Zg6ZKfUfZLm6h3bfjtKRKnAzrqI4E6f1nvwyEglqE + 5BHEXriI2+tbsrMwHQoyXVray8KY1WVrCnFFQFtb22BPSQLHu/n4GIOz7k34h7mgWOPnlW2p625U + XiuFbWJD4sdNw7encs3gkILStbaRX1WZHwb2QjfFDvMS1k1KhdktELxmZKR8bDXRzymMYwlsKInP + C2s92u+L7wesuqQI2GKeMnH6jEdFdqPwXo8ObgI6GPOxzVS6Q3BnI4SXTxeC4wQq6hQpqZS4rge0 + dfxHttwRVywdzjSKVw5ZAgMBAAEwDQYJKoZIhvcNAQELBQADggIBABVWyAQh+880dOqd/LEX7DR4 + mwVnaRnOn+Xl6gbdoKJK0KyoQWEigW+Rj4RtuBdi56NvxbBQk8UIamO0zFJzCpJ936OhoMwE3ZZE + Tto0YAVtGqPlZRBJxLQo56WzRtmpy3bHOpQsYZRTYOXjoFxG+pHhiOjJ5W1djRqyOPa3I8H3tsHi + xUOZcycHIIo6gLRBvsVuDgrI4YDtX8mw695nmbFzwDbpkf0kPAb18eihrFqKlI4JNegn1RE/zkHe + 4kC3xpaqt/XlsU39evgUb78S8ZsVgtyt+S5ywChcRunyyv5fyQm79ZRtVPVS7oPJOHWG4EpzuU1X + AqDu7mGfZW7Aks3j8OrGzmzt72CfcBuRaAomgdllMalO6iUkDVdhq2RmjBKaVbh+9aq4JB+cf88T + fcrzuB4lBWeOdwOgQuhn8zB0qiuHJ9TgTUAuVeIXHI+/I5s0FFbqSCfOraFAwbfxecSs9rEaZJvL + MQ5mB9g7LW3tRSYGNT2ujOQQygKI+RDNMDNLS5wO/M2DdSDJcyrrxZXiZp2/ob6ovRERmIIlxIoa + QkON2pJ0eLnt0Qz5wrqOhgbJSPtNUdMMD3GtGm3eZn+F0Gm0WzpMD3VVHpJYxcnvFe2pgQrS6DhR + CjkrpaYGxi0gsWxwWE+H5dFPayliO0EoLQIojSKTdRVIogvy62S9 + + + + + + S1Zn0+aW1VUkhru/d8DmEjPnZkdhN2ZNOix+QBzf6sdSQjmXpp+Yii4RuPb0SV50L8n1x+Kq4xf0HTkM4Oao2C43VkD6BvDLY38icuHrhFrlONw52HNJTLD/WdAah9P6PXBHNBbX6kch/9XMJNPrsAAj3PC7tQsakql+0fYbypaYNeNUG1bvZzWGQXI5R1AVwl0UGJnf8EVsZBgHX5noZN0Nu5fufqQAnO33oON6FuuQMtRDWl/ZvufHexfHSVMys1qrRTt7E+T0XUbn77O6aIQW5WimSLx2KsGAMPz3bJ2FzIwLHiYB3MQwTcyQOg2x+UiwcJTZ7miUE0Ma1bW0amqcDqScFKdhM1swCmZrPrZPku1/NNtuDMjeO0Gok515FVoJRrjqYTB855j1eCMvP2CoPTh8xEgHEFucqPwi1cizejKj304XXJueLeZU/aK++cUk8UJUFLjZ39AlORFAyVkwUIeyP+WXi3zJlUWmbIhvovRHc4cMQ9QrSYHVIg2ke0OBMds5LfwV8/G6uS83O+XSgaVF+PEBGKJkwoEncBV/ESEFok600NZKybhEYZ+xQ98CNE0R8bYkFJ4MSdxxk28fXgcjHaQ2uv55+TtLAbvlKvsbS7soEnh96wCT/PhUr2qHcLvqMpyMBUZBbflxSukUqPQpjnkWwC6himJMPOk= + + + + + + + zX7Quof5wSnfv4GQO12T95fcqFE7/IwLP5xMcOlQCXQsyjO2qiXmU8lAo5FX206Hq0BHMYci4sT2Jgi/tHIyh8Mj1RvOdIlQSH82pHlsX6nHLWyVKykIlha9mVl3DoVyGp3fxBV0+f1NGxnwBNELseFqDxWdOKKR2b5YS/KjocVvX1LoIXBuwrqPl7y+HLcwVxOutElkfVEKQu403ck6hWzoKftF3hqqlIocBmLJfTTVOsT3auWHsJVuuS1MvoiqAkpkY1TYfzx3NHthEaFcDY68FnHLmYp6KmGItIHizYgCJvm18oz6khhZNAjnWOjCKLbN+RpkOCUIArK2K6h3MC25dbph4yiQpGNeo9I1hayIAyw5MSYywdhvF9PYuoChGIHbezGMu+xkWXDyvivJBOTeY6Flkwp3LLA+ZAkhaukUjcBofxNr5qLt+JkonxhYvqnrTk+k3bjvMNuzIdZ6sFHnt2j6zqe8trgNhk4WJ1c3RtsmfvCmrguVn9ejY/U/4uOVWzjbFGH1qq/g1pMEyi1s7YQD2sizKLtylUutAafHsqv8AQxkVIIrs3ydvtm0+jFxYvgDaaqtVZ+ewxCtlRCubv0PsSjflRLmxDnhzGJpyZLYPqirdjHKlLawMLp0W5u09sOepxI5i0z8U5x3CTG1RyuG8l64jYOpOqLG/UCaWXWS8cF3MiWzZoJq/EnoPj9zQADuTjNUhp9ceiHwAdfzTXYZGt1pxvQ3Ro8zJrQw2pw/AbuV6etAEshCkuLnEDMfRZVaTDaf5XZG0HGVoeEFx0mT2Nnh5ida39u2kJDP+oaCqy1dACoYdHiTW9Vk+4/7hRSpZA9OYUkrig0JF1Z3fOmnlBeiznmhjFP12IYmQDLZJEdvDxTT2B0hbw1Re5TEOS2U5HZusTrspmIdjUQsSp/tb3Z2UumSTUyKe1ChsPdIEW8TMoju+qlzC+y2oxKxYbYKyFLd/HAEMFevXbcrFS/5mN7Pyvib0YGWWfjBGYv/Z1qCFEqMqLWol7JKXIXEd5Sw9kHkhUCRyfyFYZjXDBiGy1f7rNtKUI3c0fC7N5xjJqQE9GixNANKrSoL6T+jLlWqh7VQMqDKJLZFqWBnS1iGHDdIxAtI2pOjzWYevR5rr97OdhWF50Hme4q6AoGtLxmRvxSWqS3/DgD+2bDQdS56QdYrmVzMSKT75HcOVk+7O4zAHKsvYwkiCDCtZYSsEywq6UVqU+gP/8UvWjXppas3GKtshJ+WtVKBrd8M7qSlT3+0phaFRDOtWkPtXUSVhz48m2tVlRaf++2rnt1s382QCQJQexB0XKVJJv83Wfdq76hbFMbE4FNJlQNnidJrAbM4mi2h7IOcukuCqf7nZuXMTYFZzyHd/K66tT1TVohHQyBeYY7XUNHYIx7anv6/3dtzMx8MwAMLFPsJ2RUxs+Fity97D3/lBOFXy7U5gcVmK6CJrsTkyKPLACBZqj2UDlWYaeYFpo9IJv4oVLa3eZ9dXPb/Uf6A1zOg7Y83DRypnDu6d7JpJjv5DHGJrvSiNaaB4fV9gTMgClbCkeL9JoWZupOpGfg93J/iuqXpg/jBwgWCmIOfd4RF2MEAxnIGyPXRueNQNrCAWVlIoSNrge9JdLzbfqSQ0aqhDuTc/Lh5fbZ0BzaC+MsUigh27gCn3bCzzsip+Ia+pWPd4kDLKazkFBppP0ABlq+6AMSarCJJciVd9Fh0+vFjBGvkOBf/JdpYqKYRz6jfcrhh47tPiS+gBTuKPOdmw6q9yNAnwk3GvlmiGDiJMwpEo7+XVZ31ADL7NmOe2f6VwIbWBrrDpLyaw5NV2DBUbFXazHvtT9HIx6yGZKbB3LCoo2z8KeOtvDCRNDDdoekGUFFr6G6HoSLSt6BnK+Bm5rkkoNuF1Sjrx0XCfnnvYZKEhT6ToI2cWw/NX6z1TFJg1sRcDPmcTHfQ4pqsIfHuDB16vEmLcQYvBa589BXvwghycJ0ZR2cJbtUlyBPN82DGj3qH37b7vjeD52ZZumDWB1LpjKY8yNEJepbwgfyKv0YlVxQNRGhyur1tB1IAbuIiM/Xd3LpOtoPOYx4Up95al2D7mvuiHTbfQMGJYv3V2JIeo5d7S8RjdDiL9qW7anpfPh+ZOMyY68O6rFj/iUsmDrPbWgKKES3nKa9F639H/qQtG7KfHb7TpFcl/AYgVZnAhhV8LzSwO3X80OzSRK6ZAtYRbQUNmgqns4vABqMZ/MB2EUX4n0zRQBRhlhHYoEeMB41m0MlV5P1OIdhhT+RXi5egjDTUijEAe1AONSb4UrmiarpF+JrRJplhzMsIuL/pve8NLqMEHjHcL/IYUEX/qHjNuvabg3+gwOdtnfWFPCKfaze460s8Esi6WvsIbppxGq3WzMmvQZuGCVS41B3ssED6joSHkpga8UP4VYrKmEazC+fyxSasEOK+TATvDHqZa/ko1RN76QVguve1z0Amd57+amLEDBY9UY8SDKouM0idJ19v63HVdLlQDPZssj1CMwJHmu04Gflt3qNNo6jEVFDJ/F2B0NXbtnGe+zQInNN0h5V8w8UoltasacfluvDi6mkMGFyWAL3LvzX7G441tFTpUVfd5gcnTa0nd/9ffRP7jSj1YuIOSuWC7nca5NycGtyNpuecUycDF+szBYUyFbPCzskLykjzC51sMZ71zCRVz+urciKqzhpR5aP4d6B5/lhZRhnXA0fptYtyD9a41/dX1fZN+8q9ajzg8BvleAEpit0v8lRQh9BoYreVfFMrn+qHuohO6kFS9EVprR33cewXofwIs24dG6G2ymNyDdlrNZ5RC078nj+WnQ/juKslrPNN37NrCasdWBN/m4BQozXsNfj+U8gV9a9AnPTfGxMc2bcnTehEWvGRcEkDsaTfyLonCcd/p18e0bv5avDiymygojPs7C0tCJx/zPhI641VxDK09CF0O2/dtJgbInIRe/N/GsaOmsnlJit7BR2+p7Ax4fFHsSaeAL2MefWrs0B2EDSIHDd3ibab/t4lLL7kJ9ESNQ1bfkQ8ynxwWK4fiC5rXxWn4qR7G/aFV/0VbgVvx9B1uQ9WMqRnRG5w80QlUzUpbjv7VDIHUbuC/ntcxhFWoVyES7r2P1er/BNNbb3YFnuHhZvS5DTRPAUByGgZlQtl+Sjh8avd9nt18FyNAUl/rnWR2cycsfJ6Eq34Y5y6zfc4GPrIcRVnaFxHzfBvQm0J/NbrpmX3X8WD6m+zDVU41TobXqzgt906SHp1oTD+mhDrZVmnLTdVT9lpJfkRkPmSVzFzT/42Vlt6C4XhpnNwYP3wnLZqHSbmgS71Bjrn8o8eIdfnL2Y8U0Ti6Zvmu8tgpcfeZw8Kax2MYiIhtfwO7I7dmCmMu80JuDfJ9mnzmJzfkr1zLmWiJFhL8DDIMrh3AEWwwtcZ+bRlmuLZKo4LOgEzxTmEMeNOkCVHU0zRNEZH8zNsMCtzxO/C1JxmKURDm/ThN1Vv7yKUKZ50oHYkGvo/XLhokkzpsv67zny1kBvnM9UeyyyuTAX0jOImKoSVq86Rd1SlTmUgKcbyfjCMGjJ/hxMjtIehhrZkth0K2vuFDyny+ckFRvdbyX1t4sCbW5BHDmN0jKF3qUM5t4AFnGkqOz/H2zZoOOlLcb4kp2IPTBnjgbf1Y4zZBhUcvJUqJAQZr6eaYi0Nt2ry0zngQI77a0hWK88lCvxBGFvFYWhkvaK2014wU9JCO0C5O71ffBqsTqJO/djRiZkYZVmf4XwSiAhFTJMAtUyYde90VFiFtx3YQO3qoJqEUlNT7AJnJj8Uf2t6oyIqm2BeV2KXyWFaUrLrsKivAxj05px3aQeEKe2jBsQX60jpRv4MBk42kybyAHBnah21nLpNYGd/4420ou0ImjvGBGpXtyts/qe87w1ccV6oddtkM9YFNkGiESvKX5YcvEW9Ov9C4pI8XrsxhuCy/U+eSUuSiFiwAktFz67lfeoR/g6akHCsO7prknk/7/3Bbkk82cvkAnu2efSCEkUQbypOb/+0iAceit0Q6N2mTqeAHSCfHwg76ZwtulqL8y4B+qBYiASGmRTn8OITlXCYLpSM8bI7dmpT1PXX6mWLu5fsNC3S2EJ/jp9PiSz5vaf6tAixt8VV6rJ7YQQks97qbZwXfkBPRTRGFFn6i6oA5Ai4vzPIum+92KKbympQh7Y6cdAJh/ysTmfnNJTI9+0GtH2KvCLoYIwIFWxruecmukpZAVcV6hruQlkK1P75eP41LfR1k3J/pWl5/RYOw0gxoNIinv9RSuSKLrCos1MAKex3HD6DI7Z8c7n8f/RL6zFUOSVnUpzQZkaqLgDaYqPWN3FLvOuwf9+5PGxqCayZQV6K8qrfoMeoZ1HcclsfEP6pziwBvqLYiiz/kEjwwL3I1odLCXNEUBE+NM7Fzdo6XF/wE7gYiTWVXBtt75RlgamDyjIbBOK9HadQS9cpCWw/FHtqYxvTEKIhTfg/fUv5naVKxUdtMWXSDhw3/fXWBokKXVL/CTbnWZD1d09NWa6y9aApnDrmBf9lumj1qk+4njTWlYuS2lQBeyvsk/uOxjFtJJIA4ez7Wib2ROEGxF6CQ3jrmxCwkX0zX2S8BO2De+pSOsidkI7bcx6x/6tJuQG/Xu6vjr/Ss01Fa2jW8EJjIftTv8q2y/AST4CF3YwvnvN3E0Ye2fTG66QdlM4MH5QnCscqDf4UyRPqhOHpfyITF+Yjyw4bGGA+Nv/1Flp5rdlAayhNoc22deKH6Mwhgw4wbp07OPujkETcsZhm67CywCY+ulOqj25N/T+X58B9sthK7gPY2Hu0DqGXQYV6Fviab8hakYl3HEJUOZvKON6g35LBAybXotMZtPeR470rDxAc1iqJiMFrWEyAHL3iq2WluPYUBW6GS+JFMF1Z1ve+1HaVasoWxCowHTT7Ev6WQgAmC8F/+muKauDlJGsjH3lQYg92TI0+FUVYrJfpWI8pDp22ChckXyAiOAllZXukMLjo9W21K1xelYNvD2+iiG2tfcP7+vjGkSMmjnKPArNHL7y0E2HCY8GFnNuuzuqpIQ6vNguWCQTiEIY32uNTYnsX7Rqw9S09NaIb0ZaoeB1lb5mEi/SlzxXbsMrF2MM7+Mj9CTke1XsJ98iaFiROpygg3OGsS4nz9YURQ/YXsJc7Gz6h0naJunMjMfEKs84aikfo2c+GHZl1kjhr6hMO4LPANcpBy6XTuKNkUT2GjDB6d73tz0fquGq9OdodYPByonxYNNS8cilsCAXkutowNdI59VvJcHiYZPqIu0e/uhkxY8e7MieKWyZFeG1JPXDwfQszSsAdOSNiEubGOTVYiu5oSKeOomtwbePQTRBlKynk8aYt7qLVl9KTFDrV3pnBpNTeIw7GifbtmS6RiU3FTwoU3fyv2+RDNloc2Nvg4GLI7ldHvZim2XomxnHZwmnNKr4UBzjEokTbP0Sf8bSBFWfW/5xLmCH7+rsJQGSkPi9D4WE7PcW7oaCG1quPbsRVGLPJgeVynrHh7a9dCsUMcTFj9ZTwzlQzBBB7A3q2e+ZWmNxMGRbE+vsZShapXz2Uz57AKGdhCL9U8Q80u1g216Gt9h4XTYbniWTfKYLN4Rt9TQa/10f3Z8Urc0uCbZGokFlTxe5Q7r4yClYSd2kEuor0xajGGdgS9Z5aRaLuTfKfHJulokN4sW2EqnaYJTK/XnXBelt5DeP4jIYGe9SxxnYVRX1h+YYeTW2Z7GekNiTEnPHpDDAIqVYpKh5g2tntlaThJMtabOhsiezIe9qWlq4dNJnQHD+GKL7pQ56DRnBmTEdvvJKbRbEfmOkhnoEmIdT+VprPXrRzgwBWQ9qtUupPVD/ZHtfeemahXxoYMbvMyTgMnujYXINam8BSSIweSah+o4ahPi1li2z3LpGkAeZ/GcVe87HgK0xkfzeRma8ZtBIgDagHXEq4Mj997pLZvAUrAc5FbMolrDPb173rIdAm9oHysrvLZX86ryrhnrSXNDn1C1XcPa+iu9rxgfe5q3PEb4K9Jre2gq89JeoNV4yX++cS42iHLAKaZwyiN8hUTHjP3atqxhxQZRA+jxixM9Imo2+Nwly2hy0W4GNYPhWgwl+REsBhfMu36U5KWnp8OrjZmUm4kvMbTpJTaB6FWI55bv5lWY8Iz5GrOoGU3Y+gkwkJDleyC4xH5MAgBaPh64LsAggWFKgmeqKloYreI0R7YBp8y0YYARBezvJOU5EpS5fcx05PPbLmqkcEGclxkYpNqlGzFRc2dmn5qfNvIwYd1ULuWb+00qj9xdaGgTDrr0HJPLh3tL3hxLaZ9wFbnwf2Tqi7wmPHSgmHyJBUnH+t9CLGTy8i78O87RpG4CMkni3Ykk54AIOiZ+DqezfpJoHqzPYqAJgeW+Z1Ymar/jMj2ZsDWIRPQYp90PvoL3lkTX5FDLaadG1b2CGMRknqUsYqm6cP/zlpd/NvLXmtaFsJJ6u2b+7EWxEu/xPnN7UTfSLPuHWO41aXmzYkh6Dwv1H4nBLbuR5KW2ietvyeed1vTSa4Ao8tRTLc6m3pxzP/E9nmj0mzj1eJ4GzXoVnt8emaHIgbFeITnlwx1EP+2KVWLSrPOvvBzVW4YlrEVShU87MmZ4QPtVa6yjJWOsYmTwtezjzyV5VJZPwu4iSl2PwNumel0hNp7EfZlLXtpaLcIRP68p7DdbUHtEo36nxVqk7mH/AgffT69nXiXe0o7+Bar60KblmrR6PV28s48dEvjZCRYCI4yBdwWOb/eBv8iY+7F2pIRT7Ho7ozQsQwxrFJhgRZlPlb0Vauy1hiePvnsoAjgx7fWCzYRopm8vOAx5iDLvNojjM92lPnOVYyXfDOnn4HwY2gEHJl5qdmanQNdUODL/PThXHTL2zX1J/uZ3mFP1cDH6Gi6jvDo9YA2U9xThCYHfdpISUX8opHynEAbpmFFVWiSg9LxDXhPej3DV9Ap5LD8/u22P/KlLi60tRZAEnNBZ5RLJrtgEl7YfN1hRSlNsSkU2jfVVPgQBRsBliXexdKMI/SmywufOv4len92E+0amEogAcIDLfLj4axR3TxtxgbycGoKju4dGBJja9Y3dWwaiRW9C6AZ/DH9hvLW8UN+fdwcviLyRuPWm9Ow8jLHGuVxTZ6kDGUpXYNcAE6KfIABV3FKsvKDmaEYY7dNXdBk3nKac24AmzrgvTx/9ZM1WdE1vHbPRDmHhTmHHs0mDfKiOlO/Gp3lu/btvbZnV14KY8dRMRxDqZlO3vZTevSblUOC6GSVhgdny4qB1cxX8b/Fka9/GACA5jU0mVdJWZabl159RFwX1rOqCwKxSbE4UIvtdq+RxozwTIvWiQfX9eO3xWoz3G22dPILdvqrd46rY1XTLMjW7Q77//j0N8dDaHyEdfPpBobHVQHSwzfutiu8cJ4Y81jKMDjMuvfghrvsiDIK8mqRK1H58fSVx2AD0x3HDaB0W5j89hqhed92SZK7AAwWBexwUxqX8c3+vYmOepktxbW2KO6dwrbgRXya/gRhTk4H1wWgkOkz0aRiSxKaqgwdQBnhpO9HqknOKHTnaYrZirQcyVxH1b4DdByDOaMjvlbVKdqIpxPf+IVdz5ffZUuAy99F2O4G11tBUDRhzotukSNx5xzilhfdyqhgEO94rP3bmpqUkErlxrhicvd8m9W3Fa9DBygixYsfKj/A3RXszSiP5FnokcP5O3/0Fpsso02zKWgKx+B2gX9ksQQi6BlGLY4rdhMMCQe1LVnRnNkB0UEFsyf6Bnpy1FheuwqXCWNzuauoZx8RrABjP8tclkKrXjIe3u8JvHM/rAOg6UNfzdF6mB5GOgGyH06bnpAr6dBCgkSXG8X0vpX9vVGalP8Uu0Mq3NlZl5luazBF5XJtLXXCecFR4EcSlv74Q7rmGUy13+rNcBwn8VN6ZOCBseryaYRohUEg83Q/WiPR/VK0eUq2UrsSRL0RXuhtWSnJEY5OG77iCxnEjOoQZj9P8OCCPIgsJl5OI6T6CPXvsJgq7IB2LOpkwYCCX7DlAsGaH0z5AX0n0c9uqa+RTsXZD8KNwBhi+p3oYmmthZ/OTHX8V8MW5jXCzg/XM+wPYU96Jx3gSoCO7Y76Ru8xRR3kg98jLu681V8M3AlAkm2v6KRO0Wyq9zGzwo+hayLqN/pc/gwmOCH6yZ+ASteOmSvbIKqQfzGLFp9DoYXVN7S7tZBrpvYpGCi0RU46hmY6be6vmjnJpI42a6bAzBwVoYSY3LO1q7pMor9Bqc+q8ttOM6pEhVfse4fnPnqTnfz7Qk1sIjBIo0zhK9t7RoPgcUNP7hoIACjBFl7A6hvTmUN25VvgWyUfu5y9ldPWUFPQqSA//Z2j7ZWufLFhMZlyDtoAJP4oIPDygS82Ye6EGxvg9GyEASHqP7LHVEo7gzbGyZwWIEsYE2XdbD5qCLaIfhVIhfJOxnYSHZJZ/cO9vIwC5YA7IZ6jIaz7KjSs49awMJgERH82QIOnhKSn2s9xqX2ZpB43IzqUtsAmDieUAi/E2lrgTIwGbucqb98VZBe7sve7wo7t4t1wvJE9Lq79hxjjKA1hBR/2B6I1z9KE0p9Tltn35v2fkshsLzz+gLl8cOGBUt25hASp4p6h29dIMm4MkmqpvFTHlynPe/xqKFKAn7sMSbek1p+PJpHZ0Bm/0KKUeNsWkzTvaXWeC6YCnRLLqDbqONGd027+wxh75EUb40MObueMcAnZ2WNpEojv0w19fiJ+uBMwA95a69d+MoKmU8BzJZFGSAvq9pKKpVfWy/vjUTLNdx10s5Gwa4AxagyCPW11baDW6eweGJKcU2Cfk6mvVWIKnTXnmICufVaqee/qlWs0MVn6m/feKugwAFY0TQJwb7WerB3bzy6Qf3ftXFBzKcw02hJFgLBBtSH97V+scd6FhuwErmyW088JzIH8BSPVCaHIu+Zp7FZ/Xt2xdZRQiBa4R3zXJS1HM4wf1ew2eC6MyWsr5KHY0olUIzdtdHyxdlEDD8f3ClPYKAuXJszkuE6/v7g3XECN9UFQDhMPp7/7cNKr/6+0CqFXR7lGOU5Ciw2jRRgDRN9TvAhvWfeImgMLNOaER5X8+fZ9wI2J4E5xa0Y4hVrp/pyhCFCOjyRb0Y9heggaATulKYtcLKy871DHIfOIA8EgwtRDj+JiaRO+E2w9GbMLF2pms9pBocUEzofonK8uMazucjT0cKpkkvyk4D4Jj5OYzSWIR1OtekMnxbJlhLiYf4WYh3K/tZEYC3WR713R/LqBLY2uadNumuHajeOR2Tbv6+sYwjaQG99AD8bt7rmNivXuwWxNfOEvrGVDmOnh/XGXlC/UhhYIbX7FyYT/qlmoKEQuBcpk/wCw5a2jsrKJXD5HiNaWajIOzVOaqFT383UskqBZMCCpFLUSO3dTwDyaHIYXtDyNH4QWYzW+EGqy304d7x1Yym6FJq9kYKKUN6pk0qzSdLc4ujNJsY56/kkMMxGI8svwpoJc3A= + + + + + \ No newline at end of file diff --git a/xmlenc/testdata/plaintext_gcm.xml b/xmlenc/testdata/plaintext_gcm.xml new file mode 100644 index 00000000..a3747e57 --- /dev/null +++ b/xmlenc/testdata/plaintext_gcm.xml @@ -0,0 +1,104 @@ + + https://testidp2.com/idp/shibboleth + + + + + + + + + + + gYIc30qUhP+BV4KzOEZ4DBBvxc6ehHkzUgxe7RKo1L8= + + + + dPsW50R3xIlq7Sus7kFWTsmzKJ3MU5vN7SL6yACXvqt1s2bWPCf1HQuEEh1MKxsHC6fuknbdWKgHF4lhWFs35CGc6q4mA9wt4AC8XD7qN2Ps5oPmT7DNynB1G9y2p/yYIuspjjRE5yHNyuPEg6Qgi6OR22sp5bv5XOP+bslmdGGjC5xAYML7JG6iPW56fwxTXFmKwlnjWHZwYohNOzbaZ7k1GDvNNSjbJvv8+BcgsH7yJnmFS6IUlxcQs33q+XGRAki+Q8Cw0NFzCV4xLLIs8JIutXUjvI00vrHnJjEHi/6yaXITxpgRShkrn2/zbzYOCmy5JgRkVXjyccAWT0jWZSBJ20Rchov9q1PAbda6/FO1x9ln6EzqCWKEaEa0OLMiQei0P2vQ2ZoKdfm36fKKrYdQGxtHFAyWt4k7WKlw71fZFFRNLpfLuocH8pjRRfapNwfBWcfHuZshvqML/O0150+1GcUUYPZrkamPvrzekPVllL7XqKpmKe2BDGLH4t3iSwC1+NU14Hq/wlQi7HhEzxm7YVK6LQhMyH4GP9MJmoc6Yhnn4emAz25TPDj8VmhTmtXlrHcHPXCTW5axYDvXm0oQNISaAcTuLzyBF4eRiswQaEsymHQn6HDybADODKESjYMbxxD/zDy+FAMwszHaVe7bcUPa9MISgAcUV3dpLvk= + + + + MIIJJzCCCA+gAwIBAgIMJSC7cHRrXZg60Eo/MA0GCSqGSIb3DQEBCwUAMIGNMQswCQYDVQQGEwJE + RTFFMEMGA1UECgw8VmVyZWluIHp1ciBGb2VyZGVydW5nIGVpbmVzIERldXRzY2hlbiBGb3JzY2h1 + bmdzbmV0emVzIGUuIFYuMRAwDgYDVQQLDAdERk4tUEtJMSUwIwYDVQQDDBxERk4tVmVyZWluIEds + b2JhbCBJc3N1aW5nIENBMB4XDTIxMDcyODExMjIxMFoXDTIyMDgyODExMjIxMFowga8xCzAJBgNV + BAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjFFMEMGA1UECgw8VmVyZWlu + IHp1ciBGb2VyZGVydW5nIGVpbmVzIERldXRzY2hlbiBGb3JzY2h1bmdzbmV0emVzIGUuIFYuMRkw + FwYDVQQLDBBHZXNjaGFlZnRzc3RlbGxlMRwwGgYDVQQDDBN0ZXN0aWRwMi5hYWkuZGZuLmRlMIIC + IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvMXPQpcH57g+I5qLmSHTuGewKaqg/xHSkEza + 7P3dAVg4sHslBdtPN5ngoA2D2x5/zz078BszczYSeVlXH5Jj8nJ5EXesEdBTlWTk1eq4tWy1X2fW + CcALbs6RvCVAmweWyfNMGBTDdk8TG/Xn58HzXLgDlpBcoNmIiVgtYQ1z7vZyTkVhy7DhmOLDHZ0B + IhWJnl3wsmBTLwkAG41vzlWqA/03R50TcTc1QKF1St5YX7AIjaruZZs2BOTKcQhk9/vqooD8aXZ0 + O2+FAtiQivbxldZUuUuuenx2dwlMY2FxCSTwEFdyW8sAapF+9YhrRKzFEtcihAZxLR+ggqJch8Zi + gAC1I/xuFH4KUXOuOdDF4mRVMRNDYw207h2s2ur9hBSw5yRgQG/oQVO6QFr8d6taf14QDcVF3ZC8 + zxYsx0Az/HdRYPBV2urSsk+ln3vg7HOMFtUuAACU0ejeYriMpDgGzWEji4K3m9CaFkEMT4jo6zRk + OeKXpNnZsXT8tQ1huvkNG4lqNHVGLN5NI3tYPMSkRhdI+tHgRcYEn+gnRoTHfoSJAsZv/UeLH0gZ + LKDBDBmvdCADP2I4uLOEYqqh5MDtIOY5/vBN3CDw4wDO3lCzF6YhWJh336AT5baVmpZvlYe35w8u + fdAbpcKzuuB9UcvYOsYUKDBw+FucMDlttFtA5l0CAwEAAaOCBGEwggRdMFcGA1UdIARQME4wCAYG + Z4EMAQICMA0GCysGAQQBga0hgiweMA8GDSsGAQQBga0hgiwBAQQwEAYOKwYBBAGBrSGCLAEBBAkw + EAYOKwYBBAGBrSGCLAIBBAkwCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYI + KwYBBQUHAwIGCCsGAQUFBwMBMB0GA1UdDgQWBBTuOFXROs368znJJLquZbkABIi0mTAfBgNVHSME + GDAWgBRrOpiL+fJTidrgrbIyHgkf6Ko7dDAeBgNVHREEFzAVghN0ZXN0aWRwMi5hYWkuZGZuLmRl + MIGNBgNVHR8EgYUwgYIwP6A9oDuGOWh0dHA6Ly9jZHAxLnBjYS5kZm4uZGUvZGZuLWNhLWdsb2Jh + bC1nMi9wdWIvY3JsL2NhY3JsLmNybDA/oD2gO4Y5aHR0cDovL2NkcDIucGNhLmRmbi5kZS9kZm4t + Y2EtZ2xvYmFsLWcyL3B1Yi9jcmwvY2FjcmwuY3JsMIHbBggrBgEFBQcBAQSBzjCByzAzBggrBgEF + BQcwAYYnaHR0cDovL29jc3AucGNhLmRmbi5kZS9PQ1NQLVNlcnZlci9PQ1NQMEkGCCsGAQUFBzAC + hj1odHRwOi8vY2RwMS5wY2EuZGZuLmRlL2Rmbi1jYS1nbG9iYWwtZzIvcHViL2NhY2VydC9jYWNl + cnQuY3J0MEkGCCsGAQUFBzAChj1odHRwOi8vY2RwMi5wY2EuZGZuLmRlL2Rmbi1jYS1nbG9iYWwt + ZzIvcHViL2NhY2VydC9jYWNlcnQuY3J0MIIB+AYKKwYBBAHWeQIEAgSCAegEggHkAeIAdgBGpVXr + dfqRIDC1oolp9PN9ESxBdL79SbiFq/L8cP5tRwAAAXrs2cfNAAAEAwBHMEUCIQDNfyPxXrQl7gIc + Lw7wEH537JUD41i06NNZUTxBdn4iHwIgK990g8JF36529aiweqqQC59H8/T03I9yHi2N/lMthY8A + dgApeb7wnjk5IfBWc59jpXflvld9nGAK+PlNXSZcJV3HhAAAAXrs2cz2AAAEAwBHMEUCIQCLlz4B + upCeqi8KyO7T7jp8+GRlxRyWyO2C8vqbeiFD1gIgHanhzYpnfD5JwyATOH5/iCc6vqR9vJIW8ttj + DADOqSkAdwBvU3asMfAxGdiZAKRRFf93FRwR2QLBACkGjbIImjfZEwAAAXrs2cgeAAAEAwBIMEYC + IQD/h0+qUXYOK8sj+F+qoypjQ+uCHFu1b+wFJpnvQ00D/gIhAJFNPtbfAFl1m0m11u7kAuM2bPk3 + LCx6471dRixZvrLpAHcAVYHUwhaQNgFK6gubVzxT8MDkOHhwJQgXL6OqHQcT0wwAAAF67NnJKgAA + BAMASDBGAiEA/+o2fEeFg73eCZ2UawSnZcZIXycHs+9CXNRntbfRmIUCIQDPSvvsmphFvYPeQy7B + QDG+3EyrvyKqichkwKLNjgIc9TANBgkqhkiG9w0BAQsFAAOCAQEAVg7v+aFqn5443l88dXR1JGeP + 6qzL0jDB6EYREhWvxeb2JEl1kn7jvLPMF+LKatADykBWxV3L2IHxEcmtP9hDnv39t7P92FN9zssn + hHs49LZPwl3gsoErdbB1jMCkVC+0qTA0JoeEbkixlZXwarUf6UF/17jBKSLdlA3CkTv51Td7dqsl + FBihFzLxzTLpkuFYxtN8Ax5BfqbCPnNQ+XAlTenClyrgB7wzZ3qgoCS+saW7rn1MbdBcuOmUS8+A + jQnr+mBWWZJPXpZnlR7FIo/krCmxhEWpwsBf5taIguDbZ3oE92oQOtYsJ561ATAtDpxZMr91ljmk + hVoyt2aEjDtCgg== + + + + + + + AAdzZWNyZXQxm924IEWIZegn9l1NChK4GXWETDW/ca4xRwNHuV21SA25MzW2bWqqCudhmNUrUsXk+Ci8W5MrwFiLKqJkNm4NwmHFsnvpUMVHlH8raI+xLVwwa2lf/poCXml0kE8D6cbtEBBACazlvgYMHHLud5+6uSDbta1xlp8S2G6aDOzWWYJluw== + + + + + + + + https://example.com/saml/metadata + + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport + + + + + + urn:mace:dir:entitlement:common-lib-terms + + + member@testscope.aai.dfn.de + + + \ No newline at end of file diff --git a/xmlenc/xmlenc.go b/xmlenc/xmlenc.go index b0ed5bf1..719c523f 100644 --- a/xmlenc/xmlenc.go +++ b/xmlenc/xmlenc.go @@ -18,7 +18,7 @@ var RandReader = rand.Reader // XML EncryptedData or EncryptedKey element. The required type of `key` varies // depending on the implementation. type Encrypter interface { - Encrypt(key interface{}, plaintext []byte) (*etree.Element, error) + Encrypt(key interface{}, plaintext []byte, nonce []byte) (*etree.Element, error) } // Decrypter is an interface that decrypts things. The Decrypt() method returns the diff --git a/xmlenc/xmlenc_test.go b/xmlenc/xmlenc_test.go index 3bc0c3a3..cad9b90f 100644 --- a/xmlenc/xmlenc_test.go +++ b/xmlenc/xmlenc_test.go @@ -10,46 +10,65 @@ import ( is "gotest.tools/assert/cmp" ) -func TestDataAES128CBC(t *testing.T) { - RandReader = rand.New(rand.NewSource(0)) //nolint:gosec // deterministic random numbers for tests - plaintext, err := ioutil.ReadFile("testdata/encrypt-data-aes128-cbc.data") - assert.Check(t, err) - - var ciphertext string - { - encrypter := AES128CBC - cipherEl, encErr := encrypter.Encrypt([]byte("abcdefghijklmnop"), plaintext) - assert.Check(t, encErr) - - doc := etree.NewDocument() - doc.SetRoot(cipherEl) - doc.IndentTabs() - ciphertext, err = doc.WriteToString() +func TestDataAES128(t *testing.T) { + t.Run("CBC", func(t *testing.T) { + RandReader = rand.New(rand.NewSource(0)) //nolint:gosec // deterministic random numbers for tests + plaintext, err := ioutil.ReadFile("testdata/encrypt-data-aes128-cbc.data") assert.Check(t, err) - } - { - decrypter := AES128CBC - doc := etree.NewDocument() - err = doc.ReadFromString(ciphertext) - assert.Check(t, err) + var ciphertext string + { + encrypter := AES128CBC + cipherEl, encErr := encrypter.Encrypt([]byte("abcdefghijklmnop"), plaintext, nil) + assert.Check(t, encErr) - actualPlaintext, err := decrypter.Decrypt( - []byte("abcdefghijklmnop"), doc.Root()) - assert.Check(t, err) - assert.Check(t, is.DeepEqual(plaintext, actualPlaintext)) - } + doc := etree.NewDocument() + doc.SetRoot(cipherEl) + doc.IndentTabs() + ciphertext, err = doc.WriteToString() + assert.Check(t, err) + } - { - decrypter := AES128CBC - doc := etree.NewDocument() - err := doc.ReadFromFile("testdata/encrypt-data-aes128-cbc.xml") - assert.Check(t, err) + { + decrypter := AES128CBC + doc := etree.NewDocument() + err = doc.ReadFromString(ciphertext) + assert.Check(t, err) - actualPlaintext, err := decrypter.Decrypt([]byte("abcdefghijklmnop"), doc.Root()) - assert.Check(t, err) - assert.Check(t, is.DeepEqual(plaintext, actualPlaintext)) - } + actualPlaintext, err := decrypter.Decrypt( + []byte("abcdefghijklmnop"), doc.Root()) + assert.Check(t, err) + assert.Check(t, is.DeepEqual(plaintext, actualPlaintext)) + } + + { + decrypter := AES128CBC + doc := etree.NewDocument() + err := doc.ReadFromFile("testdata/encrypt-data-aes128-cbc.xml") + assert.Check(t, err) + + actualPlaintext, err := decrypter.Decrypt([]byte("abcdefghijklmnop"), doc.Root()) + assert.Check(t, err) + assert.Check(t, is.DeepEqual(plaintext, actualPlaintext)) + } + }) + + t.Run("GCM", func(t *testing.T) { + RandReader = rand.New(rand.NewSource(0)) //nolint:gosec // deterministic random numbers for tests + plaintext := "top secret message to use with gcm" + + { + encrypter := AES128GCM + cipherEl, encErr := encrypter.Encrypt([]byte("abcdefghijklmnop"), []byte(plaintext), []byte("1234567890AZ")) + assert.Check(t, encErr) + + doc := etree.NewDocument() + doc.SetRoot(cipherEl) + doc.IndentTabs() + _, err := doc.WriteToString() + assert.Check(t, err) + } + }) } /*