|
| 1 | +package saml |
| 2 | + |
| 3 | +import ( |
| 4 | + "crypto/rsa" |
| 5 | + "encoding/base64" |
| 6 | + "encoding/xml" |
| 7 | + "net/url" |
| 8 | + "testing" |
| 9 | + |
| 10 | + dsig "github.com/russellhaering/goxmldsig" |
| 11 | + "gotest.tools/assert" |
| 12 | + "gotest.tools/golden" |
| 13 | +) |
| 14 | + |
| 15 | +// Given a SAMLRequest query string, sign the query and validate signature |
| 16 | +// Using same Cert for SP and IdP in order to test |
| 17 | +func TestSigningAndValidation(t *testing.T) { |
| 18 | + type testCase struct { |
| 19 | + desc string |
| 20 | + relayState string |
| 21 | + requestType reqType |
| 22 | + wantErr bool |
| 23 | + wantRawQuery string |
| 24 | + } |
| 25 | + |
| 26 | + testCases := []testCase{ |
| 27 | + { |
| 28 | + desc: "validate signature of SAMLRequest with relayState", |
| 29 | + relayState: "AAAAAAAAAAAA", |
| 30 | + requestType: samlRequest, |
| 31 | + wantRawQuery: "SAMLRequest=PHNhbWxwOkF1dGhuUmVxdWVzdCB4bWxuczpzYW1sPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBJRD0iaWQtMDAwMjA0MDYwODBhMGMwZTEwMTIxNDE2MTgxYTFjMWUyMDIyMjQyNiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTUtMTItMDFUMDE6NTc6MDlaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9pZHAuZXhhbXBsZS5jb20vc2FtbC9zc28iIEFzc2VydGlvbkNvbnN1bWVyU2VydmljZVVSTD0iaHR0cHM6Ly9zcC5leGFtcGxlLmNvbS9zYW1sMi9hY3MiIFByb3RvY29sQmluZGluZz0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmJpbmRpbmdzOkhUVFAtUE9TVCI%2BPHNhbWw6SXNzdWVyIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6ZW50aXR5Ij5odHRwczovL3NwLmV4YW1wbGUuY29tL3NhbWwyL21ldGFkYXRhPC9zYW1sOklzc3Vlcj48c2FtbHA6TmFtZUlEUG9saWN5IEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6dHJhbnNpZW50IiBBbGxvd0NyZWF0ZT0idHJ1ZSIvPjwvc2FtbHA6QXV0aG5SZXF1ZXN0Pg%3D%3D&RelayState=AAAAAAAAAAAA&SigAlg=http%3A%2F%2Fwww.w3.org%2F2000%2F09%2Fxmldsig%23rsa-sha1&Signature=zWAF4S%2FIs7tfmEriOsT5Fm8EFOGS3iCq6OxP5i7hM%2BMPwAoXwdDz6fKH8euS1gQ3sGOZBdHD588FZLvnO1OeCxLaEsxHMVKsAZSZFLBmPPwqB6e%2B84cCwX2szOeoMROaR%2B36mdoBDRQz36JIvyBBG%2FND9x41k%2FGQuAuwk%2B9fkuE%3D", |
| 32 | + }, |
| 33 | + { |
| 34 | + desc: "validate signature of SAML request without relay state", |
| 35 | + relayState: "", |
| 36 | + requestType: samlRequest, |
| 37 | + wantRawQuery: "SAMLRequest=PHNhbWxwOkF1dGhuUmVxdWVzdCB4bWxuczpzYW1sPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBJRD0iaWQtMDAwMjA0MDYwODBhMGMwZTEwMTIxNDE2MTgxYTFjMWUyMDIyMjQyNiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTUtMTItMDFUMDE6NTc6MDlaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9pZHAuZXhhbXBsZS5jb20vc2FtbC9zc28iIEFzc2VydGlvbkNvbnN1bWVyU2VydmljZVVSTD0iaHR0cHM6Ly9zcC5leGFtcGxlLmNvbS9zYW1sMi9hY3MiIFByb3RvY29sQmluZGluZz0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmJpbmRpbmdzOkhUVFAtUE9TVCI%2BPHNhbWw6SXNzdWVyIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6ZW50aXR5Ij5odHRwczovL3NwLmV4YW1wbGUuY29tL3NhbWwyL21ldGFkYXRhPC9zYW1sOklzc3Vlcj48c2FtbHA6TmFtZUlEUG9saWN5IEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6dHJhbnNpZW50IiBBbGxvd0NyZWF0ZT0idHJ1ZSIvPjwvc2FtbHA6QXV0aG5SZXF1ZXN0Pg%3D%3D&SigAlg=http%3A%2F%2Fwww.w3.org%2F2000%2F09%2Fxmldsig%23rsa-sha1&Signature=HDdoHJSdkYh9%2BmE7RZ1LXcsAWIMJ6LuzKJgwLxH%2BQ4sKFlh8b5moFuQ%2B7rPEwoTcg9SjgCGV5rW9v8PrSU7WGKcLfAbeVwXWyU94ghjFZHEj%2BFCDpsfTD750ZPAPVnhVr0GogFZZ7c%2BEWX4NAqL4CYxDvsg56o%2BpOjw62G%2FyPDc%3D", |
| 38 | + }, |
| 39 | + { |
| 40 | + desc: "validate signature of SAML response with relay state", |
| 41 | + relayState: "AAAAAAAAAAAA", |
| 42 | + requestType: samlResponse, |
| 43 | + wantRawQuery: "SAMLResponse=PHNhbWxwOkF1dGhuUmVxdWVzdCB4bWxuczpzYW1sPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBJRD0iaWQtMDAwMjA0MDYwODBhMGMwZTEwMTIxNDE2MTgxYTFjMWUyMDIyMjQyNiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTUtMTItMDFUMDE6NTc6MDlaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9pZHAuZXhhbXBsZS5jb20vc2FtbC9zc28iIEFzc2VydGlvbkNvbnN1bWVyU2VydmljZVVSTD0iaHR0cHM6Ly9zcC5leGFtcGxlLmNvbS9zYW1sMi9hY3MiIFByb3RvY29sQmluZGluZz0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmJpbmRpbmdzOkhUVFAtUE9TVCI%2BPHNhbWw6SXNzdWVyIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6ZW50aXR5Ij5odHRwczovL3NwLmV4YW1wbGUuY29tL3NhbWwyL21ldGFkYXRhPC9zYW1sOklzc3Vlcj48c2FtbHA6TmFtZUlEUG9saWN5IEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6dHJhbnNpZW50IiBBbGxvd0NyZWF0ZT0idHJ1ZSIvPjwvc2FtbHA6QXV0aG5SZXF1ZXN0Pg%3D%3D&RelayState=AAAAAAAAAAAA&SigAlg=http%3A%2F%2Fwww.w3.org%2F2000%2F09%2Fxmldsig%23rsa-sha1&Signature=JDeiWfLgV7SZqgqU64wgtAHS%2FqtF2c3c%2B9g1vdfRHn03tm5jrgsvJtIYg1BD8HoejCoyruH3xgDz1i2qqecVcUiAdaVgVvhn0JWJ%2BzeN9YpUFTEQ4Ah1pwezlSArzuz5esgYzSkemViox313HePWZ%2Fd0FAmtdXuGHA8O0Lp%2F4Ws%3D", |
| 44 | + }, |
| 45 | + } |
| 46 | + |
| 47 | + idpMetadata := golden.Get(t, "SP_IDPMetadata_signing") |
| 48 | + s := ServiceProvider{ |
| 49 | + Key: mustParsePrivateKey(golden.Get(t, "idp_key.pem")).(*rsa.PrivateKey), |
| 50 | + Certificate: mustParseCertificate(golden.Get(t, "idp_cert.pem")), |
| 51 | + MetadataURL: mustParseURL("https://15661444.ngrok.io/saml2/metadata"), |
| 52 | + AcsURL: mustParseURL("https://15661444.ngrok.io/saml2/acs"), |
| 53 | + SignatureMethod: dsig.RSASHA1SignatureMethod, |
| 54 | + } |
| 55 | + |
| 56 | + err := xml.Unmarshal(idpMetadata, &s.IDPMetadata) |
| 57 | + idpCert, err := s.getIDPSigningCerts() |
| 58 | + |
| 59 | + assert.Check(t, err == nil) |
| 60 | + assert.Check(t, |
| 61 | + s.Certificate.Issuer.CommonName == idpCert[0].Issuer.CommonName, "expected %s, got %s", |
| 62 | + s.Certificate.Issuer.CommonName, idpCert[0].Issuer.CommonName) |
| 63 | + |
| 64 | + req := golden.Get(t, "idp_authn_request.xml") |
| 65 | + reqString := base64.StdEncoding.EncodeToString(req) |
| 66 | + |
| 67 | + for _, tc := range testCases { |
| 68 | + t.Run(tc.desc, func(t *testing.T) { |
| 69 | + relayState := tc.relayState |
| 70 | + |
| 71 | + rawQuery := string(tc.requestType) + "=" + url.QueryEscape(reqString) |
| 72 | + |
| 73 | + if relayState != "" { |
| 74 | + rawQuery += "&RelayState=" + relayState |
| 75 | + } |
| 76 | + |
| 77 | + rawQuery, err = s.signQuery(tc.requestType, rawQuery, reqString, relayState) |
| 78 | + assert.NilError(t, err, "error signing query: %s", err) |
| 79 | + |
| 80 | + assert.Equal(t, tc.wantRawQuery, rawQuery) |
| 81 | + |
| 82 | + query, err := url.ParseQuery(rawQuery) |
| 83 | + assert.NilError(t, err, "error parsing query: %s", err) |
| 84 | + |
| 85 | + err = s.validateQuerySig(query) |
| 86 | + assert.NilError(t, err, "error validating query: %s", err) |
| 87 | + }) |
| 88 | + } |
| 89 | +} |
| 90 | + |
| 91 | +// Given a raw query with an unsupported signature method, the signature should be rejected. |
| 92 | +func TestInvalidSignatureAlgorithm(t *testing.T) { |
| 93 | + rawQuery := "SAMLRequest=PHNhbWxwOkF1dGhuUmVxdWVzdCB4bWxuczpzYW1sPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBJRD0iaWQtMDAwMjA0MDYwODBhMGMwZTEwMTIxNDE2MTgxYTFjMWUyMDIyMjQyNiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTUtMTItMDFUMDE6NTc6MDlaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9pZHAuZXhhbXBsZS5jb20vc2FtbC9zc28iIEFzc2VydGlvbkNvbnN1bWVyU2VydmljZVVSTD0iaHR0cHM6Ly9zcC5leGFtcGxlLmNvbS9zYW1sMi9hY3MiIFByb3RvY29sQmluZGluZz0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmJpbmRpbmdzOkhUVFAtUE9TVCI%2BPHNhbWw6SXNzdWVyIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6ZW50aXR5Ij5odHRwczovL3NwLmV4YW1wbGUuY29tL3NhbWwyL21ldGFkYXRhPC9zYW1sOklzc3Vlcj48c2FtbHA6TmFtZUlEUG9saWN5IEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6dHJhbnNpZW50IiBBbGxvd0NyZWF0ZT0idHJ1ZSIvPjwvc2FtbHA6QXV0aG5SZXF1ZXN0Pg%3D%3D&RelayState=AAAAAAAAAAAA&SigAlg=http%3A%2F%2Fwww.w3.org%2F2000%2F09%2Fxmldsig%23rsa-sha384&Signature=zWAF4S%2FIs7tfmEriOsT5Fm8EFOGS3iCq6OxP5i7hM%2BMPwAoXwdDz6fKH8euS1gQ3sGOZBdHD588FZLvnO1OeCxLaEsxHMVKsAZSZFLBmPPwqB6e%2B84cCwX2szOeoMROaR%2B36mdoBDRQz36JIvyBBG%2FND9x41k%2FGQuAuwk%2B9fkuE%3D" |
| 94 | + |
| 95 | + idpMetadata := golden.Get(t, "SP_IDPMetadata_signing") |
| 96 | + s := ServiceProvider{ |
| 97 | + Key: mustParsePrivateKey(golden.Get(t, "idp_key.pem")).(*rsa.PrivateKey), |
| 98 | + Certificate: mustParseCertificate(golden.Get(t, "idp_cert.pem")), |
| 99 | + MetadataURL: mustParseURL("https://15661444.ngrok.io/saml2/metadata"), |
| 100 | + AcsURL: mustParseURL("https://15661444.ngrok.io/saml2/acs"), |
| 101 | + SignatureMethod: dsig.RSASHA1SignatureMethod, |
| 102 | + } |
| 103 | + |
| 104 | + err := xml.Unmarshal(idpMetadata, &s.IDPMetadata) |
| 105 | + idpCert, err := s.getIDPSigningCerts() |
| 106 | + |
| 107 | + assert.Check(t, err == nil) |
| 108 | + assert.Check(t, |
| 109 | + s.Certificate.Issuer.CommonName == idpCert[0].Issuer.CommonName, "expected %s, got %s", |
| 110 | + s.Certificate.Issuer.CommonName, idpCert[0].Issuer.CommonName) |
| 111 | + |
| 112 | + query, err := url.ParseQuery(rawQuery) |
| 113 | + assert.NilError(t, err, "error parsing query: %s", err) |
| 114 | + |
| 115 | + err = s.validateQuerySig(query) |
| 116 | + assert.Error(t, err, "unsupported signature algorithm: http://www.w3.org/2000/09/xmldsig#rsa-sha384") |
| 117 | +} |
0 commit comments