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/07/14 15:10:41 UTC
(tooling-trusted-releases) 01/01: Allow SHA-512 files to be generated without signatures
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 2f2d1e786a4ddd3aabb6d7970a4fd1b5e54d35b4
Author: Sean B. Palmer <se...@miscoranda.com>
AuthorDate: Tue Jul 14 16:10:14 2026 +0100
Allow SHA-512 files to be generated without signatures
---
atr/models/attestable.py | 1 +
atr/storage/writers/release.py | 58 +++++++------------
atr/storage/writers/revision.py | 2 -
atr/templates/draft-tools.html | 6 +-
tests/unit/test_release_writer_hashgen.py | 95 ++++++++++++++++++-------------
5 files changed, 82 insertions(+), 80 deletions(-)
diff --git a/atr/models/attestable.py b/atr/models/attestable.py
index b9483dbb..8ea29432 100644
--- a/atr/models/attestable.py
+++ b/atr/models/attestable.py
@@ -54,6 +54,7 @@ class HashEntryV2(schema.Strict):
class GeneratorV2(enum.Enum):
+ SHA512_FROM_CONTENT = "SHA512_from_content"
SHA512_FROM_SIGNATURE = "SHA512_from_signature"
diff --git a/atr/storage/writers/release.py b/atr/storage/writers/release.py
index 84329abc..249721a1 100644
--- a/atr/storage/writers/release.py
+++ b/atr/storage/writers/release.py
@@ -246,9 +246,9 @@ async def _signature_provenance_metadata_for(
version_key: safe.VersionKey,
parent_revision: sql.Revision | None,
signature_rel_path: pathlib.Path,
-) -> dict[str, str]:
+) -> dict[str, str] | None:
if parent_revision is None:
- raise datatypes.FailedError("SHA512 generation requires a parent revision with a verified signature.")
+ return None
release_key = sql.release_key(str(project_key), str(version_key))
parent_revision_number = parent_revision.safe_number
parent_number = str(parent_revision_number)
@@ -263,13 +263,10 @@ async def _signature_provenance_metadata_for(
)
if not check_results:
log.info(
- "SHA512 generation is waiting for signature verification"
+ "SHA512 generation proceeding without signature provenance"
f" release={release_key} revision={parent_number} path={signature_rel_str}"
)
- raise datatypes.FailedError(
- "Signature verification has not completed yet for"
- f" {signature_rel_path.name}. Please wait for checks to finish and try again."
- )
+ return None
latest = check_results[0]
latest_message = latest.message.strip() if isinstance(getattr(latest, "message", None), str) else None
log.info(
@@ -279,14 +276,7 @@ async def _signature_provenance_metadata_for(
f" status={latest.status.value} message={latest_message!r}"
)
if latest.status != sql.CheckResultStatus.NOTE:
- if latest_message:
- raise datatypes.FailedError(
- f"Signature verification for {signature_rel_path.name} failed: {latest_message}"
- )
- raise datatypes.FailedError(
- "Signature verification for"
- f" {signature_rel_path.name} is {latest.status.value}; cannot generate the SHA512 yet."
- )
+ return None
payload = latest.data if isinstance(latest.data, dict) else {}
metadata = {"signature_path": signature_rel_str}
for key in ("fingerprint", "key_id", "timestamp", "username"):
@@ -674,35 +664,31 @@ class CommitteeParticipant(FoundationCommitter):
if await aiofiles.os.path.exists(hash_path_in_new_revision):
raise storage.AccessError("SHA512 file already exists", status=409)
- if not await aiofiles.os.path.exists(signature_path_in_new_revision):
- raise datatypes.FailedError(
- "SHA512 generation requires a detached OpenPGP signature at"
- f" {signature_rel_path!s}. Please upload the signature first."
+ signature_metadata = None
+ if await aiofiles.os.path.exists(signature_path_in_new_revision):
+ signature_metadata = await _signature_provenance_metadata_for(
+ project_key=project_key,
+ version_key=version_key,
+ parent_revision=old_rev,
+ signature_rel_path=signature_rel_path,
)
- signature_metadata = await _signature_provenance_metadata_for(
- project_key=project_key,
- version_key=version_key,
- parent_revision=old_rev,
- signature_rel_path=signature_rel_path,
- )
-
hash_value, source_content_hash = await hashes.compute_sha512_and_content_hash(path_in_new_revision.path)
async with aiofiles.open(hash_path_in_new_revision, "w") as f:
await f.write(f"{hash_value} {rel_path.name}\n")
generated_rel = safe.RelPath(str(rel_path.parent / hash_path_rel_name))
- provenance = attestable.ProvenanceV2(
- generator=attestable.GeneratorV2.SHA512_FROM_SIGNATURE,
- metadata={
- "initiated_by": self.__asf_uid,
- "signature_path": signature_metadata["signature_path"],
- "source_content_hashes": {str(rel_path): source_content_hash},
- "source_paths": [str(rel_path)],
- **{key: value for key, value in signature_metadata.items() if (key != "signature_path")},
- },
- )
+ generator = attestable.GeneratorV2.SHA512_FROM_CONTENT
+ metadata: dict[str, Any] = {
+ "initiated_by": self.__asf_uid,
+ "source_content_hashes": {str(rel_path): source_content_hash},
+ "source_paths": [str(rel_path)],
+ }
+ if signature_metadata is not None:
+ generator = attestable.GeneratorV2.SHA512_FROM_SIGNATURE
+ metadata.update(signature_metadata)
+ provenance = attestable.ProvenanceV2(generator=generator, metadata=metadata)
return {generated_rel: provenance}
await self.__write_as.revision.create_revision_with_quarantine(
diff --git a/atr/storage/writers/revision.py b/atr/storage/writers/revision.py
index ca8f8a7a..6102f5d0 100644
--- a/atr/storage/writers/revision.py
+++ b/atr/storage/writers/revision.py
@@ -334,8 +334,6 @@ async def _verify_provenance_sources_unchanged(
# This function checks input file inodes to ensure that they match
# If they don't match, we raise an error
for generated_rel, provenance_entry in path_provenance.items():
- if provenance_entry.generator != atr.models.attestable.GeneratorV2.SHA512_FROM_SIGNATURE:
- continue
dependency_paths = [path for path in provenance_entry.metadata.get("source_paths", []) if isinstance(path, str)]
if (signature_path := provenance_entry.metadata.get("signature_path")) is not None:
if isinstance(signature_path, str) and (signature_path not in dependency_paths):
diff --git a/atr/templates/draft-tools.html b/atr/templates/draft-tools.html
index d2f434e2..0b2155a9 100644
--- a/atr/templates/draft-tools.html
+++ b/atr/templates/draft-tools.html
@@ -24,9 +24,9 @@
<h2>Tools</h2>
<h3>Generate hash files</h3>
<p>
- Generate an SHA512 hash file for this file. ATR requires a detached OpenPGP
- signature (<code>.asc</code>) alongside the file, and the signature check must
- have completed successfully, before the SHA512 can be generated.
+ Generate an SHA512 hash file for this file. If a detached OpenPGP signature
+ (<code>.asc</code>) is present alongside the file and its check has completed
+ successfully, the signature is recorded in the provenance of the SHA512 file.
</p>
<div class="d-flex gap-2 mb-4">
{{ sha512_form|safe }}
diff --git a/tests/unit/test_release_writer_hashgen.py b/tests/unit/test_release_writer_hashgen.py
index 5a6ddfb4..84908aa7 100644
--- a/tests/unit/test_release_writer_hashgen.py
+++ b/tests/unit/test_release_writer_hashgen.py
@@ -25,7 +25,6 @@ import pytest
import atr.models.attestable as attestable
import atr.models.safe as safe
import atr.models.sql as sql
-import atr.storage.datatypes as datatypes
import atr.storage.writers.release as release
@@ -129,11 +128,12 @@ async def test_generate_hash_file_creates_hash_file_and_returns_provenance(tmp_p
@pytest.mark.asyncio
-async def test_generate_hash_file_requires_signature_file(tmp_path: pathlib.Path):
+async def test_generate_hash_file_without_signature_file(tmp_path: pathlib.Path):
rel_path = pathlib.Path("artifact.tar.gz")
(tmp_path / rel_path).write_bytes(b"payload")
state_path = safe.StatePath(tmp_path)
old_revision = SimpleNamespace(safe_number=safe.RevisionNumber("00001"))
+ captured: dict[str, object] = {}
async def create_revision_with_quarantine(
project_key: safe.ProjectKey,
@@ -146,23 +146,40 @@ async def test_generate_hash_file_requires_signature_file(tmp_path: pathlib.Path
) -> None:
if modify is None:
raise RuntimeError("Expected modify callback")
- await modify(state_path, old_revision)
+ captured["path_provenance"] = await modify(state_path, old_revision)
participant = _make_participant(create_revision_with_quarantine)
- with mock.patch.object(
- release,
- "_signature_provenance_metadata_for",
- new=mock.AsyncMock(),
- ) as signature_metadata_mock:
- with pytest.raises(datatypes.FailedError, match=r"requires a detached OpenPGP signature"):
- await participant.generate_hash_file(
- safe.ProjectKey("proj"),
- safe.VersionKey("1.0"),
- rel_path,
- )
+ with (
+ mock.patch.object(
+ release,
+ "_signature_provenance_metadata_for",
+ new=mock.AsyncMock(),
+ ) as signature_metadata_mock,
+ mock.patch.object(
+ release.hashes,
+ "compute_sha512_and_content_hash",
+ new=mock.AsyncMock(return_value=("sha512hex", "blake3:source")),
+ ),
+ ):
+ await participant.generate_hash_file(
+ safe.ProjectKey("proj"),
+ safe.VersionKey("1.0"),
+ rel_path,
+ )
assert signature_metadata_mock.await_count == 0
+ assert (tmp_path / "artifact.tar.gz.sha512").read_text() == "sha512hex artifact.tar.gz\n"
+ assert captured["path_provenance"] == {
+ safe.RelPath("artifact.tar.gz.sha512"): attestable.ProvenanceV2(
+ generator=attestable.GeneratorV2.SHA512_FROM_CONTENT,
+ metadata={
+ "initiated_by": "alice",
+ "source_content_hashes": {"artifact.tar.gz": "blake3:source"},
+ "source_paths": ["artifact.tar.gz"],
+ },
+ )
+ }
@pytest.mark.asyncio
@@ -203,7 +220,7 @@ async def test_signature_provenance_metadata_for_filters_unknown_fields():
@pytest.mark.asyncio
-async def test_signature_provenance_metadata_for_requires_completed_check():
+async def test_signature_provenance_metadata_for_without_completed_check():
parent_revision = SimpleNamespace(safe_number=safe.RevisionNumber("00005"))
session = CheckResultSession([])
@@ -211,30 +228,30 @@ async def test_signature_provenance_metadata_for_requires_completed_check():
mock.patch.object(release.db, "session", return_value=session),
mock.patch.object(release.log, "info") as log_info_mock,
):
- with pytest.raises(datatypes.FailedError, match=r"has not completed yet"):
- await release._signature_provenance_metadata_for(
- project_key=safe.ProjectKey("proj"),
- version_key=safe.VersionKey("1.0"),
- parent_revision=parent_revision,
- signature_rel_path=pathlib.Path("artifact.tar.gz.asc"),
- )
- assert log_info_mock.call_count == 1
- assert "waiting for signature verification" in log_info_mock.call_args.args[0]
-
-
-@pytest.mark.asyncio
-async def test_signature_provenance_metadata_for_requires_parent_revision():
- with pytest.raises(datatypes.FailedError, match=r"requires a parent revision"):
- await release._signature_provenance_metadata_for(
+ metadata = await release._signature_provenance_metadata_for(
project_key=safe.ProjectKey("proj"),
version_key=safe.VersionKey("1.0"),
- parent_revision=None,
+ parent_revision=parent_revision,
signature_rel_path=pathlib.Path("artifact.tar.gz.asc"),
)
+ assert metadata is None
+ assert log_info_mock.call_count == 1
+ assert "proceeding without signature provenance" in log_info_mock.call_args.args[0]
+
+
+@pytest.mark.asyncio
+async def test_signature_provenance_metadata_for_without_parent_revision():
+ metadata = await release._signature_provenance_metadata_for(
+ project_key=safe.ProjectKey("proj"),
+ version_key=safe.VersionKey("1.0"),
+ parent_revision=None,
+ signature_rel_path=pathlib.Path("artifact.tar.gz.asc"),
+ )
+ assert metadata is None
@pytest.mark.asyncio
-async def test_signature_provenance_metadata_for_requires_successful_check():
+async def test_signature_provenance_metadata_for_without_successful_check():
parent_revision = SimpleNamespace(safe_number=safe.RevisionNumber("00005"))
result = SimpleNamespace(
message="No valid signature found",
@@ -248,13 +265,13 @@ async def test_signature_provenance_metadata_for_requires_successful_check():
mock.patch.object(release.db, "session", return_value=session),
mock.patch.object(release.log, "info") as log_info_mock,
):
- with pytest.raises(datatypes.FailedError, match=r"failed: No valid signature found"):
- await release._signature_provenance_metadata_for(
- project_key=safe.ProjectKey("proj"),
- version_key=safe.VersionKey("1.0"),
- parent_revision=parent_revision,
- signature_rel_path=pathlib.Path("artifact.tar.gz.asc"),
- )
+ metadata = await release._signature_provenance_metadata_for(
+ project_key=safe.ProjectKey("proj"),
+ version_key=safe.VersionKey("1.0"),
+ parent_revision=parent_revision,
+ signature_rel_path=pathlib.Path("artifact.tar.gz.asc"),
+ )
+ assert metadata is None
assert log_info_mock.call_count == 1
assert "status=concern" in log_info_mock.call_args.args[0]
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org