You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by sb...@apache.org on 2026/04/02 15:42:16 UTC

(tooling-trusted-releases) branch sbp updated (3c04a833 -> f9cfcf22)

This is an automated email from the ASF dual-hosted git repository.

sbp pushed a change to branch sbp
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git


 discard 3c04a833 Update dependencies
 discard 5be49dc1 Add an implementation of Noisy Secrets, and tests
     add 403efc3c Replace ALLOW_TESTS with dedicated Test config mode. Add additional runtime safety checks.
     new 2922aada Add an implementation of Noisy Secrets, and tests
     new 22a1626a Update dependencies
     new f9cfcf22 Remove the admin route that shows environment variables

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (3c04a833)
            \
             N -- N -- N   refs/heads/sbp (f9cfcf22)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 BUILD.md                           |   2 +-
 DEVELOPMENT.md                     |   2 +-
 Makefile                           |   4 +-
 atr/admin/__init__.py              |  38 +++---------
 atr/config.py                      | 121 +++++++++++++++++++++++++++----------
 atr/db/interaction.py              |   2 +-
 atr/docs/authorization-security.md |   5 +-
 atr/docs/running-the-server.md     |   4 +-
 atr/docs/storage-interface.md      |   2 +-
 atr/get/download.py                |   2 +-
 atr/get/projects.py                |   6 +-
 atr/get/published.py               |   4 +-
 atr/get/test.py                    |  21 +++++--
 atr/ldap.py                        |   2 +-
 atr/post/test.py                   |  10 +++
 atr/principal.py                   |   4 +-
 atr/server.py                      |  18 +++---
 atr/storage/writers/keys.py        |   4 +-
 atr/storage/writers/mail.py        |   3 +-
 atr/storage/writers/release.py     |   4 +-
 atr/tabulate.py                    |   5 +-
 atr/user.py                        |   8 +--
 atr/util.py                        |  14 +----
 atr/worker.py                      |  13 ++--
 docker-compose.yml                 |   2 +-
 tests/docker-compose.yml           |   4 +-
 tests/unit/test_ldap.py            |  12 ++--
 tests/unit/test_user.py            |  11 ++--
 tests/unit/test_vote.py            |   3 +-
 29 files changed, 185 insertions(+), 145 deletions(-)


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org


(tooling-trusted-releases) 03/03: Remove the admin route that shows environment variables

Posted by sb...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch sbp
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git

commit f9cfcf221408fb4cf7b25d1d0d43b260aed9bc18
Author: Sean B. Palmer <se...@miscoranda.com>
AuthorDate: Thu Apr 2 16:40:30 2026 +0100

    Remove the admin route that shows environment variables
---
 atr/admin/__init__.py | 13 -------------
 1 file changed, 13 deletions(-)

