From e5ae7f92c4c2fea6ae7b28b9cb86fb48da6fdf97 Mon Sep 17 00:00:00 2001 From: Joshua Gilman Date: Fri, 21 Aug 2026 19:00:13 -0700 Subject: [PATCH] fix(packages): honor APK signature key name --- internal/adapter/pkgverify/verifier.go | 57 ++++++++++++++++++++- internal/adapter/pkgverify/verifier_test.go | 34 ++++++++++-- 2 files changed, 85 insertions(+), 6 deletions(-) diff --git a/internal/adapter/pkgverify/verifier.go b/internal/adapter/pkgverify/verifier.go index 1ed1184..3146576 100644 --- a/internal/adapter/pkgverify/verifier.go +++ b/internal/adapter/pkgverify/verifier.go @@ -1,6 +1,9 @@ package pkgverify import ( + "archive/tar" + "bufio" + "compress/gzip" "context" "errors" "fmt" @@ -42,6 +45,10 @@ const ( containerRPMDBPath = "/rpmdb" // containerAPKKeysPath is the APK public-key directory. containerAPKKeysPath = "/keys" + // apkSignaturePrefix identifies the public-key basename declared by an APK signature. + apkSignaturePrefix = ".SIGN.RSA." + // maxAPKSignatureStreamSize bounds decompression while locating an APK signature. + maxAPKSignatureStreamSize int64 = 1024 * 1024 ) // Options configures a [Verifier]. @@ -143,9 +150,12 @@ func (v *Verifier) verifyRPM(ctx context.Context, request pkgrepo.VerificationRe return nil } -// verifyAPK checks one package against a key mounted under its signature basename. +// verifyAPK checks one package against the key basename declared by its signature. func (v *Verifier) verifyAPK(ctx context.Context, request pkgrepo.VerificationRequest) error { - keyName := filepath.Base(request.PublicKey) + keyName, err := apkSignatureKeyName(request.Package) + if err != nil { + return fmt.Errorf("read APK signature key: %w", err) + } containerKey := containerAPKKeysPath + "/" + keyName if err := v.runDocker(ctx, []string{ "-v", request.Package + ":" + containerAPKPath + ":ro", @@ -157,6 +167,49 @@ func (v *Verifier) verifyAPK(ctx context.Context, request pkgrepo.VerificationRe return nil } +// apkSignatureKeyName reads the public-key basename from the bounded APK signature stream. +func apkSignatureKeyName(packagePath string) (string, error) { + file, err := os.Open(packagePath) + if err != nil { + return "", fmt.Errorf("open APK: %w", err) + } + defer file.Close() + + gzipReader, err := gzip.NewReader(bufio.NewReader(file)) + if err != nil { + return "", fmt.Errorf("open APK signature stream: %w", err) + } + defer gzipReader.Close() + gzipReader.Multistream(false) + + limited := &io.LimitedReader{R: gzipReader, N: maxAPKSignatureStreamSize + 1} + tarReader := tar.NewReader(limited) + for { + header, nextErr := tarReader.Next() + if errors.Is(nextErr, io.EOF) { + break + } + if nextErr != nil { + return "", fmt.Errorf("read APK signature stream: %w", nextErr) + } + + keyName, found := strings.CutPrefix(header.Name, apkSignaturePrefix) + if !found { + continue + } + if keyName == "" || !strings.HasSuffix(keyName, ".rsa.pub") || + strings.ContainsAny(keyName, "/\\:\x00") { + return "", fmt.Errorf("APK signature key name %q is invalid", keyName) + } + return keyName, nil + } + if limited.N == 0 { + return "", errors.New("APK signature stream exceeds size limit") + } + + return "", errors.New("APK does not contain an RSA signature") +} + // runDocker runs one fixed networkless read-only verification command. func (v *Verifier) runDocker(ctx context.Context, mounts []string, image string, command ...string) error { args := []string{ diff --git a/internal/adapter/pkgverify/verifier_test.go b/internal/adapter/pkgverify/verifier_test.go index 528d70b..1c494f1 100644 --- a/internal/adapter/pkgverify/verifier_test.go +++ b/internal/adapter/pkgverify/verifier_test.go @@ -1,6 +1,8 @@ package pkgverify import ( + "archive/tar" + "compress/gzip" "context" "os" "path/filepath" @@ -82,16 +84,18 @@ func TestVerifyRPMUsesEphemeralDatabase(t *testing.T) { }, invocations[1]) } -// TestVerifyAPKPreservesSignatureKeyBasename proves apk can resolve the embedded key name. -func TestVerifyAPKPreservesSignatureKeyBasename(t *testing.T) { +// TestVerifyAPKUsesSignatureDeclaredKeyBasename proves config filenames need not match producer key names. +func TestVerifyAPKUsesSignatureDeclaredKeyBasename(t *testing.T) { skipWindows(t) t.Parallel() fake, record, environ := verifierFake(t) + packagePath := filepath.Join(t.TempDir(), "release-cli.apk") + writeAPKSignature(t, packagePath, "meigma-release-001.rsa.pub") request := pkgrepo.VerificationRequest{ Format: pkgrepo.FormatAPK, - Package: filepath.Join(t.TempDir(), "release-cli.apk"), - PublicKey: filepath.Join(t.TempDir(), "meigma-release-001.rsa.pub"), + Package: packagePath, + PublicKey: filepath.Join(t.TempDir(), "repository-apk.rsa.pub"), } err := New(Options{DockerPath: fake, Environ: environ}).Verify(context.Background(), request) require.NoError(t, err) @@ -130,6 +134,7 @@ func TestVerifyFailureDoesNotEchoPaths(t *testing.T) { Package: filepath.Join(t.TempDir(), "sensitive-package.apk"), PublicKey: filepath.Join(t.TempDir(), "sensitive-key.rsa.pub"), } + writeAPKSignature(t, request.Package, "sensitive-key.rsa.pub") err := New(Options{DockerPath: fake, Environ: environ}).Verify(context.Background(), request) require.Error(t, err) assert.Contains(t, err.Error(), "signature rejected") @@ -137,6 +142,27 @@ func TestVerifyFailureDoesNotEchoPaths(t *testing.T) { assert.NotContains(t, err.Error(), request.PublicKey) } +// writeAPKSignature writes the bounded signature stream needed by verifier tests. +func writeAPKSignature(t *testing.T, packagePath, keyName string) { + t.Helper() + + file, err := os.Create(packagePath) + require.NoError(t, err) + gzipWriter := gzip.NewWriter(file) + tarWriter := tar.NewWriter(gzipWriter) + signature := []byte("signature") + require.NoError(t, tarWriter.WriteHeader(&tar.Header{ + Name: apkSignaturePrefix + keyName, + Mode: 0o644, + Size: int64(len(signature)), + })) + _, err = tarWriter.Write(signature) + require.NoError(t, err) + require.NoError(t, tarWriter.Close()) + require.NoError(t, gzipWriter.Close()) + require.NoError(t, file.Close()) +} + // verifierFake writes one fake Docker executable and returns its isolated environment. func verifierFake(t *testing.T) (string, string, []string) { t.Helper()