Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package apko for openSUSE:Factory checked in 
at 2026-04-25 21:36:40
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/apko (Old)
 and      /work/SRC/openSUSE:Factory/.apko.new.11940 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "apko"

Sat Apr 25 21:36:40 2026 rev:109 rq:1349055 version:1.2.7

Changes:
--------
--- /work/SRC/openSUSE:Factory/apko/apko.changes        2026-04-23 
17:11:29.961710306 +0200
+++ /work/SRC/openSUSE:Factory/.apko.new.11940/apko.changes     2026-04-25 
21:37:18.845631893 +0200
@@ -1,0 +2,8 @@
+Fri Apr 24 06:06:38 UTC 2026 - Johannes Kastl 
<[email protected]>
+
+- Update to version 1.2.7:
+  * apk: verify package control hash against signed APKINDEX
+    (#2191)
+  * apk: guard non-RSA JWKS keys in DiscoverKeys (#2190)
+
+-------------------------------------------------------------------

Old:
----
  apko-1.2.6.obscpio

New:
----
  apko-1.2.7.obscpio

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ apko.spec ++++++
--- /var/tmp/diff_new_pack.V4YzIa/_old  2026-04-25 21:37:20.017679805 +0200
+++ /var/tmp/diff_new_pack.V4YzIa/_new  2026-04-25 21:37:20.017679805 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           apko
-Version:        1.2.6
+Version:        1.2.7
 Release:        0
 Summary:        Build OCI images from APK packages directly without Dockerfile
 License:        Apache-2.0

++++++ _service ++++++
--- /var/tmp/diff_new_pack.V4YzIa/_old  2026-04-25 21:37:20.057681440 +0200
+++ /var/tmp/diff_new_pack.V4YzIa/_new  2026-04-25 21:37:20.061681604 +0200
@@ -3,7 +3,7 @@
     <param name="url">https://github.com/chainguard-dev/apko.git</param>
     <param name="scm">git</param>
     <param name="exclude">.git</param>
-    <param name="revision">refs/tags/v1.2.6</param>
+    <param name="revision">refs/tags/v1.2.7</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="versionrewrite-pattern">v(.*)</param>
     <param name="changesgenerate">enable</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.V4YzIa/_old  2026-04-25 21:37:20.089682749 +0200
+++ /var/tmp/diff_new_pack.V4YzIa/_new  2026-04-25 21:37:20.093682913 +0200
@@ -3,6 +3,6 @@
                 <param 
name="url">https://github.com/chainguard-dev/apko</param>
               <param 
name="changesrevision">861f83f69e6fa9114405a2f7bb5cf6585ad00421</param></service><service
 name="tar_scm">
                 <param 
name="url">https://github.com/chainguard-dev/apko.git</param>
-              <param 
name="changesrevision">09b82d635baa11223ba5b28b421069cadcddb5d9</param></service></servicedata>
+              <param 
name="changesrevision">a118c3d604107532b5525bd4bee2fb369a6228aa</param></service></servicedata>
 (No newline at EOF)
 

++++++ apko-1.2.6.obscpio -> apko-1.2.7.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.6/pkg/apk/apk/implementation.go 
new/apko-1.2.7/pkg/apk/apk/implementation.go
--- old/apko-1.2.6/pkg/apk/apk/implementation.go        2026-04-22 
17:22:42.000000000 +0200
+++ new/apko-1.2.7/pkg/apk/apk/implementation.go        2026-04-23 
21:44:30.000000000 +0200
@@ -1058,7 +1058,11 @@
                }
                keyName := key.KeyID + ".rsa.pub"
 
-               b, err := x509.MarshalPKIXPublicKey(key.Key.(*rsa.PublicKey))
+               rsaKey, ok := key.Key.(*rsa.PublicKey)
+               if !ok {
+                       return nil, fmt.Errorf("unsupported JWKS key type %T 
for key %q: expected *rsa.PublicKey", key.Key, key.KeyID)
+               }
+               b, err := x509.MarshalPKIXPublicKey(rsaKey)
                if err != nil {
                        return nil, err
                } else if len(b) == 0 {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.6/pkg/apk/apk/implementation_test.go 
new/apko-1.2.7/pkg/apk/apk/implementation_test.go
--- old/apko-1.2.6/pkg/apk/apk/implementation_test.go   2026-04-22 
17:22:42.000000000 +0200
+++ new/apko-1.2.7/pkg/apk/apk/implementation_test.go   2026-04-23 
21:44:30.000000000 +0200
@@ -16,6 +16,11 @@
 
 import (
        "context"
+       "crypto/ecdsa"
+       "crypto/elliptic"
+       "crypto/rand"
+       "crypto/rsa"
+       "encoding/json"
        "fmt"
        "io/fs"
        "net/http"
@@ -29,6 +34,7 @@
        "testing"
 
        "github.com/stretchr/testify/require"
+       "go.step.sm/crypto/jose"
 
        "chainguard.dev/apko/pkg/apk/auth"
        apkfs "chainguard.dev/apko/pkg/apk/fs"
@@ -930,3 +936,58 @@
        require.Error(t, err, "should fail with bad auth")
        require.True(t, called, "did not make request")
 }
+
+func TestDiscoverKeysNonRSA(t *testing.T) {
+       ctx := context.Background()
+
+       ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+       require.NoError(t, err)
+
+       var jwksURL string
+       srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, 
r *http.Request) {
+               switch r.URL.Path {
+               case "/apk-configuration":
+                       w.Header().Set("Content-Type", "application/json")
+                       fmt.Fprintf(w, `{"jwks_uri":%q}`, jwksURL)
+               case "/jwks":
+                       jwks := jose.JSONWebKeySet{Keys: 
[]jose.JSONWebKey{{Key: &ecKey.PublicKey, KeyID: "ec-test"}}}
+                       require.NoError(t, json.NewEncoder(w).Encode(jwks))
+               default:
+                       http.NotFound(w, r)
+               }
+       }))
+       defer srv.Close()
+       jwksURL = srv.URL + "/jwks"
+
+       _, err = DiscoverKeys(ctx, srv.Client(), auth.StaticAuth("", "", ""), 
srv.URL)
+       require.Error(t, err, "expected typed error, not a panic")
+       require.Contains(t, err.Error(), "unsupported JWKS key type")
+}
+
+func TestDiscoverKeysRSA(t *testing.T) {
+       ctx := context.Background()
+
+       rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
+       require.NoError(t, err)
+
+       var jwksURL string
+       srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, 
r *http.Request) {
+               switch r.URL.Path {
+               case "/apk-configuration":
+                       w.Header().Set("Content-Type", "application/json")
+                       fmt.Fprintf(w, `{"jwks_uri":%q}`, jwksURL)
+               case "/jwks":
+                       jwks := jose.JSONWebKeySet{Keys: 
[]jose.JSONWebKey{{Key: &rsaKey.PublicKey, KeyID: "rsa-test"}}}
+                       require.NoError(t, json.NewEncoder(w).Encode(jwks))
+               default:
+                       http.NotFound(w, r)
+               }
+       }))
+       defer srv.Close()
+       jwksURL = srv.URL + "/jwks"
+
+       keys, err := DiscoverKeys(ctx, srv.Client(), auth.StaticAuth("", "", 
""), srv.URL)
+       require.NoError(t, err)
+       require.Len(t, keys, 1)
+       require.Equal(t, "rsa-test.rsa.pub", keys[0].ID)
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.6/pkg/apk/apk/install_test.go 
new/apko-1.2.7/pkg/apk/apk/install_test.go
--- old/apko-1.2.6/pkg/apk/apk/install_test.go  2026-04-22 17:22:42.000000000 
+0200
+++ new/apko-1.2.7/pkg/apk/apk/install_test.go  2026-04-23 21:44:30.000000000 
+0200
@@ -416,7 +416,7 @@
        return &testPackage{
                pkg:      pkg,
                file:     f.Name(),
-               checksum: base64.StdEncoding.EncodeToString(h.Sum(nil)),
+               checksum: "Q1" + base64.StdEncoding.EncodeToString(h.Sum(nil)),
        }
 }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.6/pkg/apk/apk/package_getter.go 
