From 095ad05e42db081349c4d7ec1db11fa64299a50e Mon Sep 17 00:00:00 2001 From: Christian Bormann <8774236+c2bo@users.noreply.github.com> Date: Mon, 31 Jul 2023 12:45:08 +0200 Subject: [PATCH] auto-generate examples --- .github/workflows/ghpages.yml | 6 + .github/workflows/publish.yml | 6 + .gitignore | 2 + draft-looker-oauth-jwt-cwt-status-list.md | 60 ++----- src/LICENSE.md | 202 ++++++++++++++++++++++ src/main.py | 87 ++++++++++ src/status_list.py | 4 +- src/{status_jwt.py => status_token.py} | 6 +- src/test.py | 80 --------- src/util.py | 18 +- 10 files changed, 338 insertions(+), 133 deletions(-) create mode 100644 src/LICENSE.md create mode 100644 src/main.py rename src/{status_jwt.py => status_token.py} (97%) delete mode 100644 src/test.py diff --git a/.github/workflows/ghpages.yml b/.github/workflows/ghpages.yml index a26b9db..8c0b501 100644 --- a/.github/workflows/ghpages.yml +++ b/.github/workflows/ghpages.yml @@ -26,6 +26,12 @@ jobs: id: setup run: date -u "+date=%FT%T" >>"$GITHUB_OUTPUT" + - name: "Generate examples" + run: | + python3 -m pip install --upgrade pip + python3 -m pip install -r src/requirements.txt + python3 src/main.py + - name: "Caching" uses: actions/cache@v3 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8e01218..2875ecf 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,6 +21,12 @@ jobs: id: setup run: date -u "+date=%FT%T" >>"$GITHUB_OUTPUT" + - name: "Generate examples" + run: | + python3 -m pip install --upgrade pip + python3 -m pip install -r src/requirements.txt + python3 src/main.py + - name: "Caching" uses: actions/cache@v3 with: diff --git a/.gitignore b/.gitignore index b32e3cb..be475b0 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,6 @@ archive.json draft-looker-oauth-jwt-cwt-status-list.xml package-lock.json report.xml +__pycache__ +examples/ !requirements.txt diff --git a/draft-looker-oauth-jwt-cwt-status-list.md b/draft-looker-oauth-jwt-cwt-status-list.md index d312b2f..2df0bf9 100644 --- a/draft-looker-oauth-jwt-cwt-status-list.md +++ b/draft-looker-oauth-jwt-cwt-status-list.md @@ -98,25 +98,9 @@ The following rules apply to validating a JWT-based Status List Token. Applicati 8. Relying parties MUST reject JWTs that are not valid in all other respects per "JSON Web Token (JWT)" {{RFC7519}}. -~~~ ascii-art - -{ - "typ": "statuslist+jwt", - "alg": "ES256", - "kid": "11" -} -. -{ - "iss": "https://example.com", - "sub": "https://example.com/statuslists/1", - "iat": 1683560915, - "exp": 1686232115, - "status_list": { - "bits": 1, - "lst": "H4sIAMo_jGQC_9u5GABc9QE7AgAAAA" - } -} -~~~ +~~~~~~~~~~ +{::include ./examples/status_list_jwt} +~~~~~~~~~~ ### Status List Claim Format {#jwt-status-list-claim-format} @@ -234,22 +218,11 @@ index 7 6 5 4 3 2 1 0 15 ... 10 9 8 23 ~~~ -Resulting in the byte array: - -~~~ ascii-art - -byte_array = [0xB9, 0xA3] -~~~ - -After compression and base64url encoding, the generated Status List is: - -~~~ ascii-art +Resulting in the byte array and compressed/base64url encoded status list: -"status_list": { - "bits": 1, - "lst": "H4sIAMo_jGQC_9u5GABc9QE7AgAAAA" -} -~~~ +~~~~~~~~~~ +{::include ./examples/status_list_encoding} +~~~~~~~~~~ ## Example Status List with 2-Bit Status Values @@ -290,22 +263,11 @@ index 3 2 1 0 7 6 5 4 11 10 9 8 ~~~ -Resulting in the byte array: - -~~~ ascii-art +Resulting in the byte array and compressed/base64url encoded status list: -byte_array = [0xC9, 0x44, 0xF9] -~~~ - -After compression and base64url encoding, the generated Status List is: - -~~~ ascii-art - -"status_list": { - "bits": 2, - "lst": "H4sIAMo_jGQC_zvp8hMAZLRLMQMAAAA" -} -~~~ +~~~~~~~~~~ +{::include ./examples/status_list_encoding2} +~~~~~~~~~~ # CWT Representations diff --git a/src/LICENSE.md b/src/LICENSE.md new file mode 100644 index 0000000..e72929e --- /dev/null +++ b/src/LICENSE.md @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..a403733 --- /dev/null +++ b/src/main.py @@ -0,0 +1,87 @@ +from status_list import StatusList +from status_token import StatusListToken +from datetime import datetime, timedelta +import os +import util + +key = util.EXAMPLE_KEY +iat = datetime.utcfromtimestamp(1686920170) +exp = iat + timedelta(days=7000) +gzip_time = iat.timestamp() +folder = "./examples/" + + +def statusListEncoding1Bit(): + status_list = StatusList(16, 1) + status_list.set(0, 1) + status_list.set(1, 0) + status_list.set(2, 0) + status_list.set(3, 1) + status_list.set(4, 1) + status_list.set(5, 1) + status_list.set(6, 0) + status_list.set(7, 1) + status_list.set(8, 1) + status_list.set(9, 1) + status_list.set(10, 0) + status_list.set(11, 0) + status_list.set(12, 0) + status_list.set(13, 1) + status_list.set(14, 0) + status_list.set(15, 1) + encoded = status_list.encode(mtime=gzip_time) + text = 'byte_array = [{}, {}] \nencoded = "{}"'.format( + hex(status_list.list[0]), hex(status_list.list[1]), encoded + ) + util.outputFile(folder + "status_list_encoding", text) + + +def exampleStatusList() -> StatusList: + status_list = StatusList(12, 2) + status_list.set(0, 1) + status_list.set(1, 2) + status_list.set(2, 0) + status_list.set(3, 3) + status_list.set(4, 0) + status_list.set(5, 1) + status_list.set(6, 0) + status_list.set(7, 1) + status_list.set(8, 1) + status_list.set(9, 2) + status_list.set(10, 3) + status_list.set(11, 3) + return status_list + + +def statusListEncoding2Bit(): + status_list = exampleStatusList() + encoded = status_list.encode(mtime=gzip_time) + text = 'byte_array = [{}, {}, {}] \nencoded = "{}"'.format( + hex(status_list.list[0]), + hex(status_list.list[1]), + hex(status_list.list[2]), + encoded, + ) + util.outputFile(folder + "status_list_encoding2", text) + + +def statusListJWT(): + status_list = exampleStatusList() + jwt = StatusListToken( + issuer="https://example.com", + subject="https://example.com/statuslists/1", + list=status_list, + key=key, + bits=2, + ) + status_jwt = jwt.buildJWT(iat=iat, exp=exp, mtime=gzip_time) + text = util.formatToken(status_jwt, key) + util.outputFile(folder + "status_list_jwt", text) + + +if __name__ == "__main__": + if not os.path.exists(folder): + os.makedirs(folder) + statusListEncoding1Bit() + statusListEncoding2Bit() + statusListJWT() diff --git a/src/status_list.py b/src/status_list.py index b36fbad..e7a9fba 100644 --- a/src/status_list.py +++ b/src/status_list.py @@ -41,7 +41,9 @@ def get(self, pos: int) -> int: rest = pos % self.divisor floored = pos // self.divisor shift = rest * self.bits - return (self.list[floored] & (((1 << self.bits) - 1) << shift)) >> shift + return ( + self.list[floored] & (((1 << self.bits) - 1) << shift) + ) >> shift def __str__(self): val = "" diff --git a/src/status_jwt.py b/src/status_token.py similarity index 97% rename from src/status_jwt.py rename to src/status_token.py index e579708..cc52bda 100644 --- a/src/status_jwt.py +++ b/src/status_token.py @@ -7,7 +7,8 @@ DEFAULT_ALG = "ES256" STATUS_LIST_TYP = "statuslist+jwt" -class StatusListJWT: + +class StatusListToken: list: StatusList issuer: str subject: str @@ -26,6 +27,7 @@ def __init__( ): if list is not None: self.list = list + self.bits = list.bits else: self.list = StatusList(size, bits) self.issuer = issuer @@ -74,7 +76,7 @@ def buildJWT( optional_claims: Dict = None, optional_header: Dict = None, compact=True, - mtime=None + mtime=None, ) -> str: # build claims if optional_claims is not None: diff --git a/src/test.py b/src/test.py deleted file mode 100644 index 5391d09..0000000 --- a/src/test.py +++ /dev/null @@ -1,80 +0,0 @@ -from status_list import StatusList -from status_jwt import StatusListJWT -from datetime import datetime, timedelta -import util - -# demo key, constant timestamp -key = util.EXAMPLE_KEY -iat = datetime.utcfromtimestamp(1686920170) -gzip_time = iat.timestamp() - - -test = StatusList(16, 1) -test.set(0, 1) -test.set(1, 0) -test.set(2, 0) -test.set(3, 1) -test.set(4, 1) -test.set(5, 1) -test.set(6, 0) -test.set(7, 1) -test.set(8, 1) -test.set(9, 1) -test.set(10, 0) -test.set(11, 0) -test.set(12, 0) -test.set(13, 1) -test.set(14, 0) -test.set(15, 1) -print(test) -print(bin(test.list[0]), bin(test.list[1])) -print(hex(test.list[0]), hex(test.list[1])) -encoded = test.encode(mtime=gzip_time) -print(encoded) - - -test = StatusList(12, 2) -test.set(0, 1) -test.set(1, 2) -test.set(2, 0) -test.set(3, 3) -test.set(4, 0) -test.set(5, 1) -test.set(6, 0) -test.set(7, 1) -test.set(8, 1) -test.set(9, 2) -test.set(10, 3) -test.set(11, 3) -print(test) -print(hex(test.list[0]), hex(test.list[1]), hex(test.list[2])) -encoded = test.encode(mtime=gzip_time) -print(encoded) - -jwt = StatusListJWT( - issuer="https://example.com", - subject="https://example.com/statuslists/1", - list=test, - key=key, - bits=2, -) -exp = iat + timedelta(7) -status_jwt = jwt.buildJWT( - exp=exp, - iat=iat, - optional_claims={"custom": "value"}, - optional_header={"x5c": ["here_be_dragons"]}, - mtime=gzip_time, -) -print("-----------") -print(status_jwt) -print("-----------") -print(util.formatToken(status_jwt, key)) -print("-----------") - -status_jwt = jwt.buildJWT(iat=iat, exp=exp, mtime=gzip_time) -print(status_jwt) -print("-----------") -print(util.formatToken(status_jwt, key)) -decoded_list = StatusListJWT.fromJWT(status_jwt, key) -print(decoded_list.list) diff --git a/src/util.py b/src/util.py index 5920aba..7178066 100644 --- a/src/util.py +++ b/src/util.py @@ -1,4 +1,5 @@ from jwcrypto import jwk, jwt +from textwrap import fill import json example = { @@ -9,8 +10,11 @@ "x": "I3HWm_0Ds1dPMI-IWmf4mBmH-YaeAVbPVu7vB27CxXo", "y": "6N_d5Elj9bs1htgV3okJKIdbHEpkgTmAluYKJemzn1M", "kid": "12", + "alg": "ES256", } + EXAMPLE_KEY = jwk.JWK(**example) +MAX_LENGTH = 68 def formatToken(input: str, key: jwk.JWK) -> str: @@ -23,4 +27,16 @@ def formatToken(input: str, key: jwk.JWK) -> str: def printJson(input: str) -> str: - return json.dumps(json.loads(input), sort_keys=True, indent=2) + text = json.dumps( + json.loads(input), sort_keys=True, indent=2, ensure_ascii=False + ) + return text + + +def printText(input: str) -> str: + return fill(input, width=MAX_LENGTH, break_on_hyphens=False) + + +def outputFile(file_name: str, input: str): + with open(file_name, "w") as file: + file.write(input)