Skip to content

Commit eefa7a2

Browse files
authored
Merge pull request #656 from sigstore/optional_snapshots
TUF allows optional hashes and length on snapshots
2 parents 677fc17 + 32dd395 commit eefa7a2

File tree

11 files changed

+103
-21
lines changed

11 files changed

+103
-21
lines changed

sigstore-java/src/main/java/dev/sigstore/tuf/Updater.java

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -308,17 +308,20 @@ Snapshot updateSnapshot(Root root, Timestamp timestamp)
308308
Role.Name.SNAPSHOT,
309309
timestampSnapshotVersion,
310310
Snapshot.class,
311-
timestamp.getSignedMeta().getSnapshotMeta().getLength());
311+
timestamp.getSignedMeta().getSnapshotMeta().getLengthOrDefault());
312312
if (snapshotResult.isEmpty()) {
313313
throw new FileNotFoundException(
314314
timestampSnapshotVersion + ".snapshot.json", fetcher.getSource());
315315
}
316-
// 2) check against timestamp.snapshot.hash
316+
// 2) check against timestamp.snapshot.hash, this is optional, the fallback is
317+
// that the version must match, which is handled in (4).
317318
var snapshot = snapshotResult.get();
318-
verifyHashes(
319-
"snapshot",
320-
snapshot.getRawBytes(),
321-
timestamp.getSignedMeta().getSnapshotMeta().getHashes());
319+
if (timestamp.getSignedMeta().getSnapshotMeta().getHashes().isPresent()) {
320+
verifyHashes(
321+
"snapshot",
322+
snapshot.getRawBytes(),
323+
timestamp.getSignedMeta().getSnapshotMeta().getHashes().get());
324+
}
322325
// 3) Check against threshold of root signing keys, else fail
323326
verifyDelegate(root, snapshot.getMetaResource());
324327
// 4) Check snapshot.version matches timestamp.snapshot.version, else fail.
@@ -392,17 +395,23 @@ Targets updateTargets(Root root, Snapshot snapshot)
392395
SnapshotMeta.SnapshotTarget targetMeta = snapshot.getSignedMeta().getTargetMeta("targets.json");
393396
var targetsResultMaybe =
394397
fetcher.getMeta(
395-
Role.Name.TARGETS, targetMeta.getVersion(), Targets.class, targetMeta.getLength());
398+
Role.Name.TARGETS,
399+
targetMeta.getVersion(),
400+
Targets.class,
401+
targetMeta.getLengthOrDefault());
396402
if (targetsResultMaybe.isEmpty()) {
397403
throw new FileNotFoundException(
398404
targetMeta.getVersion() + ".targets.json", fetcher.getSource());
399405
}
400406
var targetsResult = targetsResultMaybe.get();
401-
// 2) check hash against snapshot.targets.hash, else fail.
402-
verifyHashes(
403-
targetMeta.getVersion() + ".targets.json",
404-
targetsResult.getRawBytes(),
405-
targetMeta.getHashes());
407+
// 2) check hash against snapshot.targets.hash, else just make sure versions match, handled
408+
// by (4)
409+
if (targetMeta.getHashes().isPresent()) {
410+
verifyHashes(
411+
targetMeta.getVersion() + ".targets.json",
412+
targetsResult.getRawBytes(),
413+
targetMeta.getHashes().get());
414+
}
406415
// 3) check against threshold of keys as specified by trusted root.json
407416
verifyDelegate(root, targetsResult.getMetaResource());
408417
// 4) check targets.version == snapshot.targets.version, else fail.

sigstore-java/src/main/java/dev/sigstore/tuf/model/SnapshotMeta.java

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
package dev.sigstore.tuf.model;
1717

1818
import java.util.Map;
19+
import java.util.Optional;
1920
import org.immutables.gson.Gson;
2021
import org.immutables.value.Value;
22+
import org.immutables.value.Value.Derived;
2123

2224
/**
2325
* The snapshot.json metadata file lists version numbers of all metadata files other than
@@ -29,6 +31,12 @@
2931
@Value.Immutable
3032
public interface SnapshotMeta extends TufMeta {
3133

34+
/**
35+
* If no length is provided, use a default, we just use the same default as the python client:
36+
* https://github.com/theupdateframework/python-tuf/blob/22080157f438357935bfcb25b5b8429f4e4f610e/tuf/ngclient/config.py#L49
37+
*/
38+
Integer DEFAULT_MAX_LENGTH = 2000000;
39+
3240
/** Maps role and delegation role names (e.g. "targets.json") to snapshot metadata. */
3341
Map<String, SnapshotTarget> getMeta();
3442

