diff --git a/saml/error.go b/saml/error.go index 43ed33e..82e8bb8 100644 --- a/saml/error.go +++ b/saml/error.go @@ -15,4 +15,5 @@ var ( ErrInvalidAudience = errors.New("invalid audience") ErrMissingSubject = errors.New("subject missing") ErrMissingAttributeStmt = errors.New("attribute statement missing") + ErrInvalidSignature = errors.New("invalid signature") ) diff --git a/saml/response.go b/saml/response.go index 894d0e8..49c03cc 100644 --- a/saml/response.go +++ b/saml/response.go @@ -12,6 +12,7 @@ import ( "github.com/jonboulle/clockwork" saml2 "github.com/russellhaering/gosaml2" + "github.com/russellhaering/gosaml2/types" dsig "github.com/russellhaering/goxmldsig" "github.com/hashicorp/cap/saml/models/core" @@ -109,6 +110,8 @@ func (sp *ServiceProvider) ParseResponse( // This will validate the response and all assertions. response, err := ip.ValidateEncodedResponse(samlResp) + + switch { case err != nil: return nil, fmt.Errorf("%s: unable to validate encoded response: %w", op, err) @@ -151,6 +154,14 @@ func (sp *ServiceProvider) ParseResponse( } } + if !opts.skipSignatureValidation { + // func ip.ValidateEncodedResponse(...) above only requires either response or all its assertions are signed, + // but does not require both. Adding another check to validate that both of these are signed always. + if err := validateSignature(response, op); err != nil { + return nil, err + } + } + return &core.Response{Response: *response}, nil } @@ -245,3 +256,22 @@ func parsePEMCertificate(cert []byte) (*x509.Certificate, error) { return x509.ParseCertificate(block.Bytes) } + +func validateSignature(response *types.Response, op string) (error) { + + // validate child attr assertions + for _, assert := range response.Assertions { + if !assert.SignatureValidated{ + // note: at one time func ip.ValidateEncodedResponse(...) above allows all signed or all unsigned + // assertions, and will give error if there are both. We are still looping on all assertions instead of + // retrieving value for one assertion, so we do not depend on dependency implementation. + return fmt.Errorf("%s: %w", op, ErrInvalidSignature) + } + } + + // validate root response attr + if !response.SignatureValidated{ + return fmt.Errorf("%s: %w", op, ErrInvalidSignature) + } + return nil +} \ No newline at end of file diff --git a/saml/response_test.go b/saml/response_test.go index 840d886..4fdfef2 100644 --- a/saml/response_test.go +++ b/saml/response_test.go @@ -21,7 +21,6 @@ import ( var testExpiredResp = `PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHNhbWwycDpSZXNwb25zZSBEZXN0aW5hdGlvbj0iaHR0cDovL2xvY2FsaG9zdDo4MDAwL3NhbWwvYWNzIiBJRD0iXzg4NDljMmVlNTMyZmNkYjc4MWYyYTE3NzZlYWMzNzQxIiBJblJlc3BvbnNlVG89ImJjNWE1YmFhLTk0ZTAtNThhOC04NzJjLWU1MTQ5MWQyYjNlZSIgSXNzdWVJbnN0YW50PSIyMDIzLTA4LTI1VDE0OjMyOjUzLjY4MFoiIFZlcnNpb249IjIuMCIgeG1sbnM6c2FtbDJwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnhzZD0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiPjxzYW1sMjpJc3N1ZXIgeG1sbnM6c2FtbDI9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPmh0dHBzOi8vc2FtbHRlc3QuaWQvc2FtbC9pZHA8L3NhbWwyOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8+PGRzOlJlZmVyZW5jZSBVUkk9IiNfODg0OWMyZWU1MzJmY2RiNzgxZjJhMTc3NmVhYzM3NDEiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiPjxlYzpJbmNsdXNpdmVOYW1lc3BhY2VzIFByZWZpeExpc3Q9InhzZCIgeG1sbnM6ZWM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3JtPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjc2hhMjU2Ii8+PGRzOkRpZ2VzdFZhbHVlPlJWNDg1dUtHSlptTkExbzU2Z3h4aytWWmt2eE1xdGxIWkEyaUhIOFpVMVE9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPmQzTHBjNmhjU0I3YndDek1yTzN3ZlpyTmlHazVnWjhyS1JLT1FFTkRQMnErcDMrTGtEbVNCdDZ6enl4bjMzTUNTSnQrZFBIcEYxNFlNQUsvTjNQbld3U1NVcDBqNWt6T2M5S2E1TmRpYW5FME5nWW5VMHFqaEZKYlRoQVF6N2hSb3dTNEo0OWhTLzZNdVNRMFo3bkJCQ2VEZ2VENlBZUkFwS012bE90a0JHUEphTFQybVJ5L2duUStDQzZ1ZFVkSnl2U2diOW40M2x2eGRhYVpXckRLM1dnYTk4WWxrY1JITHJtUEFBTThLeFlXbmtvcGlvNllJTlU0RDVtWmpzRXNuVWtINDFXZ2N3Z21TMnh6UDNJQ25OYzNXSDlOSHJWS3A5YXQyREJ3cllESXNlczZGWGdZcStpVVdLMjE5MWpXcElDM3FWQUIwY09pbG1SWHd0RUg3Zz09PC9kczpTaWduYXR1cmVWYWx1ZT48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlERWpDQ0FmcWdBd0lCQWdJVkFNRUNRMXRqZ2hhZm01T3hXRGg5aHdaZnh0aFdNQTBHQ1NxR1NJYjNEUUVCQ3dVQU1CWXhGREFTCkJnTlZCQU1NQzNOaGJXeDBaWE4wTG1sa01CNFhEVEU0TURneU5ESXhNVFF3T1ZvWERUTTRNRGd5TkRJeE1UUXdPVm93RmpFVU1CSUcKQTFVRUF3d0xjMkZ0YkhSbGMzUXVhV1F3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRQzBaNFFYMU5GSwpzNzF1ZmJRd29Rb1c3cWtOQUpSSUFOR0E0aU0wVGhZZ2h1bDNwQytGd3JHdjM3YVR4V1hmQTFVRzluaktiYkRyZWlEQVpLbmdDZ3lqCnhqMHVKNGxBcmdrcjRBT0VqajV6WEE4MXVHSEFSZlVCY3R2UWNzWnBCSXhET3ZVVUltQWwrM05xTGdNR0YyZmt0eE1HN2tYM0dFVk4KYzFrbGJOM2RmWXNhdzVkVXJ3MjVEaGVMOW5wN0cvKzI4R3dIUHZMYjRhcHRPaU9OYkNhVnZoOVVNSEVBOUY3YzB6ZkYvY0w1Zk9wZApWYTU0d1RJMHUxMkNzRkt0NzhoNmxFR0c1alVzL3FYOWNsWm5jSk03RUZrTjNpbVBQeSswSEM4bnNwWGlIL01aVzhvMmNxV1JrcnczCk16QlpXM09qazVuUWo0MFY2TlViamI3a2ZlanpBZ01CQUFHalZ6QlZNQjBHQTFVZERnUVdCQlFUNlk5SjNUdy9oT0djOFBOVjdKRUUKNGsyWk5UQTBCZ05WSFJFRUxUQXJnZ3R6WVcxc2RHVnpkQzVwWklZY2FIUjBjSE02THk5ellXMXNkR1Z6ZEM1cFpDOXpZVzFzTDJsawpjREFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBU2szZ3VLZlRrVmhFYUlWdnhFUE5SMnczdld0M2Z3bXdKQ2NjVzk4WFhMV2dOYnUzCllhTWIyUlNuN1RoNHAzaCttZnlrMmRvbjZhdTdVeXpjMUpkMzlSTnY4MFRHNWlRb3hmQ2dwaHkxRlltbWRhU2ZPOHd2RHRIVFROaUwKQXJBeE9ZdHpmWWJ6YjVRck5OSC9nUUVOOFJKYUVmL2cvMUdUdzl4LzEwM2RTTUswUlh0bCtmUnMybmJsRDFKSktTUTNBZGh4Sy93ZQpQM2FVUHRMeFZWSjl3TU9RT2ZjeTAybCtoSE1iNnVBanNQT3BPVktxaTNNOFhtY1VaT3B4NHN3dGdHZGVvU3BlUnlydE12UndkY2NpCk5CcDlVWm9tZTQ0cVpBWUgxaXFycG1tanNmSTlwSkl0c2dXdTNrWFBqaFNmajFBSkdSMWw5Skd2SnJIa2kxaUhUQT09PC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWwycDpTdGF0dXM+PHNhbWwycDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWwycDpTdGF0dXM+PHNhbWwyOkFzc2VydGlvbiBJRD0iXzM1ZWE5MGI3MTFkNmYzODUzNDVmMGRiZGQ3ZDBlZDViIiBJc3N1ZUluc3RhbnQ9IjIwMjMtMDgtMjVUMTQ6MzI6NTMuNjgwWiIgVmVyc2lvbj0iMi4wIiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI+PHNhbWwyOklzc3Vlcj5odHRwczovL3NhbWx0ZXN0LmlkL3NhbWwvaWRwPC9zYW1sMjpJc3N1ZXI+PHNhbWwyOlN1YmplY3Q+PHNhbWwyOk5hbWVJRCBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyIgTmFtZVF1YWxpZmllcj0iaHR0cHM6Ly9zYW1sdGVzdC5pZC9zYW1sL2lkcCIgU1BOYW1lUXVhbGlmaWVyPSJodHRwOi8vc2FtbC5qdWx6L2V4YW1wbGUiIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj5tc21pdGhAc2FtbHRlc3QuaWQ8L3NhbWwyOk5hbWVJRD48c2FtbDI6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sMjpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBBZGRyZXNzPSIxMDQuMjguMzkuMzQiIEluUmVzcG9uc2VUbz0iYmM1YTViYWEtOTRlMC01OGE4LTg3MmMtZTUxNDkxZDJiM2VlIiBOb3RPbk9yQWZ0ZXI9IjIwMjMtMDgtMjVUMTQ6Mzc6NTMuNjkzWiIgUmVjaXBpZW50PSJodHRwOi8vbG9jYWxob3N0OjgwMDAvc2FtbC9hY3MiLz48L3NhbWwyOlN1YmplY3RDb25maXJtYXRpb24+PC9zYW1sMjpTdWJqZWN0PjxzYW1sMjpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAyMy0wOC0yNVQxNDozMjo1My42ODBaIiBOb3RPbk9yQWZ0ZXI9IjIwMjMtMDgtMjVUMTQ6Mzc6NTMuNjgwWiI+PHNhbWwyOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWwyOkF1ZGllbmNlPmh0dHA6Ly9zYW1sLmp1bHovZXhhbXBsZTwvc2FtbDI6QXVkaWVuY2U+PC9zYW1sMjpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDI6Q29uZGl0aW9ucz48c2FtbDI6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDIzLTA4LTI1VDE0OjMxOjU2LjA2NFoiIFNlc3Npb25JbmRleD0iX2Y3MmE2M2VlMzc4MmI0N2M4OWY2MGU4MWFkZGUwYWIwIj48c2FtbDI6U3ViamVjdExvY2FsaXR5IEFkZHJlc3M9IjEwNC4yOC4zOS4zNCIvPjxzYW1sMjpBdXRobkNvbnRleHQ+PHNhbWwyOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkUHJvdGVjdGVkVHJhbnNwb3J0PC9zYW1sMjpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWwyOkF1dGhuQ29udGV4dD48L3NhbWwyOkF1dGhuU3RhdGVtZW50PjxzYW1sMjpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWwyOkF0dHJpYnV0ZSBGcmllbmRseU5hbWU9ImVkdVBlcnNvbkVudGl0bGVtZW50IiBOYW1lPSJ1cm46b2lkOjEuMy42LjEuNC4xLjU5MjMuMS4xLjEuNyIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDp1cmkiPjxzYW1sMjpBdHRyaWJ1dGVWYWx1ZT5BbWJhc3NhZG9yPC9zYW1sMjpBdHRyaWJ1dGVWYWx1ZT48c2FtbDI6QXR0cmlidXRlVmFsdWU+Tm9uZTwvc2FtbDI6QXR0cmlidXRlVmFsdWU+PC9zYW1sMjpBdHRyaWJ1dGU+PHNhbWwyOkF0dHJpYnV0ZSBOYW1lPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDphdHRyaWJ1dGU6c3ViamVjdC1pZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDp1cmkiPjxzYW1sMjpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHNkOnN0cmluZyI+bXNtaXRoQHNhbWx0ZXN0LmlkPC9zYW1sMjpBdHRyaWJ1dGVWYWx1ZT48L3NhbWwyOkF0dHJpYnV0ZT48c2FtbDI6QXR0cmlidXRlIEZyaWVuZGx5TmFtZT0idWlkIiBOYW1lPSJ1cm46b2lkOjAuOS4yMzQyLjE5MjAwMzAwLjEwMC4xLjEiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dXJpIj48c2FtbDI6QXR0cmlidXRlVmFsdWU+bW9ydHk8L3NhbWwyOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDI6QXR0cmlidXRlPjxzYW1sMjpBdHRyaWJ1dGUgRnJpZW5kbHlOYW1lPSJ0ZWxlcGhvbmVOdW1iZXIiIE5hbWU9InVybjpvaWQ6Mi41LjQuMjAiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dXJpIj48c2FtbDI6QXR0cmlidXRlVmFsdWU+KzEtNTU1LTU1NS01NTA1PC9zYW1sMjpBdHRyaWJ1dGVWYWx1ZT48L3NhbWwyOkF0dHJpYnV0ZT48c2FtbDI6QXR0cmlidXRlIEZyaWVuZGx5TmFtZT0icm9sZSIgTmFtZT0iaHR0cHM6Ly9zYW1sdGVzdC5pZC9hdHRyaWJ1dGVzL3JvbGUiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dXJpIj48c2FtbDI6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzZDpzdHJpbmciPmphbml0b3JAc2FtbHRlc3QuaWQ8L3NhbWwyOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDI6QXR0cmlidXRlPjxzYW1sMjpBdHRyaWJ1dGUgRnJpZW5kbHlOYW1lPSJtYWlsIiBOYW1lPSJ1cm46b2lkOjAuOS4yMzQyLjE5MjAwMzAwLjEwMC4xLjMiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dXJpIj48c2FtbDI6QXR0cmlidXRlVmFsdWU+bXNtaXRoQHNhbWx0ZXN0LmlkPC9zYW1sMjpBdHRyaWJ1dGVWYWx1ZT48L3NhbWwyOkF0dHJpYnV0ZT48c2FtbDI6QXR0cmlidXRlIEZyaWVuZGx5TmFtZT0ic24iIE5hbWU9InVybjpvaWQ6Mi41LjQuNCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDp1cmkiPjxzYW1sMjpBdHRyaWJ1dGVWYWx1ZT5TbWl0aDwvc2FtbDI6QXR0cmlidXRlVmFsdWU+PC9zYW1sMjpBdHRyaWJ1dGU+PHNhbWwyOkF0dHJpYnV0ZSBGcmllbmRseU5hbWU9ImRpc3BsYXlOYW1lIiBOYW1lPSJ1cm46b2lkOjIuMTYuODQwLjEuMTEzNzMwLjMuMS4yNDEiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dXJpIj48c2FtbDI6QXR0cmlidXRlVmFsdWU+TW9ydHkgU21pdGg8L3NhbWwyOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDI6QXR0cmlidXRlPjxzYW1sMjpBdHRyaWJ1dGUgRnJpZW5kbHlOYW1lPSJnaXZlbk5hbWUiIE5hbWU9InVybjpvaWQ6Mi41LjQuNDIiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dXJpIj48c2FtbDI6QXR0cmlidXRlVmFsdWU+TW9ydGltZXI8L3NhbWwyOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDI6QXR0cmlidXRlPjwvc2FtbDI6QXR0cmlidXRlU3RhdGVtZW50Pjwvc2FtbDI6QXNzZXJ0aW9uPjwvc2FtbDJwOlJlc3BvbnNlPg==` -// TODO: add the ability to sign requests, so we can write more complete unit tests func TestServiceProvider_ParseResponse(t *testing.T) { t.Parallel() const ( @@ -62,10 +61,50 @@ func TestServiceProvider_ParseResponse(t *testing.T) { { name: "success", sp: testSp, - samlResp: base64.StdEncoding.EncodeToString([]byte(tp.SamlResponse(t, testprovider.WithResponseSigned()))), + samlResp: base64.StdEncoding.EncodeToString([]byte(tp.SamlResponse(t, testprovider.WithCompleteResponseSigned()))), opts: []saml.Option{}, requestID: testRequestId, }, + { + name: "missing signature", + sp: testSp, + samlResp: base64.StdEncoding.EncodeToString([]byte(tp.SamlResponse(t))), + opts: []saml.Option{}, + requestID: testRequestId, + wantErrContains: "response and/or assertions must be signed", + }, + { + name: "error-invalid-signature", + sp: testSp, + samlResp: base64.StdEncoding.EncodeToString([]byte(tp.SamlResponse(t, testprovider.WithJustResponseElemSigned()))), + opts: []saml.Option{}, + requestID: testRequestId, + wantErrContains: "invalid signature", + }, + { + name: "error-invalid-signature", + sp: testSp, + samlResp: base64.StdEncoding.EncodeToString([]byte(tp.SamlResponse(t, testprovider.WithJustResponseElemSigned()))), + opts: []saml.Option{}, + requestID: testRequestId, + wantErrContains: "invalid signature", + }, + { + name: "error missing signature", + sp: testSp, + samlResp: base64.StdEncoding.EncodeToString([]byte(tp.SamlResponse(t))), + opts: []saml.Option{}, + requestID: testRequestId, + wantErrContains: "response and/or assertions must be signed", + }, + { + name: "error-invalid-signature", + sp: testSp, + samlResp: base64.StdEncoding.EncodeToString([]byte(tp.SamlResponse(t))), + opts: []saml.Option{}, + requestID: testRequestId, + wantErrContains: "response and/or assertions must be signed", + }, { name: "err-assertion-missing-attribute-stmt", sp: testSp, @@ -144,7 +183,7 @@ func TestServiceProvider_ParseResponse(t *testing.T) { { name: "err-in-response-to", sp: testSp, - samlResp: base64.StdEncoding.EncodeToString([]byte(tp.SamlResponse(t, testprovider.WithResponseSigned()))), + samlResp: base64.StdEncoding.EncodeToString([]byte(tp.SamlResponse(t, testprovider.WithCompleteResponseSigned()))), requestID: "invalid-request-id", wantErrContains: "doesn't match the expected requestID (invalid-request-id)", }, @@ -152,7 +191,7 @@ func TestServiceProvider_ParseResponse(t *testing.T) { name: "expired", sp: testSp, samlResp: base64.StdEncoding.EncodeToString([]byte(tp.SamlResponse(t, - testprovider.WithResponseSigned(), + testprovider.WithCompleteResponseSigned(), testprovider.WithResponseExpired(), ))), requestID: "request-id", diff --git a/saml/test/provider.go b/saml/test/provider.go index 025577d..9cbe45f 100644 --- a/saml/test/provider.go +++ b/saml/test/provider.go @@ -431,7 +431,8 @@ func (p *TestProvider) parseRequestPost(request string) *core.AuthnRequest { } type responseOptions struct { - sign bool + signResponseElem bool + signAssertionElem bool expired bool } @@ -450,9 +451,22 @@ func defaultResponseOptions() *responseOptions { return &responseOptions{} } -func WithResponseSigned() ResponseOption { +func WithCompleteResponseSigned() ResponseOption { return func(o *responseOptions) { - o.sign = true + o.signResponseElem = true + o.signAssertionElem = true + } +} + +func WithJustAssertionElemSigned() ResponseOption { + return func(o *responseOptions) { + o.signAssertionElem = true + } +} + +func WithJustResponseElemSigned() ResponseOption { + return func(o *responseOptions) { + o.signResponseElem = true } } @@ -544,13 +558,28 @@ func (p *TestProvider) SamlResponse(t *testing.T, opts ...ResponseOption) string err = doc.ReadFromBytes(resp) r.NoError(err) - if opt.sign { + if opt.signResponseElem || opt.signAssertionElem { signCtx := dsig.NewDefaultSigningContext(p.keystore) - signed, err := signCtx.SignEnveloped(doc.Root()) - r.NoError(err) + // sign child attr assertions + if opt.signAssertionElem{ + responseEl := doc.SelectElement("Response") + for _, assert := range responseEl.FindElements("Assertion") { + signedAssert, err := signCtx.SignEnveloped(assert) + r.NoError(err) + + // replace signed assert element + responseEl.RemoveChildAt(assert.Index()) + responseEl.AddChild(signedAssert) + } + } - doc.SetRoot(signed) + // sign root attr response + if opt.signResponseElem{ + signed, err := signCtx.SignEnveloped(doc.Root()) + r.NoError(err) + doc.SetRoot(signed) + } } result, err := doc.WriteToString()