From 13371337cc0a920b5137b8c79a510d775f8b8236 Mon Sep 17 00:00:00 2001 From: Billy Lau Date: Wed, 23 Sep 2026 08:26:44 -0500 Subject: [PATCH] verifier_tools/verify: Add 2026/01 shard target for google_1p_code log Previously, `--log_type=google_1p_code` only targeted the legacy log at `developers.google.com/android/binary_transparency/google1p` (frozen at tree size 134), causing verification to fail for entries published to the active `2026/01` shard (`gstatic.com/android/binary_transparency/google1p/jwt/2026/01`). Update `google_1p_code` verification to cover both shards: 1. Add `gstatic.com/android/binary_transparency/google1p/jwt/0` to the supported checkpoint origins in `internal/checkpoint/checkpoint.go`. 2. Configure `google_1p_code (2026/01)` as the primary target in `cmd/verifier/verifier.go` with `tileHeight: 8` and verifier key name `gstatic.com/android/binary_transparency/google1p/jwt/0` (using the embedded `log_pub_key.google_system_apk.pem` public key). 3. Retain `google_1p_code (legacy)` (`tileHeight: 1`) as a fallback target so entries from the original log continue to verify. Test: - `cd verifier_tools/verify && go test -v ./...` - Added `cmd/verifier/verifier_test.go` (`TestResolveTargetsGoogle1PCode` and `TestGoogle1PCodeCheckpointSignatureVerification`) as well as test cases in `checkpoint_test.go` and `reader_test.go`. - Verified inclusion proof against both `2026/01` and legacy `google_1p_code` log entries. Change-Id: I270e6a356b646659cfe645d0006fd006d5c0b9e1 --- .../verify/cmd/verifier/verifier.go | 26 +++- .../verify/cmd/verifier/verifier_test.go | 132 ++++++++++++++++++ .../verify/internal/checkpoint/checkpoint.go | 7 +- .../internal/checkpoint/checkpoint_test.go | 10 ++ .../verify/internal/tiles/reader_test.go | 4 + 5 files changed, 174 insertions(+), 5 deletions(-) create mode 100644 verifier_tools/verify/cmd/verifier/verifier_test.go diff --git a/verifier_tools/verify/cmd/verifier/verifier.go b/verifier_tools/verify/cmd/verifier/verifier.go index 124d86f..62e7256 100644 --- a/verifier_tools/verify/cmd/verifier/verifier.go +++ b/verifier_tools/verify/cmd/verifier/verifier.go @@ -43,10 +43,12 @@ const ( LeafHashPrefix = 0 KeyNameForVerifierPixel = "pixel_transparency_log" KeyNameForVerifierG1PJWT = "developers.google.com/android/binary_transparency/google1p/0" + KeyNameForVerifierG1PJWT202601 = "gstatic.com/android/binary_transparency/google1p/jwt/0" KeyNameForVerifierG1PAPK = "gstatic.com/android/binary_transparency/google1p/apk/2026/0" KeyNameForVerifierMainlineModule = "gstatic.com/android/binary_transparency/mainline/modules/2026/0" LogBaseURLPixel = "https://developers.google.com/android/binary_transparency" LogBaseURLG1PJWT = "https://developers.google.com/android/binary_transparency/google1p" + LogBaseURLG1PJWT202601 = "https://www.gstatic.com/android/binary_transparency/google1p/jwt/2026/01" LogBaseURLG1PAPK202601 = "https://www.gstatic.com/android/binary_transparency/google1p/apk/2026/01" LogBaseURLG1PAPK202602 = "https://www.gstatic.com/android/binary_transparency/google1p/apk/2026/02" NoteVerifierG1PAPK202602 = "android.transparency.goog/google1p/apk/2026/1+fc654374+ATr9NQE0gvOtVfj5cCStUzdlflEp3oZoNHD8pImzPj5O" @@ -142,15 +144,31 @@ func resolveTargets(logType string) ([]logTarget, error) { binaryInfoFilenames: []string{ImageInfoFilename}, }) case "google_1p_code": - v, err := checkpoint.NewVerifier(googleSystemAppLogPubKey, KeyNameForVerifierG1PJWT) + // Shard 2026/01: Latest sharded log + v202601, err := checkpoint.NewVerifier(googleSystemAppLogPubKey, KeyNameForVerifierG1PJWT202601) if err != nil { - return nil, fmt.Errorf("error creating verifier for google_1p_code log: %w", err) + return nil, fmt.Errorf("error creating verifier for 2026/01 google_1p_code log: %w", err) } targets = append(targets, logTarget{ - name: "google_1p_code", + name: "google_1p_code (2026/01)", + baseURL: LogBaseURLG1PJWT202601, + checkpointPath: "checkpoint.txt", + verifier: v202601, + tileHeight: 8, + isTessera: false, + binaryInfoFilenames: []string{PackageInfoFilename}, + }) + + // Legacy log continuation fallback + vLegacy, err := checkpoint.NewVerifier(googleSystemAppLogPubKey, KeyNameForVerifierG1PJWT) + if err != nil { + return nil, fmt.Errorf("error creating verifier for legacy google_1p_code log: %w", err) + } + targets = append(targets, logTarget{ + name: "google_1p_code (legacy)", baseURL: LogBaseURLG1PJWT, checkpointPath: "checkpoint.txt", - verifier: v, + verifier: vLegacy, tileHeight: 1, isTessera: false, binaryInfoFilenames: []string{PackageInfoFilename}, diff --git a/verifier_tools/verify/cmd/verifier/verifier_test.go b/verifier_tools/verify/cmd/verifier/verifier_test.go new file mode 100644 index 000000000..d9e4f30 --- /dev/null +++ b/verifier_tools/verify/cmd/verifier/verifier_test.go @@ -0,0 +1,132 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/android/android-binary-transparency/verifier_tools/verify/internal/checkpoint" +) + +func TestResolveTargetsGoogle1PCode(t *testing.T) { + targets, err := resolveTargets("google_1p_code") + if err != nil { + t.Fatalf("resolveTargets(\"google_1p_code\") returned unexpected error: %v", err) + } + + if len(targets) != 2 { + t.Fatalf("got %d targets for google_1p_code, want 2", len(targets)) + } + + // Target 0: 2026/01 shard (primary, tileHeight 8) + t0 := targets[0] + if t0.name != "google_1p_code (2026/01)" { + t.Errorf("targets[0].name = %q, want %q", t0.name, "google_1p_code (2026/01)") + } + if t0.baseURL != LogBaseURLG1PJWT202601 { + t.Errorf("targets[0].baseURL = %q, want %q", t0.baseURL, LogBaseURLG1PJWT202601) + } + if t0.checkpointPath != "checkpoint.txt" { + t.Errorf("targets[0].checkpointPath = %q, want %q", t0.checkpointPath, "checkpoint.txt") + } + if t0.verifier.Name() != KeyNameForVerifierG1PJWT202601 { + t.Errorf("targets[0].verifier.Name() = %q, want %q", t0.verifier.Name(), KeyNameForVerifierG1PJWT202601) + } + if t0.tileHeight != 8 { + t.Errorf("targets[0].tileHeight = %d, want 8", t0.tileHeight) + } + if t0.isTessera { + t.Errorf("targets[0].isTessera = true, want false") + } + if len(t0.binaryInfoFilenames) != 1 || t0.binaryInfoFilenames[0] != PackageInfoFilename { + t.Errorf("targets[0].binaryInfoFilenames = %v, want [%q]", t0.binaryInfoFilenames, PackageInfoFilename) + } + + // Target 1: legacy developers.google.com shard (fallback, tileHeight 1) + t1 := targets[1] + if t1.name != "google_1p_code (legacy)" { + t.Errorf("targets[1].name = %q, want %q", t1.name, "google_1p_code (legacy)") + } + if t1.baseURL != LogBaseURLG1PJWT { + t.Errorf("targets[1].baseURL = %q, want %q", t1.baseURL, LogBaseURLG1PJWT) + } + if t1.checkpointPath != "checkpoint.txt" { + t.Errorf("targets[1].checkpointPath = %q, want %q", t1.checkpointPath, "checkpoint.txt") + } + if t1.verifier.Name() != KeyNameForVerifierG1PJWT { + t.Errorf("targets[1].verifier.Name() = %q, want %q", t1.verifier.Name(), KeyNameForVerifierG1PJWT) + } + if t1.tileHeight != 1 { + t.Errorf("targets[1].tileHeight = %d, want 1", t1.tileHeight) + } + if t1.isTessera { + t.Errorf("targets[1].isTessera = true, want false") + } + if len(t1.binaryInfoFilenames) != 1 || t1.binaryInfoFilenames[0] != PackageInfoFilename { + t.Errorf("targets[1].binaryInfoFilenames = %v, want [%q]", t1.binaryInfoFilenames, PackageInfoFilename) + } + + // Both targets use the same embedded public key so their 4-byte key hashes must match. + if t0.verifier.KeyHash() != t1.verifier.KeyHash() { + t.Errorf("expected identical key hash for both google_1p_code targets: got %x vs %x", + t0.verifier.KeyHash(), t1.verifier.KeyHash()) + } +} + +func TestGoogle1PCodeCheckpointSignatureVerification(t *testing.T) { + // Real signed checkpoints from the 2026/01 shard and the legacy log. + const signedCheckpoint202601 = "gstatic.com/android/binary_transparency/google1p/jwt/0\n" + + "234\n" + + "HpW7vHFFioFiMf0IglK1B3MLk80iaGOC6Ud6Etq038U=\n" + + "\n" + + "— gstatic.com/android/binary_transparency/google1p/jwt/0 qsuhszBFAiEAomG6In9+okg+Pj1Jw4JpWfignNeNXQweJxoYf9q59GMCIEyy/Ebu096WrCsT9L0Dv5D1EBHzqNQ26A1XcnNESPKX\n" + + const signedCheckpointLegacy = "developers.google.com/android/binary_transparency/google1p/0\n" + + "134\n" + + "WddUpZSJPJPm93SLwoCdKv+oJqPEqie52TZTIVNOhok=\n" + + "\n" + + "— developers.google.com/android/binary_transparency/google1p/0 qsuhszBEAiBA1NO/xnL4++iXFVwPhRsNU6AWDPEtOvDJQL3OuqCBOwIgbyMsA1l2yLvPUq8CoMNBf4E88l4XjlW4YanDLn5HRRI=\n" + + targets, err := resolveTargets("google_1p_code") + if err != nil { + t.Fatalf("resolveTargets(\"google_1p_code\") failed: %v", err) + } + + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/2026/01/checkpoint.txt": + w.Write([]byte(signedCheckpoint202601)) + case "/legacy/checkpoint.txt": + w.Write([]byte(signedCheckpointLegacy)) + default: + http.NotFound(w, r) + } + })) + defer s.Close() + + // Verify 2026/01 checkpoint with targets[0].verifier + root202601, err := checkpoint.FromURLWithPath(s.URL+"/2026/01", "checkpoint.txt", targets[0].verifier) + if err != nil { + t.Fatalf("failed to verify 2026/01 checkpoint with targets[0].verifier: %v", err) + } + if root202601.Size != 234 { + t.Errorf("root202601.Size = %d, want 234", root202601.Size) + } + + // Verify legacy checkpoint with targets[1].verifier + rootLegacy, err := checkpoint.FromURLWithPath(s.URL+"/legacy", "checkpoint.txt", targets[1].verifier) + if err != nil { + t.Fatalf("failed to verify legacy checkpoint with targets[1].verifier: %v", err) + } + if rootLegacy.Size != 134 { + t.Errorf("rootLegacy.Size = %d, want 134", rootLegacy.Size) + } + + // Cross-check: targets[0].verifier must reject legacy checkpoint and vice versa + if _, err := checkpoint.FromURLWithPath(s.URL+"/legacy", "checkpoint.txt", targets[0].verifier); err == nil { + t.Errorf("expected targets[0].verifier to reject legacy checkpoint, got nil") + } + if _, err := checkpoint.FromURLWithPath(s.URL+"/2026/01", "checkpoint.txt", targets[1].verifier); err == nil { + t.Errorf("expected targets[1].verifier to reject 2026/01 checkpoint, got nil") + } +} diff --git a/verifier_tools/verify/internal/checkpoint/checkpoint.go b/verifier_tools/verify/internal/checkpoint/checkpoint.go index 6e066b8..48cc021 100644 --- a/verifier_tools/verify/internal/checkpoint/checkpoint.go +++ b/verifier_tools/verify/internal/checkpoint/checkpoint.go @@ -41,6 +41,8 @@ const ( originIDPixel = "developers.google.com/android/binary_transparency/0\n" // originIDG1P identifies a checkpoint for the Google System APK Transparency Log. originIDG1P = "developers.google.com/android/binary_transparency/google1p/0\n" + // originIDG1PJWT202601 identifies a checkpoint for the Google System APK Transparency Log (2026/01 shard). + originIDG1PJWT202601 = "gstatic.com/android/binary_transparency/google1p/jwt/0\n" // originIDG1PAPK identifies a checkpoint for the Google 1P APK Transparency Log. originIDG1PAPK = "gstatic.com/android/binary_transparency/google1p/apk/2026/0\n" // originIDG1PAPKTessera identifies a checkpoint for the Google 1P APK Transparency Log (Tessera shard). @@ -123,6 +125,8 @@ func parseCheckpoint(ckpt string) (Root, error) { body = ckpt[len(originIDPixel):] case strings.HasPrefix(ckpt, originIDG1P): body = ckpt[len(originIDG1P):] + case strings.HasPrefix(ckpt, originIDG1PJWT202601): + body = ckpt[len(originIDG1PJWT202601):] case strings.HasPrefix(ckpt, originIDG1PAPK): body = ckpt[len(originIDG1PAPK):] case strings.HasPrefix(ckpt, originIDG1PAPKTessera): @@ -132,9 +136,10 @@ func parseCheckpoint(ckpt string) (Root, error) { case strings.HasPrefix(ckpt, originIDMainlineModuleTessera): body = ckpt[len(originIDMainlineModuleTessera):] default: - return Root{}, fmt.Errorf("invalid checkpoint - unknown origin, must be either %s, %s, %s, %s, %s, or %s", + return Root{}, fmt.Errorf("invalid checkpoint - unknown origin, must be either %s, %s, %s, %s, %s, %s, or %s", strings.TrimSpace(originIDPixel), strings.TrimSpace(originIDG1P), + strings.TrimSpace(originIDG1PJWT202601), strings.TrimSpace(originIDG1PAPK), strings.TrimSpace(originIDG1PAPKTessera), strings.TrimSpace(originIDMainlineModule), diff --git a/verifier_tools/verify/internal/checkpoint/checkpoint_test.go b/verifier_tools/verify/internal/checkpoint/checkpoint_test.go index e4045fb..b5d5ba9 100644 --- a/verifier_tools/verify/internal/checkpoint/checkpoint_test.go +++ b/verifier_tools/verify/internal/checkpoint/checkpoint_test.go @@ -133,6 +133,16 @@ func TestValidCheckpointFormat(t *testing.T) { m: "developers.google.com/android/binary_transparency/0\n10\ndGhlIHZpZXcgZnJvbSB0aGUgdHJlZSB0b3BzIGlzIGdyZWF0IQ==\n", wantSize: 10, }, + { + desc: "google 1p legacy origin", + m: "developers.google.com/android/binary_transparency/google1p/0\n134\nWddUpZSJPJPm93SLwoCdKv+oJqPEqie52TZTIVNOhok=\n", + wantSize: 134, + }, + { + desc: "google 1p jwt 2026/01 origin", + m: "gstatic.com/android/binary_transparency/google1p/jwt/0\n234\nHpW7vHFFioFiMf0IglK1B3MLk80iaGOC6Ud6Etq038U=\n", + wantSize: 234, + }, { desc: "google 1p apk origin", m: "gstatic.com/android/binary_transparency/google1p/apk/2026/0\n1797152\nIkmuYB2xKKEOLiaQkIho1o9/uGjrKDbk8xa3xXaFeHY=\n", diff --git a/verifier_tools/verify/internal/tiles/reader_test.go b/verifier_tools/verify/internal/tiles/reader_test.go index a424a89..e2ae704 100644 --- a/verifier_tools/verify/internal/tiles/reader_test.go +++ b/verifier_tools/verify/internal/tiles/reader_test.go @@ -637,6 +637,10 @@ func TestLogDirFromURL(t *testing.T) { url: "https://developers.google.com/android/binary_transparency/google1p", want: "google1p", }, + { + url: "https://www.gstatic.com/android/binary_transparency/google1p/jwt/2026/01", + want: filepath.FromSlash("google1p/jwt/2026/01"), + }, { url: "https://www.gstatic.com/android/binary_transparency/google1p/apk/2026/01", want: filepath.FromSlash("google1p/apk/2026/01"),