diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..4fcd556 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: +- package-ecosystem: gitsubmodule + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 diff --git a/.github/workflows/build-publish.yml b/.github/workflows/build-publish.yml new file mode 100644 index 0000000..056b55d --- /dev/null +++ b/.github/workflows/build-publish.yml @@ -0,0 +1,60 @@ +# Run on master branch builds. Tags a release with v{semver} +name: CI-Go-Publish +permissions: + contents: write + id-token: write + +on: + push: + branches: + - master + - main + +jobs: + go-build-publish: + name: go build publish + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + - name: Install xmlsec1 + run: sudo apt-get install -y libxmlsec1 && sudo apt-get install xmlsec1 + - name: Go Build + run: | + echo "machine github.com login machine-parsable password ${{ secrets.GH_PAT_MACHINE_PARSABLE }}" > ~/.netrc + git fetch --tags + go build -v -o bin/events + - name: Go Test + run: go test -v -race ./... + - name: Git Version + id: version + uses: codacy/git-version@2.7.1 + with: + release-branch: master + prefix: v + - name: Tag + id: tag + run: | + truncated_version=$(echo ${{ steps.version.outputs.version }} | awk -F- '{print $1}') + echo previous tag ${{ steps.version.outputs.previous-version }} + git config --global user.email "ops+machine-parsable@parsable.com" + git config --global user.name "machine-parsable" + git tag -a -m "${truncated_version}" ${truncated_version} + git push --tags + echo "new_tag=${truncated_version}" >> $GITHUB_OUTPUT + - name: Checkout common-actions repo + uses: actions/checkout@v4 + with: + repository: parsable/common-actions + path: ./common-actions + token: ${{ secrets.GH_PAT_MACHINE_PARSABLE }} + ref: v1.0.1 + - name: Release + uses: ./common-actions/release-with-changelog + with: + token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ steps.tag.outputs.new_tag }} \ No newline at end of file diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml new file mode 100644 index 0000000..9d32b84 --- /dev/null +++ b/.github/workflows/ci-test.yml @@ -0,0 +1,28 @@ +# Run on branch builds +name: CI-Go-Tests +permissions: + contents: read + id-token: write + +on: + push: + branches-ignore: + - master + - main + +jobs: + go-build-test: + name: go build test + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install xmlsec1 + run: sudo apt-get install -y libxmlsec1 && sudo apt-get install xmlsec1 + - name: Go Build + run: | + echo "machine github.com login machine-parsable password ${{ secrets.GH_PAT_MACHINE_PARSABLE }}" > ~/.netrc + go build -v -o bin/events + - name: Go Test + run: go test -v -race ./... \ No newline at end of file diff --git a/.github/workflows/stale-actions.yaml b/.github/workflows/stale-actions.yaml new file mode 100644 index 0000000..ef6db0d --- /dev/null +++ b/.github/workflows/stale-actions.yaml @@ -0,0 +1,26 @@ +name: "Mark or close stale issues and PRs" +on: + schedule: + - cron: "0 10 * * 1-5" + +jobs: + stale: + name: "Check for stale PRs" + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v8 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + # Staling issues and PR's + days-before-stale: 15 + stale-pr-label: stale + stale-pr-message: | + This PR has been automatically marked as stale because it has been open 15 days + with no activity. Remove stale label and comment on this PR or it will be closed + in 3 days. Setting this PR to draft will also prevent it from being closed. + exempt-all-milestones: true + exempt-draft-pr: true + # Time is up after 18 days + days-before-pr-close: 18 + delete-branch: true + close-pr-message: "This PR was closed because it has been stalled for 18 days with no activity." \ No newline at end of file diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..51cdb83 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,52 @@ +[allowlist] + regexes = ['''-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY-----'''] \ No newline at end of file diff --git a/authnrequest.go b/authnrequest.go index 3c826a6..1b872db 100644 --- a/authnrequest.go +++ b/authnrequest.go @@ -21,7 +21,7 @@ import ( "net/url" "time" - "github.com/RobotsAndPencils/go-saml/util" + "github.com/parsable/go-saml/util" ) func ParseCompressedEncodedRequest(b64RequestXML string) (*AuthnRequest, error) { @@ -82,10 +82,13 @@ func (r *AuthnRequest) Validate(publicCertPath string) error { // GetSignedAuthnRequest returns a singed XML document that represents a AuthnRequest SAML document func (s *ServiceProviderSettings) GetAuthnRequest() *AuthnRequest { - r := NewAuthnRequest() + r := NewAuthnRequestCustom(s.SPSignRequest) r.AssertionConsumerServiceURL = s.AssertionConsumerServiceURL r.Issuer.Url = s.IDPSSODescriptorURL - r.Signature.KeyInfo.X509Data.X509Certificate.Cert = s.PublicCert() + if s.SPSignRequest { + r.Signature[0].KeyInfo.X509Data.X509Certificate.Cert = s.PublicCert() + r.Destination = s.IDPSSOURL + } return r } @@ -105,14 +108,17 @@ func GetAuthnRequestURL(baseURL string, b64XML string, state string) (string, er } func NewAuthnRequest() *AuthnRequest { + return NewAuthnRequestCustom(true) +} + +func NewAuthnRequestCustom(sign bool) *AuthnRequest { id := util.ID() - return &AuthnRequest{ + authReq := &AuthnRequest{ XMLName: xml.Name{ Local: "samlp:AuthnRequest", }, SAMLP: "urn:oasis:names:tc:SAML:2.0:protocol", SAML: "urn:oasis:names:tc:SAML:2.0:assertion", - SAMLSIG: "http://www.w3.org/2000/09/xmldsig#", ID: id, ProtocolBinding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", Version: "2.0", @@ -122,31 +128,33 @@ func NewAuthnRequest() *AuthnRequest { Local: "saml:Issuer", }, Url: "", // caller must populate ar.AppSettings.Issuer - SAML: "urn:oasis:names:tc:SAML:2.0:assertion", }, - IssueInstant: time.Now().UTC().Format(time.RFC3339Nano), - NameIDPolicy: NameIDPolicy{ + IssueInstant: time.Now().UTC().Format(time.RFC3339), + NameIDPolicy: &NameIDPolicy{ XMLName: xml.Name{ Local: "samlp:NameIDPolicy", }, AllowCreate: true, - Format: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient", + Format: "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", }, - RequestedAuthnContext: RequestedAuthnContext{ + RequestedAuthnContext: &RequestedAuthnContext{ XMLName: xml.Name{ Local: "samlp:RequestedAuthnContext", }, - SAMLP: "urn:oasis:names:tc:SAML:2.0:protocol", Comparison: "exact", AuthnContextClassRef: AuthnContextClassRef{ XMLName: xml.Name{ Local: "saml:AuthnContextClassRef", }, - SAML: "urn:oasis:names:tc:SAML:2.0:assertion", Transport: "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport", }, }, - Signature: Signature{ + } + + if sign { + authReq.SAMLSIG = "http://www.w3.org/2000/09/xmldsig#" + authReq.Signature = make([]Signature, 1, 1) + authReq.Signature[0] = Signature{ XMLName: xml.Name{ Local: "samlsig:Signature", }, @@ -165,7 +173,7 @@ func NewAuthnRequest() *AuthnRequest { XMLName: xml.Name{ Local: "samlsig:SignatureMethod", }, - Algorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + Algorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", }, SamlsigReference: SamlsigReference{ XMLName: xml.Name{ @@ -176,18 +184,26 @@ func NewAuthnRequest() *AuthnRequest { XMLName: xml.Name{ Local: "samlsig:Transforms", }, - Transform: Transform{ - XMLName: xml.Name{ - Local: "samlsig:Transform", + Transforms: []Transform{ + { + XMLName: xml.Name{ + Local: "samlsig:Transform", + }, + Algorithm: "http://www.w3.org/2000/09/xmldsig#enveloped-signature", + }, + { + XMLName: xml.Name{ + Local: "samlsig:Transform", + }, + Algorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", }, - Algorithm: "http://www.w3.org/2000/09/xmldsig#enveloped-signature", }, }, DigestMethod: DigestMethod{ XMLName: xml.Name{ Local: "samlsig:DigestMethod", }, - Algorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + Algorithm: "http://www.w3.org/2001/04/xmlenc#sha256", }, DigestValue: DigestValue{ XMLName: xml.Name{ @@ -217,12 +233,13 @@ func NewAuthnRequest() *AuthnRequest { }, }, }, - }, + } } + return authReq } func (r *AuthnRequest) String() (string, error) { - b, err := xml.MarshalIndent(r, "", " ") + b, err := xml.Marshal(r) if err != nil { return "", err } diff --git a/authnresponse.go b/authnresponse.go index 52f1f5a..dd5813b 100644 --- a/authnresponse.go +++ b/authnresponse.go @@ -6,7 +6,7 @@ import ( "errors" "time" - "github.com/RobotsAndPencils/go-saml/util" + "github.com/parsable/go-saml/util" ) func ParseCompressedEncodedResponse(b64ResponseXML string) (*Response, error) { @@ -29,20 +29,21 @@ func ParseCompressedEncodedResponse(b64ResponseXML string) (*Response, error) { } func ParseEncodedResponse(b64ResponseXML string) (*Response, error) { - response := Response{} bytesXML, err := base64.StdEncoding.DecodeString(b64ResponseXML) if err != nil { return nil, err } - err = xml.Unmarshal(bytesXML, &response) + return ParseDecodedResponse(bytesXML) +} + +func ParseDecodedResponse(responseXML []byte) (*Response, error) { + response := Response{} + err := xml.Unmarshal(responseXML, &response) if err != nil { return nil, err } - - // There is a bug with XML namespaces in Go that's causing XML attributes with colons to not be roundtrip - // marshal and unmarshaled so we'll keep the original string around for validation. - response.originalString = string(bytesXML) - // fmt.Println(response.originalString) + // save the original response because XML Signatures are fussy + response.originalString = string(responseXML) return &response, nil } @@ -59,7 +60,7 @@ func (r *Response) Validate(s *ServiceProviderSettings) error { return errors.New("no Assertions") } - if len(r.Signature.SignatureValue.Value) == 0 { + if len(r.Signature.SignatureValue.Value) == 0 && len(r.Assertion.Signature.SignatureValue.Value) == 0 && len(r.EncryptedAssertion.Assertion.Signature.SignatureValue.Value) == 0 { return errors.New("no signature") } @@ -75,7 +76,7 @@ func (r *Response) Validate(s *ServiceProviderSettings) error { return errors.New("subject recipient mismatch, expected: " + s.AssertionConsumerServiceURL + " not " + r.Assertion.Subject.SubjectConfirmation.SubjectConfirmationData.Recipient) } - err := VerifyResponseSignature(r.originalString, s.IDPPublicCertPath) + err := r.VerifySignature(s.IDPPublicCertPath) if err != nil { return err } @@ -93,6 +94,52 @@ func (r *Response) Validate(s *ServiceProviderSettings) error { return nil } +func (r *Response) FindSignatureTagName() (string, error) { + sigRef := r.Signature.SignedInfo.SamlsigReference.URI + if len(sigRef) == 0 { + sigRef = r.Assertion.Signature.SignedInfo.SamlsigReference.URI + if len(sigRef) == 0 && r.EncryptedAssertion.Assertion != nil { + sigRef = r.EncryptedAssertion.Assertion.Signature.SignedInfo.SamlsigReference.URI + } + if len(sigRef) == 0 { + return "", errors.New("No signature found in a supported location") + } + } + if sigRef[0] != '#' { + return "", errors.New("Weird Signature Reference URI: " + sigRef) + } + if r.ID == sigRef[1:] { + return "Response", nil + } + if r.Assertion.ID == sigRef[1:] { + return "Assertion", nil + } + if r.EncryptedAssertion.Assertion != nil && r.EncryptedAssertion.Assertion.ID == sigRef[1:] { + // this ambiguity makes xmlsec1 CLI not terribly useful... + return "Assertion", nil + } + return "", errors.New("could not resolve signature reference URI: " + sigRef) +} + +func (r *Response) Decrypt(SPPrivateCertPath string) (*Response, error) { + decrypted_xml, err := Decrypt(r.originalString, SPPrivateCertPath) + if err != nil { + return nil, err + } + authnResponse := &Response{} + err = xml.Unmarshal(decrypted_xml, &authnResponse) + authnResponse.originalString = string(decrypted_xml) + return authnResponse, err +} + +func (r *Response) VerifySignature(IDPPublicCertPath string) error { + sigTagName, err := r.FindSignatureTagName() + if err != nil { + return err + } + return VerifyResponseSignature(r.originalString, IDPPublicCertPath, sigTagName) +} + func NewSignedResponse() *Response { return &Response{ XMLName: xml.Name{ @@ -140,11 +187,13 @@ func NewSignedResponse() *Response { XMLName: xml.Name{ Local: "samlsig:Transforms", }, - Transform: Transform{ - XMLName: xml.Name{ - Local: "samlsig:Transform", + Transforms: []Transform{ + { + XMLName: xml.Name{ + Local: "samlsig:Transform", + }, + Algorithm: "http://www.w3.org/2000/09/xmldsig#enveloped-signature", }, - Algorithm: "http://www.w3.org/2000/09/xmldsig#enveloped-signature", }, }, DigestMethod: DigestMethod{ diff --git a/authnresponse_test.go b/authnresponse_test.go new file mode 100644 index 0000000..543a1fb --- /dev/null +++ b/authnresponse_test.go @@ -0,0 +1,113 @@ +package saml + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestResponseSignatureOnResponse(t *testing.T) { + assertion := assert.New(t) + sp := ServiceProviderSettings{ + PublicCertPath: "./default.crt", + PrivateKeyPath: "./default.key", + IDPSSOURL: "http://www.onelogin.net", + IDPSSODescriptorURL: "http://www.onelogin.net", + IDPPublicCertPath: "./default.crt", + AssertionConsumerServiceURL: "http://localhost:8000/auth/saml/name", + SPSignRequest: true, + } + + err := sp.Init() + assertion.NoError(err) + + // Construct a SignedResponse + response := NewSignedResponse() + + var sURI strings.Builder + sURI.WriteString("#") + sURI.WriteString(response.ID) + + response.Signature.SignedInfo.SamlsigReference.URI = sURI.String() + + sXml, err := response.String() + assertion.NoError(err) + + fmt.Println("Response (XML as String) : ", sXml) + + signedXml, err := SignResponse(sXml, "./default.key") + assertion.NoError(err) + + fmt.Println("Signed Response (XML as String) : ", sXml) + + signedResponse, err := ParseDecodedResponse([]byte(signedXml)) + assertion.NoError(err) + + err = signedResponse.VerifySignature("./default.crt") + assertion.NoError(err) +} + +func TestResponseSignatureOnAssertion(t *testing.T) { + assertion := assert.New(t) + sp := ServiceProviderSettings{ + PublicCertPath: "./default.crt", + PrivateKeyPath: "./default.key", + IDPSSOURL: "http://www.onelogin.net", + IDPSSODescriptorURL: "http://www.onelogin.net", + IDPPublicCertPath: "./default.crt", + AssertionConsumerServiceURL: "http://localhost:8000/auth/saml/name", + SPSignRequest: true, + } + + err := sp.Init() + assertion.NoError(err) + + // Construct a SignedResponse + response := NewSignedResponse() + + var sURI strings.Builder + sURI.WriteString("#") + sURI.WriteString(response.ID) + + response.Assertion.Signature.SignedInfo.SamlsigReference.URI = sURI.String() + + sXml, err := response.String() + assertion.NoError(err) + + fmt.Println("Response (XML as String) : ", sXml) + + signedXml, err := SignResponse(sXml, "./default.key") + assertion.NoError(err) + + fmt.Println("Signed Response (XML as String) : ", sXml) + + signedResponse, err := ParseDecodedResponse([]byte(signedXml)) + assertion.NoError(err) + + err = signedResponse.VerifySignature("./default.crt") + assertion.NoError(err) +} + +/*func TestLoadedXmlResponse(t *testing.T) { + assertion := assert.New(t) + sp := ServiceProviderSettings{ + PublicCertPath: "./default.crt", + PrivateKeyPath: "./default.key", + IDPSSOURL: "http://www.onelogin.net", + IDPSSODescriptorURL: "http://www.onelogin.net", + IDPPublicCertPath: "./default.crt", + AssertionConsumerServiceURL: "http://localhost:8000/auth/saml/name", + SPSignRequest: true, + } + + err := sp.Init() + assertion.NoError(err) + + gpXMLResponse, err := LoadXml("./samlresponse.xml") // Feel free to change the Path to whatever your XML Response is + assertion.NoError(err) + + err = VerifyResponseSignature(gpXMLResponse, sp.PublicCertPath, "") + assertion.NoError(err) +}*/ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a151cc6 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/parsable/go-saml + +go 1.13 + +require ( + github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 + github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d + github.com/stretchr/testify v1.8.4 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..91a58bf --- /dev/null +++ b/go.sum @@ -0,0 +1,21 @@ +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/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= +github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= +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/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.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.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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= diff --git a/logoutrequest.go b/logoutrequest.go new file mode 100644 index 0000000..f2377e8 --- /dev/null +++ b/logoutrequest.go @@ -0,0 +1,192 @@ +// Copyright 2014 Matthew Baird, Andrew Mussey +// Copyright 2015 Wearable Intelligence +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package saml + +import ( + "encoding/base64" + "encoding/xml" + "time" + + "github.com/parsable/go-saml/util" +) + +// GetSignedAuthnRequest returns a singed XML document that represents a AuthnRequest SAML document +func (s *ServiceProviderSettings) GetLogoutRequest(nameID string, sessionIds... string) *LogoutRequest { + r := NewLogoutRequest(s.SPSignRequest) + r.Issuer.Url = s.IDPSSODescriptorURL + if s.SPSignRequest { + r.Signature.KeyInfo.X509Data.X509Certificate.Cert = s.PublicCert() + r.Destination = s.IDPSSOLogoutURL + } + r.NameID.Value = nameID + if len(sessionIds) > 0 { + r.SessionIndex = make([]SessionIndex, len(sessionIds)) + for idx, sid := range sessionIds { + r.SessionIndex[idx].Value = sid + r.SessionIndex[idx].XMLName.Local = "samlp:SessionIndex" + } + } + + return r +} + +func NewLogoutRequest(sign bool) *LogoutRequest { + id := util.ID() + logoutReq := &LogoutRequest{ + XMLName: xml.Name{ + Local: "samlp:LogoutRequest", + }, + SAMLP: "urn:oasis:names:tc:SAML:2.0:protocol", + SAML: "urn:oasis:names:tc:SAML:2.0:assertion", + ID: id, + Version: "2.0", + Issuer: Issuer{ + XMLName: xml.Name{ + Local: "saml:Issuer", + }, + Url: "", // caller must populate ar.AppSettings.Issuer + }, + IssueInstant: time.Now().UTC().Format(time.RFC3339), + NameID: NameID { + XMLName: xml.Name{ + Local: "saml:NameID", + }, + Value: "", // caller must populate + }, + } + + if sign { + logoutReq.SAMLSIG = "http://www.w3.org/2000/09/xmldsig#" + logoutReq.Signature = &Signature{ + XMLName: xml.Name{ + Local: "samlsig:Signature", + }, + Id: "Signature1", + SignedInfo: SignedInfo{ + XMLName: xml.Name{ + Local: "samlsig:SignedInfo", + }, + CanonicalizationMethod: CanonicalizationMethod{ + XMLName: xml.Name{ + Local: "samlsig:CanonicalizationMethod", + }, + Algorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + }, + SignatureMethod: SignatureMethod{ + XMLName: xml.Name{ + Local: "samlsig:SignatureMethod", + }, + Algorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + }, + SamlsigReference: SamlsigReference{ + XMLName: xml.Name{ + Local: "samlsig:Reference", + }, + URI: "#" + id, + Transforms: Transforms{ + XMLName: xml.Name{ + Local: "samlsig:Transforms", + }, + Transforms: []Transform{ + { + XMLName: xml.Name{ + Local: "samlsig:Transform", + }, + Algorithm: "http://www.w3.org/2000/09/xmldsig#enveloped-signature", + }, + { + XMLName: xml.Name{ + Local: "samlsig:Transform", + }, + Algorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + }, + }, + }, + DigestMethod: DigestMethod{ + XMLName: xml.Name{ + Local: "samlsig:DigestMethod", + }, + Algorithm: "http://www.w3.org/2001/04/xmlenc#sha256", + }, + DigestValue: DigestValue{ + XMLName: xml.Name{ + Local: "samlsig:DigestValue", + }, + }, + }, + }, + SignatureValue: SignatureValue{ + XMLName: xml.Name{ + Local: "samlsig:SignatureValue", + }, + }, + KeyInfo: KeyInfo{ + XMLName: xml.Name{ + Local: "samlsig:KeyInfo", + }, + X509Data: X509Data{ + XMLName: xml.Name{ + Local: "samlsig:X509Data", + }, + X509Certificate: X509Certificate{ + XMLName: xml.Name{ + Local: "samlsig:X509Certificate", + }, + Cert: "", // caller must populate cert, + }, + }, + }, + } + } + return logoutReq +} + +func (r *LogoutRequest) String() (string, error) { + b, err := xml.Marshal(r) + if err != nil { + return "", err + } + + return string(b), nil +} + +func (r *LogoutRequest) SignedString(privateKeyPath string) (string, error) { + s, err := r.String() + if err != nil { + return "", err + } + + return SignLogoutRequest(s, privateKeyPath) +} + +// GetAuthnRequestURL generate a URL for the AuthnRequest to the IdP with the SAMLRequst parameter encoded +func (r *LogoutRequest) EncodedSignedString(privateKeyPath string) (string, error) { + signed, err := r.SignedString(privateKeyPath) + if err != nil { + return "", err + } + b64XML := base64.StdEncoding.EncodeToString([]byte(signed)) + return b64XML, nil +} + +func (r *LogoutRequest) EncodedString() (string, error) { + saml, err := r.String() + if err != nil { + return "", err + } + b64XML := base64.StdEncoding.EncodeToString([]byte(saml)) + return b64XML, nil +} diff --git a/saml.go b/saml.go index 4ba8f64..23926b0 100644 --- a/saml.go +++ b/saml.go @@ -1,6 +1,6 @@ package saml -import "github.com/RobotsAndPencils/go-saml/util" +import "github.com/parsable/go-saml/util" // ServiceProviderSettings provides settings to configure server acting as a SAML Service Provider. // Expect only one IDP per SP in this configuration. If you need to configure multipe IDPs for an SP @@ -9,10 +9,11 @@ type ServiceProviderSettings struct { PublicCertPath string PrivateKeyPath string IDPSSOURL string + IDPSSOLogoutURL string IDPSSODescriptorURL string IDPPublicCertPath string AssertionConsumerServiceURL string - SPSignRequest bool + SPSignRequest bool hasInit bool publicCert string diff --git a/samlresponse.xml b/samlresponse.xml new file mode 100644 index 0000000..5d6b1a3 --- /dev/null +++ b/samlresponse.xml @@ -0,0 +1 @@ +https://auth.kochid.comsXsSLWX4wRQbU8WEoWLHDli+L1dlKJYTUmqiREYfT/I=fb2K++ys6Se5t9ZcL0K3qfy356rOY26LqQbUTv6VQGMsKQ/RScwkpE5vbFgJaiyrw9xNvD8ZK2FwieB1gMqJUsnVRPQiuYIulrKZfNQn1oFwSFhDrRD/wUdtdia7+afSspYlid6Gc5qieTiVdl2OI8rpfNeF9T399N4HB2ZR9pcjuGvTBspQVRPHAsGVQhK80LStSD8Gtk7xRKILKSl+J/+wtEpRTVE0BtzP+oRuaFvEn65ACUWOVW12liIJUawjGq9t6rPaq/bzVYrdA4Zu/yB10wa/3ZH1COyAF9h1U/W6vu70F8SoARW957F2chFrJVuMzTPNkdGQOlb/RNv0VQ==https://auth.kochid.comCOLIN.DAWSON@GAPAC.COMhttps://go.parsable.comurn:oasis:names:tc:SAML:2.0:ac:classes:TelephonyCOLINDAWSONCOLIN.DAWSON@GAPAC.COM \ No newline at end of file diff --git a/types.go b/types.go index d32b091..1cf2b3a 100644 --- a/types.go +++ b/types.go @@ -4,45 +4,47 @@ import "encoding/xml" type AuthnRequest struct { XMLName xml.Name - SAMLP string `xml:"xmlns:samlp,attr"` - SAML string `xml:"xmlns:saml,attr"` - SAMLSIG string `xml:"xmlns:samlsig,attr"` - ID string `xml:"ID,attr"` - Version string `xml:"Version,attr"` - ProtocolBinding string `xml:"ProtocolBinding,attr"` - AssertionConsumerServiceURL string `xml:"AssertionConsumerServiceURL,attr"` - IssueInstant string `xml:"IssueInstant,attr"` - AssertionConsumerServiceIndex int `xml:"AssertionConsumerServiceIndex,attr"` - AttributeConsumingServiceIndex int `xml:"AttributeConsumingServiceIndex,attr"` - Issuer Issuer `xml:"Issuer"` - NameIDPolicy NameIDPolicy `xml:"NameIDPolicy"` - RequestedAuthnContext RequestedAuthnContext `xml:"RequestedAuthnContext"` - Signature Signature `xml:"Signature,omitempty"` + SAMLP string `xml:"xmlns:samlp,attr"` + SAML string `xml:"xmlns:saml,attr"` + SAMLSIG string `xml:"xmlns:samlsig,attr,omitempty"` + ID string `xml:"ID,attr"` + Version string `xml:"Version,attr"` + ProtocolBinding string `xml:"ProtocolBinding,attr,omitempty"` + AssertionConsumerServiceURL string `xml:"AssertionConsumerServiceURL,attr"` + IssueInstant string `xml:"IssueInstant,attr"` + Destination string `xml:"Destination,attr,omitempty"` + AssertionConsumerServiceIndex int `xml:"AssertionConsumerServiceIndex,attr,omitempty"` + AttributeConsumingServiceIndex int `xml:"AttributeConsumingServiceIndex,attr,omitempty"` + ForceAuthn string `xml:"ForceAuthn,attr,omitempty"` + Issuer Issuer `xml:"Issuer"` + Signature []Signature `xml:"Signature,omitempty"` + NameIDPolicy *NameIDPolicy `xml:"NameIDPolicy,omitempty"` + RequestedAuthnContext *RequestedAuthnContext `xml:"RequestedAuthnContext,omitempty"` originalString string } type Issuer struct { XMLName xml.Name - SAML string `xml:"xmlns:saml,attr"` + SAML string `xml:"xmlns:saml,attr,omitempty"` Url string `xml:",innerxml"` } type NameIDPolicy struct { XMLName xml.Name - AllowCreate bool `xml:"AllowCreate,attr"` + AllowCreate bool `xml:"AllowCreate,attr,omitempty"` Format string `xml:"Format,attr"` } type RequestedAuthnContext struct { XMLName xml.Name - SAMLP string `xml:"xmlns:samlp,attr"` + SAMLP string `xml:"xmlns:samlp,attr,omitempty"` Comparison string `xml:"Comparison,attr"` AuthnContextClassRef AuthnContextClassRef `xml:"AuthnContextClassRef"` } type AuthnContextClassRef struct { XMLName xml.Name - SAML string `xml:"xmlns:saml,attr"` + SAML string `xml:"xmlns:saml,attr,omitempty"` Transport string `xml:",innerxml"` } @@ -56,9 +58,9 @@ type Signature struct { type SignedInfo struct { XMLName xml.Name - CanonicalizationMethod CanonicalizationMethod - SignatureMethod SignatureMethod - SamlsigReference SamlsigReference + CanonicalizationMethod CanonicalizationMethod `xml:"CanonicalizationMethod"` + SignatureMethod SignatureMethod `xml:"SignatureMethod"` + SamlsigReference SamlsigReference `xml:"Reference"` } type SignatureValue struct { @@ -68,7 +70,7 @@ type SignatureValue struct { type KeyInfo struct { XMLName xml.Name - X509Data X509Data `xml:",innerxml"` + X509Data X509Data `xml:"X509Data"` } type CanonicalizationMethod struct { @@ -91,12 +93,12 @@ type SamlsigReference struct { type X509Data struct { XMLName xml.Name - X509Certificate X509Certificate `xml:",innerxml"` + X509Certificate X509Certificate `xml:"X509Certificate"` } type Transforms struct { - XMLName xml.Name - Transform Transform + XMLName xml.Name + Transforms []Transform } type DigestMethod struct { @@ -138,15 +140,31 @@ type Extensions struct { EntityAttributes string `xml:"EntityAttributes"` } +type SSODescriptor struct { + //ArtifactResolutionServices []ArtifactResolutionServices `xml:"ArtifactResolutionService"` + SingleLogoutService []SingleLogoutService `xml:"SingleLogoutService"` + //NameIDFormats []NameIdFormat `xml:"NameIDFormat"` +} + type SPSSODescriptor struct { XMLName xml.Name ProtocolSupportEnumeration string `xml:"protocolSupportEnumeration,attr"` - SigningKeyDescriptor KeyDescriptor - EncryptionKeyDescriptor KeyDescriptor + SSODescriptor + SigningKeyDescriptor KeyDescriptor + EncryptionKeyDescriptor KeyDescriptor // SingleLogoutService SingleLogoutService `xml:"SingleLogoutService"` AssertionConsumerServices []AssertionConsumerService } +type IDPSSODescriptor struct { + XMLName xml.Name + ProtocolSupportEnumeration string `xml:"protocolSupportEnumeration,attr"` + SSODescriptor + KeyDescriptors []KeyDescriptor + SingleSignOnService []SingleSignOnService `xml:"SingleSignOnService"` + Attributes []Attribute +} + type EntityAttributes struct { XMLName xml.Name SAML string `xml:"xmlns:saml,attr"` @@ -154,9 +172,6 @@ type EntityAttributes struct { EntityAttributes []Attribute `xml:"Attribute"` // should be array?? } -type SPSSODescriptors struct { -} - type KeyDescriptor struct { XMLName xml.Name Use string `xml:"use,attr"` @@ -168,6 +183,11 @@ type SingleLogoutService struct { Location string `xml:"Location,attr"` } +type SingleSignOnService struct { + Binding string `xml:"Binding,attr"` + Location string `xml:"Location,attr"` +} + type AssertionConsumerService struct { XMLName xml.Name Binding string `xml:"Binding,attr"` @@ -186,26 +206,46 @@ type Response struct { IssueInstant string `xml:"IssueInstant,attr"` InResponseTo string `xml:"InResponseTo,attr"` - Assertion Assertion `xml:"Assertion"` - Signature Signature `xml:"Signature"` - Issuer Issuer `xml:"Issuer"` - Status Status `xml:"Status"` + Assertion Assertion `xml:"Assertion"` + EncryptedAssertion EncryptedAssertion `xml:"EncryptedAssertion"` + Signature Signature `xml:"Signature"` + Issuer Issuer `xml:"Issuer"` + Status Status `xml:"Status"` + originalString string +} + +type EncryptedData struct { + XMLName xml.Name + Type string `xml:"Type,attr"` +} + +type EncryptedAssertion struct { + XMLName xml.Name + EncryptedData *EncryptedData `xml:"EncryptedData"` - originalString string + // "Assertion" nodes are not valid here according to the SAML assertion schema, but they are implied by the + // XMLEnc standard as an intermediate form, and therefore in the files that 'xmlsec1 --decrypt' returns. + Assertion *Assertion `xml:"Assertion"` } type Assertion struct { XMLName xml.Name - ID string `xml:"ID,attr"` - Version string `xml:"Version,attr"` - XS string `xml:"xmlns:xs,attr"` - XSI string `xml:"xmlns:xsi,attr"` - SAML string `xml:"saml,attr"` - IssueInstant string `xml:"IssueInstant,attr"` - Issuer Issuer `xml:"Issuer"` + ID string `xml:"ID,attr"` + Version string `xml:"Version,attr"` + XS string `xml:"xmlns:xs,attr"` + XSI string `xml:"xmlns:xsi,attr"` + SAML string `xml:"saml,attr"` + IssueInstant string `xml:"IssueInstant,attr"` + Issuer Issuer `xml:"Issuer"` + Signature Signature `xml:"Signature"` Subject Subject Conditions Conditions AttributeStatement AttributeStatement + AuthnStatement AuthnStatement +} + +type AuthnStatement struct { + SessionIndex string `xml:"SessionIndex,attr"` } type Conditions struct { @@ -239,7 +279,7 @@ type SubjectConfirmationData struct { type NameID struct { XMLName xml.Name - Format string `xml:",attr"` + Format string `xml:",attr,omitempty"` Value string `xml:",innerxml"` } @@ -266,3 +306,45 @@ type AttributeStatement struct { XMLName xml.Name Attributes []Attribute `xml:"Attribute"` } + +type LogoutRequest struct { + XMLName xml.Name + SAMLP string `xml:"xmlns:samlp,attr"` + SAML string `xml:"xmlns:saml,attr"` + SAMLSIG string `xml:"xmlns:samlsig,attr,omitempty"` + ID string `xml:"ID,attr"` + Version string `xml:"Version,attr"` + IssueInstant string `xml:"IssueInstant,attr"` + Destination string `xml:"Destination,attr,omitempty"` + Issuer Issuer `xml:"Issuer"` + Signature *Signature `xml:"Signature,omitempty"` + NameID NameID `xml:"NameID"` + SessionIndex []SessionIndex `xml:"SessionIndex"` +} + +type SessionIndex struct { + XMLName xml.Name + Value string `xml:",innerxml"` +} + +type RoleDescriptor struct { + ValidUntil string `xml:"validUntil,attr,omitempty"` + CacheDuration string `xml:"cacheDuration,attr,omitempty"` + ProtocolSupportEnumeration string `xml:"protocolSupportEnumeration,attr"` + Signature *Signature `xml:"Signature,omitempty"` + KeyDescriptors []KeyDescriptor `xml:"KeyDescriptor,omitempty"` +} + +type Metadata struct { + XMLName xml.Name // urn:oasis:names:tc:SAML:2.0:metadata:EntityDescriptor + ID string `xml:"ID,attr,omitempty"` + EntityId string `xml:"entityID,attr"` + ValidUntil string `xml:"validUntil,attr,omitempty"` + CacheDuration string `xml:"cacheDuration,attr,omitempty"` + Signature *Signature `xml:"Signature,omitempty"` + + // note: the schema permits these elements to appear in any order an unlimited number of times + RoleDescriptor []RoleDescriptor `xml:"RoleDescriptor,omitempty"` + SPSSODescriptor *SPSSODescriptor `xml:"SPSSODescriptor,omitempty"` + IDPSSODescriptor *IDPSSODescriptor `xml:"IDPSSODescriptor,omitempty"` +} diff --git a/xmlsec.go b/xmlsec.go index 484a940..b4e94ab 100644 --- a/xmlsec.go +++ b/xmlsec.go @@ -9,8 +9,9 @@ import ( ) const ( - xmlResponseID = "urn:oasis:names:tc:SAML:2.0:protocol:Response" - xmlRequestID = "urn:oasis:names:tc:SAML:2.0:protocol:AuthnRequest" + xmlResponseID = "urn:oasis:names:tc:SAML:2.0:protocol:Response" + xmlRequestID = "urn:oasis:names:tc:SAML:2.0:protocol:AuthnRequest" + xmlLogoutRequestID = "urn:oasis:names:tc:SAML:2.0:protocol:LogoutRequest" ) // SignRequest sign a SAML 2.0 AuthnRequest @@ -20,6 +21,10 @@ func SignRequest(xml string, privateKeyPath string) (string, error) { return sign(xml, privateKeyPath, xmlRequestID) } +func SignLogoutRequest(xml string, privateKeyPath string) (string, error) { + return sign(xml, privateKeyPath, xmlLogoutRequestID) +} + // SignResponse sign a SAML 2.0 Response // `privateKeyPath` must be a path on the filesystem, xmlsec1 is run out of process // through `exec` @@ -66,8 +71,11 @@ func sign(xml string, privateKeyPath string, id string) (string, error) { // VerifyResponseSignature verify signature of a SAML 2.0 Response document // `publicCertPath` must be a path on the filesystem, xmlsec1 is run out of process // through `exec` -func VerifyResponseSignature(xml string, publicCertPath string) error { - return verify(xml, publicCertPath, xmlResponseID) +func VerifyResponseSignature(xml, publicCertPath, xmlNodeName string) error { + if xmlNodeName == "" { + xmlNodeName = xmlResponseID + } + return verify(xml, publicCertPath, xmlNodeName) } // VerifyRequestSignature verify signature of a SAML 2.0 AuthnRequest document @@ -96,8 +104,41 @@ func verify(xml string, publicCertPath string, id string) error { return nil } +func Decrypt(xml string, privateKeyPath string) ([]byte, error) { + samlXmlsecInput, err := ioutil.TempFile(os.TempDir(), "tmpes") + if err != nil { + return nil, err + } + samlXmlsecInput.WriteString(xml) + samlXmlsecInput.Close() + defer deleteTempFile(samlXmlsecInput.Name()) + + samlXmlsecOutput, err := ioutil.TempFile(os.TempDir(), "tmpds") + if err != nil { + return nil, err + } + defer deleteTempFile(samlXmlsecOutput.Name()) + + args := []string{"--decrypt", "--privkey-pem", privateKeyPath, + "--output", samlXmlsecOutput.Name(), samlXmlsecInput.Name()} + // fmt.Println("running:", "xmlsec1", args) + output, err := exec.Command("xmlsec1", args...).CombinedOutput() + if err != nil { + return nil, errors.New("error decrypting document: " + err.Error() + "; " + string(output)) + } + return ioutil.ReadAll(samlXmlsecOutput) +} + // deleteTempFile remove a file and ignore error // Intended to be called in a defer after the creation of a temp file to ensure cleanup func deleteTempFile(filename string) { _ = os.Remove(filename) } + +func LoadXml(certPath string) (string, error) { + bXML, err := ioutil.ReadFile(certPath) + if err != nil { + return "", err + } + return string(bXML), nil +} diff --git a/xmlsec_test.go b/xmlsec_test.go index 41bedee..7a90125 100644 --- a/xmlsec_test.go +++ b/xmlsec_test.go @@ -4,7 +4,7 @@ import ( "encoding/xml" "testing" - "github.com/RobotsAndPencils/go-saml/util" + "github.com/parsable/go-saml/util" "github.com/stretchr/testify/assert" ) @@ -15,7 +15,7 @@ func TestRequest(t *testing.T) { // Construct an AuthnRequest authRequest := NewAuthnRequest() - authRequest.Signature.KeyInfo.X509Data.X509Certificate.Cert = cert + authRequest.Signature[0].KeyInfo.X509Data.X509Certificate.Cert = cert b, err := xml.MarshalIndent(authRequest, "", " ") assert.NoError(err) @@ -46,6 +46,6 @@ func TestResponse(t *testing.T) { assert.NoError(err) assert.NotEmpty(signedXml) - err = VerifyRequestSignature(signedXml, "./default.crt") + err = VerifyResponseSignature(signedXml, "./default.crt", "") assert.NoError(err) }