Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 55 additions & 2 deletions internal/adapter/pkgverify/verifier.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package pkgverify

import (
"archive/tar"
"bufio"
"compress/gzip"
"context"
"errors"
"fmt"
Expand Down Expand Up @@ -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].
Expand Down Expand Up @@ -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",
Expand All @@ -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{
Expand Down
34 changes: 30 additions & 4 deletions internal/adapter/pkgverify/verifier_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package pkgverify

import (
"archive/tar"
"compress/gzip"
"context"
"os"
"path/filepath"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -130,13 +134,35 @@ 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")
assert.NotContains(t, err.Error(), request.Package)
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()
Expand Down