@@ -40,11 +48,20 @@ default SnapshotTarget getTargetMeta(String targetName) {
4048
@Value.Immutable
4149
interface SnapshotTarget {
4250

43-
/** The valid hashes for the given target's metadata. */
44-
Hashes getHashes();
51+
/** The valid hashes for the given target's metadata. This is optional and may not be present */
52+
Optional<Hashes> getHashes();
53+
54+
/**
55+
* The length in bytes of the given target's metadata. This is optional and may not be present,
56+
* use {@link #getLengthOrDefault} to delegate to the client default config.
57+
*/
58+
Optional<Integer> getLength();
4559

46-
/** The length in bytes of the given target's metadata. */
47-
int getLength();
60+
/** The length in bytes of the given target's metadata, or a default if not present */
61+
@Derived
62+
default Integer getLengthOrDefault() {
63+
return getLength().orElse(DEFAULT_MAX_LENGTH);
64+
}
4865

4966
/** The expected version of the given target's metadata. */
5067
int getVersion();

sigstore-java/src/test/java/dev/sigstore/tuf/UpdaterTest.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
import org.jetbrains.annotations.Nullable;
7575
import org.junit.jupiter.api.AfterAll;
7676
import org.junit.jupiter.api.AfterEach;
77+
import org.junit.jupiter.api.Assertions;
7778
import org.junit.jupiter.api.BeforeAll;
7879
import org.junit.jupiter.api.Test;
7980
import org.junit.jupiter.api.extension.RegisterExtension;
@@ -969,6 +970,31 @@ public void testVerifyDelegate_goodSigsAndKeysButNotInRole()
969970
}
970971
}
971972

