From 86c67e944cbfa9b8e67c097e4eb8701ea6c257ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 05:21:19 +0000 Subject: [PATCH 01/11] Bump golang.org/x/crypto from 0.33.0 to 0.45.0 Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.33.0 to 0.45.0. - [Commits](https://github.com/golang/crypto/compare/v0.33.0...v0.45.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.45.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- go.mod | 6 +++--- go.sum | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 44c05136..f3488d20 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,14 @@ module github.com/crewjam/saml -go 1.22 +go 1.24.0 require ( - github.com/golang-jwt/jwt/v5 v5.2.2 github.com/beevik/etree v1.5.0 + github.com/golang-jwt/jwt/v5 v5.2.2 github.com/google/go-cmp v0.7.0 github.com/mattermost/xml-roundtrip-validator v0.1.0 github.com/russellhaering/goxmldsig v1.4.0 - golang.org/x/crypto v0.33.0 + golang.org/x/crypto v0.45.0 gotest.tools v2.2.0+incompatible ) diff --git a/go.sum b/go.sum index d09e9615..24a60aa5 100644 --- a/go.sum +++ b/go.sum @@ -32,8 +32,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= -golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 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-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From 52b35ffe77c3097d5f4f8acc0ff15c71e885fac8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 17:24:32 +0000 Subject: [PATCH 02/11] Bump github.com/golang-jwt/jwt/v5 from 5.2.2 to 5.3.0 Bumps [github.com/golang-jwt/jwt/v5](https://github.com/golang-jwt/jwt) from 5.2.2 to 5.3.0. - [Release notes](https://github.com/golang-jwt/jwt/releases) - [Commits](https://github.com/golang-jwt/jwt/compare/v5.2.2...v5.3.0) --- updated-dependencies: - dependency-name: github.com/golang-jwt/jwt/v5 dependency-version: 5.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f3488d20..0fa5664d 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.0 require ( github.com/beevik/etree v1.5.0 - github.com/golang-jwt/jwt/v5 v5.2.2 + github.com/golang-jwt/jwt/v5 v5.3.0 github.com/google/go-cmp v0.7.0 github.com/mattermost/xml-roundtrip-validator v0.1.0 github.com/russellhaering/goxmldsig v1.4.0 diff --git a/go.sum b/go.sum index 24a60aa5..69b4d482 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 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/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= -github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= From fe4d2f7ba819bdeef4bc2a1f6f70a821175b050f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 17:24:33 +0000 Subject: [PATCH 03/11] Bump github.com/russellhaering/goxmldsig from 1.4.0 to 1.5.0 Bumps [github.com/russellhaering/goxmldsig](https://github.com/russellhaering/goxmldsig) from 1.4.0 to 1.5.0. - [Release notes](https://github.com/russellhaering/goxmldsig/releases) - [Commits](https://github.com/russellhaering/goxmldsig/compare/v1.4.0...v1.5.0) --- updated-dependencies: - dependency-name: github.com/russellhaering/goxmldsig dependency-version: 1.5.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 23 ++++------------------- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/go.mod b/go.mod index f3488d20..9b00af0b 100644 --- a/go.mod +++ b/go.mod @@ -7,13 +7,13 @@ require ( github.com/golang-jwt/jwt/v5 v5.2.2 github.com/google/go-cmp v0.7.0 github.com/mattermost/xml-roundtrip-validator v0.1.0 - github.com/russellhaering/goxmldsig v1.4.0 + github.com/russellhaering/goxmldsig v1.5.0 golang.org/x/crypto v0.45.0 gotest.tools v2.2.0+incompatible ) require ( - github.com/jonboulle/clockwork v0.2.2 // indirect + github.com/jonboulle/clockwork v0.5.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/stretchr/testify v1.10.0 // indirect ) diff --git a/go.sum b/go.sum index 24a60aa5..8efac347 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ -github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/beevik/etree v1.5.0 h1:iaQZFSDS+3kYZiGoc9uKeOkUY3nYMXOKLl6KIJxiJWs= github.com/beevik/etree v1.5.0/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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= @@ -9,25 +7,16 @@ github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeD github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -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/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -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/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU= github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/russellhaering/goxmldsig v1.4.0 h1:8UcDh/xGyQiyrW+Fq5t8f+l2DLB1+zlhYzkPUJ7Qhys= -github.com/russellhaering/goxmldsig v1.4.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= +github.com/russellhaering/goxmldsig v1.5.0 h1:AU2UkkYIUOTyZRbe08XMThaOCelArgvNfYapcmSjBNw= +github.com/russellhaering/goxmldsig v1.5.0/go.mod h1:x98CjQNFJcWfMxeOrMnMKg70lvDP6tE0nTaeUnjXDmk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= @@ -35,11 +24,7 @@ github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 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-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 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= From b28c88c850a644d4ed03efc09a6ff058f4b6c3e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 19:26:25 +0000 Subject: [PATCH 04/11] Bump golang.org/x/crypto from 0.45.0 to 0.47.0 Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.45.0 to 0.47.0. - [Commits](https://github.com/golang/crypto/compare/v0.45.0...v0.47.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.47.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 62ca0ca4..d9c03ffe 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/google/go-cmp v0.7.0 github.com/mattermost/xml-roundtrip-validator v0.1.0 github.com/russellhaering/goxmldsig v1.5.0 - golang.org/x/crypto v0.45.0 + golang.org/x/crypto v0.47.0 gotest.tools v2.2.0+incompatible ) diff --git a/go.sum b/go.sum index a25b577d..545817d2 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= 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= From 31679228549b81ce4567ae96d4fb38a11dff51bf Mon Sep 17 00:00:00 2001 From: Dinesh Udayakumar Date: Fri, 16 Jan 2026 16:57:27 -0500 Subject: [PATCH 05/11] Use flexible error assertions in XSW tests --- service_provider_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/service_provider_test.go b/service_provider_test.go index 35103d6b..6eaf09e9 100644 --- a/service_provider_test.go +++ b/service_provider_test.go @@ -1433,8 +1433,8 @@ func TestXswPermutationSevenIsRejected(t *testing.T) { 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 - assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, - "cannot validate signature on Assertion: Signature could not be verified")) + assert.Check(t, is.ErrorContains(err.(*InvalidResponseError).PrivateErr, + "cannot validate signature on Assertion:")) } func TestXswPermutationEightIsRejected(t *testing.T) { @@ -1464,8 +1464,8 @@ func TestXswPermutationEightIsRejected(t *testing.T) { 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 - assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr, - "cannot validate signature on Assertion: Signature could not be verified")) + assert.Check(t, is.ErrorContains(err.(*InvalidResponseError).PrivateErr, + "cannot validate signature on Assertion:")) } func TestXswPermutationNineIsRejected(t *testing.T) { From 24988d6453d9e1a266dabd8acdac1d8d9d24e62b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 14:16:14 +0000 Subject: [PATCH 06/11] Bump github.com/golang-jwt/jwt/v5 from 5.3.0 to 5.3.1 Bumps [github.com/golang-jwt/jwt/v5](https://github.com/golang-jwt/jwt) from 5.3.0 to 5.3.1. - [Release notes](https://github.com/golang-jwt/jwt/releases) - [Commits](https://github.com/golang-jwt/jwt/compare/v5.3.0...v5.3.1) --- updated-dependencies: - dependency-name: github.com/golang-jwt/jwt/v5 dependency-version: 5.3.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d9c03ffe..a9c80ebd 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.0 require ( github.com/beevik/etree v1.5.0 - github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/golang-jwt/jwt/v5 v5.3.1 github.com/google/go-cmp v0.7.0 github.com/mattermost/xml-roundtrip-validator v0.1.0 github.com/russellhaering/goxmldsig v1.5.0 diff --git a/go.sum b/go.sum index 545817d2..e5d91da9 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,8 @@ github.com/beevik/etree v1.5.0/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+Wem 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/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= From 7bc22c81d02287fec1524d85e4abb6e383a5ea96 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:17:20 +0000 Subject: [PATCH 07/11] Bump golang.org/x/crypto from 0.47.0 to 0.48.0 Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.47.0 to 0.48.0. - [Commits](https://github.com/golang/crypto/compare/v0.47.0...v0.48.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.48.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a9c80ebd..47bfc2c9 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/google/go-cmp v0.7.0 github.com/mattermost/xml-roundtrip-validator v0.1.0 github.com/russellhaering/goxmldsig v1.5.0 - golang.org/x/crypto v0.47.0 + golang.org/x/crypto v0.48.0 gotest.tools v2.2.0+incompatible ) diff --git a/go.sum b/go.sum index e5d91da9..4f45f714 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= -golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= 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= From 06ae334dd0d641e0ffe769c8061d0c6fddb75efe Mon Sep 17 00:00:00 2001 From: Dinesh Udayakumar Date: Tue, 24 Feb 2026 11:40:17 -0500 Subject: [PATCH 08/11] Pin lint CI Go version to go.mod Using `go-version: stable` resolved to Go 1.26, but go.mod declares go 1.24.0. golangci-lint was picking up a file from the Go 1.26 toolchain's own vendor directory: golang.org/x/crypto/chacha20poly1305/fips140only_go1.26.go This file has a `//go:build go1.26` constraint, which causes a typecheck failure when the module is built with go 1.24. That failure cascades into false-positive errors across the codebase. Switching to `go-version-file: go.mod` pins CI to the Go version declared in go.mod, ensuring toolchain and module version stay in sync. --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 688090cb..1d84be9e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: stable + go-version-file: go.mod - name: golangci-lint uses: golangci/golangci-lint-action@v7 with: From 91213eecae955a506c427182f0a204aac13b47c9 Mon Sep 17 00:00:00 2001 From: Dinesh Udayakumar Date: Tue, 24 Feb 2026 11:40:01 -0500 Subject: [PATCH 09/11] Add crypto.Signer support for KMS/HSM Check public key type instead of private key type to support crypto.Signer implementations (e.g. GCP KMS, AWS KMS, HSM) that aren't concrete *rsa.PrivateKey or *ecdsa.PrivateKey types. Supports RSA (RS256/RS384/RS512), RSA-PSS (PS256/PS384/PS512), ECDSA (ES256/ES384/ES512), and EdDSA signing methods via crypto.Signer for JWT session and tracked request signing. --- samlsp/middleware_test.go | 271 ++++++++++++++++++++++++++++++++++ samlsp/new.go | 17 ++- samlsp/request_tracker_jwt.go | 5 +- samlsp/session_jwt.go | 160 +++++++++++++++++++- service_provider.go | 32 +++- 5 files changed, 467 insertions(+), 18 deletions(-) diff --git a/samlsp/middleware_test.go b/samlsp/middleware_test.go index 418b27c5..05bcaed9 100644 --- a/samlsp/middleware_test.go +++ b/samlsp/middleware_test.go @@ -2,6 +2,11 @@ package samlsp import ( "bytes" + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/x509" @@ -17,6 +22,7 @@ import ( "testing" "time" + "github.com/golang-jwt/jwt/v5" dsig "github.com/russellhaering/goxmldsig" "gotest.tools/assert" is "gotest.tools/assert/cmp" @@ -520,3 +526,268 @@ func TestMiddlewareHandlesInvalidResponse(t *testing.T) { assert.Check(t, is.Equal("", resp.Header().Get("Location"))) assert.Check(t, is.Equal("", resp.Header().Get("Set-Cookie"))) } + +type mockSigner struct { + signer crypto.Signer +} + +func (m *mockSigner) Public() crypto.PublicKey { + return m.signer.Public() +} + +func (m *mockSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + return m.signer.Sign(rand, digest, opts) +} + +func newMockRSASigner(t *testing.T) crypto.Signer { + key := mustParsePrivateKey(golden.Get(t, "key.pem")) + return &mockSigner{signer: key.(crypto.Signer)} +} + +func TestMiddleware_WithCryptoSignerE2E(t *testing.T) { + origTimeNow := saml.TimeNow + origClock := saml.Clock + origRandReader := saml.RandReader + t.Cleanup(func() { + saml.TimeNow = origTimeNow + saml.Clock = origClock + saml.RandReader = origRandReader + }) + + saml.TimeNow = func() time.Time { + rv, _ := time.Parse("Mon Jan 2 15:04:05.999999999 MST 2006", "Mon Dec 1 01:57:09.123456789 UTC 2015") + return rv + } + saml.Clock = dsig.NewFakeClockAt(saml.TimeNow()) + saml.RandReader = &testRandomReader{} + + cert := mustParseCertificate(golden.Get(t, "cert.pem")) + idpMetadata := golden.Get(t, "idp_metadata.xml") + + var metadata saml.EntityDescriptor + if err := xml.Unmarshal(idpMetadata, &metadata); err != nil { + panic(err) + } + + mockSigner := newMockRSASigner(t) + + opts := Options{ + URL: mustParseURL("https://15661444.ngrok.io/"), + Key: mockSigner, + Certificate: cert, + IDPMetadata: &metadata, + } + + middleware, err := New(opts) + assert.Check(t, err) + + sessionProvider := DefaultSessionProvider(opts) + sessionProvider.Name = "ttt" + sessionProvider.MaxAge = 7200 * time.Second + + sessionCodec := sessionProvider.Codec.(JWTSessionCodec) + sessionCodec.MaxAge = 7200 * time.Second + sessionProvider.Codec = sessionCodec + + middleware.Session = sessionProvider + middleware.ServiceProvider.MetadataURL.Path = "/saml2/metadata" + middleware.ServiceProvider.AcsURL.Path = "/saml2/acs" + middleware.ServiceProvider.SloURL.Path = "/saml2/slo" + + t.Run("SessionEncodeDecode", func(t *testing.T) { + var tc JWTSessionClaims + if err := json.Unmarshal(golden.Get(t, "token.json"), &tc); err != nil { + t.Fatal(err) + } + + encoded, err := sessionProvider.Codec.Encode(tc) + assert.Check(t, err) + assert.Assert(t, encoded != "") + + decoded, err := sessionProvider.Codec.Decode(encoded) + assert.Check(t, err) + decodedClaims := decoded.(JWTSessionClaims) + assert.Equal(t, tc.Subject, decodedClaims.Subject) + }) + + t.Run("TrackedRequestEncodeDecode", func(t *testing.T) { + codec := middleware.RequestTracker.(CookieRequestTracker).Codec + trackedReq := TrackedRequest{ + Index: "test-index", + SAMLRequestID: "test-request-id", + URI: "/test-uri", + } + + encoded, err := codec.Encode(trackedReq) + assert.Check(t, err) + assert.Assert(t, encoded != "") + + decoded, err := codec.Decode(encoded) + assert.Check(t, err) + assert.Equal(t, trackedReq.Index, decoded.Index) + assert.Equal(t, trackedReq.SAMLRequestID, decoded.SAMLRequestID) + }) + + t.Run("RequireAccountFlow", func(t *testing.T) { + handler := middleware.RequireAccount( + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { + panic("not reached") + })) + + req, _ := http.NewRequest("GET", "/protected", nil) + resp := httptest.NewRecorder() + handler.ServeHTTP(resp, req) + + assert.Check(t, is.Equal(http.StatusFound, resp.Code)) + assert.Assert(t, resp.Header().Get("Location") != "") + assert.Assert(t, resp.Header().Get("Set-Cookie") != "") + }) + + t.Run("Metadata", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/saml2/metadata", nil) + resp := httptest.NewRecorder() + middleware.ServeHTTP(resp, req) + + assert.Check(t, is.Equal(http.StatusOK, resp.Code)) + assert.Check(t, is.Equal("application/samlmetadata+xml", + resp.Header().Get("Content-type"))) + golden.Assert(t, resp.Body.String(), "expected_middleware_metadata.xml") + }) +} + +func TestJWTSessionCodec_CryptoSignerEncodeDecode(t *testing.T) { + tests := []struct { + name string + method jwt.SigningMethod + genKey func(t *testing.T) crypto.Signer + subject string + }{ + { + name: "ECDSA-P256", + method: jwt.SigningMethodES256, + genKey: func(t *testing.T) crypto.Signer { + k, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + assert.Check(t, err) + return k + }, + subject: "test-ecdsa-p256", + }, + { + name: "ECDSA-P384", + method: jwt.SigningMethodES384, + genKey: func(t *testing.T) crypto.Signer { + k, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + assert.Check(t, err) + return k + }, + subject: "test-ecdsa-p384", + }, + { + name: "ECDSA-P521", + method: jwt.SigningMethodES512, + genKey: func(t *testing.T) crypto.Signer { + k, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + assert.Check(t, err) + return k + }, + subject: "test-ecdsa-p521", + }, + { + name: "RSA-PSS", + method: jwt.SigningMethodPS256, + genKey: func(t *testing.T) crypto.Signer { + k, err := rsa.GenerateKey(rand.Reader, 2048) + assert.Check(t, err) + return k + }, + subject: "test-rsa-pss", + }, + { + name: "EdDSA", + method: jwt.SigningMethodEdDSA, + genKey: func(t *testing.T) crypto.Signer { + _, k, err := ed25519.GenerateKey(rand.Reader) + assert.Check(t, err) + return k + }, + subject: "test-eddsa", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + origTimeNow := saml.TimeNow + t.Cleanup(func() { saml.TimeNow = origTimeNow }) + saml.TimeNow = func() time.Time { return now } + + signer := &mockSigner{signer: tt.genKey(t)} + + audience := "https://example.com/" + codec := JWTSessionCodec{ + SigningMethod: tt.method, + Audience: audience, + Issuer: audience, + MaxAge: time.Hour, + Key: signer, + } + + tc := JWTSessionClaims{ + RegisteredClaims: jwt.RegisteredClaims{ + Audience: jwt.ClaimStrings{audience}, + Issuer: audience, + Subject: tt.subject, + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(now.Add(time.Hour)), + NotBefore: jwt.NewNumericDate(now), + }, + SAMLSession: true, + } + + encoded, err := codec.Encode(tc) + assert.Check(t, err) + assert.Assert(t, encoded != "") + + decoded, err := codec.Decode(encoded) + assert.Check(t, err) + decodedClaims := decoded.(JWTSessionClaims) + assert.Equal(t, tt.subject, decodedClaims.Subject) + }) + } +} + +func TestJWTSessionCodec_UnsupportedAlgorithmReturnsError(t *testing.T) { + now := time.Now() + origTimeNow := saml.TimeNow + t.Cleanup(func() { saml.TimeNow = origTimeNow }) + saml.TimeNow = func() time.Time { return now } + + rsaKey, err := rsa.GenerateKey(rand.Reader, 2048) + assert.Check(t, err) + + signer := &mockSigner{signer: rsaKey} + + audience := "https://example.com/" + codec := JWTSessionCodec{ + SigningMethod: jwt.SigningMethodNone, + Audience: audience, + Issuer: audience, + MaxAge: time.Hour, + Key: signer, + } + + tc := JWTSessionClaims{ + RegisteredClaims: jwt.RegisteredClaims{ + Audience: jwt.ClaimStrings{audience}, + Issuer: audience, + Subject: "test", + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(now.Add(time.Hour)), + NotBefore: jwt.NewNumericDate(now), + }, + SAMLSession: true, + } + + _, err = codec.Encode(tc) + assert.Check(t, is.ErrorContains(err, "unsupported algorithm for crypto.Signer")) +} diff --git a/samlsp/new.go b/samlsp/new.go index 6a8eeb56..9fe56a59 100644 --- a/samlsp/new.go +++ b/samlsp/new.go @@ -38,7 +38,7 @@ type Options struct { } func getDefaultSigningMethod(signer crypto.Signer) jwt.SigningMethod { - if signer != nil { + if !saml.IsSignerNil(signer) { switch signer.Public().(type) { case *ecdsa.PublicKey: return jwt.SigningMethodES256 @@ -149,15 +149,18 @@ func DefaultServiceProvider(opts Options) saml.ServiceProvider { } func defaultSigningMethodForKey(key crypto.Signer) string { - switch key.(type) { - case *rsa.PrivateKey: + if saml.IsSignerNil(key) { + return "" + } + // Check public key type to support crypto.Signer implementations (KMS/HSM) + // that aren't concrete *rsa.PrivateKey or *ecdsa.PrivateKey types + switch key.Public().(type) { + case *rsa.PublicKey: return dsig.RSASHA1SignatureMethod - case *ecdsa.PrivateKey: + case *ecdsa.PublicKey: return dsig.ECDSASHA256SignatureMethod - case nil: - return "" default: - panic(fmt.Sprintf("programming error: unsupported key type %T", key)) + panic(fmt.Sprintf("programming error: unsupported public key type %T", key.Public())) } } diff --git a/samlsp/request_tracker_jwt.go b/samlsp/request_tracker_jwt.go index 52ec57e7..7645f8a7 100644 --- a/samlsp/request_tracker_jwt.go +++ b/samlsp/request_tracker_jwt.go @@ -44,11 +44,14 @@ func (s JWTTrackedRequestCodec) Encode(value TrackedRequest) (string, error) { SAMLAuthnRequest: true, } token := jwt.NewWithClaims(s.SigningMethod, claims) - return token.SignedString(s.Key) + return signToken(token, s.Key, s.SigningMethod) } // Decode returns a Tracked request from an encoded string. func (s JWTTrackedRequestCodec) Decode(signed string) (*TrackedRequest, error) { + if saml.IsSignerNil(s.Key) { + return nil, fmt.Errorf("decoding key is nil") + } parser := jwt.NewParser( jwt.WithValidMethods([]string{s.SigningMethod.Alg()}), jwt.WithTimeFunc(saml.TimeNow), diff --git a/samlsp/session_jwt.go b/samlsp/session_jwt.go index 89c34553..37525a0e 100644 --- a/samlsp/session_jwt.go +++ b/samlsp/session_jwt.go @@ -2,7 +2,16 @@ package samlsp import ( "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rand" + "crypto/rsa" + "encoding/asn1" + "encoding/base64" "errors" + "fmt" + "math/big" + "strings" "time" "github.com/golang-jwt/jwt/v5" @@ -77,17 +86,15 @@ func (c JWTSessionCodec) Encode(s Session) (string, error) { claims := s.(JWTSessionClaims) // this will panic if you pass the wrong kind of session token := jwt.NewWithClaims(c.SigningMethod, claims) - signedString, err := token.SignedString(c.Key) - if err != nil { - return "", err - } - - return signedString, nil + return signToken(token, c.Key, c.SigningMethod) } // Decode parses the serialized session that may have been returned by Encode // and returns a Session. func (c JWTSessionCodec) Decode(signed string) (Session, error) { + if saml.IsSignerNil(c.Key) { + return nil, fmt.Errorf("decoding key is nil") + } parser := jwt.NewParser( jwt.WithValidMethods([]string{c.SigningMethod.Alg()}), jwt.WithTimeFunc(saml.TimeNow), @@ -137,3 +144,144 @@ func (a Attributes) Get(key string) string { } return v[0] } + +// signToken signs a JWT token using the provided crypto.Signer. +// For concrete key types (*rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey), +// it delegates directly to jwt.Token.SignedString. For other crypto.Signer +// implementations (e.g., KMS/HSM), it uses signJWTWithCryptoSigner. +func signToken(token *jwt.Token, signer crypto.Signer, method jwt.SigningMethod) (string, error) { + if saml.IsSignerNil(signer) { + return "", fmt.Errorf("signing key is nil") + } + + switch signer.(type) { + case *rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey: + return token.SignedString(signer) + default: + return signJWTWithCryptoSigner(token, signer, method) + } +} + +// signJWTWithCryptoSigner signs a JWT token using the crypto.Signer interface. +// This allows KMS/HSM keys that implement crypto.Signer to sign JWTs. +// Supports RSA (RS256/RS384/RS512), RSA-PSS (PS256/PS384/PS512), +// ECDSA (ES256/ES384/ES512), and EdDSA signing methods. +func signJWTWithCryptoSigner(token *jwt.Token, signer crypto.Signer, method jwt.SigningMethod) (string, error) { + pubKey := signer.Public() + + // Get the signing string (header.payload) + signingString, err := token.SigningString() + if err != nil { + return "", err + } + + // EdDSA (Ed25519) requires signing the full unhashed message with crypto.Hash(0), + // unlike RSA/ECDSA which sign a pre-computed digest. + if method.Alg() == "EdDSA" { + if _, ok := pubKey.(ed25519.PublicKey); !ok { + return "", fmt.Errorf("EdDSA signing requires an Ed25519 key, got %T", pubKey) + } + sig, err := signer.Sign(rand.Reader, []byte(signingString), crypto.Hash(0)) + if err != nil { + return "", fmt.Errorf("signing with crypto.Signer: %w", err) + } + return strings.Join([]string{signingString, base64.RawURLEncoding.EncodeToString(sig)}, "."), nil + } + + // Validate that the signer's public key type matches the JWT algorithm. + alg := method.Alg() + switch { + case strings.HasPrefix(alg, "RS") || strings.HasPrefix(alg, "PS"): + if _, ok := pubKey.(*rsa.PublicKey); !ok { + return "", fmt.Errorf("algorithm %s requires an RSA key, got %T", alg, pubKey) + } + case strings.HasPrefix(alg, "ES"): + if _, ok := pubKey.(*ecdsa.PublicKey); !ok { + return "", fmt.Errorf("algorithm %s requires an ECDSA key, got %T", alg, pubKey) + } + default: + return "", fmt.Errorf("unsupported algorithm for crypto.Signer: %s", alg) + } + + // Determine hash algorithm and signer options based on signing method. + // RSA-PSS requires *rsa.PSSOptions; RSA PKCS1v15 and ECDSA use the hash directly. + var hashFunc crypto.Hash + var opts crypto.SignerOpts + switch alg { + case "RS256", "ES256": + hashFunc = crypto.SHA256 + opts = crypto.SHA256 + case "RS384", "ES384": + hashFunc = crypto.SHA384 + opts = crypto.SHA384 + case "RS512", "ES512": + hashFunc = crypto.SHA512 + opts = crypto.SHA512 + case "PS256": + hashFunc = crypto.SHA256 + opts = &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash, Hash: crypto.SHA256} + case "PS384": + hashFunc = crypto.SHA384 + opts = &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash, Hash: crypto.SHA384} + case "PS512": + hashFunc = crypto.SHA512 + opts = &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash, Hash: crypto.SHA512} + default: + return "", fmt.Errorf("unsupported signing algorithm for crypto.Signer: %s", alg) + } + + // Hash the signing string + hasher := hashFunc.New() + hasher.Write([]byte(signingString)) + digest := hasher.Sum(nil) + + // Sign using crypto.Signer + sig, err := signer.Sign(rand.Reader, digest, opts) + if err != nil { + return "", fmt.Errorf("signing with crypto.Signer: %w", err) + } + + // For ECDSA, the signature from crypto.Signer is ASN.1 DER encoded, + // but JWT expects raw R||S format + if ecPub, ok := pubKey.(*ecdsa.PublicKey); ok { + sig, err = convertECDSASignatureToJWT(sig, ecPub) + if err != nil { + return "", err + } + } + + // Encode signature and return complete JWT + return strings.Join([]string{signingString, base64.RawURLEncoding.EncodeToString(sig)}, "."), nil +} + +// convertECDSASignatureToJWT converts ASN.1 DER encoded ECDSA signature to JWT format (R||S) +func convertECDSASignatureToJWT(derSig []byte, pubKey *ecdsa.PublicKey) ([]byte, error) { + // Parse ASN.1 DER signature + var sig struct { + R, S *big.Int + } + if _, err := asn1.Unmarshal(derSig, &sig); err != nil { + return nil, fmt.Errorf("parsing ECDSA signature: %w", err) + } + + if sig.R == nil || sig.S == nil { + return nil, fmt.Errorf("invalid ECDSA signature: R or S is nil") + } + + // Calculate key size in bytes + keyBytes := (pubKey.Curve.Params().BitSize + 7) / 8 + + // Create R||S format with zero-padding + rBytes := sig.R.Bytes() + sBytes := sig.S.Bytes() + + if len(rBytes) > keyBytes || len(sBytes) > keyBytes { + return nil, fmt.Errorf("invalid ECDSA signature: component size (%d, %d) exceeds key size (%d)", len(rBytes), len(sBytes), keyBytes) + } + + result := make([]byte, 2*keyBytes) + copy(result[keyBytes-len(rBytes):keyBytes], rBytes) + copy(result[2*keyBytes-len(sBytes):], sBytes) + + return result, nil +} diff --git a/service_provider.go b/service_provider.go index c97886d0..80316050 100644 --- a/service_provider.go +++ b/service_provider.go @@ -19,6 +19,7 @@ import ( "io" "net/http" "net/url" + "reflect" "regexp" "strings" "time" @@ -555,6 +556,22 @@ func (sp *ServiceProvider) MakeAuthenticationRequest(idpURL string, binding stri return &req, nil } +// IsSignerNil returns true if the signer is nil or a typed-nil (e.g., (*rsa.PrivateKey)(nil)) +// stored in a crypto.Signer interface). A typed-nil makes the interface non-nil, so a simple +// == nil check won't catch it, but calling methods like Public() will still panic. +// This also handles nilable non-pointer types like ed25519.PrivateKey (which is a slice). +func IsSignerNil(key crypto.Signer) bool { + if key == nil { + return true + } + v := reflect.ValueOf(key) + switch v.Kind() { + case reflect.Pointer, reflect.Slice, reflect.Map, reflect.Func, reflect.Chan, reflect.Interface: + return v.IsNil() + } + return false +} + // GetSigningContext returns a dsig.SigningContext initialized based on the Service Provider's configuration func GetSigningContext(sp *ServiceProvider) (*dsig.SigningContext, error) { keyPair := tls.Certificate{ @@ -567,21 +584,28 @@ func GetSigningContext(sp *ServiceProvider) (*dsig.SigningContext, error) { // keyPair.Certificate = append(keyPair.Certificate, cert.Raw) // } + // Validate that the key type matches the signature method. + // We check the public key type to support crypto.Signer implementations + // (like KMS/HSM signers) that aren't literal *rsa.PrivateKey or *ecdsa.PrivateKey. + if IsSignerNil(sp.Key) { + return nil, fmt.Errorf("signature method %s requires a non-nil private key", sp.SignatureMethod) + } + pubKey := sp.Key.Public() switch sp.SignatureMethod { case dsig.RSASHA1SignatureMethod, dsig.RSASHA256SignatureMethod, dsig.RSASHA384SignatureMethod, dsig.RSASHA512SignatureMethod: - if _, ok := sp.Key.(*rsa.PrivateKey); !ok { - return nil, fmt.Errorf("signature method %s requires a key of type rsa.PrivateKey, not %T", sp.SignatureMethod, sp.Key) + if _, ok := pubKey.(*rsa.PublicKey); !ok { + return nil, fmt.Errorf("signature method %s requires an RSA key, got %T", sp.SignatureMethod, pubKey) } case dsig.ECDSASHA1SignatureMethod, dsig.ECDSASHA256SignatureMethod, dsig.ECDSASHA384SignatureMethod, dsig.ECDSASHA512SignatureMethod: - if _, ok := sp.Key.(*ecdsa.PrivateKey); !ok { - return nil, fmt.Errorf("signature method %s requires a key of type ecdsa.PrivateKey, not %T", sp.SignatureMethod, sp.Key) + if _, ok := pubKey.(*ecdsa.PublicKey); !ok { + return nil, fmt.Errorf("signature method %s requires an ECDSA key, got %T", sp.SignatureMethod, pubKey) } default: return nil, fmt.Errorf("invalid signing method %s", sp.SignatureMethod) From fc45bf68ef30e5ffe90664eb0e13252af8d32570 Mon Sep 17 00:00:00 2001 From: Dinesh Udayakumar Date: Tue, 3 Mar 2026 00:09:55 -0500 Subject: [PATCH 10/11] Add SHA-1 fingerprint support We need this as our ruby saml lib was supporting SHA1 fingerprint and when we migrate from ruby to golang we need this until all sub is fully migrated to golang saml sso and configured a metadata_url --- service_provider.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/service_provider.go b/service_provider.go index 80316050..c538f1ac 100644 --- a/service_provider.go +++ b/service_provider.go @@ -7,6 +7,7 @@ import ( "crypto" "crypto/ecdsa" "crypto/rsa" + "crypto/sha1" //nolint:gosec "crypto/sha256" "crypto/sha512" "crypto/tls" @@ -470,6 +471,9 @@ func parseCert(x509Data string) (*x509.Certificate, error) { func fingerprint(cert *x509.Certificate, fingerprintAlgorithm string) (string, error) { switch fingerprintAlgorithm { + case "http://www.w3.org/2000/09/xmldsig#sha1": + fp := sha1.Sum(cert.Raw) //nolint:gosec + return fingerprintFormat(fp[:]) case "http://www.w3.org/2001/04/xmlenc#sha256": fp := sha256.Sum256(cert.Raw) return fingerprintFormat(fp[:]) From e3dcd35e8f125573c70f60af6f9edc3da9a5f4e8 Mon Sep 17 00:00:00 2001 From: Karim Atef Mansour Date: Wed, 25 Mar 2026 14:40:17 +0100 Subject: [PATCH 11/11] Upgrade goxmldsig to v1.6.0 to fix CVE-2026-33487 Fixes GHSA-479m-64c4-43vc: loop variable capture in validateSignature allows signature bypass. Also upgrades beevik/etree to v1.6.0 as required by goxmldsig v1.6.0. Co-Authored-By: Claude Sonnet 4.6 --- go.mod | 4 ++-- go.sum | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 47bfc2c9..293a0e28 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,11 @@ module github.com/crewjam/saml go 1.24.0 require ( - github.com/beevik/etree v1.5.0 + github.com/beevik/etree v1.6.0 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/google/go-cmp v0.7.0 github.com/mattermost/xml-roundtrip-validator v0.1.0 - github.com/russellhaering/goxmldsig v1.5.0 + github.com/russellhaering/goxmldsig v1.6.0 golang.org/x/crypto v0.48.0 gotest.tools v2.2.0+incompatible ) diff --git a/go.sum b/go.sum index 4f45f714..89b9179c 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/beevik/etree v1.5.0 h1:iaQZFSDS+3kYZiGoc9uKeOkUY3nYMXOKLl6KIJxiJWs= github.com/beevik/etree v1.5.0/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs= +github.com/beevik/etree v1.6.0 h1:u8Kwy8pp9D9XeITj2Z0XtA5qqZEmtJtuXZRQi+j03eE= +github.com/beevik/etree v1.6.0/go.mod h1:bh4zJxiIr62SOf9pRzN7UUYaEDa9HEKafK25+sLc0Gc= 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= @@ -17,6 +19,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russellhaering/goxmldsig v1.5.0 h1:AU2UkkYIUOTyZRbe08XMThaOCelArgvNfYapcmSjBNw= github.com/russellhaering/goxmldsig v1.5.0/go.mod h1:x98CjQNFJcWfMxeOrMnMKg70lvDP6tE0nTaeUnjXDmk= +github.com/russellhaering/goxmldsig v1.6.0 h1:8fdWXEPh2k/NZNQBPFNoVfS3JmzS4ZprY/sAOpKQLks= +github.com/russellhaering/goxmldsig v1.6.0/go.mod h1:TrnaquDcYxWXfJrOjeMBTX4mLBeYAqaHEyUeWPxZlBM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=