Skip to content

Commit fceadc7

Browse files
committed
Format expires with second precision per TUF spec
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 #596 Signed-off-by: Arpit Jain <arpitjain099@gmail.com>
1 parent 4b704cd commit fceadc7

7 files changed

Lines changed: 145 additions & 20 deletions

File tree

internal/testutils/repository_data/repository/metadata/root.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
"signatures": [
33
{
44
"keyid": "74b58be26a6ff00ab2eec9b14da29038591a69c212223033f4efdf24489913f2",
5-
"sig": "60d502a798f44577a76a1d7656a60099cd3a995d3d71a0a234aadbd16e38c14611920b8aef9ed78ca4ac0c02277cd72a6fc5ef484a3d66a6c70a61199e462681eb2e667046d4fbc2be1e50cf9fe00fda8fcd6534599eddc91716bf38e7fbbf375524fdb702c74076fd37dcd401d5263783150e851bdba9ef6f9c9a08adf6b289"
5+
"sig": "198d9832af271bd30c1e5ba231a6d2cd357043757c21037ed414c3ee51f11957fb9f525ef743e38f3184d479e026dcc5159a88d75b45c48d265132be092775de64fae540756c03e627b55312cfbe77f5755a044189a9efeaba9cbe2c7d84580f606756881e45b025cebf04a0898ab55aa99081aed7699f75983737d63858171e"
66
}
77
],
88
"signed": {
99
"_type": "root",
1010
"consistent_snapshot": true,
11-
"expires": "2030-08-15T14:30:45.0000001Z",
11+
"expires": "2030-08-15T14:30:45Z",
1212
"keys": {
1313
"142919f8e933d7045abff3be450070057814da36331d7a22ccade8b35a9e3946": {
1414
"keytype": "rsa",

internal/testutils/repository_data/repository/metadata/snapshot.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
"signatures": [
33
{
44
"keyid": "8a14f637b21578cc292a67899df0e46cc160d7fd56e9beae898adb666f4fd9d6",
5-
"sig": "1fdc41488f58482d8af1dad681b9076d54c61b3f5e11bd6cf3c8102b2863471f82c0be2cccd978a9bb6afb43e07dd7e806028a883eaeafd32d5f5277d6419363ecb1475286a61996a4bb4b325b703d3bd60381227af0a3826f7f119a451086bcb5b13a525184d1b2a941ab9a270d2c9c8e584162c5857b138a38c33892e2a921"
5+
"sig": "06fbee223da0becf620e99dc8a68e127c475a3818adaa76c73bf3650baa3768c89dab60f701c8621f808891f30ebfce31ee4c1859cbf4a6e2e08990364d930755713142efe9e244e0ea3ad7468d7c1540f452cd8d890f8899363952bce943b5dd3e2e5918f81d7f1b1e91d2269b61a042611d9150ebfe79f4fca1fdca0006ca3"
66
}
77
],
88
"signed": {
99
"_type": "snapshot",
10-
"expires": "2030-08-15T14:30:45.0000001Z",
10+
"expires": "2030-08-15T14:30:45Z",
1111
"meta": {
1212
"role1.json": {
1313
"version": 1

internal/testutils/repository_data/repository/metadata/targets.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"signatures": [
33
{
44
"keyid": "282612f348dcd7fe3f19e0f890e89fad48d45335deeb91deef92873934e6fe6d",
5-
"sig": "699c0c762032f6471fceabfd2d61b80f352ad6bf2ba5fd7b114fd0b9b1ab8d94f1482776b3fef43c53183a9c9cd7f5de671cbdafdd5032bbe2c42273e953bf3ce9f99c2d46dac8802d6155082e10313e22c4886af2be113b626f2a8af930e01ed41df50a5dbe6ca4cedf5f5d2a7f3b7e7090abacc8aebd6e021ad021d3580cad"
5+
"sig": "2f887b7661d41f0a695b543feb8e9928420548fa45e4e0a9585cbce85fcd75d1a4787c0847045c38798cd2cc322ca322f3ac49293e311b60455c815aeab7cda6241cea5380f85ff0d2fb9ebe29feb3cb3600c7c9b203852e286842e3e7cccbb2d3836616fe58cd3fe46da127c9b12a3f6cab43a1e6dd1ea206044bc732f0128d"
66
}
77
],
88
"signed": {
@@ -35,7 +35,7 @@
3535
}
3636
]
3737
},
38-
"expires": "2030-08-15T14:30:45.0000001Z",
38+
"expires": "2030-08-15T14:30:45Z",
3939
"spec_version": "1.0.31",
4040
"targets": {
4141
"file1.txt": {

internal/testutils/repository_data/repository/metadata/timestamp.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
"signatures": [
33
{
44
"keyid": "142919f8e933d7045abff3be450070057814da36331d7a22ccade8b35a9e3946",
5-
"sig": "043b312da9ee1444e3ef539d943891b2690a8d75624c0f05ec148790fd698a7eb1501428167872794857e9669a451619cc796658782b1d46ecc59d1aca0db7233416e81074ef4f54fd845ad8e4216b4cd5163d815be9ecbf73f34aacd25b60c99da88cf641ba5715c37f34a6bc036061c05a42066f554714ee8647c47ae5c16e"
5+
"sig": "25e412c3e45775754eea566b1030ba40277bfa1b6740894c1f93abb2899d1e21690f06068d96a2197cad80b587754750d8de1e4644a7552689d13245af7794921dad00596b4c1b52b37735d873dab1131a3b408badec3414724731521c38b17bfed33d04a1573a6bd8f6510e98d66036cdecb6ff1657be9e5d59b84674f6471c"
66
}
77
],
88
"signed": {
99
"_type": "timestamp",
10-
"expires": "2030-08-15T14:30:45.0000001Z",
10+
"expires": "2030-08-15T14:30:45Z",
1111
"meta": {
1212
"snapshot.json": {
1313
"version": 1

metadata/expires_format_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright 2024 The Update Framework Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License
14+
//
15+
// SPDX-License-Identifier: Apache-2.0
16+
//
17+
18+
package metadata
19+
20+
import (
21+
"encoding/json"
22+
"regexp"
23+
"testing"
24+
"time"
25+
26+
"github.com/stretchr/testify/assert"
27+
)
28+
29+
// secondPrecisionUTC matches the TUF spec date/time format for "expires":
30+
// an ISO 8601 / RFC 3339 timestamp in UTC, truncated to whole seconds with
31+
// a trailing Z and no fractional-second component.
32+
var secondPrecisionUTC = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$`)
33+
34+
// expiresFromJSON pulls the raw "expires" string out of a marshaled signed body.
35+
func expiresFromJSON(t *testing.T, data []byte) string {
36+
t.Helper()
37+
var dict map[string]any
38+
if err := json.Unmarshal(data, &dict); err != nil {
39+
t.Fatalf("unmarshal signed body: %v", err)
40+
}
41+
v, ok := dict["expires"]
42+
if !ok {
43+
t.Fatalf("no expires field in %s", string(data))
44+
}
45+
s, ok := v.(string)
46+
if !ok {
47+
t.Fatalf("expires is not a string: %T (%v)", v, v)
48+
}
49+
return s
50+
}
51+
52+
// TestExpiresMarshalSecondPrecision verifies that every role type serializes
53+
// "expires" with whole-second UTC precision (YYYY-MM-DDTHH:MM:SSZ), per the TUF
54+
// spec, even when the in-memory time.Time carries sub-second precision.
55+
func TestExpiresMarshalSecondPrecision(t *testing.T) {
56+
// A time with sub-second precision (100 nanoseconds). Marshaling a raw
57+
// time.Time would emit "2030-08-15T14:30:45.0000001Z".
58+
subSecond := time.Date(2030, 8, 15, 14, 30, 45, 100, time.UTC)
59+
wantExpires := "2030-08-15T14:30:45Z"
60+
61+
cases := []struct {
62+
name string
63+
body json.Marshaler
64+
}{
65+
{"root", Root(subSecond).Signed},
66+
{"snapshot", Snapshot(subSecond).Signed},
67+
{"timestamp", Timestamp(subSecond).Signed},
68+
{"targets", Targets(subSecond).Signed},
69+
}
70+
71+
for _, tc := range cases {
72+
t.Run(tc.name, func(t *testing.T) {
73+
data, err := tc.body.MarshalJSON()
74+
assert.NoError(t, err)
75+
76+
got := expiresFromJSON(t, data)
77+
assert.Regexp(t, secondPrecisionUTC, got,
78+
"expires must be whole-second UTC per the TUF spec, got %q", got)
79+
assert.Equal(t, wantExpires, got)
80+
})
81+
}
82+
}
83+
84+
// TestExpiresMarshalUnmarshalRoundTrip confirms that the second-precision
85+
// formatting still round-trips: parsing the formatted output back yields the
86+
// truncated time, and re-marshaling is stable.
87+
func TestExpiresMarshalUnmarshalRoundTrip(t *testing.T) {
88+
subSecond := time.Date(2030, 8, 15, 14, 30, 45, 100, time.UTC)
89+
truncated := time.Date(2030, 8, 15, 14, 30, 45, 0, time.UTC)
90+
91+
root := Root(subSecond)
92+
data, err := root.Signed.MarshalJSON()
93+
assert.NoError(t, err)
94+
95+
var parsed RootType
96+
assert.NoError(t, parsed.UnmarshalJSON(data))
97+
assert.True(t, parsed.Expires.Equal(truncated),
98+
"round-tripped expires %v should equal %v", parsed.Expires, truncated)
99+
100+
// Re-marshaling the parsed value is stable and still second-precision.
101+
data2, err := parsed.MarshalJSON()
102+
assert.NoError(t, err)
103+
assert.Equal(t, "2030-08-15T14:30:45Z", expiresFromJSON(t, data2))
104+
}

metadata/marshal.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,23 @@ import (
2323
"errors"
2424
"fmt"
2525
"maps"
26+
"time"
2627
)
2728

29+
// expiresFormat is the TUF spec date/time layout for the "expires" field:
30+
// an ISO 8601 / RFC 3339 timestamp in UTC truncated to whole seconds, e.g.
31+
// "2030-01-01T00:00:00Z". A raw time.Time would otherwise be serialized by
32+
// encoding/json with sub-second (RFC3339Nano) precision, which is not valid
33+
// per the spec.
34+
const expiresFormat = "2006-01-02T15:04:05Z"
35+
36+
// formatExpires renders an "expires" time in the spec-required second-precision
37+
// UTC format. It is the single source of truth shared by the role MarshalJSON
38+
// methods so they always agree.
39+
func formatExpires(t time.Time) string {
40+
return t.UTC().Format(expiresFormat)
41+
}
42+
2843
// The following marshal/unmarshal methods override the default behavior for for each TUF type
2944
// in order to support unrecognized fields
3045

@@ -37,7 +52,7 @@ func (signed RootType) MarshalJSON() ([]byte, error) {
3752
dict["spec_version"] = signed.SpecVersion
3853
dict["consistent_snapshot"] = signed.ConsistentSnapshot
3954
dict["version"] = signed.Version
40-
dict["expires"] = signed.Expires
55+
dict["expires"] = formatExpires(signed.Expires)
4156
dict["keys"] = signed.Keys
4257
dict["roles"] = signed.Roles
4358
return json.Marshal(dict)
@@ -74,7 +89,7 @@ func (signed SnapshotType) MarshalJSON() ([]byte, error) {
7489
dict["_type"] = signed.Type
7590
dict["spec_version"] = signed.SpecVersion
7691
dict["version"] = signed.Version
77-
dict["expires"] = signed.Expires
92+
dict["expires"] = formatExpires(signed.Expires)
7893
dict["meta"] = signed.Meta
7994
return json.Marshal(dict)
8095
}
@@ -108,7 +123,7 @@ func (signed TimestampType) MarshalJSON() ([]byte, error) {
108123
dict["_type"] = signed.Type
109124
dict["spec_version"] = signed.SpecVersion
110125
dict["version"] = signed.Version
111-
dict["expires"] = signed.Expires
126+
dict["expires"] = formatExpires(signed.Expires)
112127
dict["meta"] = signed.Meta
113128
return json.Marshal(dict)
114129
}
@@ -142,7 +157,7 @@ func (signed TargetsType) MarshalJSON() ([]byte, error) {
142157
dict["_type"] = signed.Type
143158
dict["spec_version"] = signed.SpecVersion
144159
dict["version"] = signed.Version
145-
dict["expires"] = signed.Expires
160+
dict["expires"] = formatExpires(signed.Expires)
146161
dict["targets"] = signed.Targets
147162
if signed.Delegations != nil {
148163
dict["delegations"] = signed.Delegations

metadata/metadata_test.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -376,25 +376,25 @@ func TestUnrecognizedFieldRolesSigned(t *testing.T) {
376376
root.Signed.UnrecognizedFields = testUnrecognizedField
377377
rootJSON, err := root.ToBytes(false)
378378
assert.NoError(t, err)
379-
assert.Equal(t, []byte("{\"signatures\":[],\"signed\":{\"_type\":\"root\",\"consistent_snapshot\":true,\"expires\":\"2030-08-15T14:30:45.0000001Z\",\"keys\":{},\"roles\":{\"root\":{\"keyids\":[],\"threshold\":1},\"snapshot\":{\"keyids\":[],\"threshold\":1},\"targets\":{\"keyids\":[],\"threshold\":1},\"timestamp\":{\"keyids\":[],\"threshold\":1}},\"spec_version\":\"1.0.31\",\"test\":\"true\",\"version\":1}}"), rootJSON)
379+
assert.Equal(t, []byte("{\"signatures\":[],\"signed\":{\"_type\":\"root\",\"consistent_snapshot\":true,\"expires\":\"2030-08-15T14:30:45Z\",\"keys\":{},\"roles\":{\"root\":{\"keyids\":[],\"threshold\":1},\"snapshot\":{\"keyids\":[],\"threshold\":1},\"targets\":{\"keyids\":[],\"threshold\":1},\"timestamp\":{\"keyids\":[],\"threshold\":1}},\"spec_version\":\"1.0.31\",\"test\":\"true\",\"version\":1}}"), rootJSON)
380380

381381
targets := Targets(fixedExpire)
382382
targets.Signed.UnrecognizedFields = testUnrecognizedField
383383
targetsJSON, err := targets.ToBytes(false)
384384
assert.NoError(t, err)
385-
assert.Equal(t, []byte("{\"signatures\":[],\"signed\":{\"_type\":\"targets\",\"expires\":\"2030-08-15T14:30:45.0000001Z\",\"spec_version\":\"1.0.31\",\"targets\":{},\"test\":\"true\",\"version\":1}}"), targetsJSON)
385+
assert.Equal(t, []byte("{\"signatures\":[],\"signed\":{\"_type\":\"targets\",\"expires\":\"2030-08-15T14:30:45Z\",\"spec_version\":\"1.0.31\",\"targets\":{},\"test\":\"true\",\"version\":1}}"), targetsJSON)
386386

387387
snapshot := Snapshot(fixedExpire)
388388
snapshot.Signed.UnrecognizedFields = testUnrecognizedField
389389
snapshotJSON, err := snapshot.ToBytes(false)
390390
assert.NoError(t, err)
391-
assert.Equal(t, []byte("{\"signatures\":[],\"signed\":{\"_type\":\"snapshot\",\"expires\":\"2030-08-15T14:30:45.0000001Z\",\"meta\":{\"targets.json\":{\"version\":1}},\"spec_version\":\"1.0.31\",\"test\":\"true\",\"version\":1}}"), snapshotJSON)
391+
assert.Equal(t, []byte("{\"signatures\":[],\"signed\":{\"_type\":\"snapshot\",\"expires\":\"2030-08-15T14:30:45Z\",\"meta\":{\"targets.json\":{\"version\":1}},\"spec_version\":\"1.0.31\",\"test\":\"true\",\"version\":1}}"), snapshotJSON)
392392

393393
timestamp := Timestamp(fixedExpire)
394394
timestamp.Signed.UnrecognizedFields = testUnrecognizedField
395395
timestampJSON, err := timestamp.ToBytes(false)
396396
assert.NoError(t, err)
397-
assert.Equal(t, []byte("{\"signatures\":[],\"signed\":{\"_type\":\"timestamp\",\"expires\":\"2030-08-15T14:30:45.0000001Z\",\"meta\":{\"snapshot.json\":{\"version\":1}},\"spec_version\":\"1.0.31\",\"test\":\"true\",\"version\":1}}"), timestampJSON)
397+
assert.Equal(t, []byte("{\"signatures\":[],\"signed\":{\"_type\":\"timestamp\",\"expires\":\"2030-08-15T14:30:45Z\",\"meta\":{\"snapshot.json\":{\"version\":1}},\"spec_version\":\"1.0.31\",\"test\":\"true\",\"version\":1}}"), timestampJSON)
398398
}
399399
func TestUnrecognizedFieldGenericMetadata(t *testing.T) {
400400
// fixed expire
@@ -408,7 +408,7 @@ func TestUnrecognizedFieldGenericMetadata(t *testing.T) {
408408
root.UnrecognizedFields = testUnrecognizedField
409409
rootJSON, err := root.ToBytes(false)
410410
assert.NoError(t, err)
411-
assert.Equal(t, []byte("{\"signatures\":[],\"signed\":{\"_type\":\"root\",\"consistent_snapshot\":true,\"expires\":\"2030-08-15T14:30:45.0000001Z\",\"keys\":{},\"roles\":{\"root\":{\"keyids\":[],\"threshold\":1},\"snapshot\":{\"keyids\":[],\"threshold\":1},\"targets\":{\"keyids\":[],\"threshold\":1},\"timestamp\":{\"keyids\":[],\"threshold\":1}},\"spec_version\":\"1.0.31\",\"version\":1},\"test\":\"true\"}"), rootJSON)
411+
assert.Equal(t, []byte("{\"signatures\":[],\"signed\":{\"_type\":\"root\",\"consistent_snapshot\":true,\"expires\":\"2030-08-15T14:30:45Z\",\"keys\":{},\"roles\":{\"root\":{\"keyids\":[],\"threshold\":1},\"snapshot\":{\"keyids\":[],\"threshold\":1},\"targets\":{\"keyids\":[],\"threshold\":1},\"timestamp\":{\"keyids\":[],\"threshold\":1}},\"spec_version\":\"1.0.31\",\"version\":1},\"test\":\"true\"}"), rootJSON)
412412
}
413413
func TestTargetFilesCustomField(t *testing.T) {
414414
// custom JSON to test
@@ -426,7 +426,7 @@ func TestTargetFilesCustomField(t *testing.T) {
426426
targets.Signed.Targets["testTarget"] = targetFile
427427
targetsJSON, err := targets.ToBytes(false)
428428
assert.NoError(t, err)
429-
assert.Equal(t, []byte("{\"signatures\":[],\"signed\":{\"_type\":\"targets\",\"expires\":\"2030-08-15T14:30:45.0000001Z\",\"spec_version\":\"1.0.31\",\"targets\":{\"testTarget\":{\"custom\":{\"test\":true},\"hashes\":{},\"length\":0}},\"version\":1}}"), targetsJSON)
429+
assert.Equal(t, []byte("{\"signatures\":[],\"signed\":{\"_type\":\"targets\",\"expires\":\"2030-08-15T14:30:45Z\",\"spec_version\":\"1.0.31\",\"targets\":{\"testTarget\":{\"custom\":{\"test\":true},\"hashes\":{},\"length\":0}},\"version\":1}}"), targetsJSON)
430430
}
431431

432432
func TestFromBytes(t *testing.T) {
@@ -509,7 +509,10 @@ func TestToByte(t *testing.T) {
509509
root.Signatures = append(root.Signatures, Signature{KeyID: "roothash", Signature: hash["ed25519"]})
510510
rootBytes, err := root.ToBytes(false)
511511
assert.NoError(t, err)
512-
assert.Equal(t, string(testRootBytes), string(rootBytes))
512+
// Even though the input expires string carries sub-second precision, the
513+
// serialized output must use the spec-required whole-second UTC format.
514+
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}}")
515+
assert.Equal(t, string(expectedRootBytes), string(rootBytes))
513516
}
514517

515518
func TestFromFile(t *testing.T) {
@@ -573,7 +576,10 @@ func TestToFile(t *testing.T) {
573576
assert.FileExists(t, fileName)
574577
data, err := os.ReadFile(fileName)
575578
assert.NoError(t, err)
576-
assert.Equal(t, string(testRootBytes), string(data))
579+
// The input bytes carry a sub-second expires; the written output must use
580+
// the spec-required whole-second UTC format.
581+
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}}")
582+
assert.Equal(t, string(expectedBytes), string(data))
577583

578584
err = os.RemoveAll(tmpDir)
579585
assert.NoError(t, err)

0 commit comments

Comments
 (0)