973+
@Test
974+
public void testUpdate_snapshotsAndTimestampHaveNoSizeAndNoHashesInMeta()
975+
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
976+
setupMirror(
977+
"synthetic/no-size-no-hash-snapshot-timestamp",
978+
"2.root.json",
979+
"timestamp.json",
980+
"3.snapshot.json");
981+
var updater =
982+
createTimeStaticUpdater(
983+
localStorePath,
984+
UPDATER_SYNTHETIC_TRUSTED_ROOT,
985+
"2022-11-20T18:07:27Z"); // one day after
986+
Root root = updater.updateRoot();
987+
Timestamp timestamp = updater.updateTimestamp(root).get();
988+
Snapshot snapshot = updater.updateSnapshot(root, timestamp);
989+
990+
Assertions.assertTrue(timestamp.getSignedMeta().getSnapshotMeta().getHashes().isEmpty());
991+
Assertions.assertTrue(timestamp.getSignedMeta().getSnapshotMeta().getLength().isEmpty());
992+
Assertions.assertTrue(
993+
snapshot.getSignedMeta().getMeta().get("targets.json").getHashes().isEmpty());
994+
Assertions.assertTrue(
995+
snapshot.getSignedMeta().getMeta().get("targets.json").getLength().isEmpty());
996+
}
997+
972998
@Test
973999
public void canCreateMultipleUpdaters() throws IOException {
9741000
createTimeStaticUpdater(localStorePath, UPDATER_REAL_TRUSTED_ROOT);

sigstore-java/src/test/java/dev/sigstore/tuf/model/TestTufJsonLoading.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,11 @@ public void loadSnapshotJson() throws IOException {
9696
assertNotNull(rekorSnapshot.getHashes());
9797
assertEquals(
9898
"9d2e1a5842937d8e0d3e3759170b0ad15c56c5df36afc5cf73583ddd283a463b",
99-
rekorSnapshot.getHashes().getSha256());
99+
rekorSnapshot.getHashes().get().getSha256());
100100
assertEquals(
101101
"176e9e710ddddd1b357a7d7970831bae59763395a0c18976110cbd35b25e5412dc50f356ec421a7a30265670cf7aec9ed84ee944ba700ec2394b9c876645b960",
102-
rekorSnapshot.getHashes().getSha512());
103-
assertEquals(797, rekorSnapshot.getLength());
102+
rekorSnapshot.getHashes().get().getSha512());
103+
assertEquals(797, rekorSnapshot.getLength().get());
104104
assertEquals(3, rekorSnapshot.getVersion());
105105
}
106106

Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"signed":{"_type":"root","spec_version":"1.0","version":2,"expires":"2023-05-13T14:35:58Z","keys":{"0b5108e406f6d2f59ef767797b314be99d35903950ba43a2d51216eeeb8da98c":{"keytype":"ecdsa-sha2-nistp256","scheme":"ecdsa-sha2-nistp256","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKzH3HI+8f9hYlrwNynmWtYrdp7kT\n5B13ZcaQJd2gbMw3MXUwAMWksxAjNXXXselrztKQLKEJkj0CRPiXFhtdWg==\n-----END PUBLIC KEY-----\n"}},"7aecf5f0720acfb4fa873896ba05a2d8914f5b6ca90d26ac8bc0f1e491378740":{"keytype":"ecdsa-sha2-nistp256","scheme":"ecdsa-sha2-nistp256","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEs1Stkp5CNyERUPWDa9KF47KjECsx\noobAYi8NUUh5+0Rl34nYR3Y/2IQWu8l2pi9f73Qqsq3kk1cGQMCKRJu1wA==\n-----END PUBLIC KEY-----\n"}},"9354bd3deaa572ed06306ddfad457037918534ece677cf962526a6fd40112d7a":{"keytype":"ecdsa-sha2-nistp256","scheme":"ecdsa-sha2-nistp256","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJsV+S1syZdtx5HjiFN5YqRAqD2By\n4R0xDtXptW+UJlJQdfQCGAHvqtpac0edkcWVREhktEqIMbCaYSd75E/JRA==\n-----END PUBLIC KEY-----\n"}},"a041140325d05d8a7643d5649a8c4296f8e6b020fb73bf83c52319b1a7230a40":{"keytype":"ecdsa-sha2-nistp256","scheme":"ecdsa-sha2-nistp256","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEoZaB1Hu8VvuqgHvwX1mAITts2Zi\ntHhs3suizfA/XDmetnA9BoXhPpLmPJ1n+47xr4Gdr5mcrBzLbM+WcXIs9Q==\n-----END PUBLIC KEY-----\n"}},"a9c5c80b93210eeb34e6264b4b261ff6899d4dbfb8e308f8546722a2bae30687":{"keytype":"ecdsa-sha2-nistp256","scheme":"ecdsa-sha2-nistp256","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbGNtqWi9Xu7romi12qG+fHYj4SCp\nUCKAOJxXKagVyQNlS6TdJCMHWOJ+0BReT1lQsw6J/SMtc9a5J6Vj7fksBw==\n-----END PUBLIC KEY-----\n"}},"fca39ff47a3a91605f2c56501e84b4fe3b9a66b96a022275e866bd19353f93e6":{"keytype":"ecdsa-sha2-nistp256","scheme":"ecdsa-sha2-nistp256","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfcbhZ0zElnB5dqJBzKiVlofRXBh/\n2snZw32WDcUvl3+7UEtRvmTGZSaAxYCGmAc1EO2j5MGk5wkNkuwiVesd0g==\n-----END PUBLIC KEY-----\n"}}},"roles":{"root":{"keyids":["fca39ff47a3a91605f2c56501e84b4fe3b9a66b96a022275e866bd19353f93e6","0b5108e406f6d2f59ef767797b314be99d35903950ba43a2d51216eeeb8da98c","a041140325d05d8a7643d5649a8c4296f8e6b020fb73bf83c52319b1a7230a40"],"threshold":2},"snapshot":{"keyids":["9354bd3deaa572ed06306ddfad457037918534ece677cf962526a6fd40112d7a"],"threshold":1},"targets":{"keyids":["a9c5c80b93210eeb34e6264b4b261ff6899d4dbfb8e308f8546722a2bae30687"],"threshold":1},"timestamp":{"keyids":["7aecf5f0720acfb4fa873896ba05a2d8914f5b6ca90d26ac8bc0f1e491378740"],"threshold":1}},"consistent_snapshot":true},"signatures":[{"keyid":"0b5108e406f6d2f59ef767797b314be99d35903950ba43a2d51216eeeb8da98c","sig":"304502204ee7d150bbbf40dc641d1a208be4708be14022da6a86883d2c5a7282eda2659802210095a15450c1e63ff20bd5164979007fbea8a7deea68ebba7a67f8cd2901b686ca"},{"keyid":"a041140325d05d8a7643d5649a8c4296f8e6b020fb73bf83c52319b1a7230a40","sig":"3046022100845e6b95ccf906b7c44e5993384ecca0efefb0ce9495e9d125856ef4640c5906022100fc4ae0c7f5d32dcccb76b87240f8795d176b10497cced966aac4b8e3db71d0fa"},{"keyid":"fca39ff47a3a91605f2c56501e84b4fe3b9a66b96a022275e866bd19353f93e6","sig":"3045022024637aad4a82ec9416527d2bd54255c56b86ff0c1a8a316d0282ce8f0e18d797022100f51cffa088083bc3c76fe0a26746b99bf49a3b19c4692a12133872a477b6f226"}]}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"signed":{"_type":"snapshot","spec_version":"1.0","version":3,"expires":"2023-02-19T15:37:48Z","meta":{"targets.json":{"version":3}}},"signatures":[{"keyid":"9354bd3deaa572ed06306ddfad457037918534ece677cf962526a6fd40112d7a","sig":"30440220356cee8ca30ff061640f3d88a64cd42f6b3cd3b714e6f5e67596ba798e67f9a702204583f6194190c379ebc248753c64141bfcaf37153de86b7d8249afd15aa9efed"}]}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
2+
```shell
3+
cp ../test-template/2.root.json .
4+
cp -R ../root-signing-workspace tmp
5+
cd tmp
6+
# remove hashes and length from snapshots and timestamp
7+
jq -rc '.signed.meta."targets.json" |= del(.length, .hashes)' repository/snapshot.json | sponge repository/snapshot.json
8+
jq -rc '.signed.meta."snapshot.json" |= del(.length, .hashes)' repository/timestamp.json | sponge repository/timestamp.json
9+
# get valid sigs on the new snapshot metadata.
10+
tuf payload snapshot.json > payload.snapshot.json
11+
tuf sign-payload --role=snapshot payload.snapshot.json > snapshot.sigs
12+
tuf add-signatures --signatures snapshot.sigs snapshot.json
13+
cp staged/snapshot.json ../3.snapshot.json
14+
# get valid sigs on the new timestamps metadata.
15+
tuf payload timestamp.json > payload.timestamp.json
16+
tuf sign-payload --role=timestamp payload.timestamp.json > timestamp.sigs
17+
tuf add-signatures --signatures timestamp.sigs timestamp.json
18+
cp staged/timestamp.json ../timestamp.json
19+
cd ..
20+
rm -rf tmp
21+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"signed":{"_type":"timestamp","spec_version":"1.0","version":3,"expires":"2023-02-13T15:37:48Z","meta":{"snapshot.json":{"version":3}}},"signatures":[{"keyid":"7aecf5f0720acfb4fa873896ba05a2d8914f5b6ca90d26ac8bc0f1e491378740","sig":"3044022060e8b160acae47d6ecc249881c5e7b7beb790e05c519f139bf6c2c98e39cbe54022001a19e057697e5c5023911bc64616f462aed4db0424b6c784c5c03a0fb968fe6"}]}

sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/root-signing-workspace/README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11

22
# TUF repo creation steps
33

4-
You'll need the TUF cli to run these commands.
4+
You'll need the TUF cli to run these commands. This was generated pre v0.6.0,
5+
so use v0.5.2 for compatibility till this all this test data is upgraded
56
```shell
6-
go install github.com/theupdateframework/go-tuf/cmd/tuf@latest
7+
go install github.com/theupdateframework/go-tuf/cmd/tuf@v0.5.2
78
```
89

10+
if you are using the repo in root-signing-workspace, you do not need to regenerate
11+
the repository, you can work within this repo
12+
913
```shell
1014
mkdir root-signing-workspace
1115
cd root-signing-workspace
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"signed":{"_type":"root","spec_version":"1.0","version":2,"expires":"2023-05-13T14:35:58Z","keys":{"0b5108e406f6d2f59ef767797b314be99d35903950ba43a2d51216eeeb8da98c":{"keytype":"ecdsa-sha2-nistp256","scheme":"ecdsa-sha2-nistp256","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKzH3HI+8f9hYlrwNynmWtYrdp7kT\n5B13ZcaQJd2gbMw3MXUwAMWksxAjNXXXselrztKQLKEJkj0CRPiXFhtdWg==\n-----END PUBLIC KEY-----\n"}},"7aecf5f0720acfb4fa873896ba05a2d8914f5b6ca90d26ac8bc0f1e491378740":{"keytype":"ecdsa-sha2-nistp256","scheme":"ecdsa-sha2-nistp256","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEs1Stkp5CNyERUPWDa9KF47KjECsx\noobAYi8NUUh5+0Rl34nYR3Y/2IQWu8l2pi9f73Qqsq3kk1cGQMCKRJu1wA==\n-----END PUBLIC KEY-----\n"}},"9354bd3deaa572ed06306ddfad457037918534ece677cf962526a6fd40112d7a":{"keytype":"ecdsa-sha2-nistp256","scheme":"ecdsa-sha2-nistp256","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJsV+S1syZdtx5HjiFN5YqRAqD2By\n4R0xDtXptW+UJlJQdfQCGAHvqtpac0edkcWVREhktEqIMbCaYSd75E/JRA==\n-----END PUBLIC KEY-----\n"}},"a041140325d05d8a7643d5649a8c4296f8e6b020fb73bf83c52319b1a7230a40":{"keytype":"ecdsa-sha2-nistp256","scheme":"ecdsa-sha2-nistp256","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEoZaB1Hu8VvuqgHvwX1mAITts2Zi\ntHhs3suizfA/XDmetnA9BoXhPpLmPJ1n+47xr4Gdr5mcrBzLbM+WcXIs9Q==\n-----END PUBLIC KEY-----\n"}},"a9c5c80b93210eeb34e6264b4b261ff6899d4dbfb8e308f8546722a2bae30687":{"keytype":"ecdsa-sha2-nistp256","scheme":"ecdsa-sha2-nistp256","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbGNtqWi9Xu7romi12qG+fHYj4SCp\nUCKAOJxXKagVyQNlS6TdJCMHWOJ+0BReT1lQsw6J/SMtc9a5J6Vj7fksBw==\n-----END PUBLIC KEY-----\n"}},"fca39ff47a3a91605f2c56501e84b4fe3b9a66b96a022275e866bd19353f93e6":{"keytype":"ecdsa-sha2-nistp256","scheme":"ecdsa-sha2-nistp256","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfcbhZ0zElnB5dqJBzKiVlofRXBh/\n2snZw32WDcUvl3+7UEtRvmTGZSaAxYCGmAc1EO2j5MGk5wkNkuwiVesd0g==\n-----END PUBLIC KEY-----\n"}}},"roles":{"root":{"keyids":["fca39ff47a3a91605f2c56501e84b4fe3b9a66b96a022275e866bd19353f93e6","0b5108e406f6d2f59ef767797b314be99d35903950ba43a2d51216eeeb8da98c","a041140325d05d8a7643d5649a8c4296f8e6b020fb73bf83c52319b1a7230a40"],"threshold":2},"snapshot":{"keyids":["9354bd3deaa572ed06306ddfad457037918534ece677cf962526a6fd40112d7a"],"threshold":1},"targets":{"keyids":["a9c5c80b93210eeb34e6264b4b261ff6899d4dbfb8e308f8546722a2bae30687"],"threshold":1},"timestamp":{"keyids":["7aecf5f0720acfb4fa873896ba05a2d8914f5b6ca90d26ac8bc0f1e491378740"],"threshold":1}},"consistent_snapshot":true},"signatures":[{"keyid":"0b5108e406f6d2f59ef767797b314be99d35903950ba43a2d51216eeeb8da98c","sig":"304502204ee7d150bbbf40dc641d1a208be4708be14022da6a86883d2c5a7282eda2659802210095a15450c1e63ff20bd5164979007fbea8a7deea68ebba7a67f8cd2901b686ca"},{"keyid":"a041140325d05d8a7643d5649a8c4296f8e6b020fb73bf83c52319b1a7230a40","sig":"3046022100845e6b95ccf906b7c44e5993384ecca0efefb0ce9495e9d125856ef4640c5906022100fc4ae0c7f5d32dcccb76b87240f8795d176b10497cced966aac4b8e3db71d0fa"},{"keyid":"fca39ff47a3a91605f2c56501e84b4fe3b9a66b96a022275e866bd19353f93e6","sig":"3045022024637aad4a82ec9416527d2bd54255c56b86ff0c1a8a316d0282ce8f0e18d797022100f51cffa088083bc3c76fe0a26746b99bf49a3b19c4692a12133872a477b6f226"}]}

0 commit comments

Comments
 (0)