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 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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)