Format expires with second precision per TUF spec#736
Open
arpitjain099 wants to merge 1 commit into
Open
Conversation
The four role MarshalJSON methods (RootType, SnapshotType, TimestampType, TargetsType) wrote the expires field as a raw time.Time, which encoding/json serializes with RFC3339Nano sub-second precision (for example 2030-01-01T00:00:00.99008Z). The TUF spec requires a whole-second UTC timestamp of the form YYYY-MM-DDTHH:MM:SSZ. Add a small formatExpires helper and use it at all four marshal sites so they stay consistent. Unmarshal is unchanged and still accepts and round-trips sub-second input. Add expires_format_test.go covering all four role types plus a marshal then unmarshal round-trip. Update the existing marshal-output golden assertions in metadata_test.go that encoded the old sub-second output, and regenerate the on-disk signed test fixtures (root, snapshot, targets, timestamp) so their RSA-PSS signatures match the corrected second-precision body. Fixes theupdateframework#596 Signed-off-by: Arpit Jain <arpitjain099@gmail.com>
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR updates TUF metadata JSON serialization to emit the expires timestamp in spec-required whole-second UTC format (no fractional seconds) and aligns tests/fixtures accordingly.
Changes:
- Added centralized
expiresformatting helper and applied it to all roleMarshalJSONimplementations. - Updated existing unit tests to expect second-precision
expiresstrings. - Added dedicated tests + updated repository JSON fixtures/signatures to reflect the new canonical
expiresformatting.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| metadata/marshal.go | Formats expires as whole-second UTC during JSON marshaling for all role types. |
| metadata/metadata_test.go | Updates expected serialized JSON in multiple tests to match second-precision expires. |
| metadata/expires_format_test.go | Adds focused unit tests to enforce/verify second-precision UTC expires formatting and round-trip behavior. |
| internal/testutils/repository_data/repository/metadata/*.json | Updates test repository metadata fixtures (including signatures) to reflect new expires canonicalization. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return t.UTC().Format(expiresFormat) | ||
| } | ||
|
|
||
| // The following marshal/unmarshal methods override the default behavior for for each TUF type |
Comment on lines
+514
to
+515
| expectedRootBytes := []byte("{\"signatures\":[{\"keyid\":\"roothash\",\"sig\":\"1307990e6ba5ca145eb35e99182a9bec46531bc54ddf656a602c780fa0240dee\"}],\"signed\":{\"_type\":\"root\",\"consistent_snapshot\":true,\"expires\":\"2030-08-15T14:30:45Z\",\"keys\":{\"roothash\":{\"keytype\":\"ed25519\",\"keyval\":{\"public\":\"pubrootval\"},\"scheme\":\"ed25519\"},\"snapshothash\":{\"keytype\":\"ed25519\",\"keyval\":{\"public\":\"pubsval\"},\"scheme\":\"ed25519\"},\"targetshash\":{\"keytype\":\"ed25519\",\"keyval\":{\"public\":\"pubtrval\"},\"scheme\":\"ed25519\"},\"timestamphash\":{\"keytype\":\"ed25519\",\"keyval\":{\"public\":\"pubtmval\"},\"scheme\":\"ed25519\"}},\"roles\":{\"root\":{\"keyids\":[\"roothash\"],\"threshold\":1},\"snapshot\":{\"keyids\":[\"snapshothash\"],\"threshold\":1},\"targets\":{\"keyids\":[\"targetshash\"],\"threshold\":1},\"timestamp\":{\"keyids\":[\"timestamphash\"],\"threshold\":1}},\"spec_version\":\"1.0.31\",\"version\":1}}") | ||
| assert.Equal(t, string(expectedRootBytes), string(rootBytes)) |
Comment on lines
+581
to
+582
| expectedBytes := []byte("{\"signatures\":[{\"keyid\":\"roothash\",\"sig\":\"1307990e6ba5ca145eb35e99182a9bec46531bc54ddf656a602c780fa0240dee\"}],\"signed\":{\"_type\":\"root\",\"consistent_snapshot\":true,\"expires\":\"2030-08-15T14:30:45Z\",\"keys\":{\"roothash\":{\"keytype\":\"ed25519\",\"keyval\":{\"public\":\"pubrootval\"},\"scheme\":\"ed25519\"},\"snapshothash\":{\"keytype\":\"ed25519\",\"keyval\":{\"public\":\"pubsval\"},\"scheme\":\"ed25519\"},\"targetshash\":{\"keytype\":\"ed25519\",\"keyval\":{\"public\":\"pubtrval\"},\"scheme\":\"ed25519\"},\"timestamphash\":{\"keytype\":\"ed25519\",\"keyval\":{\"public\":\"pubtmval\"},\"scheme\":\"ed25519\"}},\"roles\":{\"root\":{\"keyids\":[\"roothash\"],\"threshold\":1},\"snapshot\":{\"keyids\":[\"snapshothash\"],\"threshold\":1},\"targets\":{\"keyids\":[\"targetshash\"],\"threshold\":1},\"timestamp\":{\"keyids\":[\"timestamphash\"],\"threshold\":1}},\"spec_version\":\"1.0.31\",\"version\":1}}") | ||
| assert.Equal(t, string(expectedBytes), string(data)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
Fixes the
expiresserialization so it conforms to the TUF spec date/time format.The four role
MarshalJSONmethods inmetadata/marshal.go(RootType,SnapshotType,TimestampType,TargetsType) wrote theexpiresfield as a rawtime.Time:encoding/jsonserializes atime.TimewithRFC3339Nano, so any sub-second component leaks into the output, for example2030-01-01T00:00:00.99008Z. The TUF spec requires a whole-second UTC timestamp of the formYYYY-MM-DDTHH:MM:SSZ.Changes
formatExpireshelper plus anexpiresFormatlayout constant inmetadata/marshal.go, and use the helper at all four marshal sites so they agree on a single format.metadata/expires_format_test.go:TestExpiresMarshalSecondPrecisionmarshals each of the four role types built from atime.Timethat carries sub-second precision and asserts the emittedexpiresmatches^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$.TestExpiresMarshalUnmarshalRoundTripmarshals, unmarshals, and confirms the value round-trips and re-marshals stably.metadata/metadata_test.gothat hard-coded the old sub-second output (...45.0000001Z) to the corrected...45Z. The sub-secondtestRootBytesliteral is kept as parse-direction input so the suite still exercises sub-second tolerance on unmarshal.root.json,snapshot.json,targets.json,timestamp.jsonunderinternal/testutils/repository_data/repository/metadata) so theirrsassa-pss-sha256signatures match the corrected second-precisionsignedbody. Each fixture changes only two values: theexpiresstring and the corresponding signature. The fixtures were re-signed with their existing keystore keys using the repo's owninternal/testutils/rsapss.LoadRSAPSSSignerFromPEMFilehelper, so the scheme is unchanged.The legacy parse-only fixture
1.root.jsonis intentionally left as-is: it is consumed byTestFromFile/TestFromBytesto assert sub-second parse tolerance, is never re-serialized or signature-verified in tests, and is signed by a key whose private half is not in the keystore.Testing
go vetandgofmtare clean. The new tests fail on the unpatched code (all four roles emit sub-second precision) and pass after the fix.TestCompareFromBytesFromFileToBytes,TestToFromBytes,TestKeyVerifyFailures, andTestMetadataVerifyDelegatecontinue to pass with the regenerated fixtures.Fixes #596