diff --git a/atr/admin/__init__.py b/atr/admin/__init__.py
index c584918d..31f7a5f8 100644
--- a/atr/admin/__init__.py
+++ b/atr/admin/__init__.py
@@ -491,19 +491,6 @@ async def delete_test_openpgp_keys_post(
     return await session.redirect(get.keys.keys)
 
 
-@admin.typed
-async def env(_session: web.Committer, _env: Literal["env"]) -> web.QuartResponse:
-    """
-    URL: GET /env
-
-    Display the environment variables.
-    """
-    env_vars = []
-    for key, value in os.environ.items():
-        env_vars.append(f"{key}={value}")
-    return web.TextResponse("\n".join(env_vars))
-
-
 @admin.typed
 async def keys_check_get(_session: web.Committer, _keys_check: Literal["keys/check"]) -> web.QuartResponse:
     """


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org


(tooling-trusted-releases) 02/03: Update dependencies

Posted by sb...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch sbp
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git

commit 22a1626a3b64d86e121531abd209bcc40df51e38
Author: Sean B. Palmer <se...@miscoranda.com>
AuthorDate: Thu Apr 2 16:19:02 2026 +0100

    Update dependencies
---
 .pre-commit-config.yaml |  2 +-
 pip-audit.requirements  | 10 +++---
 pyproject.toml          |  2 +-
 uv.lock                 | 92 +++++++++++++++++++++++++------------------------
 4 files changed, 55 insertions(+), 51 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index f906cd85..1aa64e94 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -106,7 +106,7 @@ repos:
         - '--disable-pip'
         - '--no-deps'
 - repo: https://github.com/oxc-project/mirrors-oxlint
-  rev: v1.57.0
+  rev: v1.58.0
   hooks:
     - id: oxlint
       name: lint JS files with Oxlint
diff --git a/pip-audit.requirements b/pip-audit.requirements
index 0d628f41..2dc28f23 100644
--- a/pip-audit.requirements
+++ b/pip-audit.requirements
@@ -5,7 +5,7 @@ aiofiles==25.1.0
     #   tooling-trusted-releases
 aiohappyeyeballs==2.6.1
     # via aiohttp
-aiohttp==3.13.4
+aiohttp==3.13.5
     # via
     #   asfpy
     #   asfquart
@@ -58,7 +58,7 @@ cffi==2.0.0
     #   cryptography
 cfgv==3.5.0
     # via pre-commit
-charset-normalizer==3.4.6
+charset-normalizer==3.4.7
     # via requests
 click==8.3.1
     # via
@@ -303,8 +303,10 @@ referencing==0.37.0
     #   jsonschema-specifications
 regex==2026.3.32
     # via djlint
-requests==2.33.0
-    # via asfpy
+requests==2.33.1
+    # via
+    #   asfpy
+    #   tooling-trusted-releases
 rfc3339-validator==0.1.4
     # via jsonschema
 rfc3986-validator==0.1.1
diff --git a/pyproject.toml b/pyproject.toml
index c105a0a2..ecdb8c69 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,7 +10,7 @@ readme = "README.md"
 requires-python = "~=3.13.0"
 dependencies = [
   "aiofiles>=24.1.0,<26.0.0",
-  "aiohttp>=3.11.14",
+  "aiohttp>=3.13.4",
   "aioshutil (>=1.5,<2.0)",
   "aiosmtplib (>=4.0.0,<6.0.0)",
   "aiosqlite>=0.21.0,<0.23.0",
diff --git a/uv.lock b/uv.lock
index 5d005be2..7719ccba 100644
--- a/uv.lock
+++ b/uv.lock
@@ -3,7 +3,7 @@ revision = 3
 requires-python = "==3.13.*"
 
 [options]
-exclude-newer = "2026-03-30T10:05:57Z"
+exclude-newer = "2026-04-02T15:18:41Z"
 
 [[package]]
 name = "aiofiles"
@@ -25,7 +25,7 @@ wheels = [
 
 [[package]]
 name = "aiohttp"
-version = "3.13.4"
+version = "3.13.5"
 source = { registry = "https://pypi.org/simple" }
 dependencies = [
     { name = "aiohappyeyeballs" },
@@ -36,25 +36,25 @@ dependencies = [
     { name = "propcache" },
     { name = "yarl" },
 ]
-sdist = { url = "https://files.pythonhosted.org/packages/45/4a/064321452809dae953c1ed6e017504e72551a26b6f5708a5a80e4bf556ff/aiohttp-3.13.4.tar.gz", hash = "sha256:d97a6d09c66087890c2ab5d49069e1e570583f7ac0314ecf98294c1b6aaebd38", size = 7859748, upload-time = "2026-03-28T17:19:40.6Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" }
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/e3/ac/892f4162df9b115b4758d615f32ec63d00f3084c705ff5526630887b9b42/aiohttp-3.13.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:63dd5e5b1e43b8fb1e91b79b7ceba1feba588b317d1edff385084fcc7a0a4538", size = 745744, upload-time = "2026-03-28T17:16:44.67Z" },
-    { url = "https://files.pythonhosted.org/packages/97/a9/c5b87e4443a2f0ea88cb3000c93a8fdad1ee63bffc9ded8d8c8e0d66efc6/aiohttp-3.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:746ac3cc00b5baea424dacddea3ec2c2702f9590de27d837aa67004db1eebc6e", size = 498178, upload-time = "2026-03-28T17:16:46.766Z" },
-    { url = "https://files.pythonhosted.org/packages/94/42/07e1b543a61250783650df13da8ddcdc0d0a5538b2bd15cef6e042aefc61/aiohttp-3.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bda8f16ea99d6a6705e5946732e48487a448be874e54a4f73d514660ff7c05d3", size = 498331, upload-time = "2026-03-28T17:16:48.9Z" },
-    { url = "https://files.pythonhosted.org/packages/20/d6/492f46bf0328534124772d0cf58570acae5b286ea25006900650f69dae0e/aiohttp-3.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b061e7b5f840391e3f64d0ddf672973e45c4cfff7a0feea425ea24e51530fc2", size = 1744414, upload-time = "2026-03-28T17:16:50.968Z" },
-    { url = "https://files.pythonhosted.org/packages/e2/4d/e02627b2683f68051246215d2d62b2d2f249ff7a285e7a858dc47d6b6a14/aiohttp-3.13.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b252e8d5cd66184b570d0d010de742736e8a4fab22c58299772b0c5a466d4b21", size = 1719226, upload-time = "2026-03-28T17:16:53.173Z" },
-    { url = "https://files.pythonhosted.org/packages/7b/6c/5d0a3394dd2b9f9aeba6e1b6065d0439e4b75d41f1fb09a3ec010b43552b/aiohttp-3.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20af8aad61d1803ff11152a26146d8d81c266aa8c5aa9b4504432abb965c36a0", size = 1782110, upload-time = "2026-03-28T17:16:55.362Z" },
-    { url = "https://files.pythonhosted.org/packages/0d/2d/c20791e3437700a7441a7edfb59731150322424f5aadf635602d1d326101/aiohttp-3.13.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:13a5cc924b59859ad2adb1478e31f410a7ed46e92a2a619d6d1dd1a63c1a855e", size = 1884809, upload-time = "2026-03-28T17:16:57.734Z" },
-    { url = "https://files.pythonhosted.org/packages/c8/94/d99dbfbd1924a87ef643833932eb2a3d9e5eee87656efea7d78058539eff/aiohttp-3.13.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:534913dfb0a644d537aebb4123e7d466d94e3be5549205e6a31f72368980a81a", size = 1764938, upload-time = "2026-03-28T17:17:00.221Z" },
-    { url = "https://files.pythonhosted.org/packages/49/61/3ce326a1538781deb89f6cf5e094e2029cd308ed1e21b2ba2278b08426f6/aiohttp-3.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:320e40192a2dcc1cf4b5576936e9652981ab596bf81eb309535db7e2f5b5672f", size = 1570697, upload-time = "2026-03-28T17:17:02.985Z" },
-    { url = "https://files.pythonhosted.org/packages/b6/77/4ab5a546857bb3028fbaf34d6eea180267bdab022ee8b1168b1fcde4bfdd/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9e587fcfce2bcf06526a43cb705bdee21ac089096f2e271d75de9c339db3100c", size = 1702258, upload-time = "2026-03-28T17:17:05.28Z" },
-    { url = "https://files.pythonhosted.org/packages/79/63/d8f29021e39bc5af8e5d5e9da1b07976fb9846487a784e11e4f4eeda4666/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9eb9c2eea7278206b5c6c1441fdd9dc420c278ead3f3b2cc87f9b693698cc500", size = 1740287, upload-time = "2026-03-28T17:17:07.712Z" },
-    { url = "https://files.pythonhosted.org/packages/55/3a/cbc6b3b124859a11bc8055d3682c26999b393531ef926754a3445b99dfef/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:29be00c51972b04bf9d5c8f2d7f7314f48f96070ca40a873a53056e652e805f7", size = 1753011, upload-time = "2026-03-28T17:17:10.053Z" },
-    { url = "https://files.pythonhosted.org/packages/e0/30/836278675205d58c1368b21520eab9572457cf19afd23759216c04483048/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90c06228a6c3a7c9f776fe4fc0b7ff647fffd3bed93779a6913c804ae00c1073", size = 1566359, upload-time = "2026-03-28T17:17:12.433Z" },
-    { url = "https://files.pythonhosted.org/packages/50/b4/8032cc9b82d17e4277704ba30509eaccb39329dc18d6a35f05e424439e32/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a533ec132f05fd9a1d959e7f34184cd7d5e8511584848dab85faefbaac573069", size = 1785537, upload-time = "2026-03-28T17:17:14.721Z" },
-    { url = "https://files.pythonhosted.org/packages/17/7d/5873e98230bde59f493bf1f7c3e327486a4b5653fa401144704df5d00211/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1c946f10f413836f82ea4cfb90200d2a59578c549f00857e03111cf45ad01ca5", size = 1740752, upload-time = "2026-03-28T17:17:17.387Z" },
-    { url = "https://files.pythonhosted.org/packages/7b/f2/13e46e0df051494d7d3c68b7f72d071f48c384c12716fc294f75d5b1a064/aiohttp-3.13.4-cp313-cp313-win32.whl", hash = "sha256:48708e2706106da6967eff5908c78ca3943f005ed6bcb75da2a7e4da94ef8c70", size = 433187, upload-time = "2026-03-28T17:17:19.523Z" },
-    { url = "https://files.pythonhosted.org/packages/ea/c0/649856ee655a843c8f8664592cfccb73ac80ede6a8c8db33a25d810c12db/aiohttp-3.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:74a2eb058da44fa3a877a49e2095b591d4913308bb424c418b77beb160c55ce3", size = 459778, upload-time = "2026-03-28T17:17:21.964Z" },
+    { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" },
+    { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" },
+    { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" },
+    { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" },
+    { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" },
+    { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" },
+    { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" },
+    { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" },
+    { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" },
+    { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" },
+    { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" },
+    { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" },
+    { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" },
+    { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" },
+    { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" },
+    { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" },
+    { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" },
 ]
 
 [[package]]
@@ -313,27 +313,27 @@ wheels = [
 
 [[package]]
 name = "charset-normalizer"
-version = "3.4.6"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" },
-    { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" },
-    { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" },
-    { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" },
-    { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" },
-    { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" },
-    { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" },
-    { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" },
-    { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" },
-    { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" },
-    { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" },
-    { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" },
-    { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" },
-    { url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" },
-    { url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" },
-    { url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" },
-    { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" },
+version = "3.4.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" },
+    { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" },
+    { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" },
+    { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" },
+    { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" },
+    { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" },
+    { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" },
+    { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" },
+    { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" },
+    { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" },
+    { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" },
+    { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" },
+    { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" },
+    { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" },
+    { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" },
+    { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" },
+    { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
 ]
 
 [[package]]
@@ -1676,7 +1676,7 @@ wheels = [
 
 [[package]]
 name = "requests"
-version = "2.33.0"
+version = "2.33.1"
 source = { registry = "https://pypi.org/simple" }
 dependencies = [
     { name = "certifi" },
@@ -1684,9 +1684,9 @@ dependencies = [
     { name = "idna" },
     { name = "urllib3" },
 ]
-sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" }
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" },
+    { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" },
 ]
 
 [[package]]
@@ -1953,6 +1953,7 @@ dependencies = [
     { name = "quart-rate-limiter" },
     { name = "quart-schema", extra = ["pydantic"] },
     { name = "quart-wtforms" },
+    { name = "requests" },
     { name = "rich" },
     { name = "semver" },
     { name = "sqlmodel" },
@@ -1983,7 +1984,7 @@ test = [
 [package.metadata]
 requires-dist = [
     { name = "aiofiles", specifier = ">=24.1.0,<26.0.0" },
-    { name = "aiohttp", specifier = ">=3.11.14" },
+    { name = "aiohttp", specifier = ">=3.13.4" },
     { name = "aioshutil", specifier = ">=1.5,<2.0" },
     { name = "aiosmtplib", specifier = ">=4.0.0,<6.0.0" },
     { name = "aiosqlite", specifier = ">=0.21.0,<0.23.0" },
@@ -2019,6 +2020,7 @@ requires-dist = [
     { name = "quart-rate-limiter", specifier = ">=0.12.1" },
     { name = "quart-schema", extras = ["pydantic"], specifier = "~=0.21" },
     { name = "quart-wtforms", specifier = "~=1.0.3" },
+    { name = "requests", specifier = ">=2.33.0" },
     { name = "rich", specifier = ">=14.0.0,<15.0.0" },
     { name = "semver", specifier = ">=3.0.4" },
     { name = "sqlmodel", specifier = "~=0.0.24" },


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org


(tooling-trusted-releases) 01/03: Add an implementation of Noisy Secrets, and tests

Posted by sb...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch sbp
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git

commit 2922aada1e24c4ca148dce55f18d1e8733e4cf9d
Author: Sean B. Palmer <se...@miscoranda.com>
AuthorDate: Thu Apr 2 16:16:23 2026 +0100

    Add an implementation of Noisy Secrets, and tests
---
 atr/noisy.py             | 301 ++++++++++++++++
 tests/unit/test_noisy.py | 902 +++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 1203 insertions(+)

diff --git a/atr/noisy.py b/atr/noisy.py
new file mode 100644
index 00000000..35e70f6b
--- /dev/null
+++ b/atr/noisy.py
@@ -0,0 +1,301 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+
+import hmac
+import secrets
+from typing import Any, Final, Literal, NewType, TypeGuard
+
+# This file is intended for standalone use
+# It follows the order of the Noisy Secrets specification
+# Therefore it breaks some ATR coding conventions
+
+# Constants not based on custom types
+
+BASE37_ALPHABET: Final[bytes] = b"0123456789_abcdefghijklmnopqrstuvwxyz"
+BASE36_ALPHABET: Final[bytes] = b"0123456789abcdefghijklmnopqrstuvwxyz"
+BASE32_ALPHABET: Final[bytes] = b"23456789abcdefghijkmnpqrstuvwxyz"
+COMPONENT_ALPHABET: Final[bytes] = b"-0123456789abcdefghijklmnopqrstuvwxyz"
+
+BASE37_ALPHABET_SET: Final[frozenset[int]] = frozenset(BASE37_ALPHABET)
+BASE36_ALPHABET_SET: Final[frozenset[int]] = frozenset(BASE36_ALPHABET)
+BASE32_ALPHABET_SET: Final[frozenset[int]] = frozenset(BASE32_ALPHABET)
+COMPONENT_ALPHABET_SET: Final[frozenset[int]] = frozenset(COMPONENT_ALPHABET)
+
+FIELD_MAP: Final[dict[int, int]] = {c: i for i, c in enumerate(BASE37_ALPHABET)}
+Q: Final[Literal[37]] = 37
+
+DOT: Final[Literal[b"."]] = b"."
+EMPTY: Final[Literal[b""]] = b""
+HYPHEN: Final[Literal[b"-"]] = b"-"
+TWO: Final[Literal[b"2"]] = b"2"
+UNDERSCORE: Final[Literal[b"_"]] = b"_"
+
+# Constants used in tests only
+
+K: Final[Literal[32]] = 32
+
+# Custom types
+
+type Prefix = Literal[b"secret"]
+type Pad = Literal[b"_"]
+
+NamespaceString = NewType("NamespaceString", bytes)
+PayloadString = NewType("PayloadString", bytes)
+InterleavedChecksumString = NewType("InterleavedChecksumString", bytes)
+NoisySecretString = NewType("NoisySecretString", bytes)
+PaddedNamespaceTag = NewType("PaddedNamespaceTag", bytes)
+MessageTag = NewType("MessageTag", bytes)
+EvenMessageTag = NewType("EvenMessageTag", MessageTag)
+OddMessageTag = NewType("OddMessageTag", MessageTag)
+ChecksumTag = NewType("ChecksumTag", bytes)
+InterleavedChecksumTag = NewType("InterleavedChecksumTag", bytes)
+NoisySecretTag = NewType("NoisySecretTag", bytes)
+
+Namespace = NewType("Namespace", NamespaceString)
+Payload = NewType("Payload", PayloadString)
+NoisySecret = NewType("NoisySecret", NoisySecretTag)
+PaddedNamespace = NewType("PaddedNamespace", PaddedNamespaceTag)
+Message = NewType("Message", MessageTag)
+EvenMessage = NewType("EvenMessage", EvenMessageTag)
+OddMessage = NewType("OddMessage", OddMessageTag)
+Checksum = NewType("Checksum", ChecksumTag)
+InterleavedChecksum = NewType("InterleavedChecksum", InterleavedChecksumTag)
+Candidate = NewType("Candidate", bytes)
+CandidateNamespace = NewType("CandidateNamespace", NamespaceString)
+CandidatePayload = NewType("CandidatePayload", PayloadString)
+ExpectedCandidate = NewType("ExpectedCandidate", NoisySecretTag)
+
+FQDN = NewType("FQDN", bytes)
+
+# Constants based on custom types
+
+PREFIX: Final[Prefix] = b"secret"
+PAD: Final[Pad] = b"_"
+
+
+def construct_namespace(fqdn: FQDN | None) -> Namespace:
+    if fqdn is None:
+        return Namespace(NamespaceString(TWO))
+    components = fqdn.split(DOT)
+    for component in components:
+        if is_component_string(component) is False:
+            raise ValueError(f"Invalid component: {component}")
+        if component.startswith(HYPHEN) or component.endswith(HYPHEN):
+            raise ValueError(f"Component cannot start or end with a hyphen: {component}")
+        if component == EMPTY:
+            raise ValueError(f"Component cannot be empty: {component}")
+    reversed_components = list(reversed(components))
+    for i, component in enumerate(reversed_components):
+        reversed_components[i] = component.replace(HYPHEN, UNDERSCORE + UNDERSCORE)
+    joined = UNDERSCORE.join(reversed_components)
+    joined_length = len(joined)
+    if joined_length > 30:
+        raise ValueError(f"Namespace is too long: {joined}")
+    length = bytes([BASE32_ALPHABET[joined_length + 1]])
+    return Namespace(NamespaceString(length + PAD + joined))
+
+
+def construct_namespace_domain(namespace: Namespace) -> FQDN | None:
+    if namespace == Namespace(NamespaceString(TWO)):
+        return None
+    suffix = namespace[2:]
+    suffix = suffix.replace(UNDERSCORE + UNDERSCORE, HYPHEN)
+    # TODO: Specification says Pad, but this isn't really Pad
+    suffix_components = suffix.split(PAD)
+    reversed_suffix_components = list(reversed(suffix_components))
+    fqdn_bytes = DOT.join(reversed_suffix_components)
+    return FQDN(fqdn_bytes)
+
+
+def construct_padded_namespace_tag(namespace_string: NamespaceString) -> PaddedNamespaceTag:
+    length = len(namespace_string)
+    padding = PAD * (32 - length)
+    return PaddedNamespaceTag(namespace_string + padding)
+
+
+def construct_padded_namespace(namespace: Namespace) -> PaddedNamespace:
+    return PaddedNamespace(construct_padded_namespace_tag(namespace))
+
+
+def construct_payload() -> Payload:
+    payload = []
+    for i in range(32):
+        payload.append(secrets.choice(BASE32_ALPHABET))
+    return Payload(PayloadString(bytes(payload)))
+
+
+def construct_message_tags(
+    namespace_string: NamespaceString, payload_string: PayloadString
+) -> tuple[EvenMessageTag, OddMessageTag]:
+    padded_namespace_tag = construct_padded_namespace_tag(namespace_string)
+    even_message_tag = EvenMessageTag(MessageTag(padded_namespace_tag[::2] + payload_string[::2]))
+    odd_message_tag = OddMessageTag(MessageTag(padded_namespace_tag[1::2] + payload_string[1::2]))
+    return (even_message_tag, odd_message_tag)
+
+
+def construct_messages(namespace: Namespace, payload: Payload) -> tuple[EvenMessage, OddMessage]:
+    even_message_tag, odd_message_tag = construct_message_tags(namespace, payload)
+    return (EvenMessage(even_message_tag), OddMessage(odd_message_tag))
+
+
+def construct_checksum_tag(message_tag: MessageTag) -> ChecksumTag:
+    return ChecksumTag(checksum_compute(message_tag))
+
+
+def construct_checksum(message: Message) -> Checksum:
+    return Checksum(construct_checksum_tag(message))
+
+
+def construct_interleaved_checksum_tag(
+    even_checksum_tag: ChecksumTag, odd_checksum_tag: ChecksumTag
+) -> InterleavedChecksumTag:
+    # TODO: Note that InterleavedChecksumTag is a subset of InterleavedChecksumString
+    interleaved_checksum_tag = InterleavedChecksumTag(
+        bytes(b for pair in zip(even_checksum_tag, odd_checksum_tag) for b in pair)
+    )
+    return InterleavedChecksumTag(interleaved_checksum_tag)
+
+
+def construct_interleaved_checksum(even_checksum: Checksum, odd_checksum: Checksum) -> InterleavedChecksum:
+    return InterleavedChecksum(construct_interleaved_checksum_tag(even_checksum, odd_checksum))
+
+
+def construct_noisy_secret_tag(namespace_string: NamespaceString, payload_string: PayloadString) -> NoisySecretTag:
+    even_message_tag, odd_message_tag = construct_message_tags(namespace_string, payload_string)
+    even_checksum_tag = construct_checksum_tag(even_message_tag)
+    odd_checksum_tag = construct_checksum_tag(odd_message_tag)
+    interleaved_checksum_tag = construct_interleaved_checksum_tag(even_checksum_tag, odd_checksum_tag)
+    return NoisySecretTag(PREFIX + PAD + namespace_string + PAD + payload_string + interleaved_checksum_tag)
+
+
+def construct_noisy_secret(fqdn: FQDN | None) -> NoisySecret:
+    namespace = construct_namespace(fqdn)
+    payload = construct_payload()
+    noisy_secret_tag = construct_noisy_secret_tag(namespace, payload)
+    return NoisySecret(noisy_secret_tag)
+
+
+def is_candidate(value: Any) -> TypeGuard[Candidate]:
+    if not isinstance(value, bytes):
+        return False
+    if len(value) != 49 and not (51 <= len(value) <= 80):
+        return False
+    return True
+
+
+def construct_candidate_namespace(candidate: Candidate) -> CandidateNamespace | None:
+    candidate_length = len(candidate)
+    namespace_range = candidate[7 : candidate_length - 41]
+    if not is_namespace_string(namespace_range):
+        return None
+    return CandidateNamespace(NamespaceString(namespace_range))
+
+
+def construct_candidate_payload(candidate: Candidate) -> CandidatePayload | None:
+    candidate_length = len(candidate)
+    payload_range = candidate[candidate_length - 40 : candidate_length - 8]
+    if not is_payload_string(payload_range):
+        return None
+    return CandidatePayload(PayloadString(payload_range))
+
+
+def construct_expected_candidate(candidate: Candidate) -> ExpectedCandidate | None:
+    candidate_namespace = construct_candidate_namespace(candidate)
+    if candidate_namespace is None:
+        return None
+    candidate_payload = construct_candidate_payload(candidate)
+    if candidate_payload is None:
+        return None
+    expected_candidate = construct_noisy_secret_tag(candidate_namespace, candidate_payload)
+    return ExpectedCandidate(expected_candidate)
+
+
+def is_noisy_secret_tag(candidate: Candidate) -> TypeGuard[NoisySecretTag]:
+    expected_candidate = construct_expected_candidate(candidate)
+    if expected_candidate is None:
+        return False
+    return hmac.compare_digest(candidate, expected_candidate)
+
+
+def checksum_compute(message: bytes | bytearray) -> bytes:
+    r0 = r1 = r2 = r3 = 0
+    for b in message:
+        feedback = (FIELD_MAP[b] + r0) % Q
+        r0 = (r1 - feedback * 6) % Q
+        r1 = (r2 - feedback * 26) % Q
+        r2 = (r3 - feedback * 21) % Q
+        r3 = (-feedback * 3) % Q
+    return bytes(
+        [
+            BASE37_ALPHABET[(-r0) % Q],
+            BASE37_ALPHABET[(-r1) % Q],
+            BASE37_ALPHABET[(-r2) % Q],
+            BASE37_ALPHABET[(-r3) % Q],
+        ]
+    )
+
+
+def is_component_string(value: bytes) -> bool:
+    return frozenset(value) <= COMPONENT_ALPHABET_SET
+
+
+def is_namespace_string(value: bytes) -> bool:
+    value_length = len(value)
+    if value_length == 0:
+        return False
+    length_prefix = value[0:1]
+    i = BASE32_ALPHABET.find(length_prefix)
+    if i in (-1, 1):
+        return False
+    if value_length == 1:
+        return (length_prefix == TWO) and (i == 0)
+    if value_length != (i + 1):
+        return False
+    if value[1:2] != UNDERSCORE:
+        return False
+    return is_namespace_string_suffix(value[2:])
+
+
+def is_namespace_string_suffix(suffix: bytes) -> bool:
+    suffix_length = len(suffix)
+    first = suffix[0]
+    if first not in BASE36_ALPHABET_SET:
+        return False
+    if suffix_length == 1:
+        return True
+    last = suffix[-1]
+    if last not in BASE36_ALPHABET_SET:
+        return False
+    if suffix_length == 2:
+        return True
+    middle = suffix[1:-1]
+    return frozenset(middle) <= BASE37_ALPHABET_SET
+
+
+def is_payload_string(value: bytes) -> bool:
+    return frozenset(value) <= BASE32_ALPHABET_SET
+
+
+def create(fqdn: bytes | None = None) -> NoisySecret:
+    noisy_secret = construct_noisy_secret(FQDN(fqdn) if (fqdn is not None) else None)
+    return NoisySecret(noisy_secret)
+
+
+def verify(bytes_value: bytes) -> bool:
+    if not is_candidate(bytes_value):
+        return False
+    return is_noisy_secret_tag(bytes_value)
diff --git a/tests/unit/test_noisy.py b/tests/unit/test_noisy.py
new file mode 100644
index 00000000..b91adde5
--- /dev/null
+++ b/tests/unit/test_noisy.py
@@ -0,0 +1,902 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+
+import itertools
+import secrets
+from typing import Final
+
+import pytest
+
+import atr.noisy as noisy
+
+BASE32_ALPHABET: Final = noisy.BASE32_ALPHABET
+BASE32_SET: Final = noisy.BASE32_ALPHABET_SET
+BASE36_ALPHABET: Final = noisy.BASE36_ALPHABET
+BASE36_SET: Final = noisy.BASE36_ALPHABET_SET
+BASE37_ALPHABET: Final = noisy.BASE37_ALPHABET
+BASE37_SET: Final = noisy.BASE37_ALPHABET_SET
+COMPONENT_SET: Final = noisy.COMPONENT_ALPHABET_SET
+FIELD_MAP: Final = noisy.FIELD_MAP
+K: Final = noisy.K
+PREFIX: Final = noisy.PREFIX
+Q: Final = noisy.Q
+
+
+def construct_namespace(fqdn: bytes | None) -> bytes:
+    return noisy.construct_namespace(noisy.FQDN(fqdn) if fqdn is not None else None)
+
+
+def construct_namespace_domain(namespace: bytes) -> bytes | None:
+    namespace = noisy.Namespace(noisy.NamespaceString(namespace))
+    return noisy.construct_namespace_domain(namespace)
+
+
+def is_namespace_string(value: bytes) -> bool:
+    return noisy.is_namespace_string(value)
+
+
+def checksum_compute(message: bytes | bytearray) -> bytes:
+    return noisy.checksum_compute(message)
+
+
+def construct_noisy_secret_tag(namespace: bytes, payload: bytes) -> bytes:
+    return noisy.construct_noisy_secret_tag(noisy.NamespaceString(namespace), noisy.PayloadString(payload))
+
+
+def domain(candidate: bytes) -> bytes | None:
+    if not noisy.is_candidate(candidate):
+        return None
+
+    namespace = noisy.construct_candidate_namespace(candidate)
+    payload = noisy.construct_candidate_payload(candidate)
+    if namespace is None or payload is None:
+        return None
+    return noisy.construct_namespace_domain(noisy.Namespace(namespace))
+
+
+_MAX_NAMESPACE: Final = b"z_" + b"a" * 29 + b"z"
+
+_REPRESENTATIVE_NAMESPACES: Final = (
+    b"2",
+    b"e_org_example",
+    b"i_org_example_api",
+    _MAX_NAMESPACE,
+)
+
+_REPRESENTATIVE_PAYLOADS: Final = (
+    b"2" * 32,
+    BASE32_ALPHABET,
+)
+
+_SPEC_VECTORS: Final = [
+    (
+        None,
+        b"2",
+        b"22222222222222222222222222222222",
+        b"secret_2_22222222222222222222222222222222_ogwraat",
+    ),
+    (
+        None,
+        b"2",
+        b"23456789abcdefghijkmnpqrstuvwxyz",
+        b"secret_2_23456789abcdefghijkmnpqrstuvwxyz11izsa_3",
+    ),
+    (
+        b"example.org",
+        b"e_org_example",
+        b"22222222222222222222222222222222",
+        b"secret_e_org_example_222222222222222222222222222222222mcnca82",
+    ),
+    (
+        b"example.org",
+        b"e_org_example",
+        b"23456789abcdefghijkmnpqrstuvwxyz",
+        b"secret_e_org_example_23456789abcdefghijkmnpqrstuvwxyztzeqda7b",
+    ),
+]
+
+
+def _det_4x4_mod(matrix: list[list[int]], mod: int) -> int:
+    m = [row[:] for row in matrix]
+    det = 1
+    for col in range(4):
+        pivot = next((row for row in range(col, 4) if m[row][col] % mod != 0), None)
+        if pivot is None:
+            return 0
+        if pivot != col:
+            m[col], m[pivot] = m[pivot], m[col]
+            det = (-det) % mod
+        inv = pow(m[col][col], mod - 2, mod)
+        for row in range(col + 1, 4):
+            factor = (m[row][col] * inv) % mod
+            for k in range(col, 4):
+                m[row][k] = (m[row][k] - factor * m[col][k]) % mod
+        det = (det * m[col][col]) % mod
+    return det
+
+
+def _evaluate_poly_low_first(coefficients: list[int], x: int, modulus: int) -> int:
+    value = 0
+    for c in reversed(coefficients):
+        value = (value * x + c) % modulus
+    return value
+
+
+def _message_pair(namespace: bytes, payload: bytes) -> tuple[bytes, bytes]:
+    padded = namespace + b"_" * (K - len(namespace))
+    return padded[::2] + payload[::2], padded[1::2] + payload[1::2]
+
+
+def _namespace_bounds(namespace: bytes) -> tuple[int, int]:
+    start = len(PREFIX) + 1
+    return start, start + len(namespace)
+
+
+def _namespace_for_length(total_length: int) -> bytes:
+    if total_length == 1:
+        return b"2"
+    joined_length = total_length - 2
+    if joined_length == 1:
+        joined = b"a"
+    elif joined_length == 2:
+        joined = b"ab"
+    else:
+        joined = b"a" + b"b" * (joined_length - 2) + b"c"
+    return bytes([BASE32_ALPHABET[len(joined) + 1]]) + b"_" + joined
+
+
+def _payload_bounds(namespace: bytes) -> tuple[int, int]:
+    start = len(PREFIX) + 1 + len(namespace) + 1
+    return start, start + K
+
+
+def _representative_vectors() -> list[tuple[bytes, bytes, bytes]]:
+    return [
+        (namespace, payload, construct_noisy_secret_tag(namespace, payload))
+        for namespace in _REPRESENTATIVE_NAMESPACES
+        for payload in _REPRESENTATIVE_PAYLOADS
+    ]
+
+
+class TestAlphabets:
+    def test_base32_excludes_ambiguous_characters(self) -> None:
+        excluded = {ord("0"), ord("1"), ord("_"), ord("l"), ord("o")}
+        assert BASE32_SET & excluded == set()
+
+    def test_base32_length(self) -> None:
+        assert len(BASE32_ALPHABET) == 32
+
+    def test_base32_sorted_ascii(self) -> None:
+        assert BASE32_ALPHABET == bytes(sorted(BASE32_ALPHABET))
+
+    def test_base32_subset_of_base37(self) -> None:
+        assert BASE32_SET.issubset(BASE37_SET)
+
+    def test_base36_length(self) -> None:
+        assert len(BASE36_ALPHABET) == 36
+
+    def test_base36_sorted_ascii(self) -> None:
+        assert BASE36_ALPHABET == bytes(sorted(BASE36_ALPHABET))
+
+    def test_base37_length(self) -> None:
+        assert len(BASE37_ALPHABET) == 37
+
+    def test_base37_matches_field_size(self) -> None:
+        assert len(BASE37_ALPHABET) == Q
+
+    def test_base37_sorted_ascii(self) -> None:
+        assert BASE37_ALPHABET == bytes(sorted(BASE37_ALPHABET))
+
+    def test_component_set_includes_hyphen_and_base36(self) -> None:
+        assert ord("-") in COMPONENT_SET
+        assert BASE36_SET.issubset(COMPONENT_SET)
+
+    def test_field_map_bijective(self) -> None:
+        assert set(FIELD_MAP.values()) == set(range(37))
+
+    def test_field_map_size(self) -> None:
+        assert len(FIELD_MAP) == 37
+
+    def test_underscore_field_element_nonzero(self) -> None:
+        assert FIELD_MAP[ord("_")] != 0
+
+    def test_underscore_not_in_base32(self) -> None:
+        assert ord("_") not in BASE32_SET
+
+    def test_underscore_not_in_base36(self) -> None:
+        assert ord("_") not in BASE36_SET
+
+    def test_zero_character_not_in_base32(self) -> None:
+        assert ord("0") not in BASE32_SET
+
+
+class TestChecksumCompute:
+    def test_all_constant_messages_produce_constant_checksums(self) -> None:
+        for ch in BASE37_ALPHABET:
+            msg = bytes([ch]) * 32
+            assert checksum_compute(msg) == bytes([ch]) * 4
+
+    def test_codeword_vanishes_at_generator_roots(self) -> None:
+        messages = [
+            b"_" * 32,
+            b"0" * 32,
+            b"secret__________________________",
+            BASE37_ALPHABET[:32],
+        ]
+        for message in messages:
+            cksum = checksum_compute(message)
+            codeword = [FIELD_MAP[b] for b in message + cksum]
+            for root in [2, 4, 8, 16]:
+                assert _evaluate_poly_low_first(codeword, root, Q) == 0
+
+    def test_codeword_vanishes_for_random_messages(self) -> None:
+        for _ in range(50):
+            message = bytes([secrets.choice(BASE37_ALPHABET) for _ in range(32)])
+            cksum = checksum_compute(message)
+            codeword = [FIELD_MAP[b] for b in message + cksum]
+            for root in [2, 4, 8, 16]:
+                assert _evaluate_poly_low_first(codeword, root, Q) == 0
+
+    def test_output_in_base37(self) -> None:
+        cksum = checksum_compute(b"_" * 32)
+        assert all(b in BASE37_SET for b in cksum)
+
+    def test_output_length(self) -> None:
+        assert len(checksum_compute(b"_" * 32)) == 4
+
+    def test_single_error_detected(self) -> None:
+        for _ in range(50):
+            message = bytes([secrets.choice(BASE37_ALPHABET) for _ in range(32)])
+            cksum = checksum_compute(message)
+            codeword = bytearray(message + cksum)
+            idx = secrets.randbelow(36)
+            original = codeword[idx]
+            while codeword[idx] == original:
+                codeword[idx] = secrets.choice(BASE37_ALPHABET)
+            recomputed = checksum_compute(bytes(codeword[:32]))
+            assert recomputed != bytes(codeword[32:36])
+
+    def test_zero_message_has_zero_checksum(self) -> None:
+        assert checksum_compute(b"0" * 32) == b"0000"
+
+
+class TestCreate:
+    def test_all_fqdn_lengths_roundtrip(self) -> None:
+        for length in range(1, 31):
+            fqdn = b"a" * length
+            secret = noisy.create(fqdn)
+            assert noisy.verify(secret) is True
+
+    def test_each_result_unique(self) -> None:
+        results = {noisy.create() for _ in range(100)}
+        assert len(results) == 100
+
+    def test_length_no_domain(self) -> None:
+        assert len(noisy.create()) == 49
+
+    def test_length_varies_with_fqdn(self) -> None:
+        for length in range(1, 31):
+            fqdn = b"a" * length
+            result = noisy.create(fqdn)
+            assert len(result) == 50 + length
+
+    def test_max_fqdn_length(self) -> None:
+        result = noisy.create(b"a" * 30)
+        assert len(result) == 80
+        assert result.startswith(b"secret_")
+        assert noisy.verify(result) is True
+
+    def test_namespace_in_output(self) -> None:
+        result = noisy.create(b"apache.org")
+        assert b"d_org_apache" in result
+
+    def test_no_domain_prefix(self) -> None:
+        assert noisy.create().startswith(b"secret_2_")
+
+    def test_only_base37_chars(self) -> None:
+        for _ in range(50):
+            result = noisy.create(b"test")
+            assert all(b in BASE37_SET for b in result)
+
+    def test_payload_uses_base32(self) -> None:
+        result = noisy.create()
+        payload = result[9:41]
+        assert len(payload) == 32
+        assert all(b in BASE32_SET for b in payload)
+
+    def test_result_verifies(self) -> None:
+        for _ in range(20):
+            assert noisy.verify(noisy.create()) is True
+
+    def test_result_with_fqdn_verifies(self) -> None:
+        for _ in range(20):
+            assert noisy.verify(noisy.create(b"example.org")) is True
+
+
+class TestDomain:
+    def test_invalid_returns_none(self) -> None:
+        assert domain(b"not a secret") is None
+
+    def test_no_domain_returns_none(self) -> None:
+        secret = noisy.create()
+        assert domain(secret) is None
+
+    def test_roundtrip_hyphenated(self) -> None:
+        secret = noisy.create(b"a-b.org")
+        assert domain(secret) == b"a-b.org"
+
+    def test_roundtrip_simple(self) -> None:
+        secret = noisy.create(b"apache.org")
+        assert domain(secret) == b"apache.org"
+
+    def test_roundtrip_subdomain(self) -> None:
+        secret = noisy.create(b"api.example.org")
+        assert domain(secret) == b"api.example.org"
+
+    def test_wrong_length_returns_none(self) -> None:
+        assert domain(b"a" * 50) is None
+
+
+class TestErrorDetection:
+    def test_adjacent_transpositions_detected(self) -> None:
+        for _, _ns, _pl, secret in _SPEC_VECTORS:
+            for pos in range(len(secret) - 1):
+                if secret[pos] == secret[pos + 1]:
+                    continue
+                mutated = bytearray(secret)
+                mutated[pos], mutated[pos + 1] = mutated[pos + 1], mutated[pos]
+                assert noisy.verify(bytes(mutated)) is False
+
+    def test_burst_of_8_consecutive_in_payload_detected(self) -> None:
+        for _, namespace, _pl, secret in _SPEC_VECTORS:
+            ns_len = len(namespace)
+            payload_start = 8 + ns_len
+            for start_offset in range(32 - 7):
+                mutated = bytearray(secret)
+                changed = False
+                for i in range(8):
+                    pos = payload_start + start_offset + i
+                    old = mutated[pos]
+                    idx = BASE32_ALPHABET.index(old)
+                    new = BASE32_ALPHABET[(idx + 1) % 32]
+                    if new != old:
+                        changed = True
+                    mutated[pos] = new
+                if changed:
+                    assert noisy.verify(bytes(mutated)) is False
+
+    def test_double_substitution_in_even_payload_positions_detected(self) -> None:
+        _, namespace, _pl, secret = _SPEC_VECTORS[3]
+        payload_start = len(namespace) + 8
+        even_positions = list(range(payload_start, payload_start + 32, 2))
+        for pos1, pos2 in itertools.combinations(even_positions, 2):
+            mutated = bytearray(secret)
+            idx1 = BASE32_ALPHABET.index(mutated[pos1])
+            idx2 = BASE32_ALPHABET.index(mutated[pos2])
+            mutated[pos1] = BASE32_ALPHABET[(idx1 + 1) % 32]
+            mutated[pos2] = BASE32_ALPHABET[(idx2 + 2) % 32]
+            if bytes(mutated) != secret:
+                assert noisy.verify(bytes(mutated)) is False
+
+    def test_double_substitution_in_odd_payload_positions_detected(self) -> None:
+        _, namespace, _pl, secret = _SPEC_VECTORS[3]
+        payload_start = len(namespace) + 8
+        odd_positions = list(range(payload_start + 1, payload_start + 32, 2))
+        for pos1, pos2 in itertools.combinations(odd_positions, 2):
+            mutated = bytearray(secret)
+            idx1 = BASE32_ALPHABET.index(mutated[pos1])
+            idx2 = BASE32_ALPHABET.index(mutated[pos2])
+            mutated[pos1] = BASE32_ALPHABET[(idx1 + 1) % 32]
+            mutated[pos2] = BASE32_ALPHABET[(idx2 + 2) % 32]
+            if bytes(mutated) != secret:
+                assert noisy.verify(bytes(mutated)) is False
+
+    def test_single_byte_corruption(self) -> None:
+        for _ in range(20):
+            secret = noisy.create(b"test")
+            mutated = bytearray(secret)
+            idx = secrets.randbelow(len(mutated))
+            original = mutated[idx]
+            while mutated[idx] == original:
+                mutated[idx] = secrets.choice(BASE37_ALPHABET)
+            assert noisy.verify(bytes(mutated)) is False
+
+    def test_single_substitutions_detected(self) -> None:
+        for _, _ns, _pl, secret in _SPEC_VECTORS:
+            for pos in range(len(secret)):
+                original = secret[pos]
+                for replacement in BASE37_ALPHABET:
+                    if replacement == original:
+                        continue
+                    mutated = bytearray(secret)
+                    mutated[pos] = replacement
+                    assert noisy.verify(bytes(mutated)) is False
+
+
+class TestGeneratorPolynomial:
+    def test_alpha_generates_all_nonzero_elements(self) -> None:
+        generated = {pow(2, i, 37) for i in range(36)}
+        assert generated == set(range(1, 37))
+
+    def test_alpha_is_primitive_root(self) -> None:
+        proper_divisors_of_36 = [1, 2, 3, 4, 6, 9, 12, 18]
+        for d in proper_divisors_of_36:
+            assert pow(2, d, 37) != 1
+        assert pow(2, 36, 37) == 1
+
+    def test_coefficients_from_roots(self) -> None:
+        poly = [1, (-2) % 37]
+        for root in [4, 8, 16]:
+            new_poly = [0] * (len(poly) + 1)
+            for i, c in enumerate(poly):
+                new_poly[i] = (new_poly[i] + c) % 37
+                new_poly[i + 1] = (new_poly[i + 1] - c * root) % 37
+            poly = new_poly
+        assert poly == [1, 7, 21, 2, 25]
+
+    def test_field_size_is_prime(self) -> None:
+        for d in range(2, int(37**0.5) + 1):
+            assert 37 % d != 0
+
+    def test_minimum_distance_all_4x4_submatrices_nonsingular(self) -> None:
+        n = 36
+        t = 4
+        alpha = 2
+        q = 37
+        parity_check = [[pow(alpha, (i + 1) * j, q) for j in range(n)] for i in range(t)]
+        for cols in itertools.combinations(range(n), t):
+            submatrix = [[parity_check[i][j] for j in cols] for i in range(t)]
+            assert _det_4x4_mod(submatrix, q) != 0
+
+    def test_roots_of_generator(self) -> None:
+        gen = [25, 2, 21, 7, 1]
+        for root in [2, 4, 8, 16]:
+            assert _evaluate_poly_low_first(gen, root, 37) == 0
+
+
+class TestGrammar:
+    def test_full_fqdn_structure(self) -> None:
+        secret = noisy.create(b"extra.airflow.apache.org")
+        namespace = b"t_org_apache_airflow_extra"
+        ns_len = len(namespace)
+        assert secret[:6] == b"secret"
+        assert secret[6:7] == b"_"
+        assert secret[7 : 7 + ns_len] == namespace
+        assert secret[7 + ns_len : 8 + ns_len] == b"_"
+        assert len(secret) == 48 + ns_len
+
+    def test_no_domain_structure(self) -> None:
+        secret = noisy.create()
+        assert secret[:6] == b"secret"
+        assert secret[6:7] == b"_"
+        assert secret[7:8] == b"2"
+        assert secret[8:9] == b"_"
+        assert len(secret) == 49
+
+    def test_simple_fqdn_structure(self) -> None:
+        secret = noisy.create(b"apache.org")
+        namespace = b"d_org_apache"
+        ns_len = len(namespace)
+        assert secret[:6] == b"secret"
+        assert secret[6:7] == b"_"
+        assert secret[7 : 7 + ns_len] == namespace
+        assert secret[7 + ns_len : 8 + ns_len] == b"_"
+        assert len(secret) == 48 + ns_len
+
+
+class TestInterleavingProtection:
+    def test_namespace_change_affects_both_messages(self) -> None:
+        payload = b"2" * 32
+        padded_before = b"7_test" + b"_" * 26
+        padded_after = b"7_xtest" + b"_" * 25
+
+        even_before = padded_before[::2] + payload[::2]
+        even_after = padded_after[::2] + payload[::2]
+        odd_before = padded_before[1::2] + payload[1::2]
+        odd_after = padded_after[1::2] + payload[1::2]
+
+        assert even_before != even_after
+        assert odd_before != odd_after
+
+
+class TestLengthGapProtection:
+    def test_delete_from_51_gives_rejected_50(self) -> None:
+        secret = noisy.create(b"a")
+        assert len(secret) == 51
+        for pos in range(len(secret)):
+            mutated = secret[:pos] + secret[pos + 1 :]
+            assert len(mutated) == 50
+            assert noisy.verify(mutated) is False
+
+    def test_insert_into_49_gives_rejected_50(self) -> None:
+        secret = noisy.create()
+        assert len(secret) == 49
+        for ch in BASE37_ALPHABET:
+            mutated = secret[:8] + bytes([ch]) + secret[8:]
+            assert len(mutated) == 50
+            assert noisy.verify(mutated) is False
+
+    def test_length_49_valid(self) -> None:
+        secret = noisy.create()
+        assert len(secret) == 49
+        assert noisy.verify(secret) is True
+
+    def test_length_50_always_rejected(self) -> None:
+        assert noisy.verify(b"a" * 50) is False
+
+    def test_length_51_valid(self) -> None:
+        secret = noisy.create(b"a")
+        assert len(secret) == 51
+        assert noisy.verify(secret) is True
+
+
+class TestMalformedStructure:
+    def test_missing_prefix(self) -> None:
+        secret = noisy.create()
+        mutated = b"abcdef" + secret[6:]
+        assert noisy.verify(mutated) is False
+
+    def test_non_base32_in_payload(self) -> None:
+        secret = noisy.create()
+        mutated = bytearray(secret)
+        mutated[9] = ord("0")
+        assert noisy.verify(bytes(mutated)) is False
+
+    def test_underscore_in_payload(self) -> None:
+        secret = noisy.create()
+        mutated = bytearray(secret)
+        mutated[9] = ord("_")
+        assert noisy.verify(bytes(mutated)) is False
+
+    def test_wrong_separator_after_namespace(self) -> None:
+        secret = noisy.create(b"test")
+        mutated = bytearray(secret)
+        sep_pos = 7 + len(construct_namespace(b"test"))
+        mutated[sep_pos] = ord("a")
+        assert noisy.verify(bytes(mutated)) is False
+
+    def test_wrong_separator_after_prefix(self) -> None:
+        secret = noisy.create()
+        mutated = bytearray(secret)
+        mutated[6] = ord("a")
+        assert noisy.verify(bytes(mutated)) is False
+
+
+class TestMutationResistance:
+    def test_no_single_deletion_validates_long_fqdn(self) -> None:
+        secret = noisy.create(b"airflow.apache.org")
+        for pos in range(len(secret)):
+            mutated = secret[:pos] + secret[pos + 1 :]
+            assert noisy.verify(mutated) is False
+
+    def test_no_single_deletion_validates_no_domain(self) -> None:
+        secret = noisy.create()
+        for pos in range(len(secret)):
+            mutated = secret[:pos] + secret[pos + 1 :]
+            assert noisy.verify(mutated) is False
+
+    def test_no_single_deletion_validates_short_fqdn(self) -> None:
+        secret = noisy.create(b"test")
+        for pos in range(len(secret)):
+            mutated = secret[:pos] + secret[pos + 1 :]
+            assert noisy.verify(mutated) is False
+
+    def test_no_single_insertion_validates_long_fqdn(self) -> None:
+        secret = noisy.create(b"airflow.apache.org")
+        for pos in range(len(secret) + 1):
+            for ch in BASE37_ALPHABET:
+                mutated = secret[:pos] + bytes([ch]) + secret[pos:]
+                assert noisy.verify(mutated) is False
+
+    def test_no_single_insertion_validates_no_domain(self) -> None:
+        secret = noisy.create()
+        for pos in range(len(secret) + 1):
+            for ch in BASE37_ALPHABET:
+                mutated = secret[:pos] + bytes([ch]) + secret[pos:]
+                assert noisy.verify(mutated) is False
+
+    def test_no_single_insertion_validates_short_fqdn(self) -> None:
+        secret = noisy.create(b"test")
+        for pos in range(len(secret) + 1):
+            for ch in BASE37_ALPHABET:
+                mutated = secret[:pos] + bytes([ch]) + secret[pos:]
+                assert noisy.verify(mutated) is False
+
+
+class TestNamespaceConstruct:
+    def test_empty_component_rejected(self) -> None:
+        with pytest.raises(ValueError):
+            construct_namespace(b"bad..example")
+
+    def test_empty_string_rejected(self) -> None:
+        with pytest.raises(ValueError):
+            construct_namespace(b"")
+
+    def test_hyphen_boundary_end_rejected(self) -> None:
+        with pytest.raises(ValueError):
+            construct_namespace(b"test-.org")
+
+    def test_hyphen_boundary_start_rejected(self) -> None:
+        with pytest.raises(ValueError):
+            construct_namespace(b"-test.org")
+
+    def test_hyphen_conversion(self) -> None:
+        assert construct_namespace(b"a-b.org") == b"b_org_a__b"
+
+    def test_invalid_chars_rejected(self) -> None:
+        with pytest.raises(ValueError):
+            construct_namespace(b"Hello.org")
+
+    def test_length_prefix_correct_for_all_fqdn_lengths(self) -> None:
+        for fqdn_len in range(1, 31):
+            fqdn = b"a" * fqdn_len
+            namespace = construct_namespace(fqdn)
+            assert namespace[0] == BASE32_ALPHABET[len(namespace) - 1]
+
+    def test_multi_component(self) -> None:
+        assert construct_namespace(b"apache.org") == b"d_org_apache"
+
+    def test_multi_level_domain(self) -> None:
+        assert construct_namespace(b"extra.airflow.apache.org") == b"t_org_apache_airflow_extra"
+
+    def test_none_produces_digit_two(self) -> None:
+        assert construct_namespace(None) == b"2"
+
+    def test_single_component(self) -> None:
+        assert construct_namespace(b"test") == b"7_test"
+
+    def test_too_long_rejected(self) -> None:
+        with pytest.raises(ValueError):
+            construct_namespace(b"a" * 31)
+
+
+class TestNamespaceDomain:
+    def test_hyphen_roundtrip(self) -> None:
+        ns = construct_namespace(b"a-b.org")
+        assert construct_namespace_domain(ns) == b"a-b.org"
+
+    def test_multi_level_roundtrip(self) -> None:
+        ns = construct_namespace(b"api.example.org")
+        assert construct_namespace_domain(ns) == b"api.example.org"
+
+    def test_none_roundtrip(self) -> None:
+        ns = construct_namespace(None)
+        assert construct_namespace_domain(ns) is None
+
+    def test_simple_roundtrip(self) -> None:
+        ns = construct_namespace(b"apache.org")
+        assert construct_namespace_domain(ns) == b"apache.org"
+
+
+class TestNamespaceStringCheck:
+    def test_empty_rejected(self) -> None:
+        assert is_namespace_string(b"") is False
+
+    def test_first_content_char_not_in_base36_rejected(self) -> None:
+        assert is_namespace_string(b"6___a") is False
+
+    def test_internal_non_base37_char_rejected(self) -> None:
+        assert is_namespace_string(b"6_a-a") is False
+
+    def test_last_content_char_not_in_base36_rejected(self) -> None:
+        assert is_namespace_string(b"6_a__") is False
+
+    def test_length_two_rejected(self) -> None:
+        assert is_namespace_string(b"ab") is False
+
+    def test_no_domain_accepted(self) -> None:
+        assert is_namespace_string(b"2") is True
+
+    def test_second_char_not_underscore_rejected(self) -> None:
+        assert is_namespace_string(b"7atest") is False
+
+    def test_too_long_rejected(self) -> None:
+        ns = b"z_" + b"a" * 30 + b"z"
+        assert len(ns) == 33
+        assert is_namespace_string(ns) is False
+
+    def test_valid_namespace_accepted(self) -> None:
+        assert is_namespace_string(b"e_org_example") is True
+
+    def test_wrong_length_prefix_rejected(self) -> None:
+        assert is_namespace_string(b"4_org_example") is False
+
+
+class TestPaddingInjectivity:
+    def test_boundary_char_ensures_injectivity(self) -> None:
+        for length in [1, *list(range(3, 33))]:
+            namespace = _namespace_for_length(length)
+            padded = namespace + b"_" * (32 - length)
+            assert padded[length - 1] != ord("_")
+            if length < 32:
+                assert padded[length] == ord("_")
+
+    def test_different_lengths_produce_different_padded_forms(self) -> None:
+        valid_lengths = [1, *list(range(3, 33))]
+        padded_forms = {}
+        for length in valid_lengths:
+            namespace = _namespace_for_length(length)
+            padded_forms[length] = namespace + b"_" * (32 - len(namespace))
+        for l1, l2 in itertools.combinations(valid_lengths, 2):
+            assert padded_forms[l1] != padded_forms[l2]
+
+    def test_underscore_internal_namespaces_still_injective(self) -> None:
+        ns_a = construct_namespace(b"a.b")
+        ns_b = construct_namespace(b"a.b.c")
+        padded_a = ns_a + b"_" * (32 - len(ns_a))
+        padded_b = ns_b + b"_" * (32 - len(ns_b))
+        assert padded_a != padded_b
+
+
+class TestRegionTargetedMutation:
+    def test_no_single_namespace_deletion(self) -> None:
+        for namespace, _payload, secret in _representative_vectors():
+            ns_start, ns_stop = _namespace_bounds(namespace)
+            for position in range(ns_start, ns_stop):
+                mutated = secret[:position] + secret[position + 1 :]
+                assert noisy.verify(mutated) is False
+
+    def test_no_single_namespace_insertion(self) -> None:
+        for namespace, _payload, secret in _representative_vectors():
+            ns_start, ns_stop = _namespace_bounds(namespace)
+            for position in range(ns_start, ns_stop + 1):
+                for inserted in BASE37_ALPHABET:
+                    mutated = secret[:position] + bytes([inserted]) + secret[position:]
+                    assert noisy.verify(mutated) is False
+
+    def test_no_single_payload_deletion(self) -> None:
+        for namespace, _payload, secret in _representative_vectors():
+            pl_start, pl_stop = _payload_bounds(namespace)
+            for position in range(pl_start, pl_stop):
+                mutated = secret[:position] + secret[position + 1 :]
+                assert noisy.verify(mutated) is False
+
+    def test_no_single_payload_insertion(self) -> None:
+        for namespace, _payload, secret in _representative_vectors():
+            pl_start, pl_stop = _payload_bounds(namespace)
+            for position in range(pl_start, pl_stop + 1):
+                for inserted in BASE32_ALPHABET:
+                    mutated = secret[:position] + bytes([inserted]) + secret[position:]
+                    assert noisy.verify(mutated) is False
+
+    def test_representative_vectors_verify(self) -> None:
+        for _namespace, _payload, secret in _representative_vectors():
+            assert noisy.verify(secret) is True
+
+
+class TestSpecVectors:
+    def test_tag_construct_matches_vector_1(self) -> None:
+        _, namespace, payload, expected = _SPEC_VECTORS[0]
+        assert construct_noisy_secret_tag(namespace, payload) == expected
+
+    def test_tag_construct_matches_vector_2(self) -> None:
+        _, namespace, payload, expected = _SPEC_VECTORS[1]
+        assert construct_noisy_secret_tag(namespace, payload) == expected
+
+    def test_tag_construct_matches_vector_3(self) -> None:
+        _, namespace, payload, expected = _SPEC_VECTORS[2]
+        assert construct_noisy_secret_tag(namespace, payload) == expected
+
+    def test_tag_construct_matches_vector_4(self) -> None:
+        _, namespace, payload, expected = _SPEC_VECTORS[3]
+        assert construct_noisy_secret_tag(namespace, payload) == expected
+
+    def test_vectors_all_verify(self) -> None:
+        for _, _ns, _pl, secret in _SPEC_VECTORS:
+            assert noisy.verify(secret) is True
+
+    def test_vectors_codewords_vanish_at_roots(self) -> None:
+        for _, namespace, payload, secret in _SPEC_VECTORS:
+            even_msg, odd_msg = _message_pair(namespace, payload)
+            even_cksum = secret[-8::2]
+            odd_cksum = secret[-7::2]
+            even_cw = [FIELD_MAP[b] for b in even_msg + even_cksum]
+            odd_cw = [FIELD_MAP[b] for b in odd_msg + odd_cksum]
+            for root in [2, 4, 8, 16]:
+                assert _evaluate_poly_low_first(even_cw, root, Q) == 0
+                assert _evaluate_poly_low_first(odd_cw, root, Q) == 0
+
+    def test_vectors_field_element_mapping(self) -> None:
+        even_msg = b"2_______________2222222222222222"
+        odd_msg = b"________________2222222222222222"
+        even_syms = [FIELD_MAP[b] for b in even_msg]
+        odd_syms = [FIELD_MAP[b] for b in odd_msg]
+        assert even_syms == [2] + ([10] * 15) + ([2] * 16)
+        assert odd_syms == ([10] * 16) + ([2] * 16)
+
+    def test_vectors_have_correct_lengths(self) -> None:
+        assert len(_SPEC_VECTORS[0][3]) == 49
+        assert len(_SPEC_VECTORS[1][3]) == 49
+        assert len(_SPEC_VECTORS[2][3]) == 61
+        assert len(_SPEC_VECTORS[3][3]) == 61
+
+    def test_vectors_namespace_domain_roundtrip(self) -> None:
+        for fqdn, namespace, _pl, _secret in _SPEC_VECTORS:
+            if fqdn is not None:
+                assert construct_namespace(fqdn) == namespace
+                assert construct_namespace_domain(namespace) == fqdn
+
+
+class TestTagConstruct:
+    def test_checksum_even_odd_positions(self) -> None:
+        namespace = b"e_org_example"
+        payload = b"2" * 32
+        tag = construct_noisy_secret_tag(namespace, payload)
+        even_msg, odd_msg = _message_pair(namespace, payload)
+        expected_even = checksum_compute(even_msg)
+        expected_odd = checksum_compute(odd_msg)
+        assert tag[-8::2] == expected_even
+        assert tag[-7::2] == expected_odd
+
+    def test_prefix_and_separators(self) -> None:
+        tag = construct_noisy_secret_tag(b"2", b"2" * 32)
+        assert tag[:7] == b"secret_"
+        assert tag[7:8] == b"2"
+        assert tag[8:9] == b"_"
+
+    def test_structure_with_domain(self) -> None:
+        namespace = b"e_org_example"
+        payload = BASE32_ALPHABET
+        tag = construct_noisy_secret_tag(namespace, payload)
+        ns_len = len(namespace)
+        assert tag[:6] == b"secret"
+        assert tag[6:7] == b"_"
+        assert tag[7 : 7 + ns_len] == namespace
+        assert tag[7 + ns_len : 8 + ns_len] == b"_"
+        assert tag[8 + ns_len : 40 + ns_len] == payload
+        assert len(tag) == 48 + ns_len
+
+
+class TestVerify:
+    def test_boundary_lengths_rejected(self) -> None:
+        assert noisy.verify(b"a" * 48) is False
+        assert noisy.verify(b"a" * 50) is False
+        assert noisy.verify(b"a" * 81) is False
+
+    def test_empty_rejected(self) -> None:
+        assert noisy.verify(b"") is False
+
+    def test_invalid_base37_chars_rejected(self) -> None:
+        secret = noisy.create(b"test")
+        mutated = bytearray(secret)
+        mutated[0] = ord("A")
+        assert noisy.verify(bytes(mutated)) is False
+
+    def test_invalid_length_base37_strings(self) -> None:
+        assert noisy.verify(b"a" * 49) is False
+        assert noisy.verify(b"a" * 80) is False
+
+    def test_truncated_left(self) -> None:
+        secret = noisy.create(b"test")
+        assert noisy.verify(secret[1:]) is False
+
+    def test_truncated_right(self) -> None:
+        secret = noisy.create(b"test")
+        assert noisy.verify(secret[:-1]) is False
+
+    def test_valid_no_domain(self) -> None:
+        secret = noisy.create()
+        assert noisy.verify(secret) is True
+
+    def test_valid_with_domain(self) -> None:
+        secret = noisy.create(b"test")
+        assert noisy.verify(secret) is True
+
+    def test_wrong_prefix(self) -> None:
+        secret = noisy.create(b"test")
+        mutated = b"secrat" + secret[6:]
+        assert noisy.verify(mutated) is False


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org