├── .github ├── CODEOWNERS └── workflows │ ├── ci.yml │ └── lint.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── examples ├── README.md ├── __init__.py ├── aiohttp_signer.py ├── curl_signer.py └── requests_signer.py ├── pyproject.toml ├── src └── aws_sdk_signers │ ├── __init__.py │ ├── _http.py │ ├── _identity.py │ ├── _io.py │ ├── _version.py │ ├── exceptions.py │ ├── interfaces │ ├── __init__.py │ ├── http.py │ ├── identity.py │ └── io.py │ ├── py.typed │ └── signers.py └── tests ├── __init__.py └── unit ├── __init__.py ├── auth ├── __init__.py ├── aws4_testsuite │ ├── LICENSE │ ├── NOTICE │ ├── get-header-key-duplicate │ │ ├── get-header-key-duplicate.authz │ │ ├── get-header-key-duplicate.creq │ │ ├── get-header-key-duplicate.req │ │ ├── get-header-key-duplicate.sreq │ │ └── get-header-key-duplicate.sts │ ├── get-header-value-multiline │ │ ├── get-header-value-multiline.authz │ │ ├── get-header-value-multiline.creq │ │ ├── get-header-value-multiline.req │ │ ├── get-header-value-multiline.sreq │ │ └── get-header-value-multiline.sts │ ├── get-header-value-order │ │ ├── get-header-value-order.authz │ │ ├── get-header-value-order.creq │ │ ├── get-header-value-order.req │ │ ├── get-header-value-order.sreq │ │ └── get-header-value-order.sts │ ├── get-header-value-trim │ │ ├── get-header-value-trim.authz │ │ ├── get-header-value-trim.creq │ │ ├── get-header-value-trim.req │ │ ├── get-header-value-trim.sreq │ │ └── get-header-value-trim.sts │ ├── get-unreserved │ │ ├── get-unreserved.authz │ │ ├── get-unreserved.creq │ │ ├── get-unreserved.req │ │ ├── get-unreserved.sreq │ │ └── get-unreserved.sts │ ├── get-utf8 │ │ ├── get-utf8.authz │ │ ├── get-utf8.creq │ │ ├── get-utf8.req │ │ ├── get-utf8.sreq │ │ └── get-utf8.sts │ ├── get-vanilla-empty-query-key │ │ ├── get-vanilla-empty-query-key.authz │ │ ├── get-vanilla-empty-query-key.creq │ │ ├── get-vanilla-empty-query-key.req │ │ ├── get-vanilla-empty-query-key.sreq │ │ └── get-vanilla-empty-query-key.sts │ ├── get-vanilla-query-order-encoded │ │ ├── get-vanilla-query-order-encoded.authz │ │ ├── get-vanilla-query-order-encoded.creq │ │ ├── get-vanilla-query-order-encoded.req │ │ ├── get-vanilla-query-order-encoded.sreq │ │ └── get-vanilla-query-order-encoded.sts │ ├── get-vanilla-query-order-key-case │ │ ├── get-vanilla-query-order-key-case.authz │ │ ├── get-vanilla-query-order-key-case.creq │ │ ├── get-vanilla-query-order-key-case.req │ │ ├── get-vanilla-query-order-key-case.sreq │ │ └── get-vanilla-query-order-key-case.sts │ ├── get-vanilla-query-order-key │ │ ├── get-vanilla-query-order-key.authz │ │ ├── get-vanilla-query-order-key.creq │ │ ├── get-vanilla-query-order-key.req │ │ ├── get-vanilla-query-order-key.sreq │ │ └── get-vanilla-query-order-key.sts │ ├── get-vanilla-query-order-value │ │ ├── get-vanilla-query-order-value.authz │ │ ├── get-vanilla-query-order-value.creq │ │ ├── get-vanilla-query-order-value.req │ │ ├── get-vanilla-query-order-value.sreq │ │ └── get-vanilla-query-order-value.sts │ ├── get-vanilla-query-unreserved │ │ ├── get-vanilla-query-unreserved.authz │ │ ├── get-vanilla-query-unreserved.creq │ │ ├── get-vanilla-query-unreserved.req │ │ ├── get-vanilla-query-unreserved.sreq │ │ └── get-vanilla-query-unreserved.sts │ ├── get-vanilla-query │ │ ├── get-vanilla-query.authz │ │ ├── get-vanilla-query.creq │ │ ├── get-vanilla-query.req │ │ ├── get-vanilla-query.sreq │ │ └── get-vanilla-query.sts │ ├── get-vanilla-utf8-query │ │ ├── get-vanilla-utf8-query.authz │ │ ├── get-vanilla-utf8-query.creq │ │ ├── get-vanilla-utf8-query.req │ │ ├── get-vanilla-utf8-query.sreq │ │ └── get-vanilla-utf8-query.sts │ ├── get-vanilla-with-session-token │ │ ├── get-vanilla-with-session-token.authz │ │ ├── get-vanilla-with-session-token.creq │ │ ├── get-vanilla-with-session-token.req │ │ ├── get-vanilla-with-session-token.sreq │ │ └── get-vanilla-with-session-token.sts │ ├── get-vanilla │ │ ├── get-vanilla.authz │ │ ├── get-vanilla.creq │ │ ├── get-vanilla.req │ │ ├── get-vanilla.sreq │ │ └── get-vanilla.sts │ ├── normalize-path │ │ ├── get-relative-relative │ │ │ ├── get-relative-relative.authz │ │ │ ├── get-relative-relative.creq │ │ │ ├── get-relative-relative.req │ │ │ ├── get-relative-relative.sreq │ │ │ └── get-relative-relative.sts │ │ ├── get-relative │ │ │ ├── get-relative.authz │ │ │ ├── get-relative.creq │ │ │ ├── get-relative.req │ │ │ ├── get-relative.sreq │ │ │ └── get-relative.sts │ │ ├── get-slash-dot-slash │ │ │ ├── get-slash-dot-slash.authz │ │ │ ├── get-slash-dot-slash.creq │ │ │ ├── get-slash-dot-slash.req │ │ │ ├── get-slash-dot-slash.sreq │ │ │ └── get-slash-dot-slash.sts │ │ ├── get-slash-pointless-dot │ │ │ ├── get-slash-pointless-dot.authz │ │ │ ├── get-slash-pointless-dot.creq │ │ │ ├── get-slash-pointless-dot.req │ │ │ ├── get-slash-pointless-dot.sreq │ │ │ └── get-slash-pointless-dot.sts │ │ ├── get-slash │ │ │ ├── get-slash.authz │ │ │ ├── get-slash.creq │ │ │ ├── get-slash.req │ │ │ ├── get-slash.sreq │ │ │ └── get-slash.sts │ │ ├── get-slashes │ │ │ ├── get-slashes.authz │ │ │ ├── get-slashes.creq │ │ │ ├── get-slashes.req │ │ │ ├── get-slashes.sreq │ │ │ └── get-slashes.sts │ │ ├── get-space │ │ │ ├── get-space.authz │ │ │ ├── get-space.creq │ │ │ ├── get-space.req │ │ │ ├── get-space.sreq │ │ │ └── get-space.sts │ │ ├── get-special-character │ │ │ ├── get-special-character.authz │ │ │ ├── get-special-character.creq │ │ │ ├── get-special-character.req │ │ │ ├── get-special-character.sreq │ │ │ └── get-special-character.sts │ │ └── normalize-path.txt │ ├── post-header-key-case │ │ ├── post-header-key-case.authz │ │ ├── post-header-key-case.creq │ │ ├── post-header-key-case.req │ │ ├── post-header-key-case.sreq │ │ └── post-header-key-case.sts │ ├── post-header-key-sort │ │ ├── post-header-key-sort.authz │ │ ├── post-header-key-sort.creq │ │ ├── post-header-key-sort.req │ │ ├── post-header-key-sort.sreq │ │ └── post-header-key-sort.sts │ ├── post-header-value-case │ │ ├── post-header-value-case.authz │ │ ├── post-header-value-case.creq │ │ ├── post-header-value-case.req │ │ ├── post-header-value-case.sreq │ │ └── post-header-value-case.sts │ ├── post-sts-token │ │ ├── post-sts-header-after │ │ │ ├── post-sts-header-after.authz │ │ │ ├── post-sts-header-after.creq │ │ │ ├── post-sts-header-after.req │ │ │ ├── post-sts-header-after.sreq │ │ │ └── post-sts-header-after.sts │ │ ├── post-sts-header-before │ │ │ ├── post-sts-header-before.authz │ │ │ ├── post-sts-header-before.creq │ │ │ ├── post-sts-header-before.req │ │ │ ├── post-sts-header-before.sreq │ │ │ └── post-sts-header-before.sts │ │ └── readme.txt │ ├── post-vanilla-empty-query-value │ │ ├── post-vanilla-empty-query-value.authz │ │ ├── post-vanilla-empty-query-value.creq │ │ ├── post-vanilla-empty-query-value.req │ │ ├── post-vanilla-empty-query-value.sreq │ │ └── post-vanilla-empty-query-value.sts │ ├── post-vanilla-query │ │ ├── post-vanilla-query.authz │ │ ├── post-vanilla-query.creq │ │ ├── post-vanilla-query.req │ │ ├── post-vanilla-query.sreq │ │ └── post-vanilla-query.sts │ ├── post-vanilla │ │ ├── post-vanilla.authz │ │ ├── post-vanilla.creq │ │ ├── post-vanilla.req │ │ ├── post-vanilla.sreq │ │ └── post-vanilla.sts │ ├── post-x-www-form-urlencoded-parameters │ │ ├── post-x-www-form-urlencoded-parameters.authz │ │ ├── post-x-www-form-urlencoded-parameters.creq │ │ ├── post-x-www-form-urlencoded-parameters.req │ │ ├── post-x-www-form-urlencoded-parameters.sreq │ │ └── post-x-www-form-urlencoded-parameters.sts │ └── post-x-www-form-urlencoded │ │ ├── post-x-www-form-urlencoded.authz │ │ ├── post-x-www-form-urlencoded.creq │ │ ├── post-x-www-form-urlencoded.req │ │ ├── post-x-www-form-urlencoded.sreq │ │ └── post-x-www-form-urlencoded.sts └── test_sigv4.py ├── test_fields.py ├── test_identity.py └── test_signers.py /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Restrict all files related to releases/GHA to 2 | # require maintainer team approval. 3 | 4 | .github/workflows/ @aws-sdk-python 5 | .github/CODEOWNERS @aws-sdk-python 6 | src/aws-sdk-signers/_version.py @aws-sdk-python 7 | pyproject.toml @aws-sdk-python 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | build: 12 | runs-on: '${{ matrix.os }}' 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | python-version: ['3.12'] 17 | os: [ubuntu-latest, macOS-latest, windows-latest] 18 | 19 | steps: 20 | - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 21 | - name: 'Set up Python ${{ matrix.python-version }}' 22 | uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d 23 | with: 24 | python-version: '${{ matrix.python-version }}' 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install -e .[test] 28 | - name: Run tests 29 | run: | 30 | python -m pytest tests 31 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint code 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 16 | - name: Set up Python 3.12 17 | uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d 18 | with: 19 | python-version: 3.12 20 | - name: Run pre-commit 21 | uses: pre-commit/action@646c83fcd040023954eafda54b4db0192ce70507 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: 'tests/unit/auth/aws4_testsuite' 2 | repos: 3 | - repo: 'https://github.com/pre-commit/pre-commit-hooks' 4 | rev: v4.5.0 5 | hooks: 6 | - id: check-yaml 7 | - id: end-of-file-fixer 8 | - id: trailing-whitespace 9 | - repo: 'https://github.com/astral-sh/ruff-pre-commit' 10 | rev: v0.4.3 11 | hooks: 12 | - id: ruff 13 | args: [ --fix ] 14 | - id: ruff-format 15 | - repo: 'https://github.com/pre-commit/mirrors-mypy' 16 | rev: 'v1.10.0' 17 | hooks: 18 | - id: mypy 19 | additional_dependencies: 20 | - pytest 21 | - freezegun 22 | exclude: '^examples/' 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.0.2 4 | 5 | ### Feature 6 | * Added new ``content_checksum_enabled`` parameter to SigV4SigningProperties. 7 | 8 | This will enable users to control the inclusion of the `X-Amz-Content-SHA256` 9 | header required by S3. This is _disabled_ by default, so you will need to 10 | set this to `True` for any S3 requests. 11 | 12 | ### Bugfixes 13 | * Fixed incorrect exclusion of `X-Amz-Content-SHA256` header from some requests. 14 | 15 | ## 0.0.1 16 | 17 | ### Features 18 | * Added SigV4Signer to sign arbitrary requests sychronously. 19 | * Added AsyncSigV4Signer to sign arbitrary requests asychronously. 20 | * Added example SigV4Auth for integrating directly with Requests' 21 | ``auth`` parameter. 22 | * Added SigV4Signer for integrating with the AIOHTTP request 23 | workflow. 24 | * Added SigV4Curl for generating signed curl commands. 25 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## AWS SDK Python Signers 2 | 3 | AWS SDK Python Signers provides stand-alone signing functionality. This enables users to 4 | create standardized request signatures (currently only SigV4) and apply them to 5 | common HTTP utilities like AIOHTTP, Curl, Postman, Requests and urllib3. 6 | 7 | This project is currently in an **Alpha** phase of development. There likely 8 | will be breakages and redesigns between minor patch versions as we collect 9 | user feedback. We strongly recommend pinning to a minor version and reviewing 10 | the changelog carefully before upgrading. 11 | 12 | As you find issues, please feel free to open an issue on this GitHub repository 13 | and we're happy to discuss direction and design decisions, along with potential 14 | bug fixes. 15 | 16 | ## Getting Started 17 | 18 | Currently, the `aws-sdk-signers` module provides two high level signers, 19 | `AsyncSigV4Signer` and `SigV4Signer`. 20 | 21 | Both of these signers takes three inputs to their primary `sign` method. 22 | 23 | * A [**SigV4SigningProperties**](https://github.com/awslabs/aws-sdk-python-signers/blob/eb78cde3b65a82ae052d632b43ba960a83643f8f/src/aws_sdk_signers/signers.py#L38-L42) object defining: 24 | * The service for the request, 25 | * The intended AWS region (e.g. us-west-2), 26 | * An optional date that will be auto-populated with the current time if not supplied, 27 | * An optional boolean, payload_signing_enabled to toggle payload signing. True by default. 28 | * An [**AWSRequest**](https://github.com/awslabs/aws-sdk-python-signers/blob/eb78cde3b65a82ae052d632b43ba960a83643f8f/src/aws_sdk_signers/_http.py#L336), similar to the AWSRequest object from boto3 or Requests. 29 | * An [**AWSCredentialIdentity**](https://github.com/awslabs/aws-sdk-python-signers/blob/eb78cde3b65a82ae052d632b43ba960a83643f8f/src/aws_sdk_signers/_identity.py#L12-L24), a dataclass holding standard AWS credential information. 30 | 31 | The signers can be used independently to build signing integrations with your favorite 32 | HTTP client or with the example code provided in the [`/examples`](https://github.com/awslabs/aws-sdk-python-signers/blob/main/examples/) directory. Currently, 33 | we have high-level code for integration with AIOHTTP, Curl, and Requests. More integrations 34 | may be introduced as we receive interest. You can find sample code for getting started 35 | with our current offerings in the [examples/README.md](https://github.com/awslabs/aws-sdk-python-signers/blob/main/examples/README.md). 36 | 37 | ## Security 38 | 39 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 40 | 41 | ## License 42 | 43 | This project is licensed under the Apache-2.0 License. 44 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Example Signers for aws-sdk-signers 2 | 3 | ## Requests 4 | We utilize the `BaseAuth` construct provided by Requests to apply our signature 5 | to each request. Our `SigV4Auth` class takes two arguments 6 | [`SigV4SigningProperties`](https://github.com/awslabs/aws-sdk-python-signers/blob/c60077450d1448fd34134618755d1b8495b36605/src/aws_sdk_signers/signers.py#L43-L48) 7 | and an [`AWSCredentialIdentity`](https://github.com/awslabs/aws-sdk-python-signers/blob/c60077450d1448fd34134618755d1b8495b36605/src/aws_sdk_signers/_identity.py#L9-L21). 8 | These will be used across requests as "immutable" input. This is currently an 9 | intentional design decision to work with Requests auth design. We'd love to 10 | hear feedback on how you feel about the current approach, we recommend checking 11 | the AIOHTTP section below for an alternative design. 12 | 13 | ### Requests Sample 14 | ```python 15 | from os import environ 16 | 17 | import requests 18 | 19 | from examples import requests_signer 20 | from aws_sdk_signers import SigV4SigningProperties, AWSCredentialIdentity 21 | 22 | SERVICE="lambda" 23 | REGION="us-west-2" 24 | 25 | # A GET request to this URL performs a "ListFunctions" invocation. 26 | # Full API documentation can be found here: 27 | # https://docs.aws.amazon.com/lambda/latest/api/API_ListFunctions.html 28 | URL='https://lambda.us-west-2.amazonaws.com/2015-03-31/functions/' 29 | 30 | def get_credentials_from_env(): 31 | """You will need to pull credentials from some source to use the signer. 32 | This will auto-populate an AWSCredentialIdentity when credentials are 33 | available through the env. 34 | 35 | You may also consider using another SDK to assume a role or pull 36 | credentials from another source. 37 | """ 38 | return AWSCredentialIdentity( 39 | access_key_id=environ["AWS_ACCESS_KEY_ID"], 40 | secret_access_key=environ["AWS_SECRET_ACCESS_KEY"], 41 | session_token=environ.get("AWS_SESSION_TOKEN"), 42 | ) 43 | 44 | # Set up our signing_properties and identity 45 | identity = get_credentials_from_env() 46 | signing_properties = SigV4SigningProperties(region=REGION, service=SERVICE) 47 | 48 | # Configure the auth class for signing 49 | sigv4_auth = requests_signer.SigV4Auth(signing_properties, identity) 50 | 51 | r = requests.get(URL, auth=sigv4_auth) 52 | ``` 53 | 54 | ## AIOHTTP 55 | For AIOHTTP, we don't have a concept of a Request object, or option to subclass an 56 | existing auth mechanism. Instead, we'll take parameters you normally pass to a Session 57 | method and use them to generate signing headers before passing them on to AIOHTTP. 58 | 59 | This signer will be configured the same way as Requests and provides an Async signing 60 | interface to be used alongside AIOHTTP. This is still a work in progress and will likely 61 | have some amount of iteration to improve performance and ergonomics as we collect feedback. 62 | 63 | ### AIOHTTP Sample 64 | ```python 65 | import asyncio 66 | from os import environ 67 | 68 | import aiohttp 69 | 70 | from examples import aiohttp_signer 71 | from aws_sdk_signers import SigV4SigningProperties, AWSCredentialIdentity 72 | 73 | SERVICE="lambda" 74 | REGION="us-west-2" 75 | 76 | # A GET request to this URL performs a "ListFunctions" invocation. 77 | # Full API documentation can be found here: 78 | # https://docs.aws.amazon.com/lambda/latest/api/API_ListFunctions.html 79 | URL='https://lambda.us-west-2.amazonaws.com/2015-03-31/functions/' 80 | 81 | def get_credentials_from_env(): 82 | """You will need to pull credentials from some source to use the signer. 83 | This will auto-populate an AWSCredentialIdentity when credentials are 84 | available through the env. 85 | 86 | You may also consider using another SDK to assume a role or pull 87 | credentials from another source. 88 | """ 89 | return AWSCredentialIdentity( 90 | access_key_id=environ["AWS_ACCESS_KEY_ID"], 91 | secret_access_key=environ["AWS_SECRET_ACCESS_KEY"], 92 | session_token=environ.get("AWS_SESSION_TOKEN"), 93 | ) 94 | 95 | # Set up our signing_properties and identity 96 | identity = get_credentials_from_env() 97 | signing_properties = SigV4SigningProperties(region=REGION, service=SERVICE) 98 | 99 | async def make_request( 100 | method: str, 101 | url: str, 102 | headers: Mapping[str, str], 103 | body: AsyncIterable[bytes] | None, 104 | ) -> None: 105 | # For more mature applications, you'll likely want to reuse this session. 106 | async with aiohttp.ClientSession() as session: 107 | signing_headers = await.signer.generate_signature(method, url, headers, body) 108 | headers.update(signing_headers) 109 | async with session.request(method, url, headers=headers, data=body) as response: 110 | print("Status:", response.status) 111 | print("Content-type:", response.headers['content-type']) 112 | 113 | body_content = await response.text() 114 | print(body_content) 115 | 116 | asyncio.run(make_request("GET", URL, {}, None)) 117 | ``` 118 | 119 | ## Curl Signer 120 | For curl, we're generating a string to be used in a terminal or invoked subprocess. 121 | This currently only supports known arguments like defining the method, headers, 122 | and a request body. We can expand this to support arbitrary curl arguments in 123 | a future version if there's demand. 124 | 125 | ### Curl Sample 126 | ```python 127 | from examples import curl_signer 128 | from aws_sdk_signers import SigV4SigningProperties, AWSCredentialIdentity 129 | 130 | from os import environ 131 | 132 | 133 | SERVICE="lambda" 134 | REGION="us-west-2" 135 | 136 | # A GET request to this URL performs a "ListFunctions" invocation. 137 | # Full API documentation can be found here: 138 | # https://docs.aws.amazon.com/lambda/latest/api/API_ListFunctions.html 139 | URL='https://lambda.us-west-2.amazonaws.com/2015-03-31/functions/' 140 | 141 | 142 | signing_properties = SigV4SigningProperties(region=REGION, service=SERVICE) 143 | identity = AWSCredentialIdentity( 144 | access_key_id=environ["AWS_ACCESS_KEY_ID"], 145 | secret_access_key=environ["AWS_SECRET_ACCESS_KEY"], 146 | session_token=environ["AWS_SESSION_TOKEN"] 147 | ) 148 | 149 | # Our curl signer doesn't need state so we 150 | # can call classmethods directly on the signer. 151 | signer = curl_signer.SigV4Curl 152 | curl_cmd = signer.generate_signed_curl_cmd( 153 | signing_properties=signing_properties, 154 | identity=identity, 155 | method="GET", 156 | url=URL, 157 | headers={}, 158 | body=None, 159 | ) 160 | print(curl_cmd) 161 | ``` 162 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-sdk-python-signers/7cf0664b60b58a96a9392f8298d114c84f2b50f0/examples/__init__.py -------------------------------------------------------------------------------- /examples/aiohttp_signer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | 5 | Sample signer using aiohttp. 6 | """ 7 | 8 | import typing 9 | from collections.abc import Mapping 10 | from urllib.parse import urlparse 11 | 12 | from aws_sdk_signers import URI, AsyncSigV4Signer, AWSRequest, Field, Fields 13 | 14 | if typing.TYPE_CHECKING: 15 | from aws_sdk_signers import AWSCredentialIdentity, SigV4SigningProperties 16 | 17 | SIGNING_HEADERS = ( 18 | "Authorization", 19 | "Date", 20 | "X-Amz-Date", 21 | "X-Amz-Security-Token", 22 | "X-Amz-Content-SHA256", 23 | ) 24 | 25 | 26 | class SigV4Signer: 27 | """Minimal Signer implementation to be used with AIOHTTP.""" 28 | 29 | def __init__( 30 | self, 31 | signing_properties: "SigV4SigningProperties", 32 | identity: "AWSCredentialIdentity", 33 | ): 34 | self._signing_properties = signing_properties 35 | self._identity = identity 36 | self._signer = AsyncSigV4Signer() 37 | 38 | async def generate_signature( 39 | self, 40 | method: str, 41 | url: str, 42 | headers: Mapping[str, str], 43 | body: typing.AsyncIterable[bytes] | None, 44 | ) -> Mapping[str, str]: 45 | """Generate signature headers for applying to request.""" 46 | url_parts = urlparse(url) 47 | uri = URI( 48 | scheme=url_parts.scheme, 49 | host=url_parts.hostname, 50 | port=url_parts.port, 51 | path=url_parts.path, 52 | query=url_parts.query, 53 | fragment=url_parts.fragment, 54 | ) 55 | fields = Fields([Field(name=k, values=[v]) for k, v in headers.items()]) 56 | awsrequest = AWSRequest( 57 | destination=uri, 58 | method=method, 59 | body=body, 60 | fields=fields, 61 | ) 62 | signed_request = await self._signer.sign( 63 | signing_properties=self._signing_properties, 64 | request=awsrequest, 65 | identity=self._identity, 66 | ) 67 | return { 68 | header: signed_request.fields[header].as_string() 69 | for header in SIGNING_HEADERS 70 | if header in signed_request.fields 71 | } 72 | -------------------------------------------------------------------------------- /examples/curl_signer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | 5 | Sample signer using Curl. 6 | """ 7 | 8 | import typing 9 | from collections.abc import Iterable, Mapping 10 | from urllib.parse import urlparse 11 | 12 | from aws_sdk_signers import URI, AWSRequest, Field, Fields, SigV4Signer 13 | 14 | if typing.TYPE_CHECKING: 15 | from aws_sdk_signers import AWSCredentialIdentity, SigV4SigningProperties 16 | 17 | 18 | class SigV4Curl: 19 | """Generates a curl command with a SigV4 signature applied.""" 20 | 21 | signer = SigV4Signer() 22 | 23 | @classmethod 24 | def generate_signed_curl_cmd( 25 | cls, 26 | signing_properties: "SigV4SigningProperties", 27 | identity: "AWSCredentialIdentity", 28 | method: str, 29 | url: str, 30 | headers: Mapping[str, str], 31 | body: Iterable[bytes] | None, 32 | ) -> str: 33 | url_parts = urlparse(url) 34 | uri = URI( 35 | scheme=url_parts.scheme, 36 | host=url_parts.hostname, 37 | port=url_parts.port, 38 | path=url_parts.path, 39 | query=url_parts.query, 40 | fragment=url_parts.fragment, 41 | ) 42 | fields = Fields([Field(name=k, values=[v]) for k, v in headers.items()]) 43 | awsrequest = AWSRequest( 44 | destination=uri, 45 | method=method, 46 | body=body, 47 | fields=fields, 48 | ) 49 | signed_request = cls.signer.sign( 50 | signing_properties=signing_properties, 51 | request=awsrequest, 52 | identity=identity, 53 | ) 54 | return cls._construct_curl_cmd(request=signed_request) 55 | 56 | @classmethod 57 | def _construct_curl_cmd(self, request: AWSRequest) -> str: 58 | cmd_list = ["curl"] 59 | cmd_list.append(f"-X {request.method.upper()}") 60 | for header in request.fields: 61 | cmd_list.append(f'-H "{header.name}: {header.as_string()}"') 62 | if request.body is not None: 63 | body_content = [i for i in request.body] 64 | # Forcing bytes to a utf-8 string, if we need arbitrary bytes for the 65 | # terminal we should add an option to write to file and use that 66 | # in the command. 67 | cmd_list.append(f"-d {b"".join(body_content).decode()}") 68 | cmd_list.append(request.destination.build()) 69 | return " ".join(cmd_list) 70 | -------------------------------------------------------------------------------- /examples/requests_signer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | 5 | Sample signer using Requests. 6 | """ 7 | 8 | import typing 9 | from urllib.parse import urlparse 10 | 11 | from aws_sdk_signers import URI, AWSRequest, Field, Fields, SigV4Signer 12 | from requests import PreparedRequest 13 | from requests.auth import AuthBase 14 | 15 | if typing.TYPE_CHECKING: 16 | from aws_sdk_signers import AWSCredentialIdentity, SigV4SigningProperties 17 | 18 | SIGNING_HEADERS = ( 19 | "Authorization", 20 | "Date", 21 | "X-Amz-Date", 22 | "X-Amz-Security-Token", 23 | "X-Amz-Content-SHA256", 24 | ) 25 | 26 | 27 | class SigV4Auth(AuthBase): 28 | """Attaches SigV4Authentication to the given Request object.""" 29 | 30 | def __init__( 31 | self, 32 | signing_properties: "SigV4SigningProperties", 33 | identity: "AWSCredentialIdentity", 34 | ): 35 | self._signing_properties = signing_properties 36 | self._identity = identity 37 | self._signer = SigV4Signer() 38 | 39 | def __eq__(self, other): 40 | return self.signing_properties == getattr(other, "signing_properties", None) 41 | 42 | def __ne__(self, other): 43 | return not self == other 44 | 45 | def __call__(self, r): 46 | self.sign_request(r) 47 | return r 48 | 49 | def sign_request(self, r: PreparedRequest): 50 | request = self.convert_to_awsrequest(r) 51 | signed_request = self._signer.sign( 52 | signing_properties=self._signing_properties, 53 | request=request, 54 | identity=self._identity, 55 | ) 56 | for header in SIGNING_HEADERS: 57 | if header in signed_request.fields: 58 | r.headers[header] = signed_request.fields[header].as_string() 59 | return r 60 | 61 | def convert_to_awsrequest(self, r: PreparedRequest) -> AWSRequest: 62 | url_parts = urlparse(r.url) 63 | uri = URI( 64 | scheme=url_parts.scheme, 65 | host=url_parts.hostname, 66 | port=url_parts.port, 67 | path=url_parts.path, 68 | query=url_parts.query, 69 | fragment=url_parts.fragment, 70 | ) 71 | fields = Fields([Field(name=k, values=[v]) for k, v in r.headers.items()]) 72 | return AWSRequest( 73 | destination=uri, 74 | method=r.method, 75 | body=r.body, 76 | fields=fields, 77 | ) 78 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "aws-sdk-signers" 7 | requires-python = ">=3.12" 8 | authors = [ 9 | {name = "Amazon Web Services"}, 10 | ] 11 | description = "Standalone HTTP Request Signers for Amazon Web Services" 12 | readme = "README.md" 13 | license = {file = "LICENSE"} 14 | keywords = ["AWS", "Signing", "SigV4", "HTTP"] 15 | classifiers = [ 16 | "Development Status :: 2 - Pre-Alpha", 17 | "Programming Language :: Python :: 3.12", 18 | "Programming Language :: Python" 19 | ] 20 | dynamic = ["version"] 21 | 22 | [project.optional-dependencies] 23 | test = [ 24 | "freezegun", 25 | "pytest", 26 | "pytest-asyncio", 27 | "mypy", 28 | "ruff", 29 | ] 30 | 31 | [tool.hatch.build.targets.wheel] 32 | packages = ["src/aws_sdk_signers"] 33 | 34 | [tool.hatch.version] 35 | path = "src/aws_sdk_signers/_version.py" 36 | 37 | [tool.ruff] 38 | line-length = 88 39 | indent-width = 4 40 | 41 | target-version = "py312" 42 | 43 | [tool.ruff.lint] 44 | select = ["E4", "E7", "E9", "F", "I", "UP"] 45 | ignore = [] 46 | 47 | fixable = ["ALL"] 48 | unfixable = [] 49 | 50 | dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" 51 | 52 | [tool.ruff.format] 53 | quote-style = "double" 54 | indent-style = "space" 55 | skip-magic-trailing-comma = false 56 | line-ending = "auto" 57 | 58 | docstring-code-format = false 59 | docstring-code-line-length = "dynamic" 60 | 61 | [tool.mypy] 62 | python_version = "3.12" 63 | strict = true 64 | 65 | [tool.pytest.ini_options] 66 | asyncio_mode = "auto" 67 | addopts = "-W error" 68 | -------------------------------------------------------------------------------- /src/aws_sdk_signers/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | 5 | AWS SDK Python Signers provides stand-alone signing functionality for use with 6 | HTTP tools such as AioHTTP, Curl, Postman, Requests, urllib3, etc. 7 | """ 8 | 9 | from __future__ import annotations 10 | 11 | from ._http import URI, AWSRequest, Field, Fields 12 | from ._identity import AWSCredentialIdentity 13 | from ._io import AsyncBytesReader 14 | from ._version import __version__ 15 | from .signers import AsyncSigV4Signer, SigV4Signer, SigV4SigningProperties 16 | 17 | __license__ = "Apache-2.0" 18 | __version__ = __version__ 19 | 20 | __all__ = ( 21 | "AsyncBytesReader", 22 | "AsyncSigV4Signer", 23 | "AWSCredentialIdentity", 24 | "AWSRequest", 25 | "Field", 26 | "Fields", 27 | "SigV4Signer", 28 | "SigV4SigningProperties", 29 | "URI", 30 | ) 31 | -------------------------------------------------------------------------------- /src/aws_sdk_signers/_http.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | 5 | 6 | NOTE TO THE READER: 7 | 8 | This file is _strictly_ temporary and subject to abrupt breaking changes 9 | including unannounced removal. For typing information, please rely on the 10 | __all__ attributes provided in the types/__init__.py file. 11 | """ 12 | 13 | from __future__ import annotations 14 | 15 | from collections import Counter, OrderedDict 16 | from collections.abc import AsyncIterable, Iterable, Iterator 17 | from copy import deepcopy 18 | from dataclasses import dataclass 19 | from functools import cached_property 20 | from typing import TypedDict 21 | from urllib.parse import urlunparse 22 | 23 | import aws_sdk_signers.interfaces.http as interfaces_http 24 | 25 | 26 | class Field(interfaces_http.Field): 27 | """A name-value pair representing a single field in an HTTP Request or Response. 28 | 29 | The kind will dictate metadata placement within an HTTP message. 30 | 31 | All field names are case insensitive and case-variance must be treated as 32 | equivalent. Names may be normalized but should be preserved for accuracy during 33 | transmission. 34 | """ 35 | 36 | def __init__( 37 | self, 38 | *, 39 | name: str, 40 | values: Iterable[str] | None = None, 41 | kind: interfaces_http.FieldPosition = interfaces_http.FieldPosition.HEADER, 42 | ): 43 | self.name = name 44 | self.values: list[str] = [val for val in values] if values is not None else [] 45 | self.kind = kind 46 | 47 | def add(self, value: str) -> None: 48 | """Append a value to a field.""" 49 | self.values.append(value) 50 | 51 | def set(self, values: list[str]) -> None: 52 | """Overwrite existing field values.""" 53 | self.values = values 54 | 55 | def remove(self, value: str) -> None: 56 | """Remove all matching entries from list.""" 57 | try: 58 | while True: 59 | self.values.remove(value) 60 | except ValueError: 61 | return 62 | 63 | def as_string(self, delimiter: str = ", ") -> str: 64 | """Get delimited string of all values. A comma followed by a space is used 65 | by default. 66 | 67 | If the ``Field`` has zero values, the empty string is returned. If the ``Field`` 68 | has exactly one value, the value is returned unmodified. 69 | 70 | For ``Field``s with more than one value, the values are joined by a comma and a 71 | space. For such multi-valued ``Field``s, any values that already contain 72 | commas or double quotes will be surrounded by double quotes. Within any values 73 | that get quoted, pre-existing double quotes and backslashes are escaped with a 74 | backslash. 75 | """ 76 | value_count = len(self.values) 77 | if value_count == 0: 78 | return "" 79 | if value_count == 1: 80 | return self.values[0] 81 | return delimiter.join(quote_and_escape_field_value(val) for val in self.values) 82 | 83 | def as_tuples(self) -> list[tuple[str, str]]: 84 | """Get list of ``name``, ``value`` tuples where each tuple represents one 85 | value.""" 86 | return [(self.name, val) for val in self.values] 87 | 88 | def __eq__(self, other: object) -> bool: 89 | """Name, values, and kind must match. 90 | 91 | Values order must match. 92 | """ 93 | if not isinstance(other, Field): 94 | return False 95 | return ( 96 | self.name == other.name 97 | and self.kind is other.kind 98 | and self.values == other.values 99 | ) 100 | 101 | def __repr__(self) -> str: 102 | return f"Field(name={self.name!r}, value={self.values!r}, kind={self.kind!r})" 103 | 104 | 105 | class Fields(interfaces_http.Fields): 106 | def __init__( 107 | self, 108 | initial: Iterable[interfaces_http.Field] | None = None, 109 | *, 110 | encoding: str = "utf-8", 111 | ): 112 | """Collection of header and trailer entries mapped by name. 113 | 114 | :param initial: Initial list of ``Field`` objects. ``Field``s can also be added 115 | and later removed. 116 | :param encoding: The string encoding to be used when converting the ``Field`` 117 | name and value from ``str`` to ``bytes`` for transmission. 118 | """ 119 | init_fields = [fld for fld in initial] if initial is not None else [] 120 | init_field_names = [self._normalize_field_name(fld.name) for fld in init_fields] 121 | fname_counter = Counter(init_field_names) 122 | repeated_names_exist = ( 123 | len(init_fields) > 0 and fname_counter.most_common(1)[0][1] > 1 124 | ) 125 | if repeated_names_exist: 126 | non_unique_names = [name for name, num in fname_counter.items() if num > 1] 127 | raise ValueError( 128 | "Field names of the initial list of fields must be unique. The " 129 | "following normalized field names appear more than once: " 130 | f"{', '.join(non_unique_names)}." 131 | ) 132 | init_tuples = zip(init_field_names, init_fields) 133 | self.entries: OrderedDict[str, interfaces_http.Field] = OrderedDict(init_tuples) 134 | self.encoding: str = encoding 135 | 136 | def set_field(self, field: interfaces_http.Field) -> None: 137 | """Alias for __setitem__ to utilize the field.name for the entry key.""" 138 | self.__setitem__(field.name, field) 139 | 140 | def __setitem__(self, name: str, field: interfaces_http.Field) -> None: 141 | """Set or override entry for a Field name.""" 142 | normalized_name = self._normalize_field_name(name) 143 | normalized_field_name = self._normalize_field_name(field.name) 144 | if normalized_name != normalized_field_name: 145 | raise ValueError( 146 | f"Supplied key {name} does not match Field.name " 147 | f"provided: {normalized_field_name}" 148 | ) 149 | self.entries[normalized_name] = field 150 | 151 | def get( 152 | self, key: str, default: interfaces_http.Field | None = None 153 | ) -> interfaces_http.Field | None: 154 | return self[key] if key in self else default 155 | 156 | def __getitem__(self, name: str) -> interfaces_http.Field: 157 | """Retrieve Field entry.""" 158 | normalized_name = self._normalize_field_name(name) 159 | return self.entries[normalized_name] 160 | 161 | def __delitem__(self, name: str) -> None: 162 | """Delete entry from collection.""" 163 | normalized_name = self._normalize_field_name(name) 164 | del self.entries[normalized_name] 165 | 166 | def get_by_type( 167 | self, kind: interfaces_http.FieldPosition 168 | ) -> list[interfaces_http.Field]: 169 | """Helper function for retrieving specific types of fields. 170 | 171 | Used to grab all headers or all trailers. 172 | """ 173 | return [entry for entry in self.entries.values() if entry.kind is kind] 174 | 175 | def extend(self, other: interfaces_http.Fields) -> None: 176 | """Merges ``entries`` of ``other`` into the current ``entries``. 177 | 178 | For every `Field` in the ``entries`` of ``other``: If the normalized name 179 | already exists in the current ``entries``, the values from ``other`` are 180 | appended. Otherwise, the ``Field`` is added to the list of ``entries``. 181 | """ 182 | for other_field in other: 183 | try: 184 | cur_field = self.__getitem__(other_field.name) 185 | for other_value in other_field.values: 186 | cur_field.add(other_value) 187 | except KeyError: 188 | self.__setitem__(other_field.name, other_field) 189 | 190 | def _normalize_field_name(self, name: str) -> str: 191 | """Normalize field names. 192 | 193 | For use as key in ``entries``. 194 | """ 195 | return name.lower() 196 | 197 | def __eq__(self, other: object) -> bool: 198 | """Encoding must match. 199 | 200 | Entries must match in values and order. 201 | """ 202 | if not isinstance(other, Fields): 203 | return False 204 | return self.encoding == other.encoding and self.entries == other.entries 205 | 206 | def __iter__(self) -> Iterator[interfaces_http.Field]: 207 | yield from self.entries.values() 208 | 209 | def __len__(self) -> int: 210 | return len(self.entries) 211 | 212 | def __repr__(self) -> str: 213 | return f"Fields({self.entries})" 214 | 215 | def __contains__(self, key: str) -> bool: 216 | return self._normalize_field_name(key) in self.entries 217 | 218 | 219 | @dataclass(kw_only=True, frozen=True) 220 | class URI(interfaces_http.URI): 221 | """Universal Resource Identifier, target location for a :py:class:`HTTPRequest`.""" 222 | 223 | scheme: str = "https" 224 | """For example ``http`` or ``https``.""" 225 | 226 | username: str | None = None 227 | """Username part of the userinfo URI component.""" 228 | 229 | password: str | None = None 230 | """Password part of the userinfo URI component.""" 231 | 232 | host: str 233 | """The hostname, for example ``amazonaws.com``.""" 234 | 235 | port: int | None = None 236 | """An explicit port number.""" 237 | 238 | path: str | None = None 239 | """Path component of the URI.""" 240 | 241 | query: str | None = None 242 | """Query component of the URI as string.""" 243 | 244 | fragment: str | None = None 245 | """Part of the URI specification, but may not be transmitted by a client.""" 246 | 247 | @property 248 | def netloc(self) -> str: 249 | """Construct netloc string in format ``{username}:{password}@{host}:{port}`` 250 | 251 | ``username``, ``password``, and ``port`` are only included if set. ``password`` 252 | is ignored, unless ``username`` is also set. 253 | """ 254 | return self._netloc 255 | 256 | # cached_property does NOT behave like property, it actually allows for setting. 257 | # Therefore we need a layer of indirection. 258 | @cached_property 259 | def _netloc(self) -> str: 260 | if self.username is not None: 261 | password = "" if self.password is None else f":{self.password}" 262 | userinfo = f"{self.username}{password}@" 263 | else: 264 | userinfo = "" 265 | 266 | if self.port is not None: 267 | port = f":{self.port}" 268 | else: 269 | port = "" 270 | 271 | host = self.host 272 | 273 | return f"{userinfo}{host}{port}" 274 | 275 | def build(self) -> str: 276 | """Construct URI string representation. 277 | 278 | Validate host. Returns a string of the form 279 | ``{scheme}://{username}:{password}@{host}:{port}{path}?{query}#{fragment}`` 280 | """ 281 | components = ( 282 | self.scheme, 283 | self.netloc, 284 | self.path or "", 285 | "", # params 286 | self.query, 287 | self.fragment, 288 | ) 289 | return urlunparse(components) 290 | 291 | def to_dict(self) -> URIParameters: 292 | return { 293 | "scheme": self.scheme, 294 | "host": self.host, 295 | "port": self.port, 296 | "path": self.path, 297 | "query": self.query, 298 | "username": self.username, 299 | "password": self.password, 300 | "fragment": self.fragment, 301 | } 302 | 303 | def __eq__(self, other: object) -> bool: 304 | if not isinstance(other, URI): 305 | return False 306 | return ( 307 | self.scheme == other.scheme 308 | and self.host == other.host 309 | and self.port == other.port 310 | and self.path == other.path 311 | and self.query == other.query 312 | and self.username == other.username 313 | and self.password == other.password 314 | and self.fragment == other.fragment 315 | ) 316 | 317 | 318 | class URIParameters(TypedDict): 319 | """TypedDict representing the parameters for the URI class. These need to be 320 | kept in sync for the `to_dict` method. 321 | """ 322 | 323 | # TODO: Unpack doesn't seem to do what we want, so we need a way to represent 324 | # returning a class' parameters as a dict. There must be a better way to do this. 325 | 326 | scheme: str 327 | username: str | None 328 | password: str | None 329 | host: str 330 | port: int | None 331 | path: str | None 332 | query: str | None 333 | fragment: str | None 334 | 335 | 336 | class AWSRequest(interfaces_http.Request): 337 | def __init__( 338 | self, 339 | *, 340 | destination: URI, 341 | method: str, 342 | body: AsyncIterable[bytes] | Iterable[bytes] | None, 343 | fields: Fields, 344 | ): 345 | self.destination = destination 346 | self.method = method 347 | self.body = body 348 | self.fields = fields 349 | 350 | def __deepcopy__( 351 | self, memo: dict[int, interfaces_http.Request] | None = None 352 | ) -> interfaces_http.Request: 353 | if memo is None: 354 | memo = {} 355 | 356 | if id(self) in memo: 357 | return memo[id(self)] 358 | 359 | # the destination doesn't need to be copied because it's immutable 360 | # the body can't be copied because it's an iterator 361 | new_instance = self.__class__( 362 | destination=self.destination, 363 | body=self.body, 364 | method=self.method, 365 | fields=deepcopy(self.fields, memo), 366 | ) 367 | memo[id(new_instance)] = new_instance 368 | return new_instance 369 | 370 | 371 | def quote_and_escape_field_value(value: str) -> str: 372 | """Escapes and quotes a single :class:`Field` value if necessary. 373 | 374 | See :func:`Field.as_string` for quoting and escaping logic. 375 | """ 376 | chars_to_quote = (",", '"') 377 | if any(char in chars_to_quote for char in value): 378 | escaped = value.replace("\\", "\\\\").replace('"', '\\"') 379 | return f'"{escaped}"' 380 | else: 381 | return value 382 | -------------------------------------------------------------------------------- /src/aws_sdk_signers/_identity.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | """ 5 | 6 | from dataclasses import dataclass 7 | from datetime import UTC, datetime 8 | 9 | from .interfaces.identity import Identity 10 | 11 | 12 | @dataclass(kw_only=True) 13 | class AWSCredentialIdentity(Identity): 14 | access_key_id: str 15 | secret_access_key: str 16 | session_token: str | None = None 17 | expiration: datetime | None = None 18 | 19 | @property 20 | def is_expired(self) -> bool: 21 | """Whether the identity is expired.""" 22 | if self.expiration is None: 23 | return False 24 | return self.expiration < datetime.now(UTC) 25 | -------------------------------------------------------------------------------- /src/aws_sdk_signers/_io.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | """ 5 | 6 | from asyncio import iscoroutinefunction 7 | from collections.abc import AsyncIterable, AsyncIterator, Awaitable, Callable 8 | from io import BytesIO 9 | from typing import ( 10 | Self, 11 | cast, 12 | ) 13 | 14 | from aws_sdk_signers.interfaces.io import AsyncByteStream, ByteStream 15 | 16 | # The default chunk size for iterating streams. 17 | _DEFAULT_CHUNK_SIZE = 1024 18 | 19 | 20 | StreamingBlob = ByteStream | AsyncByteStream | bytes | bytearray | AsyncIterable[bytes] 21 | 22 | 23 | class AsyncBytesReader: 24 | """A file-like object with an async read method.""" 25 | 26 | # BytesIO *is* a ByteStream, but mypy complains if it isn't here. 27 | _data: ByteStream | AsyncByteStream | AsyncIterable[bytes] | BytesIO | None 28 | _closed = False 29 | 30 | def __init__(self, data: StreamingBlob): 31 | """Initializes self. 32 | 33 | Data is read from the source on an as-needed basis and is not buffered. 34 | 35 | :param data: The source data to read from. 36 | """ 37 | self._remainder = b"" 38 | # pylint: disable-next=isinstance-second-argument-not-valid-type 39 | if isinstance(data, bytes | bytearray): 40 | self._data = BytesIO(data) 41 | else: 42 | self._data = data 43 | 44 | async def read(self, size: int = -1) -> bytes: 45 | """Read a number of bytes from the stream. 46 | 47 | :param size: The maximum number of bytes to read. If less than 0, all bytes 48 | will be read. 49 | """ 50 | if self._closed or not self._data: 51 | raise ValueError("I/O operation on closed file.") 52 | 53 | if isinstance(self._data, ByteStream) and not iscoroutinefunction( 54 | self._data.read 55 | ): 56 | # Python's runtime_checkable can't actually tell the difference between 57 | # sync and async, so we have to check ourselves. 58 | return self._data.read(size) 59 | 60 | if isinstance(self._data, AsyncByteStream): 61 | return await self._data.read(size) 62 | 63 | return await self._read_from_iterable( 64 | cast(AsyncIterable[bytes], self._data), size 65 | ) 66 | 67 | async def _read_from_iterable( 68 | self, iterator: AsyncIterable[bytes], size: int 69 | ) -> bytes: 70 | # This takes the iterator as an arg here just to avoid mypy complaints, since 71 | # we know it's an iterator where this is called. 72 | result = self._remainder 73 | if size < 0: 74 | async for element in iterator: 75 | result += element 76 | self._remainder = b"" 77 | return result 78 | 79 | async for element in iterator: 80 | result += element 81 | if len(result) >= size: 82 | break 83 | 84 | self._remainder = result[size:] 85 | return result[:size] 86 | 87 | def __aiter__(self) -> AsyncIterator[bytes]: 88 | return self.iter_chunks() 89 | 90 | def iter_chunks( 91 | self, chunk_size: int = _DEFAULT_CHUNK_SIZE 92 | ) -> AsyncIterator[bytes]: 93 | """Iterate over the reader in chunks of a given size. 94 | 95 | :param chunk_size: The maximum size of each chunk. If less than 0, the entire 96 | reader will be read into one chunk. 97 | """ 98 | return _AsyncByteStreamIterator(self.read, chunk_size) 99 | 100 | def readable(self) -> bool: 101 | """Returns whether the stream is readable.""" 102 | return True 103 | 104 | def writeable(self) -> bool: 105 | """Returns whether the stream is writeable.""" 106 | return False 107 | 108 | def seekable(self) -> bool: 109 | """Returns whether the stream is seekable.""" 110 | return False 111 | 112 | @property 113 | def closed(self) -> bool: 114 | """Returns whether the stream is closed.""" 115 | return self._closed 116 | 117 | def close(self) -> None: 118 | """Closes the stream, as well as the underlying stream where possible.""" 119 | if (close := getattr(self._data, "close", None)) is not None: 120 | close() 121 | self._data = None 122 | self._closed = True 123 | 124 | 125 | class SeekableAsyncBytesReader: 126 | """A file-like object with async read and seek methods.""" 127 | 128 | def __init__(self, data: StreamingBlob): 129 | """Initializes self. 130 | 131 | Data is read from the source on an as-needed basis and buffered internally so 132 | that it can be rewound safely. 133 | 134 | :param data: The source data to read from. 135 | """ 136 | # pylint: disable-next=isinstance-second-argument-not-valid-type 137 | if isinstance(data, bytes | bytearray): 138 | self._buffer = BytesIO(data) 139 | self._data_source = None 140 | elif isinstance(data, AsyncByteStream) and iscoroutinefunction(data.read): 141 | # Note that we need that iscoroutine check because python won't actually check 142 | # whether or not the read function is async. 143 | self._buffer = BytesIO() 144 | self._data_source = data 145 | else: 146 | self._buffer = BytesIO() 147 | self._data_source = AsyncBytesReader(data) 148 | 149 | async def read(self, size: int = -1) -> bytes: 150 | """Read a number of bytes from the stream. 151 | 152 | :param size: The maximum number of bytes to read. If less than 0, all bytes 153 | will be read. 154 | """ 155 | if self._data_source is None or size == 0: 156 | return self._buffer.read(size) 157 | 158 | start = self._buffer.tell() 159 | current_buffer_size = self._buffer.seek(0, 2) 160 | 161 | if size < 0: 162 | await self._read_into_buffer(size) 163 | elif (target := start + size) > current_buffer_size: 164 | amount_to_read = target - current_buffer_size 165 | await self._read_into_buffer(amount_to_read) 166 | 167 | self._buffer.seek(start, 0) 168 | return self._buffer.read(size) 169 | 170 | async def seek(self, offset: int, whence: int = 0) -> int: 171 | """Moves the cursor to a position relative to the position indicated by whence. 172 | 173 | Whence can have one of three values: 174 | 175 | * 0 => The offset is relative to the start of the stream. 176 | 177 | * 1 => The offset is relative to the current location of the cursor. 178 | 179 | * 2 => The offset is relative to the end of the stream. 180 | 181 | :param offset: The amount of movement to be done relative to whence. 182 | :param whence: The location the offset is relative to. 183 | :returns: Returns the new position of the cursor. 184 | """ 185 | if self._data_source is None: 186 | return self._buffer.seek(offset, whence) 187 | 188 | if whence >= 2: 189 | # If the seek is relative to the end of the stream, we need to read the 190 | # whole thing in from the source. 191 | self._buffer.seek(0, 2) 192 | self._buffer.write(await self._data_source.read()) 193 | return self._buffer.seek(offset, whence) 194 | 195 | start = self.tell() 196 | target = offset 197 | if whence == 1: 198 | target += start 199 | 200 | current_buffer_size = self._buffer.seek(0, 2) 201 | if current_buffer_size < target: 202 | await self._read_into_buffer(target - current_buffer_size) 203 | 204 | return self._buffer.seek(target, 0) 205 | 206 | async def _read_into_buffer(self, size: int) -> None: 207 | if self._data_source is None: 208 | return 209 | 210 | read_bytes = await self._data_source.read(size) 211 | if len(read_bytes) < size or size < 0: 212 | self._data_source = None 213 | 214 | self._buffer.seek(0, 2) 215 | self._buffer.write(read_bytes) 216 | 217 | def tell(self) -> int: 218 | """Returns the position of the cursor.""" 219 | return self._buffer.tell() 220 | 221 | def __aiter__(self) -> AsyncIterator[bytes]: 222 | return self.iter_chunks() 223 | 224 | def iter_chunks( 225 | self, chunk_size: int = _DEFAULT_CHUNK_SIZE 226 | ) -> AsyncIterator[bytes]: 227 | """Iterate over the reader in chunks of a given size. 228 | 229 | :param chunk_size: The maximum size of each chunk. If less than 0, the entire 230 | reader will be read into one chunk. 231 | """ 232 | return _AsyncByteStreamIterator(self.read, chunk_size) 233 | 234 | def readable(self) -> bool: 235 | """Returns whether the stream is readable.""" 236 | return True 237 | 238 | def writeable(self) -> bool: 239 | """Returns whether the stream is writeable.""" 240 | return False 241 | 242 | def seekable(self) -> bool: 243 | """Returns whether the stream is seekable.""" 244 | return True 245 | 246 | @property 247 | def closed(self) -> bool: 248 | """Returns whether the stream is closed.""" 249 | return self._buffer.closed 250 | 251 | def close(self) -> None: 252 | """Closes the stream, as well as the underlying stream where possible.""" 253 | if callable(close_fn := getattr(self._data_source, "close", None)): 254 | close_fn() # pylint: disable=not-callable 255 | self._data_source = None 256 | self._buffer.close() 257 | 258 | 259 | class _AsyncByteStreamIterator: 260 | """An async bytes iterator that operates over an async read method.""" 261 | 262 | def __init__(self, read: Callable[[int], Awaitable[bytes]], chunk_size: int): 263 | """Initializes self. 264 | 265 | :param read: An async callable that reads a given number of bytes from some 266 | source. 267 | 268 | :param chunk_size: The number of bytes to read in each iteration. 269 | """ 270 | self._read = read 271 | self._chunk_size = chunk_size 272 | 273 | def __aiter__(self) -> Self: 274 | return self 275 | 276 | async def __anext__(self) -> bytes: 277 | data = await self._read(self._chunk_size) 278 | if data: 279 | return data 280 | raise StopAsyncIteration 281 | -------------------------------------------------------------------------------- /src/aws_sdk_signers/_version.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | """ 5 | 6 | # This file is protected via CODEOWNERS 7 | from __future__ import annotations 8 | 9 | __version__ = "0.0.2" 10 | -------------------------------------------------------------------------------- /src/aws_sdk_signers/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | """ 5 | 6 | 7 | class AWSSDKWarning(UserWarning): ... 8 | 9 | 10 | class BaseAWSSDKException(Exception): 11 | """Top-level exception to capture SDK-related errors.""" 12 | 13 | ... 14 | 15 | 16 | class MissingExpectedParameterException(BaseAWSSDKException, ValueError): 17 | """Some APIs require specific signing properties to be present.""" 18 | 19 | ... 20 | -------------------------------------------------------------------------------- /src/aws_sdk_signers/interfaces/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-sdk-python-signers/7cf0664b60b58a96a9392f8298d114c84f2b50f0/src/aws_sdk_signers/interfaces/__init__.py -------------------------------------------------------------------------------- /src/aws_sdk_signers/interfaces/http.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | """ 5 | 6 | from __future__ import annotations 7 | 8 | from collections import OrderedDict 9 | from collections.abc import AsyncIterable, Iterable, Iterator 10 | from enum import Enum 11 | from typing import Protocol 12 | 13 | 14 | class FieldPosition(Enum): 15 | """The type of a field. 16 | 17 | Defines its placement in a request or response. 18 | """ 19 | 20 | HEADER = 0 21 | """Header field. 22 | 23 | In HTTP this is a header as defined in RFC 9110 Section 6.3. Implementations of 24 | other protocols may use this FieldPosition for similar types of metadata. 25 | """ 26 | 27 | TRAILER = 1 28 | """Trailer field. 29 | 30 | In HTTP this is a trailer as defined in RFC 9110 Section 6.5. Implementations of 31 | other protocols may use this FieldPosition for similar types of metadata. 32 | """ 33 | 34 | 35 | class Field(Protocol): 36 | """A name-value pair representing a single field in a request or response. 37 | 38 | The kind will dictate metadata placement within a message, for example as 39 | a header or trailer field in an HTTP request as defined in RFC 9110 Section 5. 40 | 41 | All field names are case insensitive and case-variance must be treated as 42 | equivalent. Names may be normalized but should be preserved for accuracy during 43 | transmission. 44 | """ 45 | 46 | name: str 47 | values: list[str] 48 | kind: FieldPosition = FieldPosition.HEADER 49 | 50 | def add(self, value: str) -> None: 51 | """Append a value to a field.""" 52 | ... 53 | 54 | def set(self, values: list[str]) -> None: 55 | """Overwrite existing field values.""" 56 | ... 57 | 58 | def remove(self, value: str) -> None: 59 | """Remove all matching entries from list.""" 60 | ... 61 | 62 | def as_string(self, delimiter: str = ", ") -> str: 63 | """Serialize the ``Field``'s values into a single line string.""" 64 | ... 65 | 66 | def as_tuples(self) -> list[tuple[str, str]]: 67 | """Get list of ``name``, ``value`` tuples where each tuple represents one 68 | value.""" 69 | ... 70 | 71 | 72 | class Fields(Protocol): 73 | """Protocol agnostic mapping of key-value pair request metadata, such as HTTP 74 | fields.""" 75 | 76 | # Entries are keyed off the name of a provided Field 77 | entries: OrderedDict[str, Field] 78 | encoding: str = "utf-8" 79 | 80 | def set_field(self, field: Field) -> None: 81 | """Alias for __setitem__ to utilize the field.name for the entry key.""" 82 | ... 83 | 84 | def __setitem__(self, name: str, field: Field) -> None: 85 | """Set entry for a Field name.""" 86 | ... 87 | 88 | def __getitem__(self, name: str) -> Field: 89 | """Retrieve Field entry.""" 90 | ... 91 | 92 | def __delitem__(self, name: str) -> None: 93 | """Delete entry from collection.""" 94 | ... 95 | 96 | def __iter__(self) -> Iterator[Field]: 97 | """Allow iteration over entries.""" 98 | ... 99 | 100 | def __len__(self) -> int: 101 | """Get total number of Field entries.""" 102 | ... 103 | 104 | def get_by_type(self, kind: FieldPosition) -> list[Field]: 105 | """Helper function for retrieving specific types of fields. 106 | 107 | Used to grab all headers or all trailers. 108 | """ 109 | ... 110 | 111 | def extend(self, other: Fields) -> None: 112 | """Merges ``entries`` of ``other`` into the current ``entries``. 113 | 114 | For every `Field` in the ``entries`` of ``other``: If the normalized name 115 | already exists in the current ``entries``, the values from ``other`` are 116 | appended. Otherwise, the ``Field`` is added to the list of ``entries``. 117 | """ 118 | ... 119 | 120 | 121 | class Request(Protocol): 122 | """Protocol-agnostic representation of a request.""" 123 | 124 | destination: URI 125 | body: AsyncIterable[bytes] | Iterable[bytes] | None 126 | 127 | 128 | class URI(Protocol): 129 | """Universal Resource Identifier, target location for a :py:class:`Request`.""" 130 | 131 | scheme: str 132 | """For example ``http`` or ``mqtts``.""" 133 | 134 | username: str | None 135 | """Username part of the userinfo URI component.""" 136 | 137 | password: str | None 138 | """Password part of the userinfo URI component.""" 139 | 140 | host: str 141 | """The hostname, for example ``amazonaws.com``.""" 142 | 143 | port: int | None 144 | """An explicit port number.""" 145 | 146 | path: str | None 147 | """Path component of the URI.""" 148 | 149 | query: str | None 150 | """Query component of the URI as string.""" 151 | 152 | fragment: str | None 153 | """Part of the URI specification, but may not be transmitted by a client.""" 154 | 155 | def build(self) -> str: 156 | """Construct URI string representation. 157 | 158 | Returns a string of the form 159 | ``{scheme}://{username}:{password}@{host}:{port}{path}?{query}#{fragment}`` 160 | """ 161 | ... 162 | 163 | @property 164 | def netloc(self) -> str: 165 | """Construct netloc string in format ``{username}:{password}@{host}:{port}``""" 166 | ... 167 | -------------------------------------------------------------------------------- /src/aws_sdk_signers/interfaces/identity.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | """ 5 | 6 | from __future__ import annotations 7 | 8 | from datetime import datetime 9 | from typing import Protocol 10 | 11 | 12 | class Identity(Protocol): 13 | """An entity available to the client representing who the user is.""" 14 | 15 | # The expiration time of the identity. If time zone is provided, 16 | # it is updated to UTC. The value must always be in UTC. 17 | expiration: datetime | None = None 18 | 19 | @property 20 | def is_expired(self) -> bool: 21 | """Whether the identity is expired.""" 22 | ... 23 | -------------------------------------------------------------------------------- /src/aws_sdk_signers/interfaces/io.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | """ 5 | 6 | from typing import Protocol, runtime_checkable 7 | 8 | 9 | @runtime_checkable 10 | class ByteStream(Protocol): 11 | """A file-like object with a read method that returns bytes.""" 12 | 13 | def read(self, size: int = -1) -> bytes: ... 14 | 15 | 16 | @runtime_checkable 17 | class AsyncByteStream(Protocol): 18 | """A file-like object with an async read method.""" 19 | 20 | async def read(self, size: int = -1) -> bytes: ... 21 | -------------------------------------------------------------------------------- /src/aws_sdk_signers/py.typed: -------------------------------------------------------------------------------- 1 | # Instruct type checkers to look for inline type annotations in this package. 2 | # See PEP 561. 3 | -------------------------------------------------------------------------------- /src/aws_sdk_signers/signers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | """ 5 | 6 | import datetime 7 | import hmac 8 | import io 9 | import warnings 10 | from collections.abc import AsyncIterable 11 | from copy import deepcopy 12 | from hashlib import sha256 13 | from typing import Required, TypedDict 14 | from urllib.parse import parse_qsl, quote 15 | 16 | from ._http import URI, AWSRequest, Field 17 | from ._identity import AWSCredentialIdentity 18 | from ._io import AsyncBytesReader 19 | from .exceptions import AWSSDKWarning, MissingExpectedParameterException 20 | 21 | HEADERS_EXCLUDED_FROM_SIGNING: tuple[str, ...] = ( 22 | "accept", 23 | "accept-encoding", 24 | "authorization", 25 | "connection", 26 | "expect", 27 | "user-agent", 28 | "x-amzn-trace-id", 29 | ) 30 | DEFAULT_PORTS: dict[str, int] = {"http": 80, "https": 443} 31 | 32 | SIGV4_TIMESTAMP_FORMAT: str = "%Y%m%dT%H%M%SZ" 33 | UNSIGNED_PAYLOAD: str = "UNSIGNED-PAYLOAD" 34 | EMPTY_SHA256_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" 35 | 36 | 37 | class SigV4SigningProperties(TypedDict, total=False): 38 | region: Required[str] 39 | service: Required[str] 40 | date: str 41 | payload_signing_enabled: bool 42 | content_checksum_enabled: bool 43 | 44 | 45 | class SigV4Signer: 46 | """ 47 | Request signer for applying the AWS Signature Version 4 algorithm. 48 | """ 49 | 50 | def sign( 51 | self, 52 | *, 53 | signing_properties: SigV4SigningProperties, 54 | request: AWSRequest, 55 | identity: AWSCredentialIdentity, 56 | ) -> AWSRequest: 57 | """Generate and apply a SigV4 Signature to a copy of the supplied request. 58 | 59 | :param signing_properties: 60 | SigV4SigningProperties to define signing primitives such as 61 | the target service, region, and date. 62 | :param request: 63 | An AWSRequest to sign prior to sending to the service. 64 | :param identity: 65 | A set of credentials representing an AWS Identity or role capacity. 66 | """ 67 | # Copy and prepopulate any missing values in the 68 | # supplied request and signing properties. 69 | self._validate_identity(identity=identity) 70 | new_signing_properties = self._normalize_signing_properties( 71 | signing_properties=signing_properties 72 | ) 73 | new_request = self._generate_new_request(request=request) 74 | self._apply_required_fields( 75 | request=new_request, 76 | signing_properties=new_signing_properties, 77 | identity=identity, 78 | ) 79 | 80 | # Construct core signing components 81 | canonical_request = self.canonical_request( 82 | signing_properties=new_signing_properties, 83 | request=new_request, 84 | ) 85 | string_to_sign = self.string_to_sign( 86 | canonical_request=canonical_request, 87 | signing_properties=new_signing_properties, 88 | ) 89 | signature = self._signature( 90 | string_to_sign=string_to_sign, 91 | secret_key=identity.secret_access_key, 92 | signing_properties=new_signing_properties, 93 | ) 94 | 95 | signing_fields = self._normalize_signing_fields(request=new_request) 96 | credential_scope = self._scope(signing_properties=new_signing_properties) 97 | credential = f"{identity.access_key_id}/{credential_scope}" 98 | authorization = self.generate_authorization_field( 99 | credential=credential, 100 | signed_headers=list(signing_fields.keys()), 101 | signature=signature, 102 | ) 103 | new_request.fields.set_field(authorization) 104 | 105 | return new_request 106 | 107 | def generate_authorization_field( 108 | self, *, credential: str, signed_headers: list[str], signature: str 109 | ) -> Field: 110 | """Generate the `Authorization` field 111 | 112 | :param credential: 113 | Credential scope string for generating the Authorization header. 114 | Defined as: 115 | //// 116 | :param signed_headers: 117 | A list of the field names used in signing. 118 | :param signature: 119 | Final hash of the SigV4 signing algorithm generated from the 120 | canonical request and string to sign. 121 | """ 122 | signed_headers_str = ";".join(signed_headers) 123 | auth_str = ( 124 | f"AWS4-HMAC-SHA256 Credential={credential}, " 125 | f"SignedHeaders={signed_headers_str}, Signature={signature}" 126 | ) 127 | return Field(name="Authorization", values=[auth_str]) 128 | 129 | def _signature( 130 | self, 131 | *, 132 | string_to_sign: str, 133 | secret_key: str, 134 | signing_properties: SigV4SigningProperties, 135 | ) -> str: 136 | """Sign the string to sign. 137 | 138 | In SigV4, a signing key is created that is scoped to a specific region and 139 | service. The date, region, service and resulting signing key are individually 140 | hashed, then the composite hash is used to sign the string to sign. 141 | 142 | DateKey = HMAC-SHA256("AWS4"+"", "") 143 | DateRegionKey = HMAC-SHA256(, "") 144 | DateRegionServiceKey = HMAC-SHA256(, "") 145 | SigningKey = HMAC-SHA256(, "aws4_request") 146 | """ 147 | assert signing_properties["date"] is not None 148 | k_date = self._hash( 149 | key=f"AWS4{secret_key}".encode(), value=signing_properties["date"][0:8] 150 | ) 151 | k_region = self._hash(key=k_date, value=signing_properties["region"]) 152 | k_service = self._hash(key=k_region, value=signing_properties["service"]) 153 | k_signing = self._hash(key=k_service, value="aws4_request") 154 | 155 | return self._hash(key=k_signing, value=string_to_sign).hex() 156 | 157 | def _hash(self, key: bytes, value: str) -> bytes: 158 | return hmac.new(key=key, msg=value.encode(), digestmod=sha256).digest() 159 | 160 | def _validate_identity(self, *, identity: AWSCredentialIdentity) -> None: 161 | """Perform runtime and expiration checks before attempting signing.""" 162 | if not isinstance(identity, AWSCredentialIdentity): 163 | raise ValueError( 164 | "Received unexpected value for identity parameter. Expected " 165 | f"AWSCredentialIdentity but received {type(identity)}." 166 | ) 167 | elif identity.is_expired: 168 | raise ValueError( 169 | f"Provided identity expired at {identity.expiration}. Please " 170 | "refresh the credentials or update the expiration parameter." 171 | ) 172 | 173 | def _normalize_signing_properties( 174 | self, *, signing_properties: SigV4SigningProperties 175 | ) -> SigV4SigningProperties: 176 | # Create copy of signing properties to avoid mutating the original 177 | new_signing_properties = SigV4SigningProperties(**signing_properties) 178 | new_signing_properties["date"] = self._resolve_signing_date( 179 | date=new_signing_properties.get("date") 180 | ) 181 | return new_signing_properties 182 | 183 | def _generate_new_request(self, *, request: AWSRequest) -> AWSRequest: 184 | return deepcopy(request) 185 | 186 | def _resolve_signing_date(self, *, date: str | None) -> str: 187 | if date is None: 188 | date_obj = datetime.datetime.now(datetime.UTC) 189 | date = date_obj.strftime(SIGV4_TIMESTAMP_FORMAT) 190 | return date 191 | 192 | def _apply_required_fields( 193 | self, 194 | *, 195 | request: AWSRequest, 196 | signing_properties: SigV4SigningProperties, 197 | identity: AWSCredentialIdentity, 198 | ) -> None: 199 | # Apply required X-Amz-Date if neither X-Amz-Date nor Date are present. 200 | if "Date" not in request.fields and "X-Amz-Date" not in request.fields: 201 | request.fields.set_field( 202 | Field(name="X-Amz-Date", values=[signing_properties["date"]]) 203 | ) 204 | # Apply required X-Amz-Security-Token if token present on identity 205 | if ( 206 | "X-Amz-Security-Token" not in request.fields 207 | and identity.session_token is not None 208 | ): 209 | request.fields.set_field( 210 | Field(name="X-Amz-Security-Token", values=[identity.session_token]) 211 | ) 212 | 213 | def canonical_request( 214 | self, *, signing_properties: SigV4SigningProperties, request: AWSRequest 215 | ) -> str: 216 | """The canonical request is a standardized string laying out the 217 | components used in the SigV4 signing algorithm. This is useful to quickly 218 | compare inputs to find signature mismatches and unintended variances. 219 | 220 | The SigV4 specification defines the canonical request to be: 221 | \n 222 | \n 223 | \n 224 | \n 225 | \n 226 | 227 | 228 | :param signing_properties: 229 | SigV4SigningProperties to define signing primitives such as 230 | the target service, region, and date. 231 | :param request: 232 | An AWSRequest to use for generating a SigV4 signature. 233 | """ 234 | # We generate the payload first to ensure any field modifications 235 | # are in place before choosing the canonical fields. 236 | canonical_payload = self._format_canonical_payload( 237 | request=request, signing_properties=signing_properties 238 | ) 239 | canonical_path = self._format_canonical_path(path=request.destination.path) 240 | canonical_query = self._format_canonical_query(query=request.destination.query) 241 | normalized_fields = self._normalize_signing_fields(request=request) 242 | canonical_fields = self._format_canonical_fields(fields=normalized_fields) 243 | return ( 244 | f"{request.method.upper()}\n" 245 | f"{canonical_path}\n" 246 | f"{canonical_query}\n" 247 | f"{canonical_fields}\n" 248 | f"{';'.join(normalized_fields)}\n" 249 | f"{canonical_payload}" 250 | ) 251 | 252 | def string_to_sign( 253 | self, 254 | *, 255 | canonical_request: str, 256 | signing_properties: SigV4SigningProperties, 257 | ) -> str: 258 | """The string to sign is the second step of our signing algorithm which 259 | concatenates the formal identifier of our signing algorithm, the signing 260 | DateTime, the scope of our credentials, and a hash of our previously 261 | generated canonical request. This is another checkpoint that can be used 262 | to ensure we're constructing our signature as intended. 263 | 264 | The SigV4 specification defines the string to sign as: 265 | Algorithm \n 266 | RequestDateTime \n 267 | CredentialScope \n 268 | HashedCanonicalRequest 269 | 270 | :param canonical_request: 271 | String generated from the `canonical_request` method. 272 | :param signing_properties: 273 | SigV4SigningProperties to define signing primitives such as 274 | the target service, region, and date. 275 | """ 276 | date = signing_properties.get("date") 277 | if date is None: 278 | raise MissingExpectedParameterException( 279 | "Cannot generate string_to_sign without a valid date " 280 | f"in your signing_properties. Current value: {date}" 281 | ) 282 | return ( 283 | "AWS4-HMAC-SHA256\n" 284 | f"{date}\n" 285 | f"{self._scope(signing_properties=signing_properties)}\n" 286 | f"{sha256(canonical_request.encode()).hexdigest()}" 287 | ) 288 | 289 | def _scope(self, signing_properties: SigV4SigningProperties) -> str: 290 | formatted_date = signing_properties["date"][0:8] 291 | region = signing_properties["region"] 292 | service = signing_properties["service"] 293 | # Scope format: ///aws4_request 294 | return f"{formatted_date}/{region}/{service}/aws4_request" 295 | 296 | def _format_canonical_path(self, *, path: str | None) -> str: 297 | if path is None: 298 | path = "/" 299 | normalized_path = _remove_dot_segments(path) 300 | return quote(string=normalized_path, safe="/%") 301 | 302 | def _format_canonical_query(self, *, query: str | None) -> str: 303 | if query is None: 304 | return "" 305 | 306 | query_params = parse_qsl(qs=query) 307 | query_parts = ( 308 | (quote(string=key, safe=""), quote(string=value, safe="")) 309 | for key, value in query_params 310 | ) 311 | # key-value pairs must be in sorted order for their encoded forms. 312 | return "&".join(f"{key}={value}" for key, value in sorted(query_parts)) 313 | 314 | def _normalize_signing_fields(self, *, request: AWSRequest) -> dict[str, str]: 315 | normalized_fields = { 316 | field.name.lower(): field.as_string(delimiter=",") 317 | for field in request.fields 318 | if field.name.lower() not in HEADERS_EXCLUDED_FROM_SIGNING 319 | } 320 | if "host" not in normalized_fields: 321 | normalized_fields["host"] = self._normalize_host_field( 322 | uri=request.destination 323 | ) 324 | 325 | return dict(sorted(normalized_fields.items())) 326 | 327 | def _normalize_host_field(self, *, uri: URI) -> str: 328 | if uri.port is not None and DEFAULT_PORTS.get(uri.scheme) == uri.port: 329 | uri_dict = uri.to_dict() 330 | uri_dict.update({"port": None}) 331 | uri = URI(**uri_dict) 332 | return uri.netloc 333 | 334 | def _format_canonical_fields(self, *, fields: dict[str, str]) -> str: 335 | return "".join( 336 | f"{key}:{' '.join(value.split())}\n" for key, value in fields.items() 337 | ) 338 | 339 | def _should_sha256_sign_payload( 340 | self, 341 | *, 342 | request: AWSRequest, 343 | signing_properties: SigV4SigningProperties, 344 | ) -> bool: 345 | # All insecure connections should be signed 346 | if request.destination.scheme != "https": 347 | return True 348 | 349 | return signing_properties.get("payload_signing_enabled", True) 350 | 351 | def _format_canonical_payload( 352 | self, 353 | *, 354 | request: AWSRequest, 355 | signing_properties: SigV4SigningProperties, 356 | ) -> str: 357 | if isinstance(request.body, AsyncIterable): 358 | raise TypeError( 359 | "An async body was attached to a synchronous signer. Please use " 360 | "AsyncSigV4Signer for async AWSRequests or ensure your body is " 361 | "of type Iterable[bytes]." 362 | ) 363 | payload_hash = self._compute_payload_hash( 364 | request=request, signing_properties=signing_properties 365 | ) 366 | if signing_properties.get("content_checksum_enabled", False): 367 | request.fields.set_field( 368 | Field(name="X-Amz-Content-SHA256", values=[payload_hash]) 369 | ) 370 | return payload_hash 371 | 372 | def _compute_payload_hash( 373 | self, *, request: AWSRequest, signing_properties: SigV4SigningProperties 374 | ) -> str: 375 | if not self._should_sha256_sign_payload( 376 | request=request, signing_properties=signing_properties 377 | ): 378 | return UNSIGNED_PAYLOAD 379 | 380 | body = request.body 381 | 382 | if body is None: 383 | return EMPTY_SHA256_HASH 384 | 385 | warnings.warn( 386 | "Payload signing is enabled. This may result in " 387 | "decreased performance for large request bodies.", 388 | AWSSDKWarning, 389 | ) 390 | 391 | checksum = sha256() 392 | if hasattr(body, "seek") and hasattr(body, "tell"): 393 | position = body.tell() 394 | for chunk in body: # type: ignore[union-attr] 395 | checksum.update(chunk) 396 | body.seek(position) 397 | else: 398 | buffer = io.BytesIO() 399 | for chunk in body: # type: ignore[union-attr] 400 | buffer.write(chunk) 401 | checksum.update(chunk) 402 | buffer.seek(0) 403 | request.body = buffer 404 | return checksum.hexdigest() 405 | 406 | 407 | class AsyncSigV4Signer: 408 | """ 409 | Request signer for applying the AWS Signature Version 4 algorithm. 410 | """ 411 | 412 | async def sign( 413 | self, 414 | *, 415 | signing_properties: SigV4SigningProperties, 416 | request: AWSRequest, 417 | identity: AWSCredentialIdentity, 418 | ) -> AWSRequest: 419 | """Generate and apply a SigV4 Signature to a copy of the supplied request. 420 | 421 | :param signing_properties: 422 | SigV4SigningProperties to define signing primitives such as 423 | the target service, region, and date. 424 | :param request: 425 | An AWSRequest to sign prior to sending to the service. 426 | :param identity: 427 | A set of credentials representing an AWS Identity or role capacity. 428 | """ 429 | # Copy and prepopulate any missing values in the 430 | # supplied request and signing properties. 431 | 432 | await self._validate_identity(identity=identity) 433 | new_signing_properties = await self._normalize_signing_properties( 434 | signing_properties=signing_properties 435 | ) 436 | new_request = await self._generate_new_request(request=request) 437 | await self._apply_required_fields( 438 | request=new_request, 439 | signing_properties=new_signing_properties, 440 | identity=identity, 441 | ) 442 | 443 | # Construct core signing components 444 | canonical_request = await self.canonical_request( 445 | signing_properties=signing_properties, 446 | request=request, 447 | ) 448 | string_to_sign = await self.string_to_sign( 449 | canonical_request=canonical_request, 450 | signing_properties=new_signing_properties, 451 | ) 452 | signature = await self._signature( 453 | string_to_sign=string_to_sign, 454 | secret_key=identity.secret_access_key, 455 | signing_properties=new_signing_properties, 456 | ) 457 | 458 | signing_fields = await self._normalize_signing_fields(request=request) 459 | credential_scope = await self._scope(signing_properties=new_signing_properties) 460 | credential = f"{identity.access_key_id}/{credential_scope}" 461 | authorization = await self.generate_authorization_field( 462 | credential=credential, 463 | signed_headers=list(signing_fields.keys()), 464 | signature=signature, 465 | ) 466 | new_request.fields.set_field(authorization) 467 | return new_request 468 | 469 | async def generate_authorization_field( 470 | self, *, credential: str, signed_headers: list[str], signature: str 471 | ) -> Field: 472 | """Generate the `Authorization` field 473 | 474 | :param credential: 475 | Credential scope string for generating the Authorization header. 476 | Defined as: 477 | //// 478 | :param signed_headers: 479 | A list of the field names used in signing. 480 | :param signature: 481 | Final hash of the SigV4 signing algorithm generated from the 482 | canonical request and string to sign. 483 | """ 484 | signed_headers_str = ";".join(signed_headers) 485 | auth_str = ( 486 | f"AWS4-HMAC-SHA256 Credential={credential}, " 487 | f"SignedHeaders={signed_headers_str}, Signature={signature}" 488 | ) 489 | return Field(name="Authorization", values=[auth_str]) 490 | 491 | async def _signature( 492 | self, 493 | *, 494 | string_to_sign: str, 495 | secret_key: str, 496 | signing_properties: SigV4SigningProperties, 497 | ) -> str: 498 | """Sign the string to sign. 499 | 500 | In SigV4, a signing key is created that is scoped to a specific region and 501 | service. The date, region, service and resulting signing key are individually 502 | hashed, then the composite hash is used to sign the string to sign. 503 | 504 | DateKey = HMAC-SHA256("AWS4"+"", "") 505 | DateRegionKey = HMAC-SHA256(, "") 506 | DateRegionServiceKey = HMAC-SHA256(, "") 507 | SigningKey = HMAC-SHA256(, "aws4_request") 508 | """ 509 | assert signing_properties.get("date") is not None 510 | k_date = await self._hash( 511 | key=f"AWS4{secret_key}".encode(), value=signing_properties["date"][0:8] 512 | ) 513 | k_region = await self._hash(key=k_date, value=signing_properties["region"]) 514 | k_service = await self._hash(key=k_region, value=signing_properties["service"]) 515 | k_signing = await self._hash(key=k_service, value="aws4_request") 516 | final_hash = await self._hash(key=k_signing, value=string_to_sign) 517 | 518 | return final_hash.hex() 519 | 520 | async def _hash(self, key: bytes, value: str) -> bytes: 521 | return hmac.new(key=key, msg=value.encode(), digestmod=sha256).digest() 522 | 523 | async def _validate_identity(self, *, identity: AWSCredentialIdentity) -> None: 524 | """Perform runtime and expiration checks before attempting signing.""" 525 | if not isinstance(identity, AWSCredentialIdentity): 526 | raise ValueError( 527 | "Received unexpected value for identity parameter. Expected " 528 | f"AWSCredentialIdentity but received {type(identity)}." 529 | ) 530 | elif identity.is_expired: 531 | raise ValueError( 532 | f"Provided identity expired at {identity.expiration}. Please " 533 | "refresh the credentials or update the expiration parameter." 534 | ) 535 | 536 | async def _normalize_signing_properties( 537 | self, *, signing_properties: SigV4SigningProperties 538 | ) -> SigV4SigningProperties: 539 | # Create copy of signing properties to avoid mutating the original 540 | new_signing_properties = SigV4SigningProperties(**signing_properties) 541 | new_signing_properties["date"] = await self._resolve_signing_date( 542 | date=new_signing_properties.get("date") 543 | ) 544 | return new_signing_properties 545 | 546 | async def _generate_new_request(self, *, request: AWSRequest) -> AWSRequest: 547 | return deepcopy(request) 548 | 549 | async def _resolve_signing_date(self, *, date: str | None) -> str: 550 | if date is None: 551 | date_obj = datetime.datetime.now(datetime.UTC) 552 | date = date_obj.strftime(SIGV4_TIMESTAMP_FORMAT) 553 | return date 554 | 555 | async def _apply_required_fields( 556 | self, 557 | *, 558 | request: AWSRequest, 559 | signing_properties: SigV4SigningProperties, 560 | identity: AWSCredentialIdentity, 561 | ) -> None: 562 | # Apply required X-Amz-Date if neither X-Amz-Date nor Date are present. 563 | if "Date" not in request.fields and "X-Amz-Date" not in request.fields: 564 | request.fields.set_field( 565 | Field(name="X-Amz-Date", values=[signing_properties["date"]]) 566 | ) 567 | # Apply required X-Amz-Security-Token if token present on identity 568 | if ( 569 | "X-Amz-Security-Token" not in request.fields 570 | and identity.session_token is not None 571 | ): 572 | request.fields.set_field( 573 | Field(name="X-Amz-Security-Token", values=[identity.session_token]) 574 | ) 575 | 576 | async def canonical_request( 577 | self, *, signing_properties: SigV4SigningProperties, request: AWSRequest 578 | ) -> str: 579 | """The canonical request is a standardized string laying out the 580 | components used in the SigV4 signing algorithm. This is useful to quickly 581 | compare inputs to find signature mismatches and unintended variances. 582 | 583 | The SigV4 specification defines the canonical request to be: 584 | \n 585 | \n 586 | \n 587 | \n 588 | \n 589 | 590 | 591 | :param signing_properties: 592 | SigV4SigningProperties to define signing primitives such as 593 | the target service, region, and date. 594 | :param request: 595 | An AWSRequest to use for generating a SigV4 signature. 596 | """ 597 | # We generate the payload first to ensure any field modifications 598 | # are in place before choosing the canonical fields. 599 | canonical_payload = await self._format_canonical_payload( 600 | request=request, signing_properties=signing_properties 601 | ) 602 | canonical_path = await self._format_canonical_path( 603 | path=request.destination.path 604 | ) 605 | canonical_query = await self._format_canonical_query( 606 | query=request.destination.query 607 | ) 608 | normalized_fields = await self._normalize_signing_fields(request=request) 609 | canonical_fields = await self._format_canonical_fields(fields=normalized_fields) 610 | return ( 611 | f"{request.method.upper()}\n" 612 | f"{canonical_path}\n" 613 | f"{canonical_query}\n" 614 | f"{canonical_fields}\n" 615 | f"{';'.join(normalized_fields)}\n" 616 | f"{canonical_payload}" 617 | ) 618 | 619 | async def string_to_sign( 620 | self, 621 | *, 622 | canonical_request: str, 623 | signing_properties: SigV4SigningProperties, 624 | ) -> str: 625 | """The string to sign is the second step of our signing algorithm which 626 | concatenates the formal identifier of our signing algorithm, the signing 627 | DateTime, the scope of our credentials, and a hash of our previously 628 | generated canonical request. This is another checkpoint that can be used 629 | to ensure we're constructing our signature as intended. 630 | 631 | The SigV4 specification defines the string to sign as: 632 | Algorithm \n 633 | RequestDateTime \n 634 | CredentialScope \n 635 | HashedCanonicalRequest 636 | 637 | :param canonical_request: 638 | String generated from the `canonical_request` method. 639 | :param signing_properties: 640 | SigV4SigningProperties to define signing primitives such as 641 | the target service, region, and date. 642 | """ 643 | date = signing_properties.get("date") 644 | if date is None: 645 | raise MissingExpectedParameterException( 646 | "Cannot generate string_to_sign without a valid date " 647 | f"in your signing_properties. Current value: {date}" 648 | ) 649 | scope = await self._scope(signing_properties=signing_properties) 650 | return ( 651 | "AWS4-HMAC-SHA256\n" 652 | f"{date}\n" 653 | f"{scope}\n" 654 | f"{sha256(canonical_request.encode()).hexdigest()}" 655 | ) 656 | 657 | async def _scope(self, signing_properties: SigV4SigningProperties) -> str: 658 | formatted_date = signing_properties["date"][0:8] 659 | region = signing_properties["region"] 660 | service = signing_properties["service"] 661 | # Scope format: ///aws4_request 662 | return f"{formatted_date}/{region}/{service}/aws4_request" 663 | 664 | async def _format_canonical_path(self, *, path: str | None) -> str: 665 | if path is None: 666 | path = "/" 667 | normalized_path = _remove_dot_segments(path) 668 | return quote(string=normalized_path, safe="/%") 669 | 670 | async def _format_canonical_query(self, *, query: str | None) -> str: 671 | if query is None: 672 | return "" 673 | 674 | query_params = parse_qsl(qs=query) 675 | query_parts = ( 676 | (quote(string=key, safe=""), quote(string=value, safe="")) 677 | for key, value in query_params 678 | ) 679 | # key-value pairs must be in sorted order for their encoded forms. 680 | return "&".join(f"{key}={value}" for key, value in sorted(query_parts)) 681 | 682 | async def _normalize_signing_fields(self, *, request: AWSRequest) -> dict[str, str]: 683 | normalized_fields = { 684 | field.name.lower(): field.as_string(delimiter=",") 685 | for field in request.fields 686 | if field.name.lower() not in HEADERS_EXCLUDED_FROM_SIGNING 687 | } 688 | if "host" not in normalized_fields: 689 | normalized_fields["host"] = await self._normalize_host_field( 690 | uri=request.destination 691 | ) 692 | 693 | return dict(sorted(normalized_fields.items())) 694 | 695 | async def _normalize_host_field(self, *, uri: URI) -> str: 696 | if uri.port is not None and DEFAULT_PORTS.get(uri.scheme) == uri.port: 697 | uri_dict = uri.to_dict() 698 | uri_dict.update({"port": None}) 699 | uri = URI(**uri_dict) 700 | return uri.netloc 701 | 702 | async def _format_canonical_fields(self, *, fields: dict[str, str]) -> str: 703 | return "".join( 704 | f"{key}:{' '.join(value.split())}\n" for key, value in fields.items() 705 | ) 706 | 707 | async def _should_sha256_sign_payload( 708 | self, 709 | *, 710 | request: AWSRequest, 711 | signing_properties: SigV4SigningProperties, 712 | ) -> bool: 713 | # All insecure connections should be signed 714 | if request.destination.scheme != "https": 715 | return True 716 | 717 | return signing_properties.get("payload_signing_enabled", True) 718 | 719 | async def _format_canonical_payload( 720 | self, 721 | *, 722 | request: AWSRequest, 723 | signing_properties: SigV4SigningProperties, 724 | ) -> str: 725 | payload_hash = await self._compute_payload_hash( 726 | request=request, signing_properties=signing_properties 727 | ) 728 | if signing_properties.get("content_checksum_enabled", False): 729 | request.fields.set_field( 730 | Field(name="X-Amz-Content-SHA256", values=[payload_hash]) 731 | ) 732 | return payload_hash 733 | 734 | async def _compute_payload_hash( 735 | self, *, request: AWSRequest, signing_properties: SigV4SigningProperties 736 | ) -> str: 737 | if not await self._should_sha256_sign_payload( 738 | request=request, signing_properties=signing_properties 739 | ): 740 | return UNSIGNED_PAYLOAD 741 | 742 | body = request.body 743 | 744 | if body is None: 745 | return EMPTY_SHA256_HASH 746 | 747 | if not isinstance(request.body, AsyncIterable): 748 | raise TypeError( 749 | "A sync body was attached to an asynchronous signer. Please use " 750 | "SigV4Signer for sync AWSRequests or ensure your body is " 751 | "of type AsyncIterable[bytes]." 752 | ) 753 | warnings.warn( 754 | "Payload signing is enabled. This may result in " 755 | "decreased performance for large request bodies.", 756 | AWSSDKWarning, 757 | ) 758 | 759 | checksum = sha256() 760 | if hasattr(body, "seek") and hasattr(body, "tell"): 761 | position = body.tell() 762 | async for chunk in body: # type: ignore[union-attr] 763 | checksum.update(chunk) 764 | body.seek(position) 765 | else: 766 | buffer = io.BytesIO() 767 | async for chunk in body: # type: ignore[union-attr] 768 | buffer.write(chunk) 769 | checksum.update(chunk) 770 | buffer.seek(0) 771 | request.body = AsyncBytesReader(buffer) 772 | return checksum.hexdigest() 773 | 774 | 775 | def _remove_dot_segments(path: str, remove_consecutive_slashes: bool = True) -> str: 776 | """Removes dot segments from a path per :rfc:`3986#section-5.2.4`. 777 | Optionally removes consecutive slashes, true by default. 778 | :param path: The path to modify. 779 | :param remove_consecutive_slashes: Whether to remove consecutive slashes. 780 | :returns: The path with dot segments removed. 781 | """ 782 | output = [] 783 | for segment in path.split("/"): 784 | if segment == ".": 785 | continue 786 | elif segment != "..": 787 | output.append(segment) 788 | elif output: 789 | output.pop() 790 | if path.startswith("/") and (not output or output[0]): 791 | output.insert(0, "") 792 | if output and path.endswith(("/.", "/..")): 793 | output.append("") 794 | result = "/".join(output) 795 | if remove_consecutive_slashes: 796 | result = result.replace("//", "/") 797 | return result 798 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-sdk-python-signers/7cf0664b60b58a96a9392f8298d114c84f2b50f0/tests/__init__.py -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-sdk-python-signers/7cf0664b60b58a96a9392f8298d114c84f2b50f0/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-sdk-python-signers/7cf0664b60b58a96a9392f8298d114c84f2b50f0/tests/unit/auth/__init__.py -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/NOTICE: -------------------------------------------------------------------------------- 1 | AWS Signature Version 4 Test Suite 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-header-key-duplicate/get-header-key-duplicate.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;my-header1;x-amz-date, Signature=c9d5ea9f3f72853aea855b47ea873832890dbdd183b4468f858259531a5138ea -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-header-key-duplicate/get-header-key-duplicate.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | 4 | host:example.amazonaws.com 5 | my-header1:value2,value2,value1 6 | x-amz-date:20150830T123600Z 7 | 8 | host;my-header1;x-amz-date 9 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-header-key-duplicate/get-header-key-duplicate.req: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host:example.amazonaws.com 3 | My-Header1:value2 4 | My-Header1:value2 5 | My-Header1:value1 6 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-header-key-duplicate/get-header-key-duplicate.sreq: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host:example.amazonaws.com 3 | My-Header1:value2 4 | My-Header1:value2 5 | My-Header1:value1 6 | X-Amz-Date:20150830T123600Z 7 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;my-header1;x-amz-date, Signature=c9d5ea9f3f72853aea855b47ea873832890dbdd183b4468f858259531a5138ea -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-header-key-duplicate/get-header-key-duplicate.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | dc7f04a3abfde8d472b0ab1a418b741b7c67174dad1551b4117b15527fbe966c -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-header-value-multiline/get-header-value-multiline.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;my-header1;x-amz-date, Signature=cfd34249e4b1c8d6b91ef74165d41a32e5fab3306300901bb65a51a73575eefd -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-header-value-multiline/get-header-value-multiline.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | 4 | host:example.amazonaws.com 5 | my-header1:value1 value2 value3 6 | x-amz-date:20150830T123600Z 7 | 8 | host;my-header1;x-amz-date 9 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-header-value-multiline/get-header-value-multiline.req: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host:example.amazonaws.com 3 | My-Header1:value1 4 | value2 5 | value3 6 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-header-value-multiline/get-header-value-multiline.sreq: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host:example.amazonaws.com 3 | My-Header1:value1 4 | value2 5 | value3 6 | X-Amz-Date:20150830T123600Z 7 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;my-header1;x-amz-date, Signature=cfd34249e4b1c8d6b91ef74165d41a32e5fab3306300901bb65a51a73575eefd -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-header-value-multiline/get-header-value-multiline.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | e99419459a677bc11de234014be3c4e72c1ea5b454ceb58b613061f5d7a162e8 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-header-value-order/get-header-value-order.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;my-header1;x-amz-date, Signature=08c7e5a9acfcfeb3ab6b2185e75ce8b1deb5e634ec47601a50643f830c755c01 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-header-value-order/get-header-value-order.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | 4 | host:example.amazonaws.com 5 | my-header1:value4,value1,value3,value2 6 | x-amz-date:20150830T123600Z 7 | 8 | host;my-header1;x-amz-date 9 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-header-value-order/get-header-value-order.req: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host:example.amazonaws.com 3 | My-Header1:value4 4 | My-Header1:value1 5 | My-Header1:value3 6 | My-Header1:value2 7 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-header-value-order/get-header-value-order.sreq: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host:example.amazonaws.com 3 | My-Header1:value4 4 | My-Header1:value1 5 | My-Header1:value3 6 | My-Header1:value2 7 | X-Amz-Date:20150830T123600Z 8 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;my-header1;x-amz-date, Signature=08c7e5a9acfcfeb3ab6b2185e75ce8b1deb5e634ec47601a50643f830c755c01 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-header-value-order/get-header-value-order.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 31ce73cd3f3d9f66977ad3dd957dc47af14df92fcd8509f59b349e9137c58b86 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-header-value-trim/get-header-value-trim.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;my-header1;my-header2;x-amz-date, Signature=acc3ed3afb60bb290fc8d2dd0098b9911fcaa05412b367055dee359757a9c736 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-header-value-trim/get-header-value-trim.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | 4 | host:example.amazonaws.com 5 | my-header1:value1 6 | my-header2:"a b c" 7 | x-amz-date:20150830T123600Z 8 | 9 | host;my-header1;my-header2;x-amz-date 10 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-header-value-trim/get-header-value-trim.req: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host:example.amazonaws.com 3 | My-Header1: value1 4 | My-Header2: "a b c" 5 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-header-value-trim/get-header-value-trim.sreq: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host:example.amazonaws.com 3 | My-Header1: value1 4 | My-Header2: "a b c" 5 | X-Amz-Date:20150830T123600Z 6 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;my-header1;my-header2;x-amz-date, Signature=acc3ed3afb60bb290fc8d2dd0098b9911fcaa05412b367055dee359757a9c736 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-header-value-trim/get-header-value-trim.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | a726db9b0df21c14f559d0a978e563112acb1b9e05476f0a6a1c7d68f28605c7 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-unreserved/get-unreserved.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=07ef7494c76fa4850883e2b006601f940f8a34d404d0cfa977f52a65bbf5f24f -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-unreserved/get-unreserved.creq: -------------------------------------------------------------------------------- 1 | GET 2 | /-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-unreserved/get-unreserved.req: -------------------------------------------------------------------------------- 1 | GET /-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-unreserved/get-unreserved.sreq: -------------------------------------------------------------------------------- 1 | GET /-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=07ef7494c76fa4850883e2b006601f940f8a34d404d0cfa977f52a65bbf5f24f -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-unreserved/get-unreserved.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 6a968768eefaa713e2a6b16b589a8ea192661f098f37349f4e2c0082757446f9 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-utf8/get-utf8.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=8318018e0b0f223aa2bbf98705b62bb787dc9c0e678f255a891fd03141be5d85 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-utf8/get-utf8.creq: -------------------------------------------------------------------------------- 1 | GET 2 | /%E1%88%B4 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-utf8/get-utf8.req: -------------------------------------------------------------------------------- 1 | GET /ሴ HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-utf8/get-utf8.sreq: -------------------------------------------------------------------------------- 1 | GET /ሴ HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=8318018e0b0f223aa2bbf98705b62bb787dc9c0e678f255a891fd03141be5d85 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-utf8/get-utf8.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 2a0a97d02205e45ce2e994789806b19270cfbbb0921b278ccf58f5249ac42102 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-empty-query-key/get-vanilla-empty-query-key.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=a67d582fa61cc504c4bae71f336f98b97f1ea3c7a6bfe1b6e45aec72011b9aeb -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-empty-query-key/get-vanilla-empty-query-key.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | Param1=value1 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-empty-query-key/get-vanilla-empty-query-key.req: -------------------------------------------------------------------------------- 1 | GET /?Param1=value1 HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-empty-query-key/get-vanilla-empty-query-key.sreq: -------------------------------------------------------------------------------- 1 | GET /?Param1=value1 HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=a67d582fa61cc504c4bae71f336f98b97f1ea3c7a6bfe1b6e45aec72011b9aeb -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-empty-query-key/get-vanilla-empty-query-key.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 1e24db194ed7d0eec2de28d7369675a243488e08526e8c1c73571282f7c517ab -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-query-order-encoded/get-vanilla-query-order-encoded.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=371d3713e185cc334048618a97f809c9ffe339c62934c032af5a0e595648fcac -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-query-order-encoded/get-vanilla-query-order-encoded.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | %E1%88%B4=Value1&Param=Value2&Param-3=Value3 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-query-order-encoded/get-vanilla-query-order-encoded.req: -------------------------------------------------------------------------------- 1 | GET /?Param-3=Value3&Param=Value2&%E1%88%B4=Value1 HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-query-order-encoded/get-vanilla-query-order-encoded.sreq: -------------------------------------------------------------------------------- 1 | GET /?Param-3=Value3&Param=Value2&%E1%88%B4=Value1 HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=371d3713e185cc334048618a97f809c9ffe339c62934c032af5a0e595648fcac -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-query-order-encoded/get-vanilla-query-order-encoded.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 868294f5c38bd141c4972a373a76654f1418a8e4fc18b2e7903ae45e8ae0ec71 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-query-order-key-case/get-vanilla-query-order-key-case.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=b97d918cfa904a5beff61c982a1b6f458b799221646efd99d3219ec94cdf2500 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-query-order-key-case/get-vanilla-query-order-key-case.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | Param1=value1&Param2=value2 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-query-order-key-case/get-vanilla-query-order-key-case.req: -------------------------------------------------------------------------------- 1 | GET /?Param2=value2&Param1=value1 HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-query-order-key-case/get-vanilla-query-order-key-case.sreq: -------------------------------------------------------------------------------- 1 | GET /?Param2=value2&Param1=value1 HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=b97d918cfa904a5beff61c982a1b6f458b799221646efd99d3219ec94cdf2500 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-query-order-key-case/get-vanilla-query-order-key-case.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 816cd5b414d056048ba4f7c5386d6e0533120fb1fcfa93762cf0fc39e2cf19e0 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-query-order-key/get-vanilla-query-order-key.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=eedbc4e291e521cf13422ffca22be7d2eb8146eecf653089df300a15b2382bd1 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-query-order-key/get-vanilla-query-order-key.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | Param1=Value1&Param1=value2 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-query-order-key/get-vanilla-query-order-key.req: -------------------------------------------------------------------------------- 1 | GET /?Param1=value2&Param1=Value1 HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-query-order-key/get-vanilla-query-order-key.sreq: -------------------------------------------------------------------------------- 1 | GET /?Param1=value2&Param1=Value1 HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=eedbc4e291e521cf13422ffca22be7d2eb8146eecf653089df300a15b2382bd1 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-query-order-key/get-vanilla-query-order-key.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 704b4cef673542d84cdff252633f065e8daeba5f168b77116f8b1bcaf3d38f89 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-query-order-value/get-vanilla-query-order-value.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5772eed61e12b33fae39ee5e7012498b51d56abc0abb7c60486157bd471c4694 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-query-order-value/get-vanilla-query-order-value.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | Param1=value1&Param1=value2 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-query-order-value/get-vanilla-query-order-value.req: -------------------------------------------------------------------------------- 1 | GET /?Param1=value2&Param1=value1 HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-query-order-value/get-vanilla-query-order-value.sreq: -------------------------------------------------------------------------------- 1 | GET /?Param1=value2&Param1=value1 HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5772eed61e12b33fae39ee5e7012498b51d56abc0abb7c60486157bd471c4694 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-query-order-value/get-vanilla-query-order-value.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | c968629d70850097a2d8781c9bf7edcb988b04cac14cca9be4acc3595f884606 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-query-unreserved/get-vanilla-query-unreserved.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=9c3e54bfcdf0b19771a7f523ee5669cdf59bc7cc0884027167c21bb143a40197 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-query-unreserved/get-vanilla-query-unreserved.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | -._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz=-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-query-unreserved/get-vanilla-query-unreserved.req: -------------------------------------------------------------------------------- 1 | GET /?-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz=-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-query-unreserved/get-vanilla-query-unreserved.sreq: -------------------------------------------------------------------------------- 1 | GET /?-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz=-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=9c3e54bfcdf0b19771a7f523ee5669cdf59bc7cc0884027167c21bb143a40197 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-query-unreserved/get-vanilla-query-unreserved.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | c30d4703d9f799439be92736156d47ccfb2d879ddf56f5befa6d1d6aab979177 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-query/get-vanilla-query.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-query/get-vanilla-query.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-query/get-vanilla-query.req: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-query/get-vanilla-query.sreq: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-query/get-vanilla-query.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | bb579772317eb040ac9ed261061d46c1f17a8133879d6129b6e1c25292927e63 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-utf8-query/get-vanilla-utf8-query.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=2cdec8eed098649ff3a119c94853b13c643bcf08f8b0a1d91e12c9027818dd04 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-utf8-query/get-vanilla-utf8-query.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | %E1%88%B4=bar 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-utf8-query/get-vanilla-utf8-query.req: -------------------------------------------------------------------------------- 1 | GET /?ሴ=bar HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-utf8-query/get-vanilla-utf8-query.sreq: -------------------------------------------------------------------------------- 1 | GET /?ሴ=bar HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=2cdec8eed098649ff3a119c94853b13c643bcf08f8b0a1d91e12c9027818dd04 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-utf8-query/get-vanilla-utf8-query.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | eb30c5bed55734080471a834cc727ae56beb50e5f39d1bff6d0d38cb192a7073 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-with-session-token/get-vanilla-with-session-token.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature=07ec1639c89043aa0e3e2de82b96708f198cceab042d4a97044c66dd9f74e7f8 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-with-session-token/get-vanilla-with-session-token.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | x-amz-security-token:6e86291e8372ff2a2260956d9b8aae1d763fbf315fa00fa31553b73ebf194267 7 | 8 | host;x-amz-date;x-amz-security-token 9 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-with-session-token/get-vanilla-with-session-token.req: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-with-session-token/get-vanilla-with-session-token.sreq: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | X-Amz-Security-Token:6e86291e8372ff2a2260956d9b8aae1d763fbf315fa00fa31553b73ebf194267 5 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla-with-session-token/get-vanilla-with-session-token.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 067b36aa60031588cea4a4cde1f21215227a047690c72247f1d70b32fbbfad2b -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla/get-vanilla.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla/get-vanilla.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla/get-vanilla.req: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla/get-vanilla.sreq: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/get-vanilla/get-vanilla.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | bb579772317eb040ac9ed261061d46c1f17a8133879d6129b6e1c25292927e63 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-relative-relative/get-relative-relative.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-relative-relative/get-relative-relative.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-relative-relative/get-relative-relative.req: -------------------------------------------------------------------------------- 1 | GET /example1/example2/../.. HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-relative-relative/get-relative-relative.sreq: -------------------------------------------------------------------------------- 1 | GET /example1/example2/../.. HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-relative-relative/get-relative-relative.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | bb579772317eb040ac9ed261061d46c1f17a8133879d6129b6e1c25292927e63 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-relative/get-relative.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-relative/get-relative.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-relative/get-relative.req: -------------------------------------------------------------------------------- 1 | GET /example/.. HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-relative/get-relative.sreq: -------------------------------------------------------------------------------- 1 | GET /example/.. HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-relative/get-relative.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | bb579772317eb040ac9ed261061d46c1f17a8133879d6129b6e1c25292927e63 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-slash-dot-slash/get-slash-dot-slash.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-slash-dot-slash/get-slash-dot-slash.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-slash-dot-slash/get-slash-dot-slash.req: -------------------------------------------------------------------------------- 1 | GET /./ HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-slash-dot-slash/get-slash-dot-slash.sreq: -------------------------------------------------------------------------------- 1 | GET /./ HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-slash-dot-slash/get-slash-dot-slash.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | bb579772317eb040ac9ed261061d46c1f17a8133879d6129b6e1c25292927e63 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-slash-pointless-dot/get-slash-pointless-dot.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=ef75d96142cf21edca26f06005da7988e4f8dc83a165a80865db7089db637ec5 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-slash-pointless-dot/get-slash-pointless-dot.creq: -------------------------------------------------------------------------------- 1 | GET 2 | /example 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-slash-pointless-dot/get-slash-pointless-dot.req: -------------------------------------------------------------------------------- 1 | GET /./example HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-slash-pointless-dot/get-slash-pointless-dot.sreq: -------------------------------------------------------------------------------- 1 | GET /./example HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=ef75d96142cf21edca26f06005da7988e4f8dc83a165a80865db7089db637ec5 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-slash-pointless-dot/get-slash-pointless-dot.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 214d50c111a8edc4819da6a636336472c916b5240f51e9a51b5c3305180cf702 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-slash/get-slash.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-slash/get-slash.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-slash/get-slash.req: -------------------------------------------------------------------------------- 1 | GET // HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-slash/get-slash.sreq: -------------------------------------------------------------------------------- 1 | GET // HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-slash/get-slash.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | bb579772317eb040ac9ed261061d46c1f17a8133879d6129b6e1c25292927e63 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-slashes/get-slashes.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=9a624bd73a37c9a373b5312afbebe7a714a789de108f0bdfe846570885f57e84 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-slashes/get-slashes.creq: -------------------------------------------------------------------------------- 1 | GET 2 | /example/ 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-slashes/get-slashes.req: -------------------------------------------------------------------------------- 1 | GET //example// HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-slashes/get-slashes.sreq: -------------------------------------------------------------------------------- 1 | GET //example// HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=9a624bd73a37c9a373b5312afbebe7a714a789de108f0bdfe846570885f57e84 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-slashes/get-slashes.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | cb96b4ac96d501f7c5c15bc6d67b3035061cfced4af6585ad927f7e6c985c015 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-space/get-space.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=652487583200325589f1fba4c7e578f72c47cb61beeca81406b39ddec1366741 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-space/get-space.creq: -------------------------------------------------------------------------------- 1 | GET 2 | /example%20space/ 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-space/get-space.req: -------------------------------------------------------------------------------- 1 | GET /example%20space/ HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-space/get-space.sreq: -------------------------------------------------------------------------------- 1 | GET /example%20space/ HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=652487583200325589f1fba4c7e578f72c47cb61beeca81406b39ddec1366741 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-space/get-space.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 63ee75631ed7234ae61b5f736dfc7754cdccfedbff4b5128a915706ee9390d86 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-special-character/get-special-character.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=a853c9b21b528b19643d00910d35b83a10c366a10833ceefb45edd6c80e40f27 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-special-character/get-special-character.creq: -------------------------------------------------------------------------------- 1 | GET 2 | /example/%24delete 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-special-character/get-special-character.req: -------------------------------------------------------------------------------- 1 | GET /example/$delete HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-special-character/get-special-character.sreq: -------------------------------------------------------------------------------- 1 | GET /example/$delete HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=a853c9b21b528b19643d00910d35b83a10c366a10833ceefb45edd6c80e40f27 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/get-special-character/get-special-character.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 4053e45b5cef7cec5e17f736b1c12b3faf0388fd4c0bd24326386f132039ce5c -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/normalize-path/normalize-path.txt: -------------------------------------------------------------------------------- 1 | A note about signing requests to Amazon S3: 2 | 3 | In exception to this, you do not normalize URI paths for requests to Amazon S3. For example, if you have a bucket with an object named my-object//example//photo.user, use that path. Normalizing the path to my-object/example/photo.user will cause the request to fail. For more information, see Task 1: Create a Canonical Request in the Amazon Simple Storage Service API Reference: http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html#canonical-request -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-header-key-case/post-header-key-case.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5da7c1a2acd57cee7505fc6676e4e544621c30862966e37dddb68e92efbe5d6b -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-header-key-case/post-header-key-case.creq: -------------------------------------------------------------------------------- 1 | POST 2 | / 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-header-key-case/post-header-key-case.req: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-header-key-case/post-header-key-case.sreq: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5da7c1a2acd57cee7505fc6676e4e544621c30862966e37dddb68e92efbe5d6b -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-header-key-case/post-header-key-case.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 553f88c9e4d10fc9e109e2aeb65f030801b70c2f6468faca261d401ae622fc87 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-header-key-sort/post-header-key-sort.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;my-header1;x-amz-date, Signature=c5410059b04c1ee005303aed430f6e6645f61f4dc9e1461ec8f8916fdf18852c -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-header-key-sort/post-header-key-sort.creq: -------------------------------------------------------------------------------- 1 | POST 2 | / 3 | 4 | host:example.amazonaws.com 5 | my-header1:value1 6 | x-amz-date:20150830T123600Z 7 | 8 | host;my-header1;x-amz-date 9 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-header-key-sort/post-header-key-sort.req: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Host:example.amazonaws.com 3 | My-Header1:value1 4 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-header-key-sort/post-header-key-sort.sreq: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Host:example.amazonaws.com 3 | My-Header1:value1 4 | X-Amz-Date:20150830T123600Z 5 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;my-header1;x-amz-date, Signature=c5410059b04c1ee005303aed430f6e6645f61f4dc9e1461ec8f8916fdf18852c -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-header-key-sort/post-header-key-sort.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 9368318c2967cf6de74404b30c65a91e8f6253e0a8659d6d5319f1a812f87d65 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-header-value-case/post-header-value-case.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;my-header1;x-amz-date, Signature=cdbc9802e29d2942e5e10b5bccfdd67c5f22c7c4e8ae67b53629efa58b974b7d -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-header-value-case/post-header-value-case.creq: -------------------------------------------------------------------------------- 1 | POST 2 | / 3 | 4 | host:example.amazonaws.com 5 | my-header1:VALUE1 6 | x-amz-date:20150830T123600Z 7 | 8 | host;my-header1;x-amz-date 9 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-header-value-case/post-header-value-case.req: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Host:example.amazonaws.com 3 | My-Header1:VALUE1 4 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-header-value-case/post-header-value-case.sreq: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Host:example.amazonaws.com 3 | My-Header1:VALUE1 4 | X-Amz-Date:20150830T123600Z 5 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;my-header1;x-amz-date, Signature=cdbc9802e29d2942e5e10b5bccfdd67c5f22c7c4e8ae67b53629efa58b974b7d -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-header-value-case/post-header-value-case.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | d51ced243e649e3de6ef63afbbdcbca03131a21a7103a1583706a64618606a93 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-sts-token/post-sts-header-after/post-sts-header-after.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5da7c1a2acd57cee7505fc6676e4e544621c30862966e37dddb68e92efbe5d6b -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-sts-token/post-sts-header-after/post-sts-header-after.creq: -------------------------------------------------------------------------------- 1 | POST 2 | / 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-sts-token/post-sts-header-after/post-sts-header-after.req: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-sts-token/post-sts-header-after/post-sts-header-after.sreq: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | X-Amz-Security-Token:AQoDYXdzEPT//////////wEXAMPLEtc764bNrC9SAPBSM22wDOk4x4HIZ8j4FZTwdQWLWsKWHGBuFqwAeMicRXmxfpSPfIeoIYRqTflfKD8YUuwthAx7mSEI/qkPpKPi/kMcGdQrmGdeehM4IC1NtBmUpp2wUE8phUZampKsburEDy0KPkyQDYwT7WZ0wq5VSXDvp75YU9HFvlRd8Tx6q6fE8YQcHNVXAkiY9q6d+xo0rKwT38xVqr7ZD0u0iPPkUL64lIZbqBAz+scqKmlzm8FDrypNC9Yjc8fPOLn9FX9KSYvKTr4rvx3iSIlTJabIQwj2ICCR/oLxBA== 5 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5da7c1a2acd57cee7505fc6676e4e544621c30862966e37dddb68e92efbe5d6b -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-sts-token/post-sts-header-after/post-sts-header-after.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 553f88c9e4d10fc9e109e2aeb65f030801b70c2f6468faca261d401ae622fc87 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-sts-token/post-sts-header-before/post-sts-header-before.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature=85d96828115b5dc0cfc3bd16ad9e210dd772bbebba041836c64533a82be05ead -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-sts-token/post-sts-header-before/post-sts-header-before.creq: -------------------------------------------------------------------------------- 1 | POST 2 | / 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | x-amz-security-token:AQoDYXdzEPT//////////wEXAMPLEtc764bNrC9SAPBSM22wDOk4x4HIZ8j4FZTwdQWLWsKWHGBuFqwAeMicRXmxfpSPfIeoIYRqTflfKD8YUuwthAx7mSEI/qkPpKPi/kMcGdQrmGdeehM4IC1NtBmUpp2wUE8phUZampKsburEDy0KPkyQDYwT7WZ0wq5VSXDvp75YU9HFvlRd8Tx6q6fE8YQcHNVXAkiY9q6d+xo0rKwT38xVqr7ZD0u0iPPkUL64lIZbqBAz+scqKmlzm8FDrypNC9Yjc8fPOLn9FX9KSYvKTr4rvx3iSIlTJabIQwj2ICCR/oLxBA== 7 | 8 | host;x-amz-date;x-amz-security-token 9 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-sts-token/post-sts-header-before/post-sts-header-before.req: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | X-Amz-Security-Token:AQoDYXdzEPT//////////wEXAMPLEtc764bNrC9SAPBSM22wDOk4x4HIZ8j4FZTwdQWLWsKWHGBuFqwAeMicRXmxfpSPfIeoIYRqTflfKD8YUuwthAx7mSEI/qkPpKPi/kMcGdQrmGdeehM4IC1NtBmUpp2wUE8phUZampKsburEDy0KPkyQDYwT7WZ0wq5VSXDvp75YU9HFvlRd8Tx6q6fE8YQcHNVXAkiY9q6d+xo0rKwT38xVqr7ZD0u0iPPkUL64lIZbqBAz+scqKmlzm8FDrypNC9Yjc8fPOLn9FX9KSYvKTr4rvx3iSIlTJabIQwj2ICCR/oLxBA== -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-sts-token/post-sts-header-before/post-sts-header-before.sreq: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | X-Amz-Security-Token:AQoDYXdzEPT//////////wEXAMPLEtc764bNrC9SAPBSM22wDOk4x4HIZ8j4FZTwdQWLWsKWHGBuFqwAeMicRXmxfpSPfIeoIYRqTflfKD8YUuwthAx7mSEI/qkPpKPi/kMcGdQrmGdeehM4IC1NtBmUpp2wUE8phUZampKsburEDy0KPkyQDYwT7WZ0wq5VSXDvp75YU9HFvlRd8Tx6q6fE8YQcHNVXAkiY9q6d+xo0rKwT38xVqr7ZD0u0iPPkUL64lIZbqBAz+scqKmlzm8FDrypNC9Yjc8fPOLn9FX9KSYvKTr4rvx3iSIlTJabIQwj2ICCR/oLxBA== 5 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature=85d96828115b5dc0cfc3bd16ad9e210dd772bbebba041836c64533a82be05ead -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-sts-token/post-sts-header-before/post-sts-header-before.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | c237e1b440d4c63c32ca95b5b99481081cb7b13c7e40434868e71567c1a882f6 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-sts-token/readme.txt: -------------------------------------------------------------------------------- 1 | A note about using temporary security credentials: 2 | 3 | You can use temporary security credentials provided by the AWS Security Token Service (AWS STS) to sign a request. The process is the same as using long-term credentials but requires an additional HTTP header or query string parameter for the security token. The name of the header or query string parameter is X-Amz-Security-Token, and the value is the session token (the string that you received from AWS STS when you obtained temporary security credentials). 4 | 5 | When you add X-Amz-Security-Token, some services require that you include this parameter in the canonical (signed) request. For other services, you add this parameter at the end, after you calculate the signature. For details see the API reference documentation for that service. 6 | 7 | The test suite has 2 examples: 8 | 9 | post-sts-header-before - The X-Amz-Security-Token header is part of the canonical request. 10 | 11 | post-sts-header-after - The X-Amz-Security-Token header is added to the request after you calculate the signature. 12 | 13 | The test suite uses this example value for X-Amz-Security-Token: 14 | 15 | AQoDYXdzEPT//////////wEXAMPLEtc764bNrC9SAPBSM22wDOk4x4HIZ8j4FZTwdQWLWsKWHGBuFqwAeMicRXmxfpSPfIeoIYRqTflfKD8YUuwthAx7mSEI/qkPpKPi/kMcGdQrmGdeehM4IC1NtBmUpp2wUE8phUZampKsburEDy0KPkyQDYwT7WZ0wq5VSXDvp75YU9HFvlRd8Tx6q6fE8YQcHNVXAkiY9q6d+xo0rKwT38xVqr7ZD0u0iPPkUL64lIZbqBAz+scqKmlzm8FDrypNC9Yjc8fPOLn9FX9KSYvKTr4rvx3iSIlTJabIQwj2ICCR/oLxBA== -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-vanilla-empty-query-value/post-vanilla-empty-query-value.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=28038455d6de14eafc1f9222cf5aa6f1a96197d7deb8263271d420d138af7f11 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-vanilla-empty-query-value/post-vanilla-empty-query-value.creq: -------------------------------------------------------------------------------- 1 | POST 2 | / 3 | Param1=value1 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-vanilla-empty-query-value/post-vanilla-empty-query-value.req: -------------------------------------------------------------------------------- 1 | POST /?Param1=value1 HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-vanilla-empty-query-value/post-vanilla-empty-query-value.sreq: -------------------------------------------------------------------------------- 1 | POST /?Param1=value1 HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=28038455d6de14eafc1f9222cf5aa6f1a96197d7deb8263271d420d138af7f11 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-vanilla-empty-query-value/post-vanilla-empty-query-value.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 9d659678c1756bb3113e2ce898845a0a79dbbc57b740555917687f1b3340fbbd -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-vanilla-query/post-vanilla-query.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=28038455d6de14eafc1f9222cf5aa6f1a96197d7deb8263271d420d138af7f11 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-vanilla-query/post-vanilla-query.creq: -------------------------------------------------------------------------------- 1 | POST 2 | / 3 | Param1=value1 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-vanilla-query/post-vanilla-query.req: -------------------------------------------------------------------------------- 1 | POST /?Param1=value1 HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-vanilla-query/post-vanilla-query.sreq: -------------------------------------------------------------------------------- 1 | POST /?Param1=value1 HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=28038455d6de14eafc1f9222cf5aa6f1a96197d7deb8263271d420d138af7f11 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-vanilla-query/post-vanilla-query.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 9d659678c1756bb3113e2ce898845a0a79dbbc57b740555917687f1b3340fbbd -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-vanilla/post-vanilla.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5da7c1a2acd57cee7505fc6676e4e544621c30862966e37dddb68e92efbe5d6b -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-vanilla/post-vanilla.creq: -------------------------------------------------------------------------------- 1 | POST 2 | / 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-vanilla/post-vanilla.req: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-vanilla/post-vanilla.sreq: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5da7c1a2acd57cee7505fc6676e4e544621c30862966e37dddb68e92efbe5d6b -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-vanilla/post-vanilla.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 553f88c9e4d10fc9e109e2aeb65f030801b70c2f6468faca261d401ae622fc87 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-x-www-form-urlencoded-parameters/post-x-www-form-urlencoded-parameters.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=1a72ec8f64bd914b0e42e42607c7fbce7fb2c7465f63e3092b3b0d39fa77a6fe -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-x-www-form-urlencoded-parameters/post-x-www-form-urlencoded-parameters.creq: -------------------------------------------------------------------------------- 1 | POST 2 | / 3 | 4 | content-type:application/x-www-form-urlencoded; charset=utf8 5 | host:example.amazonaws.com 6 | x-amz-date:20150830T123600Z 7 | 8 | content-type;host;x-amz-date 9 | 9095672bbd1f56dfc5b65f3e153adc8731a4a654192329106275f4c7b24d0b6e -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-x-www-form-urlencoded-parameters/post-x-www-form-urlencoded-parameters.req: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Content-Type:application/x-www-form-urlencoded; charset=utf8 3 | Host:example.amazonaws.com 4 | X-Amz-Date:20150830T123600Z 5 | 6 | Param1=value1 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-x-www-form-urlencoded-parameters/post-x-www-form-urlencoded-parameters.sreq: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Content-Type:application/x-www-form-urlencoded; charset=utf8 3 | Host:example.amazonaws.com 4 | X-Amz-Date:20150830T123600Z 5 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=1a72ec8f64bd914b0e42e42607c7fbce7fb2c7465f63e3092b3b0d39fa77a6fe 6 | 7 | Param1=value1 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-x-www-form-urlencoded-parameters/post-x-www-form-urlencoded-parameters.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 2e1cf7ed91881a30569e46552437e4156c823447bf1781b921b5d486c568dd1c -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-x-www-form-urlencoded/post-x-www-form-urlencoded.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=ff11897932ad3f4e8b18135d722051e5ac45fc38421b1da7b9d196a0fe09473a -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-x-www-form-urlencoded/post-x-www-form-urlencoded.creq: -------------------------------------------------------------------------------- 1 | POST 2 | / 3 | 4 | content-type:application/x-www-form-urlencoded 5 | host:example.amazonaws.com 6 | x-amz-date:20150830T123600Z 7 | 8 | content-type;host;x-amz-date 9 | 9095672bbd1f56dfc5b65f3e153adc8731a4a654192329106275f4c7b24d0b6e -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-x-www-form-urlencoded/post-x-www-form-urlencoded.req: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Content-Type:application/x-www-form-urlencoded 3 | Host:example.amazonaws.com 4 | X-Amz-Date:20150830T123600Z 5 | 6 | Param1=value1 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-x-www-form-urlencoded/post-x-www-form-urlencoded.sreq: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Content-Type:application/x-www-form-urlencoded 3 | Host:example.amazonaws.com 4 | X-Amz-Date:20150830T123600Z 5 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=ff11897932ad3f4e8b18135d722051e5ac45fc38421b1da7b9d196a0fe09473a 6 | 7 | Param1=value1 -------------------------------------------------------------------------------- /tests/unit/auth/aws4_testsuite/post-x-www-form-urlencoded/post-x-www-form-urlencoded.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 42a5e5bb34198acb3e84da4f085bb7927f2bc277ca766e6d19c73c2154021281 -------------------------------------------------------------------------------- /tests/unit/auth/test_sigv4.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pathlib 3 | import re 4 | from collections.abc import Iterable 5 | from datetime import UTC, datetime 6 | from http.server import BaseHTTPRequestHandler 7 | from io import BytesIO 8 | 9 | import pytest 10 | from aws_sdk_signers import ( 11 | URI, 12 | AsyncBytesReader, 13 | AWSCredentialIdentity, 14 | AWSRequest, 15 | Field, 16 | Fields, 17 | ) 18 | from aws_sdk_signers.exceptions import AWSSDKWarning 19 | from aws_sdk_signers.signers import ( 20 | SIGV4_TIMESTAMP_FORMAT, 21 | AsyncSigV4Signer, 22 | SigV4Signer, 23 | SigV4SigningProperties, 24 | ) 25 | from freezegun import freeze_time 26 | 27 | SECRET_KEY: str = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY" 28 | ACCESS_KEY: str = "AKIDEXAMPLE" 29 | SERVICE: str = "service" 30 | REGION: str = "us-east-1" 31 | 32 | DATE: datetime = datetime( 33 | year=2015, month=8, day=30, hour=12, minute=36, second=0, tzinfo=UTC 34 | ) 35 | DATE_STR: str = DATE.strftime(SIGV4_TIMESTAMP_FORMAT) 36 | 37 | TESTSUITE_DIR: pathlib.Path = ( 38 | pathlib.Path(__file__).absolute().parent / "aws4_testsuite" 39 | ) 40 | 41 | 42 | class RawRequest(BaseHTTPRequestHandler): 43 | def __init__(self, raw_request: bytes): 44 | self.rfile: BytesIO = BytesIO(initial_bytes=raw_request) 45 | self.raw_requestline: bytes = self.rfile.readline() 46 | self.error_code: int | None = None 47 | self.error_message: str | None = None 48 | self.parse_request() 49 | 50 | def send_error( 51 | self, code: int, message: str | None = None, explain: str | None = None 52 | ) -> None: 53 | self.error_code = code 54 | self.error_message = message 55 | 56 | 57 | class SignatureTestCase: 58 | def __init__(self, test_case: str) -> None: 59 | self.name: str = os.path.basename(test_case) 60 | base_path: pathlib.Path = TESTSUITE_DIR / test_case 61 | 62 | self.raw_request: bytes = (base_path / f"{self.name}.req").read_bytes() 63 | self.canonical_request: str = (base_path / f"{self.name}.creq").read_text() 64 | self.string_to_sign: str = (base_path / f"{self.name}.sts").read_text() 65 | self.authorization_header: str = (base_path / f"{self.name}.authz").read_text() 66 | self.signed_request: str = (base_path / f"{self.name}.sreq").read_text() 67 | self.credentials: AWSCredentialIdentity = AWSCredentialIdentity( 68 | access_key_id=ACCESS_KEY, 69 | secret_access_key=SECRET_KEY, 70 | session_token=self.get_token(), 71 | ) 72 | 73 | def get_token(self) -> str | None: 74 | token_pattern = r"^x-amz-security-token:(.*)$" 75 | token_match = re.search(token_pattern, self.canonical_request, re.MULTILINE) 76 | return token_match.group(1) if token_match else None 77 | 78 | 79 | def generate_test_cases() -> Iterable[str]: 80 | for dirpath, dirnames, filenames in os.walk(TESTSUITE_DIR): 81 | # Skip over tests without a request file 82 | if not any(f.endswith(".req") for f in filenames): 83 | continue 84 | 85 | test_case_name = os.path.relpath(dirpath, TESTSUITE_DIR).replace(os.sep, "/") 86 | 87 | yield test_case_name 88 | 89 | 90 | @pytest.mark.parametrize("test_case_name", generate_test_cases()) 91 | @freeze_time("2015-08-30 12:36:00") 92 | def test_signature_version_4_sync(test_case_name: str) -> None: 93 | signer = SigV4Signer() 94 | _test_signature_version_4_sync(test_case_name, signer) 95 | 96 | 97 | def _test_signature_version_4_sync(test_case_name: str, signer: SigV4Signer) -> None: 98 | test_case = SignatureTestCase(test_case_name) 99 | request = create_request_from_raw_request(test_case) 100 | 101 | signing_props = SigV4SigningProperties( 102 | region=REGION, 103 | service=SERVICE, 104 | date=DATE_STR, 105 | ) 106 | with pytest.warns(AWSSDKWarning): 107 | actual_canonical_request = signer.canonical_request( 108 | signing_properties=signing_props, request=request 109 | ) 110 | assert test_case.canonical_request == actual_canonical_request 111 | actual_string_to_sign = signer.string_to_sign( 112 | canonical_request=actual_canonical_request, signing_properties=signing_props 113 | ) 114 | assert test_case.string_to_sign == actual_string_to_sign 115 | with pytest.warns(AWSSDKWarning): 116 | signed_request = signer.sign( 117 | signing_properties=signing_props, 118 | request=request, 119 | identity=test_case.credentials, 120 | ) 121 | assert ( 122 | signed_request.fields["Authorization"].as_string() 123 | == test_case.authorization_header 124 | ) 125 | 126 | 127 | @pytest.mark.parametrize("test_case_name", generate_test_cases()) 128 | @freeze_time("2015-08-30 12:36:00") 129 | async def test_signature_version_4_async(test_case_name: str) -> None: 130 | signer = AsyncSigV4Signer() 131 | await _test_signature_version_4_async(test_case_name, signer) 132 | 133 | 134 | async def _test_signature_version_4_async( 135 | test_case_name: str, signer: AsyncSigV4Signer 136 | ) -> None: 137 | test_case = SignatureTestCase(test_case_name) 138 | request = create_request_from_raw_request(test_case, async_body=True) 139 | 140 | signing_props = SigV4SigningProperties( 141 | region=REGION, 142 | service=SERVICE, 143 | date=DATE_STR, 144 | ) 145 | with pytest.warns(AWSSDKWarning): 146 | actual_canonical_request = await signer.canonical_request( 147 | signing_properties=signing_props, request=request 148 | ) 149 | assert test_case.canonical_request == actual_canonical_request 150 | actual_string_to_sign = await signer.string_to_sign( 151 | canonical_request=actual_canonical_request, signing_properties=signing_props 152 | ) 153 | assert test_case.string_to_sign == actual_string_to_sign 154 | with pytest.warns(AWSSDKWarning): 155 | signed_request = await signer.sign( 156 | signing_properties=signing_props, 157 | request=request, 158 | identity=test_case.credentials, 159 | ) 160 | assert ( 161 | signed_request.fields["Authorization"].as_string() 162 | == test_case.authorization_header 163 | ) 164 | 165 | 166 | def create_request_from_raw_request( 167 | test_case: SignatureTestCase, async_body: bool = False 168 | ) -> AWSRequest: 169 | raw = RawRequest(raw_request=test_case.raw_request) 170 | if raw.error_code is not None: 171 | raise Exception(raw.error_message) 172 | 173 | request_method = raw.command 174 | fields = Fields() 175 | for k, v in raw.headers.items(): 176 | if k in fields: 177 | fields[k].add(value=v) 178 | else: 179 | field = Field(name=k, values=[v]) 180 | fields.set_field(field=field) 181 | fields.set_field(Field(name="X-Amz-Date", values=[DATE_STR])) 182 | if test_case.credentials.session_token is not None: 183 | fields.set_field( 184 | Field( 185 | name="X-Amz-Security-Token", 186 | values=[test_case.credentials.session_token], 187 | ) 188 | ) 189 | body: BytesIO | AsyncBytesReader = raw.rfile 190 | if async_body: 191 | body = AsyncBytesReader(raw.rfile) 192 | # BaseHTTPRequestHandler encodes the first line of the request 193 | # as 'iso-8859-1', so we need to decode this into utf-8. 194 | if isinstance(path := raw.path, str): 195 | path = path.encode(encoding="iso-8859-1").decode(encoding="utf-8") 196 | if "?" in path: 197 | path, query = path.split(sep="?", maxsplit=1) 198 | else: 199 | query = "" 200 | host = raw.headers.get("host", "") 201 | url = URI(host=host, path=path, query=query) 202 | return AWSRequest( 203 | destination=url, 204 | method=request_method, 205 | fields=fields, 206 | body=body, 207 | ) 208 | -------------------------------------------------------------------------------- /tests/unit/test_fields.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import pytest 5 | from aws_sdk_signers import Field, Fields 6 | from aws_sdk_signers.interfaces.http import FieldPosition 7 | 8 | 9 | def test_field_single_valued_basics() -> None: 10 | field = Field(name="fname", values=["fval"], kind=FieldPosition.HEADER) 11 | assert field.name == "fname" 12 | assert field.kind == FieldPosition.HEADER 13 | assert field.values == ["fval"] 14 | assert field.as_string() == "fval" 15 | assert field.as_tuples() == [("fname", "fval")] 16 | 17 | 18 | def test_field_multi_valued_basics() -> None: 19 | field = Field(name="fname", values=["fval1", "fval2"], kind=FieldPosition.HEADER) 20 | assert field.name == "fname" 21 | assert field.kind == FieldPosition.HEADER 22 | assert field.values == ["fval1", "fval2"] 23 | assert field.as_string() == "fval1, fval2" 24 | assert field.as_tuples() == [("fname", "fval1"), ("fname", "fval2")] 25 | 26 | 27 | @pytest.mark.parametrize( 28 | "values,expected", 29 | [ 30 | # Single-valued fields are serialized without any quoting or escaping. 31 | (["val1"], "val1"), 32 | (['"val1"'], '"val1"'), 33 | (['"'], '"'), 34 | (['val"1'], 'val"1'), 35 | (["val\\1"], "val\\1"), 36 | # Multi-valued fields are joined with one comma and one space as separator. 37 | (["val1", "val2"], "val1, val2"), 38 | (["val1", "val2", "val3", "val4"], "val1, val2, val3, val4"), 39 | (["©väl", "val2"], "©väl, val2"), 40 | # Values containing commas must be double-quoted. 41 | (["val1", "val2,val3", "val4"], 'val1, "val2,val3", val4'), 42 | (["v,a,l,1", "val2"], '"v,a,l,1", val2'), 43 | # In strings that get quoted, pre-existing double quotes are escaped with a 44 | # single backslash. The second backslash below is for escaping the actual 45 | # backslash in the string for Python. 46 | (["slc", '4,196"'], 'slc, "4,196\\""'), 47 | (['"val1"', "val2"], '"\\"val1\\"", val2'), 48 | (["val1", '"'], 'val1, "\\""'), 49 | (['val1:2",val3:4"', "val5"], '"val1:2\\",val3:4\\"", val5'), 50 | # If quoting happens, backslashes are also escaped. The following case is a 51 | # single backslash getting serialized into two backslashes. Python escaping 52 | # accounts for each actual backslash being written as two. 53 | (["foo,bar\\", "val2"], '"foo,bar\\\\", val2'), 54 | ], 55 | ) 56 | def test_field_serialization(values: list[str], expected: str) -> None: 57 | field = Field(name="_", values=values) 58 | assert field.as_string() == expected 59 | 60 | 61 | @pytest.mark.parametrize( 62 | "field,expected_repr", 63 | [ 64 | ( 65 | Field(name="fname", values=["fval1", "fval2"], kind=FieldPosition.HEADER), 66 | "Field(name='fname', value=['fval1', 'fval2'], kind=)", 67 | ), 68 | ( 69 | Field(name="fname", kind=FieldPosition.TRAILER), 70 | "Field(name='fname', value=[], kind=)", 71 | ), 72 | ( 73 | Field(name="fname"), 74 | "Field(name='fname', value=[], kind=)", 75 | ), 76 | ], 77 | ) 78 | def test_field_repr(field: Field, expected_repr: str) -> None: 79 | assert repr(field) == expected_repr 80 | 81 | 82 | @pytest.mark.parametrize( 83 | "f1,f2", 84 | [ 85 | ( 86 | Field(name="fname", values=["fval1", "fval2"], kind=FieldPosition.TRAILER), 87 | Field(name="fname", values=["fval1", "fval2"], kind=FieldPosition.TRAILER), 88 | ), 89 | ( 90 | Field(name="fname", values=["fval1", "fval2"]), 91 | Field(name="fname", values=["fval1", "fval2"]), 92 | ), 93 | ( 94 | Field(name="fname"), 95 | Field(name="fname"), 96 | ), 97 | ], 98 | ) 99 | def test_field_equality(f1: Field, f2: Field) -> None: 100 | assert f1 == f2 101 | 102 | 103 | @pytest.mark.parametrize( 104 | "f1,f2", 105 | [ 106 | ( 107 | Field(name="fname", values=["fval1", "fval2"], kind=FieldPosition.HEADER), 108 | Field(name="fname", values=["fval1", "fval2"], kind=FieldPosition.TRAILER), 109 | ), 110 | ( 111 | Field(name="fname", values=["fval1", "fval2"], kind=FieldPosition.HEADER), 112 | Field(name="fname", values=["fval2", "fval1"], kind=FieldPosition.HEADER), 113 | ), 114 | ( 115 | Field(name="fname", values=["fval1", "fval2"], kind=FieldPosition.HEADER), 116 | Field(name="fname", values=["fval1"], kind=FieldPosition.HEADER), 117 | ), 118 | ( 119 | Field(name="fname1", values=["fval1", "fval2"], kind=FieldPosition.HEADER), 120 | Field(name="fname2", values=["fval1", "fval2"], kind=FieldPosition.HEADER), 121 | ), 122 | ], 123 | ) 124 | def test_field_inqueality(f1: Field, f2: Field) -> None: 125 | assert f1 != f2 126 | 127 | 128 | @pytest.mark.parametrize( 129 | "fs1,fs2", 130 | [ 131 | ( 132 | Fields([Field(name="fname", values=["fval1", "fval2"])]), 133 | Fields([Field(name="fname", values=["fval1", "fval2"])]), 134 | ), 135 | ], 136 | ) 137 | def test_fields_equality(fs1: Fields, fs2: Fields) -> None: 138 | assert fs1 == fs2 139 | 140 | 141 | @pytest.mark.parametrize( 142 | "fs1,fs2", 143 | [ 144 | ( 145 | Fields(), 146 | Fields([Field(name="fname")]), 147 | ), 148 | ( 149 | Fields([Field(name="fname1")]), 150 | Fields([Field(name="fname2")]), 151 | ), 152 | ( 153 | Fields(encoding="utf-1"), 154 | Fields(encoding="utf-2"), 155 | ), 156 | ( 157 | Fields([Field(name="fname", values=["val1"])]), 158 | Fields([Field(name="fname", values=["val2"])]), 159 | ), 160 | ( 161 | Fields([Field(name="fname", values=["val2", "val1"])]), 162 | Fields([Field(name="fname", values=["val1", "val2"])]), 163 | ), 164 | ( 165 | Fields([Field(name="f1"), Field(name="f2")]), 166 | Fields([Field(name="f2"), Field(name="f1")]), 167 | ), 168 | ], 169 | ) 170 | def test_fields_inequality(fs1: Fields, fs2: Fields) -> None: 171 | assert fs1 != fs2 172 | 173 | 174 | @pytest.mark.parametrize( 175 | "initial_fields", 176 | [ 177 | [ 178 | Field(name="fname1", values=["val1"]), 179 | Field(name="fname1", values=["val2"]), 180 | ], 181 | # uniqueness is checked _after_ normaling field names 182 | [ 183 | Field(name="fNaMe1", values=["val1"]), 184 | Field(name="fname1", values=["val2"]), 185 | ], 186 | ], 187 | ) 188 | def test_repeated_initial_field_names(initial_fields: list[Field]) -> None: 189 | with pytest.raises(ValueError): 190 | Fields(initial_fields) 191 | 192 | 193 | @pytest.mark.parametrize( 194 | "fields,expected_length", 195 | [ 196 | (Fields(), 0), 197 | (Fields([Field(name="fname1")]), 1), 198 | (Fields(encoding="utf-1"), 0), 199 | (Fields([Field(name="fname", values=["val2", "val1"])]), 1), 200 | (Fields([Field(name="f1"), Field(name="f2")]), 2), 201 | ], 202 | ) 203 | def test_fields_length_value(fields: Fields, expected_length: int) -> None: 204 | assert len(fields) == expected_length 205 | 206 | 207 | @pytest.mark.parametrize( 208 | "fields,expected_repr", 209 | [ 210 | ( 211 | Fields([Field(name="fname1")]), 212 | ( 213 | "Fields(OrderedDict({'fname1': Field(name='fname1', value=[], " 214 | "kind=)}))" 215 | ), 216 | ), 217 | ], 218 | ) 219 | def test_fields_repr(fields: Field, expected_repr: str) -> None: 220 | assert repr(fields) == expected_repr 221 | 222 | 223 | @pytest.mark.parametrize( 224 | "fields,key,contained", 225 | [ 226 | (Fields(), "bad_key", False), 227 | (Fields([Field(name="fname1")]), "FNAME1", True), 228 | (Fields([Field(name="fname1")]), "fname1", True), 229 | (Fields([Field(name="fname2")]), "fname1", False), 230 | (Fields([Field(name="f1"), Field(name="f2")]), "f1", True), 231 | (Fields([Field(name="f1"), Field(name="f2")]), "f3", False), 232 | ], 233 | ) 234 | def test_fields_contains(fields: Fields, key: str, contained: bool) -> None: 235 | assert (key in fields) is contained 236 | 237 | 238 | @pytest.mark.parametrize( 239 | "fields,key,expected", 240 | [ 241 | (Fields(), "bad_key", None), 242 | (Fields([Field(name="fname1")]), "FNAME1", Field(name="fname1")), 243 | (Fields([Field(name="fname1")]), "fname1", Field(name="fname1")), 244 | (Fields([Field(name="fname2")]), "fname1", None), 245 | (Fields([Field(name="f1"), Field(name="f2")]), "f1", Field(name="f1")), 246 | (Fields([Field(name="f1"), Field(name="f2")]), "f2", Field(name="f2")), 247 | (Fields([Field(name="f1"), Field(name="f2")]), "f3", None), 248 | ], 249 | ) 250 | def test_fields_getitem(fields: Fields, key: str, expected: Field | None) -> None: 251 | assert fields.get(key) == expected 252 | 253 | 254 | def test_fields_get_index() -> None: 255 | fields = Fields([Field(name="f1"), Field(name="f2")]) 256 | assert fields["f1"] == Field(name="f1") 257 | 258 | 259 | def test_fields_get_missing_index() -> None: 260 | fields = Fields([Field(name="fname1")]) 261 | with pytest.raises(KeyError): 262 | fields["fname2"] 263 | 264 | 265 | @pytest.mark.parametrize( 266 | "fields,field", 267 | [ 268 | (Fields(), Field(name="fname1")), 269 | (Fields([Field(name="fname1", values=["1", "2"])]), Field(name="fname1")), 270 | (Fields([Field(name="f1"), Field(name="f2")]), Field(name="f3")), 271 | ], 272 | ) 273 | def test_fields_setitem(fields: Fields, field: Field) -> None: 274 | fields[field.name] = field 275 | assert field.name in fields 276 | assert fields[field.name] == field 277 | 278 | 279 | @pytest.mark.parametrize( 280 | "fields,field", 281 | [ 282 | (Fields(), Field(name="fname1")), 283 | (Fields([Field(name="fname1", values=["1", "2"])]), Field(name="fname1")), 284 | (Fields([Field(name="f1"), Field(name="f2")]), Field(name="f3")), 285 | ], 286 | ) 287 | def test_fields_set_field(fields: Fields, field: Field) -> None: 288 | fields.set_field(field) 289 | assert field.name in fields 290 | assert fields[field.name] == field 291 | 292 | 293 | @pytest.mark.parametrize( 294 | "fields,field_name,expected_keys", 295 | [ 296 | (Fields([Field(name="fname1", values=["1", "2"])]), "fname1", []), 297 | (Fields([Field(name="f1"), Field(name="f2")]), "f2", ["f1"]), 298 | ], 299 | ) 300 | def test_fields_delitem( 301 | fields: Fields, field_name: str, expected_keys: list[str] 302 | ) -> None: 303 | assert field_name in fields 304 | del fields[field_name] 305 | assert field_name not in fields 306 | 307 | # Ensure we don't delete anything unexpected 308 | assert len(fields) == len(expected_keys) 309 | for key in expected_keys: 310 | assert key in fields 311 | 312 | 313 | def test_fields_delitem_missing() -> None: 314 | fields = Fields([Field(name="fname1")]) 315 | with pytest.raises(KeyError): 316 | del fields["fname2"] 317 | -------------------------------------------------------------------------------- /tests/unit/test_identity.py: -------------------------------------------------------------------------------- 1 | from datetime import UTC, datetime, timedelta 2 | 3 | import pytest 4 | from aws_sdk_signers import AWSCredentialIdentity 5 | 6 | 7 | @pytest.mark.parametrize( 8 | "access_key_id,secret_access_key,session_token,expiration", 9 | [ 10 | ( 11 | "AKID1234EXAMPLE", 12 | "SECRET1234", 13 | None, 14 | None, 15 | ), 16 | ( 17 | "AKID1234EXAMPLE", 18 | "SECRET1234", 19 | "SESS_TOKEN_1234", 20 | None, 21 | ), 22 | ( 23 | "AKID1234EXAMPLE", 24 | "SECRET1234", 25 | None, 26 | datetime(2024, 5, 1, 0, 0, 0, tzinfo=UTC), 27 | ), 28 | ( 29 | "AKID1234EXAMPLE", 30 | "SECRET1234", 31 | "SESS_TOKEN_1234", 32 | datetime(2024, 5, 1, 0, 0, 0, tzinfo=UTC), 33 | ), 34 | ], 35 | ) 36 | def test_aws_credential_identity( 37 | access_key_id: str, 38 | secret_access_key: str, 39 | session_token: str | None, 40 | expiration: datetime | None, 41 | ) -> None: 42 | creds = AWSCredentialIdentity( 43 | access_key_id=access_key_id, 44 | secret_access_key=secret_access_key, 45 | session_token=session_token, 46 | expiration=expiration, 47 | ) 48 | assert creds.access_key_id == access_key_id 49 | assert creds.secret_access_key == secret_access_key 50 | assert creds.session_token == session_token 51 | assert creds.expiration == expiration 52 | 53 | 54 | @pytest.mark.parametrize( 55 | "access_key_id,secret_access_key,session_token,expiration,is_expired", 56 | [ 57 | ( 58 | "AKID1234EXAMPLE", 59 | "SECRET1234", 60 | "SESS_TOKEN_1234", 61 | None, 62 | False, 63 | ), 64 | ( 65 | "AKID1234EXAMPLE", 66 | "SECRET1234", 67 | None, 68 | datetime(2024, 5, 1, 0, 0, 0, tzinfo=UTC), 69 | True, 70 | ), 71 | ( 72 | "AKID1234EXAMPLE", 73 | "SECRET1234", 74 | "SESS_TOKEN_1234", 75 | datetime.now(UTC) + timedelta(hours=1), 76 | False, 77 | ), 78 | ], 79 | ) 80 | def test_aws_credential_identity_expired( 81 | access_key_id: str, 82 | secret_access_key: str, 83 | session_token: str | None, 84 | expiration: datetime | None, 85 | is_expired: bool, 86 | ) -> None: 87 | creds = AWSCredentialIdentity( 88 | access_key_id=access_key_id, 89 | secret_access_key=secret_access_key, 90 | session_token=session_token, 91 | expiration=expiration, 92 | ) 93 | assert creds.is_expired is is_expired 94 | -------------------------------------------------------------------------------- /tests/unit/test_signers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | """ 5 | 6 | import re 7 | import typing 8 | from datetime import UTC, datetime 9 | from io import BytesIO 10 | 11 | import pytest 12 | from aws_sdk_signers import ( 13 | URI, 14 | AsyncSigV4Signer, 15 | AWSCredentialIdentity, 16 | AWSRequest, 17 | Fields, 18 | SigV4Signer, 19 | SigV4SigningProperties, 20 | ) 21 | 22 | SIGV4_RE = re.compile( 23 | r"AWS4-HMAC-SHA256 " 24 | r"Credential=(?P\w+)/\d+/" 25 | r"(?P[a-z0-9-]+)/" 26 | ) 27 | 28 | 29 | @pytest.fixture(scope="module") 30 | def aws_identity() -> AWSCredentialIdentity: 31 | return AWSCredentialIdentity( 32 | access_key_id="AKID123456", 33 | secret_access_key="EXAMPLE1234SECRET", 34 | session_token="X123456SESSION", 35 | ) 36 | 37 | 38 | @pytest.fixture(scope="module") 39 | def signing_properties() -> SigV4SigningProperties: 40 | return SigV4SigningProperties( 41 | region="us-west-2", 42 | service="ec2", 43 | payload_signing_enabled=False, 44 | ) 45 | 46 | 47 | @pytest.fixture(scope="module") 48 | def aws_request() -> AWSRequest: 49 | return AWSRequest( 50 | destination=URI( 51 | scheme="https", 52 | host="127.0.0.1", 53 | port=8000, 54 | ), 55 | method="GET", 56 | body=BytesIO(b"123456"), 57 | fields=Fields({}), 58 | ) 59 | 60 | 61 | class TestSigV4Signer: 62 | SIGV4_SYNC_SIGNER = SigV4Signer() 63 | 64 | def test_sign( 65 | self, 66 | aws_identity: AWSCredentialIdentity, 67 | aws_request: AWSRequest, 68 | signing_properties: SigV4SigningProperties, 69 | ) -> None: 70 | signed_request = self.SIGV4_SYNC_SIGNER.sign( 71 | signing_properties=signing_properties, 72 | request=aws_request, 73 | identity=aws_identity, 74 | ) 75 | assert isinstance(signed_request, AWSRequest) 76 | assert signed_request is not aws_request 77 | assert "authorization" in signed_request.fields 78 | authorization_field = signed_request.fields["authorization"] 79 | assert SIGV4_RE.match(authorization_field.as_string()) 80 | 81 | @typing.no_type_check 82 | def test_sign_with_invalid_identity( 83 | self, aws_request: AWSRequest, signing_properties: SigV4SigningProperties 84 | ) -> None: 85 | """Ignore typing as we're testing an invalid input state.""" 86 | identity = object() 87 | assert not isinstance(identity, AWSCredentialIdentity) 88 | with pytest.raises(ValueError): 89 | self.SIGV4_SYNC_SIGNER.sign( 90 | signing_properties=signing_properties, 91 | request=aws_request, 92 | identity=identity, 93 | ) 94 | 95 | def test_sign_with_expired_identity( 96 | self, aws_request: AWSRequest, signing_properties: SigV4SigningProperties 97 | ) -> None: 98 | identity = AWSCredentialIdentity( 99 | access_key_id="AKID123456", 100 | secret_access_key="EXAMPLE1234SECRET", 101 | session_token="X123456SESSION", 102 | expiration=datetime(1970, 1, 1, tzinfo=UTC), 103 | ) 104 | with pytest.raises(ValueError): 105 | self.SIGV4_SYNC_SIGNER.sign( 106 | signing_properties=signing_properties, 107 | request=aws_request, 108 | identity=identity, 109 | ) 110 | 111 | 112 | class TestAsyncSigV4Signer: 113 | SIGV4_ASYNC_SIGNER = AsyncSigV4Signer() 114 | 115 | async def test_sign( 116 | self, 117 | aws_identity: AWSCredentialIdentity, 118 | aws_request: AWSRequest, 119 | signing_properties: SigV4SigningProperties, 120 | ) -> None: 121 | signed_request = await self.SIGV4_ASYNC_SIGNER.sign( 122 | signing_properties=signing_properties, 123 | request=aws_request, 124 | identity=aws_identity, 125 | ) 126 | assert isinstance(signed_request, AWSRequest) 127 | assert signed_request is not aws_request 128 | assert "authorization" in signed_request.fields 129 | authorization_field = signed_request.fields["authorization"] 130 | assert SIGV4_RE.match(authorization_field.as_string()) 131 | 132 | @typing.no_type_check 133 | async def test_sign_with_invalid_identity( 134 | self, aws_request: AWSRequest, signing_properties: SigV4SigningProperties 135 | ) -> None: 136 | """Ignore typing as we're testing an invalid input state.""" 137 | identity = object() 138 | assert not isinstance(identity, AWSCredentialIdentity) 139 | with pytest.raises(ValueError): 140 | await self.SIGV4_ASYNC_SIGNER.sign( 141 | signing_properties=signing_properties, 142 | request=aws_request, 143 | identity=identity, 144 | ) 145 | 146 | async def test_sign_with_expired_identity( 147 | self, aws_request: AWSRequest, signing_properties: SigV4SigningProperties 148 | ) -> None: 149 | identity = AWSCredentialIdentity( 150 | access_key_id="AKID123456", 151 | secret_access_key="EXAMPLE1234SECRET", 152 | session_token="X123456SESSION", 153 | expiration=datetime(1970, 1, 1, tzinfo=UTC), 154 | ) 155 | with pytest.raises(ValueError): 156 | await self.SIGV4_ASYNC_SIGNER.sign( 157 | signing_properties=signing_properties, 158 | request=aws_request, 159 | identity=identity, 160 | ) 161 | --------------------------------------------------------------------------------