diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..ac0a9f6c4 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: [SAML-Toolkits] diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8112f204a..3b804c640 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,27 +8,15 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-20.04] # macos-latest - ruby-version: [2.6, 2.7, 3.0, 3.1, 3.2, jruby-9.3, jruby-9.4, truffleruby] - include: - - os: macos-latest - ruby-version: 2.6 - - os: macos-latest - ruby-version: 2.7 - - os: macos-latest - ruby-version: 3.0 - - os: macos-latest - ruby-version: 3.1 - - os: macos-latest - ruby-version: 3.2 - - os: macos-latest - ruby-version: jruby-9.3 - # 2023/03/07 - JRuby 9.4 on MacOS is skipped for now. - # Seems to be a JRuby-side issue. - # - os: macos-latest - # ruby-version: jruby-9.4 - - os: macos-latest - ruby-version: truffleruby + os: + - ubuntu-20.04 + - macos-latest + ruby-version: + - 3.0 + - 3.1 + - 3.2 + - jruby-9.4 + - truffleruby runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 diff --git a/.rubocop.yml b/.rubocop.yml index 2adc19e6d..c0296bdfd 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -6,7 +6,7 @@ require: - rubocop-rake AllCops: - TargetRubyVersion: 2.6 + TargetRubyVersion: 3.0 NewCops: enable Include: - 'lib/**/*' diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 131a5f49f..1daf9bf4c 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,12 +1,12 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2023-03-08 09:38:19 UTC using RuboCop version 1.45.1. +# on 2024-07-08 09:28:53 UTC using RuboCop version 1.64.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 18 +# Offense count: 19 # This cop supports safe autocorrection (--autocorrect). Layout/EmptyLineAfterGuardClause: Exclude: @@ -14,12 +14,13 @@ Layout/EmptyLineAfterGuardClause: - 'lib/onelogin/ruby-saml/idp_metadata_parser.rb' - 'lib/onelogin/ruby-saml/logoutrequest.rb' - 'lib/onelogin/ruby-saml/logoutresponse.rb' + - 'lib/onelogin/ruby-saml/metadata.rb' - 'lib/onelogin/ruby-saml/response.rb' - 'lib/onelogin/ruby-saml/saml_message.rb' - 'lib/onelogin/ruby-saml/slo_logoutrequest.rb' - 'lib/onelogin/ruby-saml/slo_logoutresponse.rb' -# Offense count: 10 +# Offense count: 9 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines, beginning_only, ending_only @@ -33,6 +34,12 @@ Layout/EmptyLinesAroundClassBody: - 'lib/onelogin/ruby-saml/slo_logoutresponse.rb' - 'lib/xml_security.rb' +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Layout/EmptyLinesAroundMethodBody: + Exclude: + - 'lib/onelogin/ruby-saml/slo_logoutrequest.rb' + # Offense count: 12 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. @@ -59,15 +66,13 @@ Layout/EndOfLine: Exclude: - 'lib/onelogin/ruby-saml/setting_error.rb' -# Offense count: 5 +# Offense count: 3 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowForAlignment, AllowBeforeTrailingComments, ForceEqualSignAlignment. Layout/ExtraSpacing: Exclude: - - 'lib/onelogin/ruby-saml/authrequest.rb' - 'lib/onelogin/ruby-saml/logoutrequest.rb' - 'lib/onelogin/ruby-saml/response.rb' - - 'lib/onelogin/ruby-saml/slo_logoutresponse.rb' # Offense count: 6 # This cop supports safe autocorrection (--autocorrect). @@ -125,15 +130,14 @@ Layout/SpaceAroundEqualsInParameterDefault: - 'lib/onelogin/ruby-saml/response.rb' - 'lib/onelogin/ruby-saml/utils.rb' -# Offense count: 18 +# Offense count: 16 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowForAlignment, EnforcedStyleForExponentOperator. +# Configuration parameters: AllowForAlignment, EnforcedStyleForExponentOperator, EnforcedStyleForRationalLiterals. # SupportedStylesForExponentOperator: space, no_space +# SupportedStylesForRationalLiterals: space, no_space Layout/SpaceAroundOperators: Exclude: - - 'lib/onelogin/ruby-saml/authrequest.rb' - 'lib/onelogin/ruby-saml/response.rb' - - 'lib/onelogin/ruby-saml/slo_logoutresponse.rb' - 'lib/onelogin/ruby-saml/utils.rb' - 'lib/xml_security.rb' @@ -147,7 +151,7 @@ Layout/SpaceInsideBlockBraces: - 'lib/onelogin/ruby-saml/idp_metadata_parser.rb' - 'lib/onelogin/ruby-saml/utils.rb' -# Offense count: 41 +# Offense count: 37 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces. # SupportedStyles: space, no_space, compact @@ -156,13 +160,12 @@ Layout/SpaceInsideHashLiteralBraces: Exclude: - 'lib/onelogin/ruby-saml/authrequest.rb' - 'lib/onelogin/ruby-saml/logoutrequest.rb' - - 'lib/onelogin/ruby-saml/metadata.rb' - 'lib/onelogin/ruby-saml/response.rb' - 'lib/onelogin/ruby-saml/settings.rb' - 'lib/onelogin/ruby-saml/slo_logoutresponse.rb' - 'lib/xml_security.rb' -# Offense count: 3 +# Offense count: 4 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: final_newline, final_blank_line @@ -170,6 +173,7 @@ Layout/TrailingEmptyLines: Exclude: - 'lib/onelogin/ruby-saml/http_error.rb' - 'lib/onelogin/ruby-saml/setting_error.rb' + - 'lib/onelogin/ruby-saml/settings.rb' - 'lib/onelogin/ruby-saml/validation_error.rb' # Offense count: 4 @@ -185,10 +189,14 @@ Lint/NoReturnInBeginEndBlocks: Exclude: - 'lib/onelogin/ruby-saml/idp_metadata_parser.rb' -# Offense count: 1 -Lint/ShadowingOuterLocalVariable: +# Offense count: 2 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: strict, consistent +Lint/SymbolConversion: Exclude: - - 'lib/onelogin/ruby-saml/response.rb' + - 'lib/onelogin/ruby-saml/idp_metadata_parser.rb' + - 'lib/onelogin/ruby-saml/settings.rb' # Offense count: 1 # Configuration parameters: AllowedPatterns. @@ -198,12 +206,14 @@ Lint/UnreachableLoop: - 'lib/onelogin/ruby-saml/saml_message.rb' # Offense count: 3 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: AutoCorrect. Lint/UselessAssignment: Exclude: - 'lib/onelogin/ruby-saml/logging.rb' - - 'lib/onelogin/ruby-saml/response.rb' + - 'lib/onelogin/ruby-saml/slo_logoutrequest.rb' -# Offense count: 39 +# Offense count: 42 # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. Metrics/AbcSize: Max: 100 @@ -219,12 +229,12 @@ Metrics/BlockLength: Metrics/ClassLength: Max: 652 -# Offense count: 23 +# Offense count: 25 # Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/CyclomaticComplexity: Max: 21 -# Offense count: 58 +# Offense count: 59 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: Max: 63 @@ -234,12 +244,12 @@ Metrics/MethodLength: Metrics/ParameterLists: MaxOptionalParameters: 4 -# Offense count: 22 +# Offense count: 24 # Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/PerceivedComplexity: Max: 22 -# Offense count: 7 +# Offense count: 13 Naming/AccessorMethodName: Exclude: - 'lib/onelogin/ruby-saml/settings.rb' @@ -251,17 +261,19 @@ Naming/AccessorMethodName: # AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS Naming/FileName: Exclude: + - 'Rakefile.rb' - 'lib/onelogin/ruby-saml.rb' - 'lib/ruby-saml.rb' # Offense count: 2 +# This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyleForLeadingUnderscores. # SupportedStylesForLeadingUnderscores: disallowed, required, optional Naming/MemoizedInstanceVariableName: Exclude: - 'lib/onelogin/ruby-saml/response.rb' -# Offense count: 3 +# Offense count: 4 # Configuration parameters: NamePrefix, ForbiddenPrefixes, AllowedMethods, MethodDefinitionMacros. # NamePrefix: is_, has_, have_ # ForbiddenPrefixes: is_, has_, have_ @@ -294,6 +306,13 @@ Performance/CollectionLiteralInLoop: Exclude: - 'lib/onelogin/ruby-saml/response.rb' +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: AllowRegexpMatch. +Performance/RedundantEqualityComparisonBlock: + Exclude: + - 'lib/onelogin/ruby-saml/settings.rb' + # Offense count: 5 # This cop supports unsafe autocorrection (--autocorrect-all). Performance/StringInclude: @@ -325,7 +344,7 @@ Style/AccessorGrouping: - 'lib/onelogin/ruby-saml/settings.rb' - 'lib/onelogin/ruby-saml/slo_logoutrequest.rb' -# Offense count: 5 +# Offense count: 6 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: prefer_alias, prefer_alias_method @@ -333,6 +352,7 @@ Style/Alias: Exclude: - 'lib/onelogin/ruby-saml/attributes.rb' - 'lib/onelogin/ruby-saml/response.rb' + - 'lib/onelogin/ruby-saml/settings.rb' - 'lib/onelogin/ruby-saml/slo_logoutrequest.rb' # Offense count: 13 @@ -361,6 +381,7 @@ Style/ConditionalAssignment: - 'lib/onelogin/ruby-saml/authrequest.rb' - 'lib/onelogin/ruby-saml/logoutresponse.rb' - 'lib/onelogin/ruby-saml/response.rb' + - 'lib/onelogin/ruby-saml/slo_logoutrequest.rb' - 'lib/xml_security.rb' # Offense count: 8 @@ -375,26 +396,47 @@ Style/Documentation: - 'lib/onelogin/ruby-saml/logging.rb' - 'lib/xml_security.rb' -# Offense count: 1 +# Offense count: 2 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. Style/GuardClause: Exclude: - 'lib/onelogin/ruby-saml/response.rb' + - 'lib/onelogin/ruby-saml/settings.rb' + +# Offense count: 3 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: AllowedReceivers. +# AllowedReceivers: Thread.current +Style/HashEachMethods: + Exclude: + - 'lib/onelogin/ruby-saml/metadata.rb' + - 'lib/onelogin/ruby-saml/settings.rb' + +# Offense count: 4 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. +# SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys +# SupportedShorthandSyntax: always, never, either, consistent, either_consistent +Style/HashSyntax: + Exclude: + - 'lib/onelogin/ruby-saml/settings.rb' -# Offense count: 60 +# Offense count: 66 # This cop supports safe autocorrection (--autocorrect). Style/IfUnlessModifier: Exclude: - 'lib/onelogin/ruby-saml/authrequest.rb' - 'lib/onelogin/ruby-saml/error_handling.rb' - 'lib/onelogin/ruby-saml/idp_metadata_parser.rb' + - 'lib/onelogin/ruby-saml/logoutrequest.rb' - 'lib/onelogin/ruby-saml/logoutresponse.rb' - 'lib/onelogin/ruby-saml/metadata.rb' - 'lib/onelogin/ruby-saml/response.rb' - 'lib/onelogin/ruby-saml/saml_message.rb' - 'lib/onelogin/ruby-saml/settings.rb' - 'lib/onelogin/ruby-saml/slo_logoutrequest.rb' + - 'lib/onelogin/ruby-saml/slo_logoutresponse.rb' - 'lib/onelogin/ruby-saml/utils.rb' - 'lib/xml_security.rb' @@ -413,15 +455,47 @@ Style/OptionalBooleanParameter: - 'lib/onelogin/ruby-saml/utils.rb' - 'lib/xml_security.rb' -# Offense count: 2 +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Style/RedundantBegin: + Exclude: + - 'lib/onelogin/ruby-saml/utils.rb' + +# Offense count: 8 +# This cop supports safe autocorrection (--autocorrect). +Style/RedundantRegexpArgument: + Exclude: + - 'lib/onelogin/ruby-saml/saml_message.rb' + - 'lib/onelogin/ruby-saml/utils.rb' + - 'lib/xml_security.rb' + +# Offense count: 3 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, AllowInnerSlashes. +# SupportedStyles: slashes, percent_r, mixed +Style/RegexpLiteral: + Exclude: + - 'lib/onelogin/ruby-saml/response.rb' + - 'lib/onelogin/ruby-saml/slo_logoutrequest.rb' + +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. +# AllowedMethods: present?, blank?, presence, try, try! +Style/SafeNavigation: + Exclude: + - 'lib/onelogin/ruby-saml/slo_logoutrequest.rb' + +# Offense count: 4 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: Mode. Style/StringConcatenation: Exclude: - 'lib/onelogin/ruby-saml/response.rb' - 'lib/onelogin/ruby-saml/saml_message.rb' + - 'lib/onelogin/ruby-saml/slo_logoutrequest.rb' -# Offense count: 450 +# Offense count: 443 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. # SupportedStyles: single_quotes, double_quotes @@ -443,7 +517,23 @@ Style/StringLiterals: - 'lib/onelogin/ruby-saml/utils.rb' - 'lib/xml_security.rb' -# Offense count: 100 +# Offense count: 3 +# This cop supports safe autocorrection (--autocorrect). +Style/SuperArguments: + Exclude: + - 'lib/onelogin/ruby-saml/authrequest.rb' + - 'lib/onelogin/ruby-saml/logoutrequest.rb' + - 'lib/onelogin/ruby-saml/slo_logoutresponse.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, MinSize. +# SupportedStyles: percent, brackets +Style/SymbolArray: + Exclude: + - 'lib/onelogin/ruby-saml/settings.rb' + +# Offense count: 103 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns. # URISchemes: http, https diff --git a/CHANGELOG.md b/CHANGELOG.md index a80f11dce..475797554 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,20 @@ # Ruby SAML Changelog +### 1.17.0 +* [#673](https://github.com/SAML-Toolkits/ruby-saml/pull/673) Add `Settings#sp_cert_multi` paramter to facilitate SP certificate and key rotation. +* [#673](https://github.com/SAML-Toolkits/ruby-saml/pull/673) Support multiple simultaneous SP decryption keys via `Settings#sp_cert_multi` parameter. +* [#673](https://github.com/SAML-Toolkits/ruby-saml/pull/673) Deprecate `Settings#certificate_new` parameter. +* [#673](https://github.com/SAML-Toolkits/ruby-saml/pull/673) `:check_sp_cert_expiration` will use the first non-expired certificate/key when signing/decrypting. It will raise an error only if there are no valid certificates/keys. +* [#673](https://github.com/SAML-Toolkits/ruby-saml/pull/673) `:check_sp_cert_expiration` now validates the certificate `not_before` condition; previously it was only validating `not_after`. +* [#673](https://github.com/SAML-Toolkits/ruby-saml/pull/673) `:check_sp_cert_expiration` now causes the generated SP metadata to exclude any inactive/expired certificates. + +### 1.16.0 (Oct 09, 2023) +* [#671](https://github.com/SAML-Toolkits/ruby-saml/pull/671) Add support on LogoutRequest with Encrypted NameID + ### 1.15.0 (Jan 04, 2023) * [#650](https://github.com/SAML-Toolkits/ruby-saml/pull/650) Replace strip! by strip on compute_digest method * [#638](https://github.com/SAML-Toolkits/ruby-saml/pull/638) Fix dateTime format for the validUntil attribute of the generated metadata -* [#576](https://github.com/SAML-Toolkits/ruby-saml/pull/576) Support idp cert multi with string keys +* [#576](https://github.com/SAML-Toolkits/ruby-saml/pull/576) Support `Settings#idp_cert_multi` with string keys * [#567](https://github.com/SAML-Toolkits/ruby-saml/pull/567) Improve Code quality * Add info about new repo, new maintainer, new security contact * Fix tests, Adjust dependencies, Add ruby 3.2 and new jruby versions tests to the CI. Add coveralls support @@ -48,7 +59,7 @@ * Support Process Transform * Raise SettingError if invoking an action with no endpoint defined on the settings * Made IdpMetadataParser more extensible for subclasses -*[#548](https://github.com/onelogin/ruby-saml/pull/548) Add :skip_audience option +* [#548](https://github.com/onelogin/ruby-saml/pull/548) Add :skip_audience option * [#555](https://github.com/onelogin/ruby-saml/pull/555) Define 'soft' variable to prevent exception when doc cert is invalid * Improve documentation @@ -107,7 +118,6 @@ * [#423](https://github.com/onelogin/ruby-saml/pull/423) Allow format_cert to work with chained certificates * [#422](https://github.com/onelogin/ruby-saml/pull/422) Use to_s for requested attribute value - ### 1.5.0 (August 31, 2017) * [#400](https://github.com/onelogin/ruby-saml/pull/400) When validating Signature use stored IdP certficate if Signature contains no info about Certificate * [#402](https://github.com/onelogin/ruby-saml/pull/402) Fix validate_response_state method that rejected SAMLResponses when using idp_cert_multi and idp_cert and idp_cert_fingerprint were not provided. @@ -116,7 +126,6 @@ * [#374](https://github.com/onelogin/ruby-saml/issues/374) Support more than one level of StatusCode * [#405](https://github.com/onelogin/ruby-saml/pull/405) Support ADFS encrypted key (Accept KeyInfo nodes with no ds namespace) - ### 1.4.3 (May 18, 2017) * Added SubjectConfirmation Recipient validation * [#393](https://github.com/onelogin/ruby-saml/pull/393) Implement IdpMetadataParser#parse_to_hash diff --git a/Gemfile b/Gemfile index 05ecdf933..cfbda3cde 100644 --- a/Gemfile +++ b/Gemfile @@ -7,9 +7,9 @@ gemspec gem 'minitest', '~> 5.18', require: false gem 'mocha', '~> 2.0', require: false gem 'rake', '~> 13.0' -gem 'rubocop', '~> 1.45.1', require: false -gem 'rubocop-minitest', '~> 0.22.2', require: false -gem 'rubocop-performance', '~> 1.16.0', require: false +gem 'rubocop', '~> 1.64.1', require: false +gem 'rubocop-minitest', '~> 0.35.0', require: false +gem 'rubocop-performance', '~> 1.21.1', require: false gem 'rubocop-rake', '~> 0.6.0', require: false gem 'simplecov', '~> 0.22', require: false gem 'simplecov-lcov', '~> 0.8', require: false diff --git a/README.md b/README.md index f8d97b2e5..ec714d515 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,6 @@ We created a demo project for Rails 4 that uses the latest version of this libra ### Supported Ruby Versions -* 2.6 -* 2.7 * 3.0 * 3.1 * 3.2 @@ -683,7 +681,7 @@ signature validation process will fail at the Identity Provider. Ruby SAML supports EncryptedAssertion. The Identity Provider will encrypt the Assertion with the public cert of the Service Provider. The Service Provider will decrypt the EncryptedAssertion with its private key. -You may enable EncryptedAssertion as follows. This will add `` to your +You may enable EncryptedAssertion as follows. This will add `` to your SP Metadata XML, to be read by the IdP. ```ruby @@ -720,6 +718,48 @@ validation fails. You may disable such exceptions using the `settings.security[: settings.security[:soft] = true # Do not raise error on failed signature/certificate validations ``` +#### Advanced SP Certificate Usage & Key Rollover + +Ruby SAML provides the `settings.sp_cert_multi` parameter to enable the following +advanced usage scenarios: +- Rotating SP certificates and private keys without disruption of service. +- Specifying separate SP certificates for signing and encryption. + +The `sp_cert_multi` parameter replaces `certificate` and `private_key` +(you may not specify both pparameters at the same time.) `sp_cert_multi` has the following shape: + +```ruby +settings.sp_cert_multi = { + signing: [ + { certificate: cert1, private_key: private_key1 }, + { certificate: cert2, private_key: private_key2 } + ], + encryption: [ + { certificate: cert1, private_key: private_key1 }, + { certificate: cert3, private_key: private_key1 } + ], +} +``` + +Certificate rotation is acheived by inserting new certificates at the bottom of each list, +and then removing the old certificates from the top of the list once your IdPs have migrated. +A common practice is for apps to publish the current SP metadata at a URL endpoint and have +the IdP regularly poll for updates. + +Note the following: +- You may re-use the same certificate and/or private key in multiple places, including for both signing and encryption. +- The IdP should attempt to verify signatures with *all* `:signing` certificates, + and permit if *any one* succeeds. When signing, Ruby SAML will use the first SP certificate + in the `sp_cert_multi[:signing]` array. This will be the first active/non-expired certificate + in the array if `settings.security[:check_sp_cert_expiration]` is true. +- The IdP may encrypt with any of the SP certificates in the `sp_cert_multi[:encryption]` + array. When decrypting, Ruby SAML attempt to decrypt with each SP private key in + `sp_cert_multi[:encryption]` until the decryption is successful. This will skip private + keys for inactive/expired certificates if `:check_sp_cert_expiration` is true. +- If `:check_sp_cert_expiration` is true, the generated SP metadata XML will not include + inactive/expired certificates. This avoids validation errors when the IdP reads the SP + metadata. + #### Audience Validation A service provider should only consider a SAML response valid if the IdP includes an @@ -743,29 +783,6 @@ is invalid using the `settings.security[:strict_audience_validation]` parameter. settings.security[:strict_audience_validation] = true ``` -#### Key Rollover - -To update the SP X.509 certificate and private key without disruption of service, you may define the parameter -`settings.certificate_new`. This will publish the new SP certificate in your metadata so that your IdP counterparties -may cache it in preparation for rollover. - -For example, if you to rollover from `CERT A` to `CERT B`. Before rollover, your settings should look as follows. -Both `CERT A` and `CERT B` will now appear in your SP metadata, however `CERT A` will still be used for signing -and encryption at this time. - -```ruby - settings.certificate = "CERT A" - settings.private_key = "PRIVATE KEY FOR CERT A" - settings.certificate_new = "CERT B" -``` - -After the IdP has cached `CERT B`, you may then change your settings as follows: - -```ruby - settings.certificate = "CERT B" - settings.private_key = "PRIVATE KEY FOR CERT B" -``` - ## Single Log Out Ruby SAML supports SP-initiated Single Logout and IdP-Initiated Single Logout. diff --git a/lib/onelogin/ruby-saml/authrequest.rb b/lib/onelogin/ruby-saml/authrequest.rb index af155216d..3554f2896 100644 --- a/lib/onelogin/ruby-saml/authrequest.rb +++ b/lib/onelogin/ruby-saml/authrequest.rb @@ -75,9 +75,10 @@ def create_params(settings, params={}) request = deflate(request) if settings.compress_request base64_request = encode(request) request_params = {"SAMLRequest" => base64_request} + sp_signing_key = settings.get_sp_signing_key - if settings.idp_sso_service_binding == Utils::BINDINGS[:redirect] && settings.security[:authn_requests_signed] && settings.private_key - params['SigAlg'] = settings.security[:signature_method] + if settings.idp_sso_service_binding == Utils::BINDINGS[:redirect] && settings.security[:authn_requests_signed] && sp_signing_key + params['SigAlg'] = settings.security[:signature_method] url_string = OneLogin::RubySaml::Utils.build_query( type: 'SAMLRequest', data: base64_request, @@ -85,7 +86,7 @@ def create_params(settings, params={}) sig_alg: params['SigAlg'] ) sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method]) - signature = settings.get_sp_key.sign(sign_algorithm.new, url_string) + signature = sp_signing_key.sign(sign_algorithm.new, url_string) params['Signature'] = encode(signature) end @@ -183,15 +184,13 @@ def create_xml_document(settings) end def sign_document(document, settings) - if settings.idp_sso_service_binding == Utils::BINDINGS[:post] && settings.security[:authn_requests_signed] && settings.private_key && settings.certificate - private_key = settings.get_sp_key - cert = settings.get_sp_cert + cert, private_key = settings.get_sp_signing_pair + if settings.idp_sso_service_binding == Utils::BINDINGS[:post] && settings.security[:authn_requests_signed] && private_key && cert document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method]) end document end - end end end diff --git a/lib/onelogin/ruby-saml/logoutrequest.rb b/lib/onelogin/ruby-saml/logoutrequest.rb index d233eb415..9b5d083bd 100644 --- a/lib/onelogin/ruby-saml/logoutrequest.rb +++ b/lib/onelogin/ruby-saml/logoutrequest.rb @@ -72,8 +72,9 @@ def create_params(settings, params={}) request = deflate(request) if settings.compress_request base64_request = encode(request) request_params = {"SAMLRequest" => base64_request} + sp_signing_key = settings.get_sp_signing_key - if settings.idp_slo_service_binding == Utils::BINDINGS[:redirect] && settings.security[:logout_requests_signed] && settings.private_key + if settings.idp_slo_service_binding == Utils::BINDINGS[:redirect] && settings.security[:logout_requests_signed] && sp_signing_key params['SigAlg'] = settings.security[:signature_method] url_string = OneLogin::RubySaml::Utils.build_query( type: 'SAMLRequest', @@ -82,7 +83,7 @@ def create_params(settings, params={}) sig_alg: params['SigAlg'] ) sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method]) - signature = settings.get_sp_key.sign(sign_algorithm.new, url_string) + signature = settings.get_sp_signing_key.sign(sign_algorithm.new, url_string) params['Signature'] = encode(signature) end @@ -141,9 +142,8 @@ def create_xml_document(settings) def sign_document(document, settings) # embed signature - if settings.idp_slo_service_binding == Utils::BINDINGS[:post] && settings.security[:logout_requests_signed] && settings.private_key && settings.certificate - private_key = settings.get_sp_key - cert = settings.get_sp_cert + cert, private_key = settings.get_sp_signing_pair + if settings.idp_slo_service_binding == Utils::BINDINGS[:post] && settings.security[:logout_requests_signed] && private_key && cert document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method]) end diff --git a/lib/onelogin/ruby-saml/metadata.rb b/lib/onelogin/ruby-saml/metadata.rb index 3e4b27311..5284198a0 100644 --- a/lib/onelogin/ruby-saml/metadata.rb +++ b/lib/onelogin/ruby-saml/metadata.rb @@ -64,29 +64,14 @@ def add_sp_sso_element(root, settings) } end - # Add KeyDescriptor if messages will be signed / encrypted - # with SP certificate, and new SP certificate if any + # Add KeyDescriptor elements for SP certificates. def add_sp_certificates(sp_sso, settings) - cert = settings.get_sp_cert - cert_new = settings.get_sp_cert_new - - [cert, cert_new].each do |sp_cert| - next unless sp_cert - - cert_text = Base64.encode64(sp_cert.to_der).gsub("\n", '') - kd = sp_sso.add_element "md:KeyDescriptor", { "use" => "signing" } - ki = kd.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"} - xd = ki.add_element "ds:X509Data" - xc = xd.add_element "ds:X509Certificate" - xc.text = cert_text - - next unless settings.security[:want_assertions_encrypted] - - kd2 = sp_sso.add_element "md:KeyDescriptor", { "use" => "encryption" } - ki2 = kd2.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"} - xd2 = ki2.add_element "ds:X509Data" - xc2 = xd2.add_element "ds:X509Certificate" - xc2.text = cert_text + certs = settings.get_sp_certs + + certs[:signing].each { |cert, _| add_sp_cert_element(sp_sso, cert, :signing) } + + if settings.security[:want_assertions_encrypted] + certs[:encryption].each { |cert, _| add_sp_cert_element(sp_sso, cert, :encryption) } end sp_sso @@ -155,8 +140,7 @@ def add_extras(root, _settings) def embed_signature(meta_doc, settings) return unless settings.security[:metadata_signed] - private_key = settings.get_sp_key - cert = settings.get_sp_cert + cert, private_key = settings.get_sp_signing_pair return unless private_key && cert meta_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method]) @@ -174,6 +158,18 @@ def output_xml(meta_doc, pretty_print) ret end + + private + + def add_sp_cert_element(sp_sso, cert, use) + return unless cert + cert_text = Base64.encode64(cert.to_der).gsub("\n", '') + kd = sp_sso.add_element "md:KeyDescriptor", { "use" => use.to_s } + ki = kd.add_element "ds:KeyInfo", { "xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#" } + xd = ki.add_element "ds:X509Data" + xc = xd.add_element "ds:X509Certificate" + xc.text = cert_text + end end end end diff --git a/lib/onelogin/ruby-saml/response.rb b/lib/onelogin/ruby-saml/response.rb index 2cbed23bc..90664d772 100644 --- a/lib/onelogin/ruby-saml/response.rb +++ b/lib/onelogin/ruby-saml/response.rb @@ -300,7 +300,7 @@ def issuers end nodes = issuer_response_nodes + issuer_assertion_nodes - nodes.map { |node| Utils.element_text(node) }.compact.uniq + nodes.filter_map { |node| Utils.element_text(node) }.uniq end end @@ -901,9 +901,9 @@ def name_id_node begin encrypted_node = xpath_first_from_signed_assertion('/a:Subject/a:EncryptedID') if encrypted_node - node = decrypt_nameid(encrypted_node) + decrypt_nameid(encrypted_node) else - node = xpath_first_from_signed_assertion('/a:Subject/a:NameID') + xpath_first_from_signed_assertion('/a:Subject/a:NameID') end end end @@ -955,7 +955,7 @@ def xpath_from_signed_assertion(subelt=nil) # @return [XMLSecurity::SignedDocument] The SAML Response with the assertion decrypted # def generate_decrypted_document - if settings.nil? || !settings.get_sp_key + if settings.nil? || settings.get_sp_decryption_keys.empty? raise ValidationError.new('An EncryptedAssertion found and no SP private key found on the settings to decrypt it. Be sure you provided the :settings parameter at the initialize method') end @@ -993,28 +993,28 @@ def decrypt_assertion(encrypted_assertion_node) end # Decrypts an EncryptedID element - # @param encryptedid_node [REXML::Element] The EncryptedID element + # @param encrypted_id_node [REXML::Element] The EncryptedID element # @return [REXML::Document] The decrypted EncrypedtID element # - def decrypt_nameid(encryptedid_node) - decrypt_element(encryptedid_node, %r{(.*)}m) + def decrypt_nameid(encrypted_id_node) + decrypt_element(encrypted_id_node, /(.*<\/(\w+:)?NameID>)/m) end - # Decrypts an EncryptedID element - # @param encryptedid_node [REXML::Element] The EncryptedID element - # @return [REXML::Document] The decrypted EncrypedtID element + # Decrypts an EncryptedAttribute element + # @param encrypted_attribute_node [REXML::Element] The EncryptedAttribute element + # @return [REXML::Document] The decrypted EncryptedAttribute element # - def decrypt_attribute(encryptedattribute_node) - decrypt_element(encryptedattribute_node, %r{(.*)}m) + def decrypt_attribute(encrypted_attribute_node) + decrypt_element(encrypted_attribute_node, /(.*<\/(\w+:)?Attribute>)/m) end # Decrypt an element - # @param encryptedid_node [REXML::Element] The encrypted element - # @param rgrex string Regex + # @param encrypt_node [REXML::Element] The encrypted element + # @param regexp [Regexp] The regular expression to extract the decrypted data # @return [REXML::Document] The decrypted element # - def decrypt_element(encrypt_node, rgrex) - if settings.nil? || !settings.get_sp_key + def decrypt_element(encrypt_node, regexp) + if settings.nil? || settings.get_sp_decryption_keys.empty? raise ValidationError.new('An ' + encrypt_node.name + ' found and no SP private key found on the settings to decrypt it') end @@ -1024,10 +1024,11 @@ def decrypt_element(encrypt_node, rgrex) node_header = '' end - elem_plaintext = OneLogin::RubySaml::Utils.decrypt_data(encrypt_node, settings.get_sp_key) + elem_plaintext = OneLogin::RubySaml::Utils.decrypt_multi(encrypt_node, settings.get_sp_decryption_keys) + # If we get some problematic noise in the plaintext after decrypting. # This quick regexp parse will grab only the Element and discard the noise. - elem_plaintext = elem_plaintext.match(rgrex)[0] + elem_plaintext = elem_plaintext.match(regexp)[0] # To avoid namespace errors if saml namespace is not defined # create a parent node first with the namespace defined diff --git a/lib/onelogin/ruby-saml/saml_message.rb b/lib/onelogin/ruby-saml/saml_message.rb index e31f63845..7677925ae 100644 --- a/lib/onelogin/ruby-saml/saml_message.rb +++ b/lib/onelogin/ruby-saml/saml_message.rb @@ -20,7 +20,7 @@ class SamlMessage ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion" PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol" - BASE64_FORMAT = %r(\A([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?\Z).freeze + BASE64_FORMAT = %r(\A([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?\Z) @@mutex = Mutex.new # @return [Nokogiri::XML::Schema] Gets the schema object of the SAML 2.0 Protocol schema diff --git a/lib/onelogin/ruby-saml/settings.rb b/lib/onelogin/ruby-saml/settings.rb index a5bab077b..935a0e562 100644 --- a/lib/onelogin/ruby-saml/settings.rb +++ b/lib/onelogin/ruby-saml/settings.rb @@ -62,8 +62,8 @@ def initialize(overrides = {}, keep_security_attributes = false) attr_accessor :attributes_index attr_accessor :force_authn attr_accessor :certificate - attr_accessor :certificate_new attr_accessor :private_key + attr_accessor :sp_cert_multi attr_accessor :authn_context attr_accessor :authn_context_comparison attr_accessor :authn_context_decl_ref @@ -72,6 +72,7 @@ def initialize(overrides = {}, keep_security_attributes = false) attr_accessor :security attr_accessor :soft # Deprecated + attr_accessor :certificate_new attr_accessor :assertion_consumer_logout_service_url attr_reader :assertion_consumer_logout_service_binding attr_accessor :issuer @@ -182,10 +183,7 @@ def get_fingerprint # @return [OpenSSL::X509::Certificate|nil] Build the IdP certificate from the settings (previously format it) # def get_idp_cert - return nil if idp_cert.nil? || idp_cert.empty? - - formatted_cert = OneLogin::RubySaml::Utils.format_cert(idp_cert) - OpenSSL::X509::Certificate.new(formatted_cert) + OneLogin::RubySaml::Utils.build_cert_object(idp_cert) end # @return [Hash with 2 arrays of OpenSSL::X509::Certificate] Build multiple IdP certificates from the settings. @@ -202,45 +200,69 @@ def get_idp_cert_multi next if !certs_for_type || certs_for_type.empty? certs_for_type.each do |idp_cert| - formatted_cert = OneLogin::RubySaml::Utils.format_cert(idp_cert) - certs[type].push(OpenSSL::X509::Certificate.new(formatted_cert)) + certs[type].push(OneLogin::RubySaml::Utils.build_cert_object(idp_cert)) end end certs end - # @return [OpenSSL::X509::Certificate|nil] Build the SP certificate from the settings (previously format it) - # - def get_sp_cert - return nil if certificate.nil? || certificate.empty? + # @return [Hash>>] + # Build the SP certificates and private keys from the settings. If + # check_sp_cert_expiration is true, only returns certificates and private keys + # that are not expired. + def get_sp_certs + certs = get_all_sp_certs + return certs unless security[:check_sp_cert_expiration] + + active_certs = { signing: [], encryption: [] } + certs.each do |use, pairs| + next if pairs.empty? - formatted_cert = OneLogin::RubySaml::Utils.format_cert(certificate) - cert = OpenSSL::X509::Certificate.new(formatted_cert) + pairs = pairs.select { |cert, _| !cert || OneLogin::RubySaml::Utils.is_cert_active(cert) } + raise OneLogin::RubySaml::ValidationError.new("The SP certificate expired.") if pairs.empty? - if security[:check_sp_cert_expiration] && OneLogin::RubySaml::Utils.is_cert_expired(cert) - raise OneLogin::RubySaml::ValidationError.new("The SP certificate expired.") + active_certs[use] = pairs.freeze end + active_certs.freeze + end - cert + # @return [Array] + # The SP signing certificate and private key. + def get_sp_signing_pair + get_sp_certs[:signing].first end - # @return [OpenSSL::X509::Certificate|nil] Build the New SP certificate from the settings (previously format it) - # - def get_sp_cert_new - return nil if certificate_new.nil? || certificate_new.empty? + # @return [OpenSSL::X509::Certificate] The SP signing certificate. + # @deprecated Use get_sp_signing_pair or get_sp_certs instead. + def get_sp_cert + node = get_sp_signing_pair + node[0] if node + end - formatted_cert = OneLogin::RubySaml::Utils.format_cert(certificate_new) - OpenSSL::X509::Certificate.new(formatted_cert) + # @return [OpenSSL::PKey::RSA] The SP signing key. + def get_sp_signing_key + node = get_sp_signing_pair + node[1] if node end - # @return [OpenSSL::PKey::RSA] Build the SP private from the settings (previously format it) - # - def get_sp_key - return nil if private_key.nil? || private_key.empty? + # @deprecated Use get_sp_signing_key or get_sp_certs instead. + alias_method :get_sp_key, :get_sp_signing_key - formatted_private_key = OneLogin::RubySaml::Utils.format_private_key(private_key) - OpenSSL::PKey::RSA.new(formatted_private_key) + # @return [Array] The SP decryption keys. + def get_sp_decryption_keys + ary = get_sp_certs[:encryption].map { |pair| pair[1] } + ary.compact! + ary.uniq!(&:to_pem) + ary.freeze + end + + # @return [OpenSSL::X509::Certificate|nil] Build the New SP certificate from the settings. + # + # @deprecated Use get_sp_certs instead + def get_sp_cert_new + node = get_sp_certs[:signing].last + node[0] if node end def idp_binding_from_embed_sign @@ -279,6 +301,85 @@ def get_binding(value) lowercase_url_encoding: false }.freeze }.freeze + + private + + # @return [Hash>>] + # Build the SP certificates and private keys from the settings. Returns all + # certificates and private keys, even if they are expired. + def get_all_sp_certs + validate_sp_certs_params! + get_sp_certs_multi || get_sp_certs_single + end + + # Validate certificate, certificate_new, private_key, and sp_cert_multi params. + def validate_sp_certs_params! + multi = sp_cert_multi && !sp_cert_multi.empty? + cert = certificate && !certificate.empty? + cert_new = certificate_new && !certificate_new.empty? + pk = private_key && !private_key.empty? + if multi && (cert || cert_new || pk) + raise ArgumentError.new("Cannot specify both sp_cert_multi and certificate, certificate_new, private_key parameters") + end + end + + # Get certs from certificate, certificate_new, and private_key parameters. + def get_sp_certs_single + certs = { :signing => [], :encryption => [] } + + sp_key = OneLogin::RubySaml::Utils.build_private_key_object(private_key) + cert = OneLogin::RubySaml::Utils.build_cert_object(certificate) + if cert || sp_key + ary = [cert, sp_key].freeze + certs[:signing] << ary + certs[:encryption] << ary + end + + cert_new = OneLogin::RubySaml::Utils.build_cert_object(certificate_new) + if cert_new + ary = [cert_new, sp_key].freeze + certs[:signing] << ary + certs[:encryption] << ary + end + + certs + end + + # Get certs from get_sp_cert_multi parameter. + def get_sp_certs_multi + return if sp_cert_multi.nil? || sp_cert_multi.empty? + + raise ArgumentError.new("sp_cert_multi must be a Hash") unless sp_cert_multi.is_a?(Hash) + + certs = { :signing => [], :encryption => [] }.freeze + + [:signing, :encryption].each do |type| + certs_for_type = sp_cert_multi[type] || sp_cert_multi[type.to_s] + next if !certs_for_type || certs_for_type.empty? + + unless certs_for_type.is_a?(Array) && certs_for_type.all? { |cert| cert.is_a?(Hash) } + raise ArgumentError.new("sp_cert_multi :#{type} node must be an Array of Hashes") + end + + certs_for_type.each do |pair| + cert = pair[:certificate] || pair['certificate'] || pair[:cert] || pair['cert'] + key = pair[:private_key] || pair['private_key'] || pair[:key] || pair['key'] + + unless cert && key + raise ArgumentError.new("sp_cert_multi :#{type} node Hashes must specify keys :certificate and :private_key") + end + + certs[type] << [ + OneLogin::RubySaml::Utils.build_cert_object(cert), + OneLogin::RubySaml::Utils.build_private_key_object(key) + ].freeze + end + end + + certs.each { |_, ary| ary.freeze } + certs + end end end end + diff --git a/lib/onelogin/ruby-saml/slo_logoutrequest.rb b/lib/onelogin/ruby-saml/slo_logoutrequest.rb index acbdec65b..2199064c0 100644 --- a/lib/onelogin/ruby-saml/slo_logoutrequest.rb +++ b/lib/onelogin/ruby-saml/slo_logoutrequest.rb @@ -63,10 +63,7 @@ def is_valid?(collect_errors = false) # @return [String] Gets the NameID of the Logout Request. # def name_id - @name_id ||= begin - node = REXML::XPath.first(document, "/p:LogoutRequest/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION }) - Utils.element_text(node) - end + @name_id ||= Utils.element_text(name_id_node) end alias_method :nameid, :name_id @@ -74,15 +71,49 @@ def name_id # @return [String] Gets the NameID Format of the Logout Request. # def name_id_format - @name_id_node ||= REXML::XPath.first(document, "/p:LogoutRequest/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION }) @name_id_format ||= - if @name_id_node&.attribute("Format") - @name_id_node.attribute("Format").value + if name_id_node && name_id_node.attribute("Format") + name_id_node.attribute("Format").value end end alias_method :nameid_format, :name_id_format + def name_id_node + @name_id_node ||= + begin + encrypted_node = REXML::XPath.first(document, "/p:LogoutRequest/a:EncryptedID", { "p" => PROTOCOL, "a" => ASSERTION }) + if encrypted_node + node = decrypt_nameid(encrypted_node) + else + node = REXML::XPath.first(document, "/p:LogoutRequest/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION }) + end + end + end + + # Decrypts an EncryptedID element + # @param encrypted_id_node [REXML::Element] The EncryptedID element + # @return [REXML::Document] The decrypted EncrypedtID element + # + def decrypt_nameid(encrypted_id_node) + + if settings.nil? || settings.get_sp_decryption_keys.empty? + raise ValidationError.new('An ' + encrypted_id_node.name + ' found and no SP private key found on the settings to decrypt it') + end + + elem_plaintext = OneLogin::RubySaml::Utils.decrypt_multi(encrypted_id_node, settings.get_sp_decryption_keys) + # If we get some problematic noise in the plaintext after decrypting. + # This quick regexp parse will grab only the Element and discard the noise. + elem_plaintext = elem_plaintext.match(/(.*<\/(\w+:)?NameID>)/m)[0] + + # To avoid namespace errors if saml namespace is not defined + # create a parent node first with the namespace defined + node_header = '' + elem_plaintext = node_header + elem_plaintext + '' + doc = REXML::Document.new(elem_plaintext) + doc.root[0] + end + # @return [String|nil] Gets the ID attribute from the Logout Request. if exists. # def id diff --git a/lib/onelogin/ruby-saml/slo_logoutresponse.rb b/lib/onelogin/ruby-saml/slo_logoutresponse.rb index 19e386f20..d8386c428 100644 --- a/lib/onelogin/ruby-saml/slo_logoutresponse.rb +++ b/lib/onelogin/ruby-saml/slo_logoutresponse.rb @@ -81,9 +81,10 @@ def create_params(settings, request_id = nil, logout_message = nil, params = {}, response = deflate(response) if settings.compress_response base64_response = encode(response) response_params = {"SAMLResponse" => base64_response} + sp_signing_key = settings.get_sp_signing_key - if settings.idp_slo_service_binding == Utils::BINDINGS[:redirect] && settings.security[:logout_responses_signed] && settings.private_key - params['SigAlg'] = settings.security[:signature_method] + if settings.idp_slo_service_binding == Utils::BINDINGS[:redirect] && settings.security[:logout_responses_signed] && sp_signing_key + params['SigAlg'] = settings.security[:signature_method] url_string = OneLogin::RubySaml::Utils.build_query( type: 'SAMLResponse', data: base64_response, @@ -91,7 +92,7 @@ def create_params(settings, request_id = nil, logout_message = nil, params = {}, sig_alg: params['SigAlg'] ) sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method]) - signature = settings.get_sp_key.sign(sign_algorithm.new, url_string) + signature = sp_signing_key.sign(sign_algorithm.new, url_string) params['Signature'] = encode(signature) end @@ -152,9 +153,8 @@ def create_xml_document(settings, request_id = nil, logout_message = nil, status def sign_document(document, settings) # embed signature - if settings.idp_slo_service_binding == Utils::BINDINGS[:post] && settings.private_key && settings.certificate - private_key = settings.get_sp_key - cert = settings.get_sp_cert + cert, private_key = settings.get_sp_signing_pair + if settings.idp_slo_service_binding == Utils::BINDINGS[:post] && private_key && cert document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method]) end diff --git a/lib/onelogin/ruby-saml/utils.rb b/lib/onelogin/ruby-saml/utils.rb index 274252818..3c91b29a5 100644 --- a/lib/onelogin/ruby-saml/utils.rb +++ b/lib/onelogin/ruby-saml/utils.rb @@ -27,21 +27,29 @@ class Utils | (\d+)W # 8: Weeks ) - $/x.freeze + $/x UUID_PREFIX = +'_' - # Checks if the x509 cert provided is expired - # - # @param cert [Certificate] The x509 certificate + # Checks if the x509 cert provided is expired. # + # @param cert [OpenSSL::X509::Certificate|String] The x509 certificate. + # @return [true|false] Whether the certificate is expired. def self.is_cert_expired(cert) - if cert.is_a?(String) - cert = OpenSSL::X509::Certificate.new(cert) - end + cert = OpenSSL::X509::Certificate.new(cert) if cert.is_a?(String) cert.not_after < Time.now end + # Checks if the x509 cert provided has both started and has not expired. + # + # @param cert [OpenSSL::X509::Certificate|String] The x509 certificate. + # @return [true|false] Whether the certificate is currently active. + def self.is_cert_active(cert) + cert = OpenSSL::X509::Certificate.new(cert) if cert.is_a?(String) + now = Time.now + cert.not_before <= now && cert.not_after >= now + end + # Interprets a ISO8601 duration value relative to a given timestamp. # # @param duration [String] The duration, as a string. @@ -121,6 +129,28 @@ def self.format_private_key(key) "-----BEGIN #{key_label}-----\n#{key}\n-----END #{key_label}-----" end + # Given a certificate string, return an OpenSSL::X509::Certificate object. + # + # @param cert [String] The original certificate + # @return [OpenSSL::X509::Certificate] The certificate object + # + def self.build_cert_object(cert) + return nil if cert.nil? || cert.empty? + + OpenSSL::X509::Certificate.new(format_cert(cert)) + end + + # Given a private key string, return an OpenSSL::PKey::RSA object. + # + # @param cert [String] The original private key + # @return [OpenSSL::PKey::RSA] The private key object + # + def self.build_private_key_object(private_key) + return nil if private_key.nil? || private_key.empty? + + OpenSSL::PKey::RSA.new(format_private_key(private_key)) + end + # Build the Query String signature that will be used in the HTTP-Redirect binding # to generate the Signature # @param params [Hash] Parameters to build the Query String @@ -192,7 +222,7 @@ def self.escape_request_param(param, lowercase_url_encoding) # Validate the Signature parameter sent on the HTTP-Redirect binding # @param params [Hash] Parameters to be used in the validation process - # @option params [OpenSSL::X509::Certificate] cert The Identity provider public certtificate + # @option params [OpenSSL::X509::Certificate] cert The IDP public certificate # @option params [String] sig_alg The SigAlg parameter # @option params [String] signature The Signature parameter (base64 encoded) # @option params [String] query_string The full GET Query String to be compared @@ -227,9 +257,29 @@ def self.status_error_msg(error_msg, raw_status_code = nil, status_message = nil error_msg end + # Obtains the decrypted string from an Encrypted node element in XML, + # given multiple private keys to try. + # @param encrypted_node [REXML::Element] The Encrypted element + # @param private_keys [Array] The Service provider private key + # @return [String] The decrypted data + def self.decrypt_multi(encrypted_node, private_keys) + raise ArgumentError.new('private_keys must be specified') if !private_keys || private_keys.empty? + + error = nil + private_keys.each do |key| + begin + return decrypt_data(encrypted_node, key) + rescue OpenSSL::PKey::PKeyError => e + error ||= e + end + end + + raise(error) if error + end + # Obtains the decrypted string from an Encrypted node element in XML - # @param encrypted_node [REXML::Element] The Encrypted element - # @param private_key [OpenSSL::PKey::RSA] The Service provider private key + # @param encrypted_node [REXML::Element] The Encrypted element + # @param private_key [OpenSSL::PKey::RSA] The Service provider private key # @return [String] The decrypted data def self.decrypt_data(encrypted_node, private_key) encrypt_data = REXML::XPath.first( @@ -293,7 +343,7 @@ def self.retrieve_symetric_key_reference(encrypt_data) # Obtains the deciphered text # @param cipher_text [String] The ciphered text - # @param symmetric_key [String] The symetric key used to encrypt the text + # @param symmetric_key [String] The symmetric key used to encrypt the text # @param algorithm [String] The encrypted algorithm # @return [String] The deciphered text def self.retrieve_plaintext(cipher_text, symmetric_key, algorithm) diff --git a/lib/onelogin/ruby-saml/version.rb b/lib/onelogin/ruby-saml/version.rb index 43bad7395..e61b094e7 100644 --- a/lib/onelogin/ruby-saml/version.rb +++ b/lib/onelogin/ruby-saml/version.rb @@ -2,6 +2,6 @@ module OneLogin module RubySaml - VERSION = '1.15.0' + VERSION = '2.0.0' end end diff --git a/ruby-saml.gemspec b/ruby-saml.gemspec index f7460fc40..5383b56d2 100644 --- a/ruby-saml.gemspec +++ b/ruby-saml.gemspec @@ -20,7 +20,7 @@ Gem::Specification.new do |s| s.rdoc_options = ["--charset=UTF-8"] s.require_paths = ["lib"] s.rubygems_version = %q{1.3.7} - s.required_ruby_version = '>= 2.6.0' + s.required_ruby_version = '>= 3.0' s.summary = %q{SAML Ruby Tookit} s.add_dependency('nokogiri', '>= 1.13.10') diff --git a/test/helpers/certificate_helper.rb b/test/helpers/certificate_helper.rb new file mode 100644 index 000000000..e826fbfb5 --- /dev/null +++ b/test/helpers/certificate_helper.rb @@ -0,0 +1,44 @@ +require 'openssl' + +module CertificateHelper + extend self + + def generate_pair(not_before: nil, not_after: nil) + key = generate_key + cert = generate_cert(key, not_before: not_before, not_after: not_after) + [cert, key] + end + + def generate_pair_hash(not_before: nil, not_after: nil) + cert, key = generate_pair(not_before: not_before, not_after: not_after) + { certificate: cert.to_pem, private_key: key.to_pem } + end + + def generate_key + OpenSSL::PKey::RSA.new(1024) + end + + def generate_cert(key = generate_key, not_before: nil, not_after: nil) + cert = OpenSSL::X509::Certificate.new + cert.version = 2 + cert.serial = 0 + cert.not_before = not_before || Time.now - one_year + cert.not_after = not_after || Time.now + one_year + cert.public_key = key.public_key + cert.subject = OpenSSL::X509::Name.parse "/DC=org/DC=ruby-saml/CN=Ruby SAML CA" + cert.issuer = cert.subject # self-signed + factory = OpenSSL::X509::ExtensionFactory.new + factory.subject_certificate = cert + factory.issuer_certificate = cert + cert.add_extension factory.create_extension("basicConstraints","CA:TRUE", true) + cert.add_extension factory.create_extension("keyUsage","keyCertSign, cRLSign", true) + cert.sign(key, OpenSSL::Digest::SHA1.new) + cert + end + + private + + def one_year + 3600 * 24 * 365 + end +end diff --git a/test/logout_requests/slo_request_encrypted_nameid.xml b/test/logout_requests/slo_request_encrypted_nameid.xml new file mode 100644 index 000000000..f9b185e77 --- /dev/null +++ b/test/logout_requests/slo_request_encrypted_nameid.xml @@ -0,0 +1,6 @@ + + https://app.onelogin.com/saml/metadata/SOMEACCOUNT + L99BsKQL2iq5chjY+wRj6AH3jYxv9L4tscPJaDdsPWvPG47toC903oxEhjd1p9EMWkSPqD/HclvRhjcNVmhfUa3clTMM5PpZS1+oin2cDNFgKDkEaCXsGRgfn44uUKbEfUHNaljC72qh0lBLnoJe7ZkJHbFMbsA8Cd4UBtHzp4Y= + +dLZt52QiV39ltBeRNUev0jlD9ReI7lM3EDgfktPgKeIs6bvsLz9feWhlnydd+NjbwXTsBQjEhm80/O8szYZZZpQB3H+khA76HJoFeDdhDgnVMqeXVWVkeSjcDFHg6TPLPyydSNcsBPBOqP093xCF7je0PUgkK45cj6aN/hs7TckxCbeuOv/klz6jxc24TyNoGg3Z1TA/HlS2ePVY77LhQgqhsZIL52LTG3BjAHVvpzSXyuYbeR5OeiYIM028Xyl + + \ No newline at end of file diff --git a/test/logoutrequest_test.rb b/test/logoutrequest_test.rb index 69e8839a5..71091953a 100644 --- a/test/logoutrequest_test.rb +++ b/test/logoutrequest_test.rb @@ -110,7 +110,6 @@ class RequestTest < Minitest::Test end describe "signing with HTTP-POST binding" do - before do settings.security[:logout_requests_signed] = true settings.idp_slo_service_binding = :post @@ -153,7 +152,7 @@ class RequestTest < Minitest::Test assert_match %r[], inflated end - it "created a signed logout request" do + it "create a signed logout request" do settings.compress_request = true unauth_req = OneLogin::RubySaml::Logoutrequest.new @@ -165,6 +164,17 @@ class RequestTest < Minitest::Test assert_match %r[], inflated end + it "create an uncompressed signed logout request" do + settings.compress_request = false + + params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings) + request_xml = Base64.decode64(params["SAMLRequest"]) + + assert_match %r[([a-zA-Z0-9/+=]+)], request_xml + assert_match %r[], request_xml + assert_match %r[], request_xml + end + it "create a signed logout request with 256 digest and signature method" do settings.compress_request = false settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA256 @@ -172,7 +182,6 @@ class RequestTest < Minitest::Test params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings) request_xml = Base64.decode64(params["SAMLRequest"]) - assert_match %r[([a-zA-Z0-9/+=]+)], request_xml assert_match %r[], request_xml assert_match %r[], request_xml @@ -190,6 +199,53 @@ class RequestTest < Minitest::Test assert_match %r[], request_xml assert_match %r[], request_xml end + + it "create a signed logout request using the first certificate and key" do + settings.compress_request = false + settings.certificate = nil + settings.private_key = nil + settings.sp_cert_multi = { + signing: [ + { certificate: ruby_saml_cert_text, private_key: ruby_saml_key_text }, + CertificateHelper.generate_pair_hash + ] + } + + params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings) + request_xml = Base64.decode64(params["SAMLRequest"]) + + assert_match %r[([a-zA-Z0-9/+=]+)], request_xml + assert_match %r[], request_xml + assert_match %r[], request_xml + end + + it "create a signed logout request using the first valid certificate and key when :check_sp_cert_expiration is true" do + settings.compress_request = false + settings.certificate = nil + settings.private_key = nil + settings.security[:check_sp_cert_expiration] = true + settings.sp_cert_multi = { + signing: [ + { certificate: ruby_saml_cert_text, private_key: ruby_saml_key_text }, + CertificateHelper.generate_pair_hash + ] + } + + params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings) + request_xml = Base64.decode64(params["SAMLRequest"]) + + assert_match %r[([a-zA-Z0-9/+=]+)], request_xml + assert_match %r[], request_xml + assert_match %r[], request_xml + end + + it "raises error when no valid certs and :check_sp_cert_expiration is true" do + settings.security[:check_sp_cert_expiration] = true + + assert_raises(OneLogin::RubySaml::ValidationError, 'The SP certificate expired.') do + OneLogin::RubySaml::Logoutrequest.new.create_params(settings) + end + end end describe "signing with HTTP-Redirect binding" do @@ -269,6 +325,41 @@ class RequestTest < Minitest::Test assert_equal signature_algorithm, OpenSSL::Digest::SHA512 assert cert.public_key.verify(signature_algorithm.new, Base64.decode64(params['Signature']), query_string) end + + it "create a signature parameter using the first certificate and key" do + settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1 + settings.compress_request = false + settings.certificate = nil + settings.private_key = nil + settings.sp_cert_multi = { + signing: [ + { certificate: ruby_saml_cert_text, private_key: ruby_saml_key_text }, + CertificateHelper.generate_pair_hash + ] + } + + params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') + assert params['SAMLRequest'] + assert params[:RelayState] + assert params['Signature'] + assert_equal params['SigAlg'], XMLSecurity::Document::RSA_SHA1 + + query_string = "SAMLRequest=#{CGI.escape(params['SAMLRequest'])}" + query_string << "&RelayState=#{CGI.escape(params[:RelayState])}" + query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}" + + signature_algorithm = XMLSecurity::BaseDocument.new.algorithm(params['SigAlg']) + assert_equal signature_algorithm, OpenSSL::Digest::SHA1 + assert cert.public_key.verify(signature_algorithm.new, Base64.decode64(params['Signature']), query_string) + end + + it "raises error when no valid certs and :check_sp_cert_expiration is true" do + settings.security[:check_sp_cert_expiration] = true + + assert_raises(OneLogin::RubySaml::ValidationError, 'The SP certificate expired.') do + OneLogin::RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') + end + end end describe "DEPRECATED: signing with HTTP-POST binding via :embed_sign" do diff --git a/test/logoutresponse_test.rb b/test/logoutresponse_test.rb index 57025d2a4..d38541072 100644 --- a/test/logoutresponse_test.rb +++ b/test/logoutresponse_test.rb @@ -315,7 +315,7 @@ class RubySamlTest < Minitest::Test assert_equal(CGI.unescape(query), CGI.unescape(original_query)) # Make normalised signature based on our modified params. sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method]) - signature = settings.get_sp_key.sign(sign_algorithm.new, query) + signature = settings.get_sp_signing_key.sign(sign_algorithm.new, query) params['Signature'] = Base64.encode64(signature).gsub(/\n/, "") # Re-create the Logoutresponse based on these modified parameters, # and ask it to validate the signature. It will do it incorrectly, @@ -351,7 +351,7 @@ class RubySamlTest < Minitest::Test assert_equal(CGI.unescape(query), CGI.unescape(original_query)) # Make normalised signature based on our modified params. sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method]) - signature = settings.get_sp_key.sign(sign_algorithm.new, query) + signature = settings.get_sp_signing_key.sign(sign_algorithm.new, query) params['Signature'] = Base64.encode64(signature).gsub(/\n/, "") # Re-create the Logoutresponse based on these modified parameters, # and ask it to validate the signature. Provide the altered parameter diff --git a/test/metadata_test.rb b/test/metadata_test.rb index 3de585f4d..e09415a53 100644 --- a/test/metadata_test.rb +++ b/test/metadata_test.rb @@ -217,17 +217,42 @@ class MetadataTest < Minitest::Test it "generates Service Provider Metadata with X509Certificate for encrypt" do assert_equal 4, key_descriptors.length assert_equal "signing", key_descriptors[0].attribute("use").value - assert_equal "encryption", key_descriptors[1].attribute("use").value - assert_equal "signing", key_descriptors[2].attribute("use").value + assert_equal "signing", key_descriptors[1].attribute("use").value + assert_equal "encryption", key_descriptors[2].attribute("use").value assert_equal "encryption", key_descriptors[3].attribute("use").value assert_equal 4, cert_nodes.length - assert_equal cert_nodes[0].text, cert_nodes[1].text - assert_equal cert_nodes[2].text, cert_nodes[3].text + assert_equal cert_nodes[0].text, cert_nodes[2].text + assert_equal cert_nodes[1].text, cert_nodes[3].text assert validate_xml!(xml_text, "saml-schema-metadata-2.0.xsd") end end + describe "with check_sp_cert_expiration and expired keys" do + before do + settings.security[:want_assertions_encrypted] = true + settings.security[:check_sp_cert_expiration] = true + valid_pair = CertificateHelper.generate_pair_hash + early_pair = CertificateHelper.generate_pair_hash(not_before: Time.now + 60) + expired_pair = CertificateHelper.generate_pair_hash(not_after: Time.now - 60) + settings.certificate = nil + settings.certificate_new = nil + settings.private_key = nil + settings.sp_cert_multi = { + signing: [valid_pair, early_pair, expired_pair], + encryption: [valid_pair, early_pair, expired_pair] + } + end + + it "generates Service Provider Metadata with X509Certificate for encrypt" do + assert_equal 2, key_descriptors.length + assert_equal "signing", key_descriptors[0].attribute("use").value + assert_equal "encryption", key_descriptors[1].attribute("use").value + + assert_equal 2, cert_nodes.length + assert validate_xml!(xml_text, "saml-schema-metadata-2.0.xsd") + end + end end describe "when attribute service is configured with multiple attribute values" do diff --git a/test/request_test.rb b/test/request_test.rb index b7f4a2513..cdc7c291f 100644 --- a/test/request_test.rb +++ b/test/request_test.rb @@ -27,6 +27,8 @@ class RequestTest < Minitest::Test end it "create the deflated SAMLRequest URL parameter including the Destination" do + skip "This test fails on this specific JRuby version" if defined?(JRUBY_VERSION) && JRUBY_VERSION == "9.2.17.0" + auth_url = OneLogin::RubySaml::Authrequest.new.create(settings) payload = CGI.unescape(auth_url.split("=").last) decoded = Base64.decode64(payload) @@ -266,6 +268,49 @@ class RequestTest < Minitest::Test assert_match %r[], request_xml assert_match %r[], request_xml end + + it "creates a signed request using the first certificate and key" do + settings.certificate = nil + settings.private_key = nil + settings.sp_cert_multi = { + signing: [ + { certificate: ruby_saml_cert_text, private_key: ruby_saml_key_text }, + CertificateHelper.generate_pair_hash + ] + } + + params = OneLogin::RubySaml::Authrequest.new.create_params(settings) + + request_xml = Base64.decode64(params["SAMLRequest"]) + assert_match %r[([a-zA-Z0-9/+=]+)], request_xml + assert_match %r[], request_xml + end + + it "creates a signed request using the first valid certificate and key when :check_sp_cert_expiration is true" do + settings.certificate = nil + settings.private_key = nil + settings.security[:check_sp_cert_expiration] = true + settings.sp_cert_multi = { + signing: [ + { certificate: ruby_saml_cert_text, private_key: ruby_saml_key_text }, + CertificateHelper.generate_pair_hash + ] + } + + params = OneLogin::RubySaml::Authrequest.new.create_params(settings) + + request_xml = Base64.decode64(params["SAMLRequest"]) + assert_match %r[([a-zA-Z0-9/+=]+)], request_xml + assert_match %r[], request_xml + end + + it "raises error when no valid certs and :check_sp_cert_expiration is true" do + settings.security[:check_sp_cert_expiration] = true + + assert_raises(OneLogin::RubySaml::ValidationError, 'The SP certificate expired.') do + OneLogin::RubySaml::Authrequest.new.create_params(settings) + end + end end describe "#create_params signing with HTTP-Redirect binding" do @@ -296,7 +341,6 @@ class RequestTest < Minitest::Test signature_algorithm = XMLSecurity::BaseDocument.new.algorithm(params['SigAlg']) assert_equal signature_algorithm, OpenSSL::Digest::SHA1 - assert cert.public_key.verify(signature_algorithm.new, Base64.decode64(params['Signature']), query_string) end @@ -315,6 +359,41 @@ class RequestTest < Minitest::Test assert_equal signature_algorithm, OpenSSL::Digest::SHA256 assert cert.public_key.verify(signature_algorithm.new, Base64.decode64(params['Signature']), query_string) end + + it "create a signature parameter using the first certificate and key" do + settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1 + settings.compress_request = false + settings.certificate = nil + settings.private_key = nil + settings.sp_cert_multi = { + signing: [ + { certificate: ruby_saml_cert_text, private_key: ruby_saml_key_text }, + CertificateHelper.generate_pair_hash + ] + } + + params = OneLogin::RubySaml::Authrequest.new.create_params(settings, :RelayState => 'http://example.com') + assert params['SAMLRequest'] + assert params[:RelayState] + assert params['Signature'] + assert_equal params['SigAlg'], XMLSecurity::Document::RSA_SHA1 + + query_string = "SAMLRequest=#{CGI.escape(params['SAMLRequest'])}" + query_string << "&RelayState=#{CGI.escape(params[:RelayState])}" + query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}" + + signature_algorithm = XMLSecurity::BaseDocument.new.algorithm(params['SigAlg']) + assert_equal signature_algorithm, OpenSSL::Digest::SHA1 + assert cert.public_key.verify(signature_algorithm.new, Base64.decode64(params['Signature']), query_string) + end + + it "raises error when no valid certs and :check_sp_cert_expiration is true" do + settings.security[:check_sp_cert_expiration] = true + + assert_raises(OneLogin::RubySaml::ValidationError, 'The SP certificate expired.') do + OneLogin::RubySaml::Authrequest.new.create_params(settings, :RelayState => 'http://example.com') + end + end end it "create the saml:AuthnContextClassRef element correctly" do diff --git a/test/response_test.rb b/test/response_test.rb index 5c6e10b59..4a7d0874c 100644 --- a/test/response_test.rb +++ b/test/response_test.rb @@ -1374,7 +1374,7 @@ def generate_audience_error(expected, actual) end describe '#sign_document' do - it 'Sign an unsigned SAML Response XML and initiate the SAML object with it' do + it 'sign an unsigned SAML Response XML and initiate the SAML object with it' do xml = Base64.decode64(fixture("test_sign.xml")) document = XMLSecurity::Document.new(xml) @@ -1404,11 +1404,9 @@ def generate_audience_error(expected, actual) @no_signed_assertion = OneLogin::RubySaml::Response.new(response_document_valid_signed, :settings => settings) end - it 'returns false if :want_assertion_signed enabled and Assertion not signed' do assert !@no_signed_assertion.send(:validate_signed_elements) assert_includes @no_signed_assertion.errors, "The Assertion of the Response is not signed and the SP requires it" - end it 'returns true if :want_assertion_signed enabled and Assertion is signed' do @@ -1568,6 +1566,14 @@ def generate_audience_error(expected, actual) end end + it "is not possible to decrypt the assertion if private key has expired and :check_sp_expiration is true" do + settings.certificate = ruby_saml_cert_text + settings.security[:check_sp_cert_expiration] = true + assert_raises(OneLogin::RubySaml::ValidationError, "The SP certificate expired.") do + OneLogin::RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings) + end + end + it "is possible to decrypt the assertion if private key" do response = OneLogin::RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings) @@ -1587,6 +1593,26 @@ def generate_audience_error(expected, actual) assert decrypted.name, "Assertion" end + it "is possible to decrypt the assertion with one invalid and one valid private key" do + settings.private_key = nil + settings.sp_cert_multi = { + encryption: [ + CertificateHelper.generate_pair_hash, + { certificate: ruby_saml_cert_text, private_key: ruby_saml_key_text } + ] + } + response = OneLogin::RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings) + + encrypted_assertion_node = REXML::XPath.first( + response.document, + "(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)", + { "p" => "urn:oasis:names:tc:SAML:2.0:protocol", "a" => "urn:oasis:names:tc:SAML:2.0:assertion" } + ) + decrypted = response.send(:decrypt_assertion, encrypted_assertion_node) + + assert decrypted.name, "Assertion" + end + it "is possible to decrypt the assertion if private key provided and EncryptedKey RetrievalMethod presents in response" do settings.private_key = ruby_saml_key_text resp = read_response('response_with_retrieval_method.xml') diff --git a/test/settings_test.rb b/test/settings_test.rb index 790b72056..60672130c 100644 --- a/test/settings_test.rb +++ b/test/settings_test.rb @@ -18,7 +18,7 @@ class SettingsTest < Minitest::Test :sp_name_qualifier, :name_identifier_format, :name_identifier_value, :name_identifier_value_requested, :sessionindex, :attributes_index, :passive, :force_authn, :compress_request, :double_quote_xml_attribute_values, :message_max_bytesize, - :security, :certificate, :private_key, + :security, :certificate, :private_key, :certificate_new, :sp_cert_multi, :authn_context, :authn_context_comparison, :authn_context_decl_ref, :assertion_consumer_logout_service_url ] @@ -382,7 +382,6 @@ class SettingsTest < Minitest::Test @settings.get_sp_cert_new } end - end describe "#get_sp_key" do @@ -408,7 +407,6 @@ class SettingsTest < Minitest::Test @settings.get_sp_key } end - end describe "#get_fingerprint" do @@ -440,5 +438,302 @@ class SettingsTest < Minitest::Test assert fingerprint.downcase == ruby_saml_cert_fingerprint.downcase end end + + describe "#get_sp_certs (base cases)" do + let(:cert_text1) { ruby_saml_cert_text } + let(:cert_text2) { ruby_saml_cert2.to_pem } + let(:cert_text3) { CertificateHelper.generate_cert.to_pem } + let(:key_text1) { ruby_saml_key_text } + let(:key_text2) { CertificateHelper.generate_key.to_pem } + + it "returns certs for single case" do + @settings.certificate = cert_text1 + @settings.private_key = key_text1 + + actual = @settings.get_sp_certs + expected = [[cert_text1, key_text1]] + assert_equal [:signing, :encryption], actual.keys + assert_equal expected, actual[:signing].map {|ary| ary.map(&:to_pem) } + assert_equal expected, actual[:encryption].map {|ary| ary.map(&:to_pem) } + end + + it "returns certs for single case with new cert" do + @settings.certificate = cert_text1 + @settings.certificate_new = cert_text2 + @settings.private_key = key_text1 + + actual = @settings.get_sp_certs + expected = [[cert_text1, key_text1], [cert_text2, key_text1]] + assert_equal [:signing, :encryption], actual.keys + assert_equal expected, actual[:signing].map {|ary| ary.map(&:to_pem) } + assert_equal expected, actual[:encryption].map {|ary| ary.map(&:to_pem) } + end + + it "returns certs for multi case" do + @settings.sp_cert_multi = { + signing: [{ certificate: cert_text1, private_key: key_text1 }, + { certificate: cert_text2, private_key: key_text1 }], + encryption: [{ certificate: cert_text2, private_key: key_text1 }, + { certificate: cert_text3, private_key: key_text2 }] + } + + actual = @settings.get_sp_certs + expected_signing = [[cert_text1, key_text1], [cert_text2, key_text1]] + expected_encryption = [[cert_text2, key_text1], [cert_text3, key_text2]] + assert_equal [:signing, :encryption], actual.keys + assert_equal expected_signing, actual[:signing].map {|ary| ary.map(&:to_pem) } + assert_equal expected_encryption, actual[:encryption].map {|ary| ary.map(&:to_pem) } + end + + it "sp_cert_multi allows sending only signing" do + @settings.sp_cert_multi = { + signing: [{ certificate: cert_text1, private_key: key_text1 }, + { certificate: cert_text2, private_key: key_text1 }] + } + + actual = @settings.get_sp_certs + expected_signing = [[cert_text1, key_text1], [cert_text2, key_text1]] + assert_equal [:signing, :encryption], actual.keys + assert_equal expected_signing, actual[:signing].map {|ary| ary.map(&:to_pem) } + assert_equal [], actual[:encryption] + end + + it "raises error when sp_cert_multi is not a Hash" do + @settings.sp_cert_multi = 'invalid_type' + + error_message = 'sp_cert_multi must be a Hash' + assert_raises ArgumentError, error_message do + @settings.get_sp_certs + end + end + + it "raises error when sp_cert_multi does not contain an Array of Hashes" do + @settings.sp_cert_multi = { signing: 'invalid_type' } + + error_message = 'sp_cert_multi :signing node must be an Array of Hashes' + assert_raises ArgumentError, error_message do + @settings.get_sp_certs + end + end + + it "raises error when sp_cert_multi inner node missing :certificate" do + @settings.sp_cert_multi = { signing: [{ private_key: key_text1 }] } + + error_message = 'sp_cert_multi :signing node Hashes must specify keys :certificate and :private_key' + assert_raises ArgumentError, error_message do + @settings.get_sp_certs + end + end + + it "raises error when sp_cert_multi inner node missing :private_key" do + @settings.sp_cert_multi = { signing: [{ certificate: cert_text1 }] } + + error_message = 'sp_cert_multi :signing node Hashes must specify keys :certificate and :private_key' + assert_raises ArgumentError, error_message do + @settings.get_sp_certs + end + end + + it "handles sp_cert_multi with string keys" do + @settings.sp_cert_multi = { + 'signing' => [{ 'certificate' => cert_text1, 'private_key' => key_text1 }], + 'encryption' => [{ 'certificate' => cert_text2, 'private_key' => key_text1 }] + } + + actual = @settings.get_sp_certs + expected_signing = [[cert_text1, key_text1]] + expected_encryption = [[cert_text2, key_text1]] + assert_equal [:signing, :encryption], actual.keys + assert_equal expected_signing, actual[:signing].map {|ary| ary.map(&:to_pem) } + assert_equal expected_encryption, actual[:encryption].map {|ary| ary.map(&:to_pem) } + end + + it "handles sp_cert_multi with alternate inner keys :cert and :key" do + @settings.sp_cert_multi = { + signing: [{ cert: cert_text1, key: key_text1 }], + encryption: [{ 'cert' => cert_text2, 'key' => key_text1 }] + } + + actual = @settings.get_sp_certs + expected_signing = [[cert_text1, key_text1]] + expected_encryption = [[cert_text2, key_text1]] + assert_equal [:signing, :encryption], actual.keys + assert_equal expected_signing, actual[:signing].map {|ary| ary.map(&:to_pem) } + assert_equal expected_encryption, actual[:encryption].map {|ary| ary.map(&:to_pem) } + end + + it "raises error when both sp_cert_multi and certificate are specified" do + @settings.sp_cert_multi = { signing: [{ certificate: cert_text1, private_key: key_text1 }] } + @settings.certificate = cert_text1 + + error_message = 'Cannot specify both sp_cert_multi and certificate, certificate_new, private_key parameters' + assert_raises ArgumentError, error_message do + @settings.get_sp_certs + end + end + + it "raises error when both sp_cert_multi and certificate_new are specified" do + @settings.sp_cert_multi = { signing: [{ certificate: cert_text1, private_key: key_text1 }] } + @settings.certificate_new = cert_text2 + + error_message = 'Cannot specify both sp_cert_multi and certificate, certificate_new, private_key parameters' + assert_raises ArgumentError, error_message do + @settings.get_sp_certs + end + end + + it "raises error when both sp_cert_multi and private_key are specified" do + @settings.sp_cert_multi = { signing: [{ certificate: cert_text1, private_key: key_text1 }] } + @settings.private_key = key_text1 + + error_message = 'Cannot specify both sp_cert_multi and certificate, certificate_new, private_key parameters' + assert_raises ArgumentError, error_message do + @settings.get_sp_certs + end + end + end + + describe "#get_sp_certs" do + let(:valid_pair) { CertificateHelper.generate_pair_hash } + let(:early_pair) { CertificateHelper.generate_pair_hash(not_before: Time.now + 60) } + let(:expired_pair) { CertificateHelper.generate_pair_hash(not_after: Time.now - 60) } + + it "returns all certs when check_sp_cert_expiration is false" do + @settings.security = { check_sp_cert_expiration: false } + @settings.sp_cert_multi = { signing: [valid_pair, expired_pair], encryption: [valid_pair, early_pair] } + + actual = @settings.get_sp_certs + expected_signing = [valid_pair, expired_pair].map(&:values) + expected_encryption = [valid_pair, early_pair].map(&:values) + assert_equal expected_signing, actual[:signing].map {|ary| ary.map(&:to_pem) } + assert_equal expected_encryption, actual[:encryption].map {|ary| ary.map(&:to_pem) } + end + + it "returns only active certs when check_sp_cert_expiration is true" do + @settings.security = { check_sp_cert_expiration: true } + @settings.sp_cert_multi = { signing: [valid_pair, expired_pair], encryption: [valid_pair, early_pair] } + + actual = @settings.get_sp_certs + expected_active = [valid_pair].map(&:values) + assert_equal expected_active, actual[:signing].map {|ary| ary.map(&:to_pem) } + assert_equal expected_active, actual[:encryption].map {|ary| ary.map(&:to_pem) } + end + + it "raises error when all certificates are expired in signing and check_sp_cert_expiration is true" do + @settings.security = { check_sp_cert_expiration: true } + @settings.sp_cert_multi = { signing: [expired_pair], encryption: [valid_pair] } + + assert_raises OneLogin::RubySaml::ValidationError do + @settings.get_sp_certs + end + end + + it "raises error when all certificates are expired in encryption and check_sp_cert_expiration is true" do + @settings.security = { check_sp_cert_expiration: true } + @settings.sp_cert_multi = { signing: [valid_pair], encryption: [expired_pair] } + + assert_raises OneLogin::RubySaml::ValidationError do + @settings.get_sp_certs + end + end + + it "returns empty arrays for signing and encryption if no pairs are present" do + @settings.sp_cert_multi = { signing: [], encryption: [] } + + actual = @settings.get_sp_certs + assert_empty actual[:signing] + assert_empty actual[:encryption] + end + end + + describe "#get_sp_signing_pair and #get_sp_signing_key" do + let(:valid_pair) { CertificateHelper.generate_pair_hash } + let(:early_pair) { CertificateHelper.generate_pair_hash(not_before: Time.now + 60) } + let(:expired) { CertificateHelper.generate_pair_hash(not_after: Time.now - 60) } + + it "returns nil when no signing pairs are present" do + @settings.sp_cert_multi = { signing: [] } + + assert_nil @settings.get_sp_signing_pair + assert_nil @settings.get_sp_signing_key + end + + it "returns the first pair if check_sp_cert_expiration is false" do + @settings.security = { check_sp_cert_expiration: false } + @settings.sp_cert_multi = { signing: [early_pair, expired, valid_pair] } + + assert_equal early_pair.values, @settings.get_sp_signing_pair.map(&:to_pem) + assert_equal early_pair[:private_key], @settings.get_sp_signing_key.to_pem + end + + it "returns the first active pair when check_sp_cert_expiration is true" do + @settings.security = { check_sp_cert_expiration: true } + @settings.sp_cert_multi = { signing: [early_pair, expired, valid_pair] } + + assert_equal valid_pair.values, @settings.get_sp_signing_pair.map(&:to_pem) + assert_equal valid_pair[:private_key], @settings.get_sp_signing_key.to_pem + end + + it "raises error when all certificates are expired and check_sp_cert_expiration is true" do + @settings.security = { check_sp_cert_expiration: true } + @settings.sp_cert_multi = { signing: [early_pair, expired] } + + assert_raises OneLogin::RubySaml::ValidationError do + @settings.get_sp_signing_pair + end + + assert_raises OneLogin::RubySaml::ValidationError do + @settings.get_sp_signing_key + end + end + end + + describe "#get_sp_decryption_keys" do + let(:valid_pair) { CertificateHelper.generate_pair_hash } + let(:early_pair) { CertificateHelper.generate_pair_hash(not_before: Time.now + 60) } + let(:expired_pair) { CertificateHelper.generate_pair_hash(not_after: Time.now - 60) } + + it "returns an empty array when no decryption pairs are present" do + @settings.sp_cert_multi = { encryption: [] } + + assert_empty @settings.get_sp_decryption_keys + end + + it "returns all keys when check_sp_cert_expiration is false" do + @settings.security = { check_sp_cert_expiration: false } + @settings.sp_cert_multi = { encryption: [early_pair, expired_pair, valid_pair] } + + expected_keys = [early_pair, expired_pair, valid_pair].map { |pair| pair[:private_key] } + actual_keys = @settings.get_sp_decryption_keys.map(&:to_pem) + assert_equal expected_keys, actual_keys + end + + it "returns only keys of active certificates when check_sp_cert_expiration is true" do + @settings.security = { check_sp_cert_expiration: true } + @settings.sp_cert_multi = { encryption: [early_pair, expired_pair, valid_pair] } + + expected_keys = [valid_pair[:private_key]] + actual_keys = @settings.get_sp_decryption_keys.map(&:to_pem) + assert_equal expected_keys, actual_keys + end + + it "raises error when all certificates are expired and check_sp_cert_expiration is true" do + @settings.security = { check_sp_cert_expiration: true } + @settings.sp_cert_multi = { encryption: [early_pair, expired_pair] } + + assert_raises OneLogin::RubySaml::ValidationError do + @settings.get_sp_decryption_keys + end + end + + it "removes duplicates" do + @settings.sp_cert_multi = { encryption: [early_pair, valid_pair, early_pair, valid_pair] } + + expected_keys = [early_pair, valid_pair].map { |pair| pair[:private_key] } + actual_keys = @settings.get_sp_decryption_keys.map(&:to_pem) + + assert_equal expected_keys, actual_keys + end + end end end diff --git a/test/slo_logoutrequest_test.rb b/test/slo_logoutrequest_test.rb index b5d4a3fd5..f8fca39ef 100644 --- a/test/slo_logoutrequest_test.rb +++ b/test/slo_logoutrequest_test.rb @@ -10,6 +10,7 @@ class RubySamlTest < Minitest::Test let(:settings) { OneLogin::RubySaml::Settings.new } let(:logout_request) { OneLogin::RubySaml::SloLogoutrequest.new(logout_request_document) } + let(:logout_request_encrypted_nameid) { OneLogin::RubySaml::SloLogoutrequest.new(logout_request_encrypted_nameid_document) } let(:invalid_logout_request) { OneLogin::RubySaml::SloLogoutrequest.new(invalid_logout_request_document) } before do @@ -87,6 +88,18 @@ class RubySamlTest < Minitest::Test it "extract the value of the name id element" do assert_equal "someone@example.org", logout_request.nameid end + + it 'is not possible when encryptID but no private key' do + assert_raises(OneLogin::RubySaml::ValidationError, "An EncryptedID found and no SP private key found on the settings to decrypt it") do + assert_equal "someone@example.org", logout_request_encrypted_nameid.nameid + end + end + + it "extract the value of the name id element inside an EncryptedId" do + settings.private_key = ruby_saml_key_text + logout_request_encrypted_nameid.settings = settings + assert_equal "someone@example.org", logout_request_encrypted_nameid.nameid + end end describe "#nameid_format" do @@ -95,6 +108,18 @@ class RubySamlTest < Minitest::Test it "extract the format attribute of the name id element" do assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", logout_request.nameid_format end + + it 'is not possible when encryptID but no private key' do + assert_raises(OneLogin::RubySaml::ValidationError, "An EncryptedID found and no SP private key found on the settings to decrypt it") do + assert_equal "someone@example.org", logout_request_encrypted_nameid.nameid + end + end + + it "extract the format attribute of the name id element" do + settings.private_key = ruby_saml_key_text + logout_request_encrypted_nameid.settings = settings + assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", logout_request_encrypted_nameid.nameid_format + end end describe "#issuer" do @@ -256,11 +281,13 @@ class RubySamlTest < Minitest::Test logout_request.settings.idp_entity_id = 'https://app.onelogin.com/saml/metadata/SOMEACCOUNT' assert logout_request.send(:validate_issuer) end + it "return false when the issuer of the Logout Request does not match the IdP entityId" do logout_request.settings.idp_entity_id = 'http://idp.example.com/invalid' assert !logout_request.send(:validate_issuer) assert_includes logout_request.errors, "Doesn't match the issuer, expected: <#{logout_request.settings.idp_entity_id}>, but was: " end + it "raise when the issuer of the Logout Request does not match the IdP entityId" do logout_request.settings.idp_entity_id = 'http://idp.example.com/invalid' logout_request.soft = false @@ -375,7 +402,7 @@ class RubySamlTest < Minitest::Test assert_equal(CGI.unescape(query), CGI.unescape(original_query)) # Make normalised signature based on our modified params. sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method]) - signature = settings.get_sp_key.sign(sign_algorithm.new, query) + signature = settings.get_sp_signing_key.sign(sign_algorithm.new, query) params['Signature'] = Base64.encode64(signature).gsub(/\n/, "") # Construct SloLogoutrequest and ask it to validate the signature. # It will do it incorrectly, because it will compute it based on re-encoded @@ -410,7 +437,7 @@ class RubySamlTest < Minitest::Test assert_equal(CGI.unescape(query), CGI.unescape(original_query)) # Make normalised signature based on our modified params. sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method]) - signature = settings.get_sp_key.sign(sign_algorithm.new, query) + signature = settings.get_sp_signing_key.sign(sign_algorithm.new, query) params['Signature'] = Base64.encode64(signature).gsub(/\n/, "") # Construct SloLogoutrequest and ask it to validate the signature. # Provide the altered parameter in its raw URI-encoded form, @@ -449,7 +476,7 @@ class RubySamlTest < Minitest::Test sign_algorithm = XMLSecurity::BaseDocument.new.algorithm( settings.security[:signature_method] ) - signature = settings.get_sp_key.sign(sign_algorithm.new, query) + signature = settings.get_sp_signing_key.sign(sign_algorithm.new, query) params['Signature'] = downcased_escape(Base64.encode64(signature).gsub(/\n/, "")) # Then parameters are usually unescaped, like we manage them in rails @@ -527,7 +554,6 @@ class RubySamlTest < Minitest::Test logout_request_sign_test.settings = settings assert !logout_request_sign_test.send(:validate_signature) assert_includes logout_request_sign_test.errors, "Invalid Signature on Logout Request" - end end end diff --git a/test/slo_logoutresponse_test.rb b/test/slo_logoutresponse_test.rb index 04a9a4b09..a1696d24d 100644 --- a/test/slo_logoutresponse_test.rb +++ b/test/slo_logoutresponse_test.rb @@ -99,7 +99,6 @@ class SloLogoutresponseTest < Minitest::Test end describe "signing with HTTP-POST binding" do - before do settings.idp_sso_service_binding = :redirect settings.idp_slo_service_binding = :post @@ -143,6 +142,7 @@ class SloLogoutresponseTest < Minitest::Test it "create a signed logout response" do logout_request.settings = settings + params = OneLogin::RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message") response_xml = Base64.decode64(params["SAMLResponse"]) @@ -154,6 +154,7 @@ class SloLogoutresponseTest < Minitest::Test it "create a signed logout response with 256 digest and signature methods" do settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA256 settings.security[:digest_method] = XMLSecurity::Document::SHA256 + logout_request.settings = settings params = OneLogin::RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message") @@ -175,6 +176,54 @@ class SloLogoutresponseTest < Minitest::Test assert_match(//, response_xml) assert_match(//, response_xml) end + + it "create a signed logout response using the first certificate and key" do + settings.certificate = nil + settings.private_key = nil + settings.sp_cert_multi = { + signing: [ + { certificate: ruby_saml_cert_text, private_key: ruby_saml_key_text }, + CertificateHelper.generate_pair_hash + ] + } + logout_request.settings = settings + + params = OneLogin::RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message") + + response_xml = Base64.decode64(params["SAMLResponse"]) + assert_match %r[([a-zA-Z0-9/+=]+)], response_xml + assert_match(//, response_xml) + assert_match(//, response_xml) + end + + it "create a signed logout response using the first valid certificate and key when :check_sp_cert_expiration is true" do + settings.certificate = nil + settings.private_key = nil + settings.security[:check_sp_cert_expiration] = true + settings.sp_cert_multi = { + signing: [ + { certificate: ruby_saml_cert_text, private_key: ruby_saml_key_text }, + CertificateHelper.generate_pair_hash + ] + } + logout_request.settings = settings + + params = OneLogin::RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message") + + response_xml = Base64.decode64(params["SAMLResponse"]) + assert_match %r[([a-zA-Z0-9/+=]+)], response_xml + assert_match(//, response_xml) + assert_match(//, response_xml) + end + + it "raises error when no valid certs and :check_sp_cert_expiration is true" do + settings.security[:check_sp_cert_expiration] = true + logout_request.settings = settings + + assert_raises(OneLogin::RubySaml::ValidationError, 'The SP certificate expired.') do + OneLogin::RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message") + end + end end describe "signing with HTTP-Redirect binding" do @@ -261,10 +310,44 @@ class SloLogoutresponseTest < Minitest::Test assert_equal signature_algorithm, OpenSSL::Digest::SHA512 assert cert.public_key.verify(signature_algorithm.new, Base64.decode64(params['Signature']), query_string) end + + it "create a signature parameter using the first certificate and key" do + settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1 + settings.compress_request = false + settings.certificate = nil + settings.private_key = nil + settings.sp_cert_multi = { + signing: [ + { certificate: ruby_saml_cert_text, private_key: ruby_saml_key_text }, + CertificateHelper.generate_pair_hash + ] + } + + params = OneLogin::RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message", :RelayState => 'http://example.com') + assert params['SAMLResponse'] + assert params[:RelayState] + assert params['Signature'] + assert_equal params['SigAlg'], XMLSecurity::Document::RSA_SHA1 + + query_string = "SAMLResponse=#{CGI.escape(params['SAMLResponse'])}" + query_string << "&RelayState=#{CGI.escape(params[:RelayState])}" + query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}" + + signature_algorithm = XMLSecurity::BaseDocument.new.algorithm(params['SigAlg']) + assert_equal signature_algorithm, OpenSSL::Digest::SHA1 + assert cert.public_key.verify(signature_algorithm.new, Base64.decode64(params['Signature']), query_string) + end + + it "raises error when no valid certs and :check_sp_cert_expiration is true" do + settings.security[:check_sp_cert_expiration] = true + + assert_raises(OneLogin::RubySaml::ValidationError, 'The SP certificate expired.') do + OneLogin::RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message", :RelayState => 'http://example.com') + end + end end describe "DEPRECATED: signing with HTTP-POST binding via :embed_sign" do - before do settings.compress_response = false settings.security[:logout_responses_signed] = true diff --git a/test/test_helper.rb b/test/test_helper.rb index 7b5679cc6..8fd82b762 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -19,6 +19,7 @@ require 'minitest/autorun' require 'mocha/minitest' require 'timecop' +Dir[File.expand_path('../helpers/**/*.rb', __FILE__)].each { |f| require f } Bundler.require :default, :test @@ -239,6 +240,15 @@ def logout_request_document_with_name_id_format @logout_request_document_with_name_id_format end + def logout_request_encrypted_nameid_document + unless @logout_request_encrypted_nameid_document + xml = read_logout_request("slo_request_encrypted_nameid.xml") + deflated = Zlib::Deflate.deflate(xml, 9)[2..-5] + @logout_request_encrypted_nameid_document = Base64.encode64(deflated) + end + @logout_request_encrypted_nameid_document + end + def logout_request_xml_with_session_index @logout_request_xml_with_session_index ||= File.read(File.join(File.dirname(__FILE__), 'logout_requests', 'slo_request_with_session_index.xml')) end diff --git a/test/utils_test.rb b/test/utils_test.rb index ade7d8850..0ab958abe 100644 --- a/test/utils_test.rb +++ b/test/utils_test.rb @@ -85,7 +85,6 @@ def result(duration, reference = 0) invalid_chained_certificate1 = read_certificate("invalid_chained_certificate1") assert_equal formatted_chained_certificate, OneLogin::RubySaml::Utils.format_cert(invalid_chained_certificate1) end - end describe ".format_private_key" do @@ -148,7 +147,49 @@ def result(duration, reference = 0) end end - describe "build_query" do + describe '.build_cert_object' do + it 'returns a certificate object for valid certificate string' do + cert_object = OneLogin::RubySaml::Utils.build_cert_object(ruby_saml_cert_text) + assert_instance_of OpenSSL::X509::Certificate, cert_object + end + + it 'returns nil for nil certificate string' do + assert_nil OneLogin::RubySaml::Utils.build_cert_object(nil) + end + + it 'returns nil for empty certificate string' do + assert_nil OneLogin::RubySaml::Utils.build_cert_object('') + end + + it 'raises error when given an invalid certificate string' do + assert_raises OpenSSL::X509::CertificateError do + OneLogin::RubySaml::Utils.build_cert_object('Foobar') + end + end + end + + describe '.build_private_key_object' do + it 'returns a private key object for valid private key string' do + private_key_object = OneLogin::RubySaml::Utils.build_private_key_object(ruby_saml_key_text) + assert_instance_of OpenSSL::PKey::RSA, private_key_object + end + + it 'returns nil for nil private key string' do + assert_nil OneLogin::RubySaml::Utils.build_private_key_object(nil) + end + + it 'returns nil for empty private key string' do + assert_nil OneLogin::RubySaml::Utils.build_private_key_object('') + end + + it 'raises error when given an invalid private key string' do + assert_raises OpenSSL::PKey::RSAError do + OneLogin::RubySaml::Utils.build_private_key_object('Foobar') + end + end + end + + describe ".build_query" do it "returns the query string" do params = {} params[:type] = "SAMLRequest" @@ -160,7 +201,7 @@ def result(duration, reference = 0) end end - describe "#verify_signature" do + describe ".verify_signature" do before do @params = {} @params[:cert] = ruby_saml_cert @@ -179,7 +220,7 @@ def result(duration, reference = 0) end end - describe "#status_error_msg" do + describe ".status_error_msg" do it "returns a error msg with status_code and status message" do error_msg = "The status code of the Logout Response was not Success" status_code = "urn:oasis:names:tc:SAML:2.0:status:Requester" @@ -214,7 +255,7 @@ def result(duration, reference = 0) end end - describe 'uri_match' do + describe '.uri_match?' do it 'matches two urls' do destination = 'http://www.example.com/test?var=stuff' settings = 'http://www.example.com/test?var=stuff' @@ -264,7 +305,7 @@ def result(duration, reference = 0) end end - describe 'element_text' do + describe '.element_text' do it 'returns the element text' do element = REXML::Document.new('element text').elements.first assert_equal 'element text', OneLogin::RubySaml::Utils.element_text(element) @@ -298,7 +339,117 @@ def result(duration, reference = 0) element = REXML::Document.new('').elements.first assert_equal '', OneLogin::RubySaml::Utils.element_text(element) end + end + end + + describe '.decrypt_multi' do + let(:private_key) { ruby_saml_key } + let(:invalid_key1) { CertificateHelper.generate_key } + let(:invalid_key2) { CertificateHelper.generate_key } + let(:settings) { OneLogin::RubySaml::Settings.new(:private_key => private_key.to_pem) } + let(:response) { OneLogin::RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings) } + let(:encrypted) do + REXML::XPath.first( + response.document, + "(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)", + { "p" => "urn:oasis:names:tc:SAML:2.0:protocol", "a" => "urn:oasis:names:tc:SAML:2.0:assertion" } + ) + end + + it 'successfully decrypts with the first private key' do + assert_match /\A