new/apko-1.2.7/pkg/apk/apk/package_getter.go
--- old/apko-1.2.6/pkg/apk/apk/package_getter.go        2026-04-22 
17:22:42.000000000 +0200
+++ new/apko-1.2.7/pkg/apk/apk/package_getter.go        2026-04-23 
21:44:30.000000000 +0200
@@ -170,6 +170,11 @@
                return nil, fmt.Errorf("expanding %s: %w", pkg.PackageName(), 
err)
        }
 
+       if err := verifyControlHash(pkg, exp.ControlHash); err != nil {
+               _ = exp.Close()
+               return nil, err
+       }
+
        // If we don't have a cache, we're done.
        if d.cache == nil {
                return exp, nil
@@ -178,6 +183,43 @@
        return d.cachePackage(ctx, pkg, exp, cacheDir)
 }
 
+// sha1File returns the SHA-1 of the file at path.
+func sha1File(path string) ([]byte, error) {
+       f, err := os.Open(path)
+       if err != nil {
+               return nil, err
+       }
+       defer f.Close()
+       h := sha1.New() //nolint:gosec // this is what apk tools is using
+       if _, err := io.Copy(h, f); err != nil {
+               return nil, err
+       }
+       return h.Sum(nil), nil
+}
+
+// verifyControlHash compares the SHA-1 of the downloaded package's control
+// section against the Q1-prefixed base64 checksum recorded in the signed
+// APKINDEX (or lock file). Without this check a compromised mirror or
+// poisoned cache could substitute arbitrary package contents even though
+// the index itself is signature-verified.
+func verifyControlHash(pkg InstallablePackage, controlHash []byte) error {
+       chk := pkg.ChecksumString()
+       if !strings.HasPrefix(chk, "Q1") {
+               return fmt.Errorf("package %q has unexpected checksum format: 
%q", pkg.PackageName(), chk)
+       }
+       expected, err := base64.StdEncoding.DecodeString(chk[2:])
+       if err != nil {
+               return fmt.Errorf("package %q has malformed checksum %q: %w", 
pkg.PackageName(), chk, err)
+       }
+       if len(expected) == 0 {
+               return fmt.Errorf("package %q has empty checksum", 
pkg.PackageName())
+       }
+       if !bytes.Equal(expected, controlHash) {
+               return fmt.Errorf("package %q control hash mismatch: expected 
%x, got %x", pkg.PackageName(), expected, controlHash)
+       }
+       return nil
+}
+
 // fetchPackage fetches a package from the network or local filesystem.
 func (d *defaultPackageGetter) fetchPackage(ctx context.Context, pkg 
FetchablePackage) (io.ReadCloser, error) {
        log := clog.FromContext(ctx)
@@ -322,8 +364,21 @@
        if err != nil {
                return nil, err
        }
+
+       // Recompute the hash of the on-disk control file rather than trusting
+       // the content-addressable filename. A missed check here would let a
+       // tampered or corrupted cache entry be served without the verification
+       // that getPackageImpl applies on the fetch path.
+       ctlHash, err := sha1File(ctl)
+       if err != nil {
+               return nil, fmt.Errorf("hashing cached control %q: %w", ctl, 
err)
+       }
+       if err := verifyControlHash(pkg, ctlHash); err != nil {
+               return nil, fmt.Errorf("cached %q: %w", ctl, err)
+       }
+
        exp.ControlFile = ctl
-       exp.ControlHash = checksum
+       exp.ControlHash = ctlHash
        exp.ControlSize = cf.Size()
 
        control, err := exp.ControlData()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.6/pkg/apk/apk/package_getter_test.go 
new/apko-1.2.7/pkg/apk/apk/package_getter_test.go
--- old/apko-1.2.6/pkg/apk/apk/package_getter_test.go   2026-04-22 
17:22:42.000000000 +0200
+++ new/apko-1.2.7/pkg/apk/apk/package_getter_test.go   2026-04-23 
21:44:30.000000000 +0200
@@ -2,6 +2,7 @@
 
 import (
        "context"
+       "encoding/base64"
        "fmt"
        "io"
        "net/http"
@@ -263,3 +264,89 @@
        require.Error(t, err, "unable to expand package")
        require.True(t, called, "did not make request")
 }
+
+// TestGetPackage_ChecksumMismatch confirms that a package served by a 
repository
+// that does not match the checksum recorded in the (signed) APKINDEX is 
rejected
+// rather than silently installed. This guards against compromised mirrors or
+// poisoned caches substituting package contents.
+func TestGetPackage_ChecksumMismatch(t *testing.T) {
+       tampered := testPkg
+       // Flip one byte of the recorded checksum so the downloaded content's
+       // real control-section SHA-1 will not match.
+       tampered.Checksum = append([]byte(nil), testPkg.Checksum...)
+       tampered.Checksum[0] ^= 0xff
+
+       repo := Repository{URI: fmt.Sprintf("%s/%s", testAlpineRepos, testArch)}
+       repoWithIndex := repo.WithIndex(&APKIndex{Packages: 
[]*Package{&tampered}})
+       pkg := NewRepositoryPackage(&tampered, repoWithIndex)
+       ctx := context.Background()
+
+       tmpDir := t.TempDir()
+       httpClient := &http.Client{Transport: &testLocalTransport{root: 
testPrimaryPkgDir, basenameOnly: true}}
+       a := newDefaultPackageGetter(httpClient, &cache{
+               dir:     tmpDir,
+               offline: false,
+               shared:  NewCache(false),
+       }, auth.DefaultAuthenticators)
+
+       _, err := a.GetPackage(ctx, pkg)
+       require.Error(t, err, "expected checksum mismatch to be detected")
+       require.Contains(t, err.Error(), "control hash mismatch")
+}
+
+// TestCachedPackage_TamperedControl confirms that a cache entry whose
+// on-disk control file no longer matches its content-addressable filename
+// is rejected rather than served. This protects against cache corruption
+// or tampering after an entry was originally written.
+func TestCachedPackage_TamperedControl(t *testing.T) {
+       repo := Repository{URI: fmt.Sprintf("%s/%s", testAlpineRepos, testArch)}
+       repoWithIndex := repo.WithIndex(&APKIndex{Packages: 
[]*Package{&testPkg}})
+       pkg := NewRepositoryPackage(&testPkg, repoWithIndex)
+       ctx := context.Background()
+
+       tmpDir := t.TempDir()
+       httpClient := &http.Client{Transport: &testLocalTransport{root: 
testPrimaryPkgDir, basenameOnly: true}}
+       a := newDefaultPackageGetter(httpClient, &cache{
+               dir:     tmpDir,
+               offline: false,
+               shared:  NewCache(false),
+       }, auth.DefaultAuthenticators)
+
+       // Populate the cache.
+       exp, err := a.GetPackage(ctx, pkg)
+       require.NoError(t, err, "populating cache")
+       ctlPath := exp.ControlFile
+       require.FileExists(t, ctlPath)
+
+       cacheDir := filepath.Dir(ctlPath)
+
+       // Tamper with the cached control file. Overwrite with different bytes
+       // so its SHA-1 no longer matches the content-addressable filename.
+       require.NoError(t, os.WriteFile(ctlPath, []byte("tampered"), 0o644))
+
+       _, err = a.cachedPackage(ctx, pkg, cacheDir)
+       require.Error(t, err, "expected tampered cache entry to be rejected")
+       require.Contains(t, err.Error(), "control hash mismatch")
+}
+
+func TestVerifyControlHash(t *testing.T) {
+       want := make([]byte, 20)
+       for i := range want {
+               want[i] = byte(i)
+       }
+       pkg := &testPackage{
+               pkg:      &Package{Name: "example"},
+               checksum: "Q1" + base64.StdEncoding.EncodeToString(want),
+       }
+
+       require.NoError(t, verifyControlHash(pkg, want))
+
+       bad := append([]byte(nil), want...)
+       bad[0] ^= 0xff
+       require.Error(t, verifyControlHash(pkg, bad))
+
+       require.Error(t, verifyControlHash(&testPackage{pkg: &Package{Name: 
"x"}, checksum: ""}, want))
+       require.Error(t, verifyControlHash(&testPackage{pkg: &Package{Name: 
"x"}, checksum: "Q1"}, want))
+       require.Error(t, verifyControlHash(&testPackage{pkg: &Package{Name: 
"x"}, checksum: "Q1!!!"}, want))
+       require.Error(t, verifyControlHash(&testPackage{pkg: &Package{Name: 
"x"}, checksum: "raw-no-prefix"}, want))
+}

++++++ apko.obsinfo ++++++
--- /var/tmp/diff_new_pack.V4YzIa/_old  2026-04-25 21:37:20.853713981 +0200
+++ /var/tmp/diff_new_pack.V4YzIa/_new  2026-04-25 21:37:20.865714472 +0200
@@ -1,5 +1,5 @@
 name: apko
-version: 1.2.6
-mtime: 1776871362
-commit: 09b82d635baa11223ba5b28b421069cadcddb5d9
+version: 1.2.7
+mtime: 1776973470
+commit: a118c3d604107532b5525bd4bee2fb369a6228aa
 

++++++ vendor.tar.gz ++++++
/work/SRC/openSUSE:Factory/apko/vendor.tar.gz 
/work/SRC/openSUSE:Factory/.apko.new.11940/vendor.tar.gz differ: char 134, line 
2

Reply via email to