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-07-08 17:38:39
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/apko (Old)
 and      /work/SRC/openSUSE:Factory/.apko.new.1982 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "apko"

Wed Jul  8 17:38:39 2026 rev:124 rq:1364475 version:1.2.23

Changes:
--------
--- /work/SRC/openSUSE:Factory/apko/apko.changes        2026-07-06 
12:36:39.236418155 +0200
+++ /work/SRC/openSUSE:Factory/.apko.new.1982/apko.changes      2026-07-08 
17:41:50.048545322 +0200
@@ -1,0 +2,7 @@
+Wed Jul 08 05:10:51 UTC 2026 - Johannes Kastl 
<[email protected]>
+
+- Update to version 1.2.23:
+  * Add contents.runtime_keyring for runtime APK trust (#2297)
+  * Use flightCache for index cache implementation (#2175)
+
+-------------------------------------------------------------------

Old:
----
  apko-1.2.22.obscpio

New:
----
  apko-1.2.23.obscpio

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

Other differences:
------------------
++++++ apko.spec ++++++
--- /var/tmp/diff_new_pack.UeLLrt/_old  2026-07-08 17:41:51.588598989 +0200
+++ /var/tmp/diff_new_pack.UeLLrt/_new  2026-07-08 17:41:51.588598989 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           apko
-Version:        1.2.22
+Version:        1.2.23
 Release:        0
 Summary:        Build OCI images from APK packages directly without Dockerfile
 License:        Apache-2.0

++++++ _service ++++++
--- /var/tmp/diff_new_pack.UeLLrt/_old  2026-07-08 17:41:51.640600801 +0200
+++ /var/tmp/diff_new_pack.UeLLrt/_new  2026-07-08 17:41:51.644600940 +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.22</param>
+    <param name="revision">refs/tags/v1.2.23</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="versionrewrite-pattern">v(.*)</param>
     <param name="changesgenerate">enable</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.UeLLrt/_old  2026-07-08 17:41:51.672601916 +0200
+++ /var/tmp/diff_new_pack.UeLLrt/_new  2026-07-08 17:41:51.676602055 +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">f9189c2afb477dd0f7ecab00f3751f0888553c4b</param></service></servicedata>
+              <param 
name="changesrevision">9d4b52d913063421165620da75b7926bbe211321</param></service></servicedata>
 (No newline at EOF)
 

++++++ apko-1.2.22.obscpio -> apko-1.2.23.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.22/docs/apko_file.md 
new/apko-1.2.23/docs/apko_file.md
--- old/apko-1.2.22/docs/apko_file.md   2026-07-03 18:11:53.000000000 +0200
+++ new/apko-1.2.23/docs/apko_file.md   2026-07-07 16:15:27.000000000 +0200
@@ -113,6 +113,16 @@
    Notice that you need to package name under `packages` with the label e.g `- 
alpine-baselayout@local`.
  - `packages` defines a list of alpine packages to install inside the image
  - `keyring` PGP keys to add to the keyring for verifying packages.
+ - `runtime_repositories` defines a list of alpine repositories that are 
written to
+   `/etc/apk/repositories` in the image but are not used at build time, so 
`apk add` in the
+   running container pulls from them.
+ - `runtime_keyring` defines public keys installed into `/etc/apk/keys` after 
package
+   resolution, so runtime `apk add` against `runtime_repositories` can verify 
packages from
+   mirrors that re-sign them. Each entry is an inline `{name, content}` 
object: `content` is a
+   PEM-encoded RSA public key, and `name` is the filename the key is written 
to, which must
+   match the filename the repository's APKINDEX signature references 
(`.SIGN.RSA256.<name>`).
+   These keys are a runtime trust anchor only and are never consulted during 
build-time
+   package resolution.
 
 ### Entrypoint top level element
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.22/examples/runtime-keyring.yaml 
new/apko-1.2.23/examples/runtime-keyring.yaml
--- old/apko-1.2.22/examples/runtime-keyring.yaml       1970-01-01 
01:00:00.000000000 +0100
+++ new/apko-1.2.23/examples/runtime-keyring.yaml       2026-07-07 
16:15:27.000000000 +0200
@@ -0,0 +1,36 @@
+# Demonstrates contents.runtime_keyring: public keys installed into
+# /etc/apk/keys *after* the build, so runtime `apk add` against a
+# runtime_repositories mirror (which re-signs packages with its own key)
+# verifies without --allow-untrusted. Runtime trust only — these keys are
+# not used to verify packages during the build.
+contents:
+  keyring:
+    - https://packages.wolfi.dev/os/wolfi-signing.rsa.pub
+  repositories:
+    - https://packages.wolfi.dev/os
+  packages:
+    - wolfi-base
+  # The repo whose packages the key above verifies at runtime. Seeded into
+  # /etc/apk/repositories in the image; not used at build time.
+  runtime_repositories:
+    - https://packages.example.com/os
+  # Each entry is an inline {name, content} public key; the name must match
+  # the filename the repository's APKINDEX signature references.
+  runtime_keyring:
+    - name: example-mirror.rsa.pub
+      content: |
+        -----BEGIN PUBLIC KEY-----
+        MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtfLcP/XCqh9zuL8J1sIu
+        ADYZt1xafrimFkOMHgldT+0Mpi3ORy6lf29Ccc+4Hfe2P/ImlB89THtIjMNGk58C
+        fZVod8z2cc52aV5YvNbFDxSoOPSc4D+IqL9bE6ICc0HS2kef1Rm6ZY1G8JoNs2zR
+        nquL6XyV7+gPC9KNhGi16f8qyhvMy3dkGPmqtXuI+j7p0XtDB1/e3xe+Mg+kQ47A
+        DiE2XB+9nbvOKOXuuUscerhEnvYPLRjDQECkRzLN76CPJsGwoBn7In/kMFecQuLD
+        LbYPdu/s8Pvz4wDurWOzUShbrF6Ka9ARRZVN57+4u6k+zuIi0XF5UCHicrjl6jYR
+        FQIDAQAB
+        -----END PUBLIC KEY-----
+
+cmd: /bin/sh -l
+
+archs:
+  - x86_64
+  - aarch64
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.22/internal/gen-jsonschema/main.go 
new/apko-1.2.23/internal/gen-jsonschema/main.go
--- old/apko-1.2.22/internal/gen-jsonschema/main.go     2026-07-03 
18:11:53.000000000 +0200
+++ new/apko-1.2.23/internal/gen-jsonschema/main.go     2026-07-07 
16:15:27.000000000 +0200
@@ -28,6 +28,7 @@
                log.Fatal(err)
        }
        schema := r.Reflect(types.ImageConfiguration{})
+
        b := new(bytes.Buffer)
        enc := json.NewEncoder(b)
        enc.SetIndent("", "  ")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.22/pkg/apk/apk/cache.go 
new/apko-1.2.23/pkg/apk/apk/cache.go
--- old/apko-1.2.22/pkg/apk/apk/cache.go        2026-07-03 18:11:53.000000000 
+0200
+++ new/apko-1.2.23/pkg/apk/apk/cache.go        2026-07-07 16:15:27.000000000 
+0200
@@ -82,6 +82,17 @@
        delete(f.cache, key)
 }
 
+// ForgetFunc removes all keys for which fn returns true.
+func (f *flightCache[K, V]) ForgetFunc(fn func(K) bool) {
+       f.mux.Lock()
+       defer f.mux.Unlock()
+       for k := range f.cache {
+               if fn(k) {
+                       delete(f.cache, k)
+               }
+       }
+}
+
 type Cache struct {
        etagCache  *sync.Map
        headFlight *singleflight.Group
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.22/pkg/apk/apk/cache_test.go 
new/apko-1.2.23/pkg/apk/apk/cache_test.go
--- old/apko-1.2.22/pkg/apk/apk/cache_test.go   2026-07-03 18:11:53.000000000 
+0200
+++ new/apko-1.2.23/pkg/apk/apk/cache_test.go   2026-07-07 16:15:27.000000000 
+0200
@@ -15,6 +15,7 @@
 package apk
 
 import (
+       "strings"
        "sync"
        "sync/atomic"
        "testing"
@@ -100,3 +101,31 @@
 
        require.EqualValues(t, 1, called.Load(), "Function should only be 
called once")
 }
+
+func TestFlightCacheForgetFunc(t *testing.T) {
+       s := newFlightCache[string, int]()
+
+       for k, v := range map[string]int{"a-1": 1, "a-2": 2, "b-1": 3} {
+               _, err := s.Do(k, func() (int, error) { return v, nil })
+               require.NoError(t, err)
+       }
+
+       // Forget all keys starting with "a-".
+       s.ForgetFunc(func(k string) bool {
+               return strings.HasPrefix(k, "a-")
+       })
+
+       // "a-*" keys should be evicted, so new values are computed.
+       r, err := s.Do("a-1", func() (int, error) { return 100, nil })
+       require.NoError(t, err)
+       require.Equal(t, 100, r)
+
+       r, err = s.Do("a-2", func() (int, error) { return 200, nil })
+       require.NoError(t, err)
+       require.Equal(t, 200, r)
+
+       // "b-1" should still be cached.
+       r, err = s.Do("b-1", func() (int, error) { return 999, nil })
+       require.NoError(t, err)
+       require.Equal(t, 3, r, "b-1 should still return the cached value")
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.22/pkg/apk/apk/index.go 
new/apko-1.2.23/pkg/apk/apk/index.go
--- old/apko-1.2.22/pkg/apk/apk/index.go        2026-07-03 18:11:53.000000000 
+0200
+++ new/apko-1.2.23/pkg/apk/apk/index.go        2026-07-07 16:15:27.000000000 
+0200
@@ -55,49 +55,21 @@
 // We just hold the parsed index in memory rather than re-parsing it every 
time,
 // which requires gunzipping, which is (somewhat) expensive.
 var globalIndexCache = &indexCache{
-       modtimes:  map[string]time.Time{},
-       urlToEtag: map[string]string{},
+       indexes:  newFlightCache[cacheKey, NamedIndex](),
+       modtimes: map[string]time.Time{},
 }
 
-type indexResult struct {
-       idx NamedIndex
-       err error
+type cacheKey struct {
+       url  string
+       etag string // Only used for remote indexes.
 }
 
 type indexCache struct {
-       // For remote indexes.
-       onces     sync.Map
-       urlToEtag map[string]string
-       etagMu    sync.Mutex // guards urlToEtag
+       indexes *flightCache[cacheKey, NamedIndex]
 
        // For local indexes.
        sync.Mutex
        modtimes map[string]time.Time
-
-       // etag|filename -> indexResult
-       indexes sync.Map
-}
-
-func (i *indexCache) forget(key string) {
-       i.onces.Delete(key)
-       i.indexes.Delete(key)
-}
-
-func (i *indexCache) store(key string, idx NamedIndex, err error) {
-       i.indexes.Store(key, indexResult{
-               idx: idx,
-               err: err,
-       })
-}
-
-func (i *indexCache) load(key string) (NamedIndex, error) {
-       v, ok := i.indexes.Load(key)
-       if !ok {
-               return nil, fmt.Errorf("indexCache did not see key %q after 
writing it", key)
-       }
-       result := v.(indexResult)
-
-       return result.idx, result.err
 }
 
 func (i *indexCache) get(ctx context.Context, repoName, repoURL string, keys 
map[string][]byte, arch string, opts *indexOpts) (NamedIndex, error) {
@@ -159,30 +131,21 @@
                        return fetchAndParse(etag)
                }
 
-               key := fmt.Sprintf("%s@%s", u, etag)
-
-               once, _ := i.onces.LoadOrStore(key, &sync.Once{})
-               once.(*sync.Once).Do(func() {
-                       // If we've seen this URL before, delete any references 
to old indexes so we can GC them.
-                       // Lock reads/writes to the map, without blocking the 
fetchAndParse goroutine.
-                       i.etagMu.Lock()
-                       prev, ok := i.urlToEtag[u]
-                       if ok {
-                               prevKey := fmt.Sprintf("%s@%s", u, prev)
-                               i.forget(prevKey)
-                       }
-                       i.etagMu.Unlock()
-
-                       idx, err := fetchAndParse(etag)
-                       i.store(key, idx, err)
+               key := cacheKey{url: u, etag: etag}
+               idx, err := i.indexes.Do(key, func() (NamedIndex, error) {
+                       return fetchAndParse(etag)
+               })
 
-                       // Record the current etag for this URL so we can GC it 
later.
-                       i.etagMu.Lock()
-                       i.urlToEtag[u] = etag
-                       i.etagMu.Unlock()
+               // Remove any stale entries with the same URL but a different 
etag.
+               // This races with concurrent callers that may have a different 
etag: a
+               // slower goroutine with an older etag could evict a newer 
entry. This is
+               // harmless — the next call will see the current etag via HEAD, 
re-fetch,
+               // and repopulate the cache.
+               i.indexes.ForgetFunc(func(k cacheKey) bool {
+                       return k.url == u && k.etag != etag
                })
 
-               return i.load(key)
+               return idx, err
        } else {
                i.Lock()
                defer i.Unlock()
@@ -193,24 +156,28 @@
                        return nil, fmt.Errorf("stat: %w", err)
                }
 
+               key := cacheKey{url: u}
+
+               // Evict the cached entry if the file has changed since we last 
saw it.
+               // On first access, ok is false and Do below handles the cache 
miss.
                mod := stat.ModTime()
                before, ok := i.modtimes[u]
-               if !ok || mod.After(before) {
+               if ok && mod.After(before) {
+                       i.indexes.Forget(key)
+               }
+               i.modtimes[u] = mod
+
+               return i.indexes.Do(key, func() (NamedIndex, error) {
                        b, err := os.ReadFile(u)
                        if err != nil {
                                return nil, fmt.Errorf("reading file: %w", err)
                        }
-                       // If this is the first time or it has changed since 
the last time...
                        idx, err := parseRepositoryIndex(ctx, u, keys, arch, b, 
opts)
                        if err != nil {
-                               i.store(u, nil, err)
-                       } else {
-                               i.store(u, 
NewNamedRepositoryWithIndex(repoName, repoRef.WithIndex(idx)), nil)
+                               return nil, err
                        }
-                       i.modtimes[u] = mod
-               }
-
-               return i.load(u)
+                       return NewNamedRepositoryWithIndex(repoName, 
repoRef.WithIndex(idx)), nil
+               })
        }
 }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.22/pkg/build/build_implementation.go 
new/apko-1.2.23/pkg/build/build_implementation.go
--- old/apko-1.2.22/pkg/build/build_implementation.go   2026-07-03 
18:11:53.000000000 +0200
+++ new/apko-1.2.23/pkg/build/build_implementation.go   2026-07-07 
16:15:27.000000000 +0200
@@ -198,6 +198,10 @@
                return nil, fmt.Errorf("failed to install certificates: %w", 
err)
        }
 
+       if err := bc.installRuntimeKeyring(ctx); err != nil {
+               return nil, fmt.Errorf("failed to install runtime keyring: %w", 
err)
+       }
+
        if err := bc.s6.WriteSupervisionTree(ctx, bc.ic.Entrypoint.Services); 
err != nil {
                return nil, fmt.Errorf("failed to write supervision tree: %w", 
err)
        }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.22/pkg/build/runtime_keyring.go 
new/apko-1.2.23/pkg/build/runtime_keyring.go
--- old/apko-1.2.22/pkg/build/runtime_keyring.go        1970-01-01 
01:00:00.000000000 +0100
+++ new/apko-1.2.23/pkg/build/runtime_keyring.go        2026-07-07 
16:15:27.000000000 +0200
@@ -0,0 +1,139 @@
+// Copyright 2025 Chainguard, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package build
+
+import (
+       "bytes"
+       "cmp"
+       "context"
+       "crypto/rsa"
+       "crypto/x509"
+       "encoding/pem"
+       "errors"
+       "fmt"
+       "io/fs"
+       "path/filepath"
+       "slices"
+
+       "go.opentelemetry.io/otel"
+
+       "chainguard.dev/apko/pkg/build/types"
+)
+
+// Directory apk reads trusted signing keys from. Keys placed here let runtime
+// `apk add` verify packages from runtime repositories. We are using these for
+// fs.FS so should omit the leading /
+const apkKeysDir = "etc/apk/keys"
+
+// installRuntimeKeyring writes contents.runtime_keyring's inline public keys 
to
+// /etc/apk/keys. It runs after package installation (post-FixateWorld), so the
+// keys are a runtime trust anchor only and never participate in build-time
+// package resolution.
+//
+// Keys are content-bearing in the config, so the build stays reproducible:
+// identical key content and names produce identical output, with no host paths
+// leaking into the image or the locked configuration.
+func (bc *Context) installRuntimeKeyring(ctx context.Context) error {
+       _, span := otel.Tracer("apko").Start(ctx, "installRuntimeKeyring")
+       defer span.End()
+
+       if len(bc.ic.Contents.RuntimeKeyring) == 0 {
+               return nil
+       }
+
+       builtTime, err := bc.GetBuildDateEpoch()
+       if err != nil {
+               return fmt.Errorf("failed to get build date epoch: %w", err)
+       }
+
+       if err := bc.fs.MkdirAll(apkKeysDir, 0o755); err != nil {
+               return fmt.Errorf("failed to create apk keys directory: %w", 
err)
+       }
+
+       // Sort by name for a deterministic write order. Duplicate names never 
reach
+       // this point: Validate rejects them on the merged configuration 
(includes
+       // are folded in at parse time), so duplicate policy lives there, not 
here.
+       keys := slices.Clone(bc.ic.Contents.RuntimeKeyring)
+       slices.SortFunc(keys, func(a, b types.RuntimeKeyringEntry) int { return 
cmp.Compare(a.Name, b.Name) })
+
+       for _, key := range keys {
+               encoded, err := parsePublicKey(key.Content)
+               if err != nil {
+                       return fmt.Errorf("failed to parse runtime keyring 
entry %q: %w", key.Name, err)
+               }
+
+               // Name is validated against certNameRegex during configuration
+               // validation, so it can't escape the directory.
+               keyPath := filepath.Join(apkKeysDir, key.Name)
+
+               // Refuse to overwrite an existing key. By this point 
InitKeyring has
+               // already installed the distro trust roots (e.g. 
wolfi-signing.rsa.pub),
+               // so a name collision would silently replace a packaged key 
and change
+               // runtime verification behaviour. Lstat, not Stat: a symlink 
at this
+               // path (even dangling) must count as existing, or the write 
below would
+               // follow it and land the key outside /etc/apk/keys.
+               //
+               // Note: on base-image builds inherited keys live in a lower 
layer, not
+               // bc.fs, so this only catches collisions within the layer 
being built.
+               if _, err := bc.fs.Lstat(keyPath); err == nil {
+                       return fmt.Errorf("runtime keyring entry %q collides 
with an existing /etc/apk/keys entry", key.Name)
+               } else if !errors.Is(err, fs.ErrNotExist) {
+                       return fmt.Errorf("failed to stat %s: %w", keyPath, err)
+               }
+
+               if err := bc.fs.WriteFile(keyPath, encoded, 0o644); err != nil {
+                       return fmt.Errorf("failed to write runtime keyring 
entry %s: %w", keyPath, err)
+               }
+               if err := bc.fs.Chtimes(keyPath, builtTime, builtTime); err != 
nil {
+                       return fmt.Errorf("failed to change times on runtime 
keyring entry %s: %w", keyPath, err)
+               }
+       }
+
+       return nil
+}
+
+// parsePublicKey validates that pemData is exactly one PEM-encoded RSA public
+// key and returns it re-encoded. Re-encoding drops any trailing bytes around 
the
+// block, keeping the written file canonical and reproducible.
+func parsePublicKey(pemData string) ([]byte, error) {
+       if pemData == "" {
+               return nil, fmt.Errorf("no key data provided")
+       }
+
+       block, rest := pem.Decode([]byte(pemData))
+       if block == nil {
+               return nil, fmt.Errorf("no PEM block found")
+       }
+       if block.Type != "PUBLIC KEY" {
+               return nil, fmt.Errorf("expected PUBLIC KEY block, got %s", 
block.Type)
+       }
+       if next, _ := pem.Decode(rest); next != nil {
+               return nil, fmt.Errorf("multiple PEM blocks found; only one is 
allowed")
+       }
+
+       pub, err := x509.ParsePKIXPublicKey(block.Bytes)
+       if err != nil {
+               return nil, fmt.Errorf("failed to parse public key: %w", err)
+       }
+       if _, ok := pub.(*rsa.PublicKey); !ok {
+               return nil, fmt.Errorf("expected an RSA public key, got %T", 
pub)
+       }
+
+       var buf bytes.Buffer
+       if err := pem.Encode(&buf, &pem.Block{Type: "PUBLIC KEY", Bytes: 
block.Bytes}); err != nil {
+               return nil, fmt.Errorf("failed to re-encode public key: %w", 
err)
+       }
+       return buf.Bytes(), nil
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.22/pkg/build/runtime_keyring_test.go 
new/apko-1.2.23/pkg/build/runtime_keyring_test.go
--- old/apko-1.2.22/pkg/build/runtime_keyring_test.go   1970-01-01 
01:00:00.000000000 +0100
+++ new/apko-1.2.23/pkg/build/runtime_keyring_test.go   2026-07-07 
16:15:27.000000000 +0200
@@ -0,0 +1,316 @@
+// Copyright 2025 Chainguard, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package build
+
+import (
+       "crypto/ecdsa"
+       "crypto/elliptic"
+       "crypto/rand"
+       "crypto/rsa"
+       "crypto/x509"
+       "encoding/json"
+       "encoding/pem"
+       "fmt"
+       "path/filepath"
+       "strings"
+       "testing"
+       "time"
+
+       apkfs "chainguard.dev/apko/pkg/apk/fs"
+       "chainguard.dev/apko/pkg/build/types"
+       "chainguard.dev/apko/pkg/options"
+
+       "github.com/google/go-cmp/cmp"
+)
+
+// rsaPublicKeyPEM returns a PEM-encoded ("PUBLIC KEY") RSA public key.
+func rsaPublicKeyPEM(t *testing.T) string {
+       t.Helper()
+       key, err := rsa.GenerateKey(rand.Reader, 2048)
+       if err != nil {
+               t.Fatalf("generating RSA key: %v", err)
+       }
+       der, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
+       if err != nil {
+               t.Fatalf("marshaling RSA public key: %v", err)
+       }
+       return string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: 
der}))
+}
+
+func ecPublicKeyPEM(t *testing.T) string {
+       t.Helper()
+       key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+       if err != nil {
+               t.Fatalf("generating EC key: %v", err)
+       }
+       der, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
+       if err != nil {
+               t.Fatalf("marshaling EC public key: %v", err)
+       }
+       return string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: 
der}))
+}
+
+func inlineKey(name, content string) types.RuntimeKeyringEntry {
+       return types.RuntimeKeyringEntry{Name: name, Content: content}
+}
+
+func TestInstallRuntimeKeyring(t *testing.T) {
+       epoch := time.Unix(1337, 0)
+       keyPEM := rsaPublicKeyPEM(t)
+
+       tests := []struct {
+               name string
+               keys []types.RuntimeKeyringEntry
+               // existing files under /etc/apk/keys, modelling keys 
InitKeyring
+               // installed before installRuntimeKeyring runs.
+               existing map[string][]byte
+               wantErr  bool
+               wantKeys []string // basenames expected under /etc/apk/keys
+       }{{
+               name: "empty is a no-op",
+               keys: nil,
+       }, {
+               name:     "writes a single key",
+               keys:     
[]types.RuntimeKeyringEntry{inlineKey("mirror.rsa.pub", keyPEM)},
+               wantKeys: []string{"mirror.rsa.pub"},
+       }, {
+               name: "writes alongside an existing distro key",
+               keys: []types.RuntimeKeyringEntry{inlineKey("mirror.rsa.pub", 
keyPEM)},
+               existing: map[string][]byte{
+                       filepath.Join(apkKeysDir, "wolfi-signing.rsa.pub"): 
[]byte("existing-distro-key"),
+               },
+               wantKeys: []string{"mirror.rsa.pub", "wolfi-signing.rsa.pub"},
+       }, {
+               name: "refuses to overwrite an existing key",
+               keys: 
[]types.RuntimeKeyringEntry{inlineKey("wolfi-signing.rsa.pub", keyPEM)},
+               existing: map[string][]byte{
+                       filepath.Join(apkKeysDir, "wolfi-signing.rsa.pub"): 
[]byte("existing-distro-key"),
+               },
+               wantErr: true,
+       }, {
+               name:    "same name, different content collides",
+               keys:    []types.RuntimeKeyringEntry{inlineKey("dup.rsa.pub", 
keyPEM), inlineKey("dup.rsa.pub", rsaPublicKeyPEM(t))},
+               wantErr: true,
+       }, {
+               name:    "rejects a non-RSA key",
+               keys:    []types.RuntimeKeyringEntry{inlineKey("ec.rsa.pub", 
ecPublicKeyPEM(t))},
+               wantErr: true,
+       }, {
+               name:    "rejects malformed PEM",
+               keys:    []types.RuntimeKeyringEntry{inlineKey("bad.rsa.pub", 
"not a pem block")},
+               wantErr: true,
+       }}
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       t.Setenv("SOURCE_DATE_EPOCH", fmt.Sprintf("%d", 
epoch.Unix()))
+                       fsys := apkfs.NewMemFS()
+                       for path, content := range tt.existing {
+                               if err := fsys.MkdirAll(filepath.Dir(path), 
0o755); err != nil {
+                                       t.Fatalf("mkdir for existing %s: %v", 
path, err)
+                               }
+                               if err := fsys.WriteFile(path, content, 0o644); 
err != nil {
+                                       t.Fatalf("write existing %s: %v", path, 
err)
+                               }
+                       }
+
+                       bc := &Context{
+                               o:  options.Options{SourceDateEpoch: epoch},
+                               ic: types.ImageConfiguration{Contents: 
types.ImageContents{RuntimeKeyring: tt.keys}},
+                               fs: fsys,
+                       }
+
+                       err := bc.installRuntimeKeyring(t.Context())
+                       if (err != nil) != tt.wantErr {
+                               t.Fatalf("installRuntimeKeyring() error = %v, 
wantErr %v", err, tt.wantErr)
+                       }
+                       if tt.wantErr {
+                               return
+                       }
+
+                       for _, name := range tt.wantKeys {
+                               path := filepath.Join(apkKeysDir, name)
+                               stat, err := fsys.Stat(path)
+                               if err != nil {
+                                       t.Fatalf("expected key %s: %v", path, 
err)
+                               }
+                               // Distro keys placed by setup keep their 
original mtime; only
+                               // installed runtime keys get the deterministic 
epoch.
+                               if _, isSetup := tt.existing[path]; !isSetup {
+                                       if mode := stat.Mode().Perm(); mode != 
0o644 {
+                                               t.Errorf("key %s mode = %v, 
want 0644", name, mode)
+                                       }
+                                       if !stat.ModTime().Equal(epoch) {
+                                               t.Errorf("key %s mtime = %v, 
want %v", name, stat.ModTime(), epoch)
+                                       }
+                               }
+                       }
+               })
+       }
+}
+
+// A symlink at the key's path must never be silently written through — that
+// would land the key outside /etc/apk/keys. The existence check uses Lstat so
+// a symlink (even dangling, which Stat reports as not-exist) counts as a
+// collision on a real filesystem. MemFS's Lstat follows links (a known quirk,
+// documented in memfs.go), so there a dangling link surfaces as a write error
+// instead — either way the install fails and nothing is written through the
+// link.
+func TestInstallRuntimeKeyringRefusesSymlink(t *testing.T) {
+       t.Setenv("SOURCE_DATE_EPOCH", "1337")
+       fsys := apkfs.NewMemFS()
+       if err := fsys.MkdirAll(apkKeysDir, 0o755); err != nil {
+               t.Fatalf("mkdir: %v", err)
+       }
+       if err := fsys.Symlink("/outside/target.rsa.pub", 
filepath.Join(apkKeysDir, "mirror.rsa.pub")); err != nil {
+               t.Fatalf("symlink: %v", err)
+       }
+
+       bc := &Context{
+               o: options.Options{SourceDateEpoch: time.Unix(1337, 0)},
+               ic: types.ImageConfiguration{Contents: types.ImageContents{
+                       RuntimeKeyring: 
[]types.RuntimeKeyringEntry{inlineKey("mirror.rsa.pub", rsaPublicKeyPEM(t))},
+               }},
+               fs: fsys,
+       }
+
+       if err := bc.installRuntimeKeyring(t.Context()); err == nil {
+               t.Fatal("installRuntimeKeyring() = nil, want an error for a 
symlinked key path")
+       }
+       if _, err := fsys.Stat("/outside/target.rsa.pub"); err == nil {
+               t.Fatal("key content was written through the symlink outside 
/etc/apk/keys")
+       }
+}
+
+// Runtime keys are content-bearing and written deterministically, so two runs
+// with identical input produce byte-identical files.
+func TestInstallRuntimeKeyringDeterministic(t *testing.T) {
+       epoch := time.Unix(1337, 0)
+       keyPEM := rsaPublicKeyPEM(t)
+       keys := []types.RuntimeKeyringEntry{inlineKey("b.rsa.pub", keyPEM), 
inlineKey("a.rsa.pub", keyPEM)}
+
+       run := func() map[string][]byte {
+               t.Setenv("SOURCE_DATE_EPOCH", fmt.Sprintf("%d", epoch.Unix()))
+               fsys := apkfs.NewMemFS()
+               bc := &Context{
+                       o:  options.Options{SourceDateEpoch: epoch},
+                       ic: types.ImageConfiguration{Contents: 
types.ImageContents{RuntimeKeyring: keys}},
+                       fs: fsys,
+               }
+               if err := bc.installRuntimeKeyring(t.Context()); err != nil {
+                       t.Fatalf("installRuntimeKeyring: %v", err)
+               }
+               out := map[string][]byte{}
+               for _, name := range []string{"a.rsa.pub", "b.rsa.pub"} {
+                       data, err := fsys.ReadFile(filepath.Join(apkKeysDir, 
name))
+                       if err != nil {
+                               t.Fatalf("read %s: %v", name, err)
+                       }
+                       out[name] = data
+               }
+               return out
+       }
+
+       if diff := cmp.Diff(run(), run()); diff != "" {
+               t.Errorf("non-deterministic output (-run1 +run2):\n%s", diff)
+       }
+}
+
+// Runtime keys live in a distinct field from the build keyring 
(contents.keyring),
+// which is the only thing InitKeyring consumes — so they are never 
build-trusted.
+func TestRuntimeKeyringDisjointFromBuildKeyring(t *testing.T) {
+       ic := types.ImageConfiguration{Contents: types.ImageContents{
+               RuntimeKeyring: 
[]types.RuntimeKeyringEntry{inlineKey("mirror.rsa.pub", rsaPublicKeyPEM(t))},
+       }}
+       if len(ic.Contents.Keyring) != 0 {
+               t.Errorf("runtime_keyring leaked into contents.keyring (the 
InitKeyring source): %v", ic.Contents.Keyring)
+       }
+}
+
+func TestParsePublicKey(t *testing.T) {
+       keyPEM := rsaPublicKeyPEM(t)
+
+       tests := []struct {
+               name    string
+               content string
+               wantErr bool
+       }{
+               {name: "valid RSA public key", content: keyPEM},
+               {name: "empty", content: "", wantErr: true},
+               {name: "not a PEM block", content: "garbage", wantErr: true},
+               {name: "EC key rejected", content: ecPublicKeyPEM(t), wantErr: 
true},
+               {name: "certificate block rejected", content: testCertPEM, 
wantErr: true},
+               {name: "multiple blocks rejected", content: keyPEM + keyPEM, 
wantErr: true},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       got, err := parsePublicKey(tt.content)
+                       if (err != nil) != tt.wantErr {
+                               t.Fatalf("parsePublicKey() error = %v, wantErr 
%v", err, tt.wantErr)
+                       }
+                       if tt.wantErr {
+                               return
+                       }
+                       if block, _ := pem.Decode(got); block == nil || 
block.Type != "PUBLIC KEY" {
+                               t.Errorf("re-encoded output is not a PUBLIC KEY 
PEM block")
+                       }
+               })
+       }
+}
+
+// Re-encoding canonicalizes the key: trailing text around the block is 
dropped.
+func TestParsePublicKeyDropsTrailingBytes(t *testing.T) {
+       keyPEM := rsaPublicKeyPEM(t)
+       got, err := parsePublicKey(keyPEM + "\ntrailing junk that is not pem\n")
+       if err != nil {
+               t.Fatalf("parsePublicKey: %v", err)
+       }
+       if strings.Contains(string(got), "trailing junk") {
+               t.Errorf("re-encoded key retained trailing bytes:\n%s", got)
+       }
+}
+
+// The DoD invariant: inline runtime keys are content-addressed. The serialized
+// configuration carries the key content (so identity is reproducible) but 
never
+// a host path, temp dir, or file:// URI.
+func TestRuntimeKeyringSerializationCarriesContentNotPath(t *testing.T) {
+       keyPEM := rsaPublicKeyPEM(t)
+       ic := types.ImageConfiguration{Contents: types.ImageContents{
+               RuntimeKeyring: 
[]types.RuntimeKeyringEntry{inlineKey("mirror.rsa.pub", keyPEM)},
+       }}
+
+       a, err := json.Marshal(ic)
+       if err != nil {
+               t.Fatalf("marshal: %v", err)
+       }
+       b, err := json.Marshal(ic)
+       if err != nil {
+               t.Fatalf("marshal: %v", err)
+       }
+       if string(a) != string(b) {
+               t.Errorf("identical configs marshaled differently:\n%s\n%s", a, 
b)
+       }
+
+       s := string(a)
+       if !strings.Contains(s, "runtime_keyring") || !strings.Contains(s, 
"BEGIN PUBLIC KEY") {
+               t.Errorf("serialized config missing runtime keyring 
content:\n%s", s)
+       }
+       for _, banned := range []string{"file://", "/tmp/", "/etc/apk/keys", 
string(filepath.Separator) + "var" + string(filepath.Separator)} {
+               if strings.Contains(s, banned) {
+                       t.Errorf("serialized config leaked a path substring 
%q:\n%s", banned, s)
+               }
+       }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.22/pkg/build/types/image_configuration.go 
new/apko-1.2.23/pkg/build/types/image_configuration.go
--- old/apko-1.2.22/pkg/build/types/image_configuration.go      2026-07-03 
18:11:53.000000000 +0200
+++ new/apko-1.2.23/pkg/build/types/image_configuration.go      2026-07-07 
16:15:27.000000000 +0200
@@ -40,7 +40,7 @@
 // directory traversal.
 var certNameRegex = regexp.MustCompile(`^[a-zA-Z0-9_-]+[a-zA-Z0-9_.-]*$`)
 
-// Attempt to probe an upstream VCS URL if known.
+// ProbeVCSUrl attempts to probe an upstream VCS URL if known.
 func (ic *ImageConfiguration) ProbeVCSUrl(ctx context.Context, imageConfigPath 
string) {
        log := clog.FromContext(ctx)
 
@@ -174,6 +174,9 @@
 
 func (i *ImageContents) MergeInto(target *ImageContents) error {
        target.Keyring = slices.Concat(i.Keyring, target.Keyring)
+       // Load-bearing: the locked config is produced via input.MergeInto, so a
+       // field omitted here is silently dropped from the dedup key and 
/etc/apko.json.
+       target.RuntimeKeyring = slices.Concat(i.RuntimeKeyring, 
target.RuntimeKeyring)
        target.BuildRepositories = slices.Concat(i.BuildRepositories, 
target.BuildRepositories)
        target.RuntimeOnlyRepositories = 
slices.Concat(i.RuntimeOnlyRepositories, target.RuntimeOnlyRepositories)
        target.Repositories = slices.Concat(i.Repositories, target.Repositories)
@@ -244,6 +247,22 @@
                        }
                }
        }
+       seen := make(map[string]struct{}, len(ic.Contents.RuntimeKeyring))
+       for _, key := range ic.Contents.RuntimeKeyring {
+               if key.Name == "" {
+                       return fmt.Errorf("configured runtime_keyring entry has 
no name")
+               }
+               if key.Content == "" {
+                       return fmt.Errorf("runtime_keyring entry %q has no 
content", key.Name)
+               }
+               if !certNameRegex.MatchString(key.Name) {
+                       return fmt.Errorf("runtime_keyring entry %q has an 
invalid name, it must match %s", key.Name, certNameRegex.String())
+               }
+               if _, dup := seen[key.Name]; dup {
+                       return fmt.Errorf("runtime_keyring entry %q is a 
duplicate", key.Name)
+               }
+               seen[key.Name] = struct{}{}
+       }
        return nil
 }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/apko-1.2.22/pkg/build/types/image_configuration_test.go 
new/apko-1.2.23/pkg/build/types/image_configuration_test.go
--- old/apko-1.2.22/pkg/build/types/image_configuration_test.go 2026-07-03 
18:11:53.000000000 +0200
+++ new/apko-1.2.23/pkg/build/types/image_configuration_test.go 2026-07-07 
16:15:27.000000000 +0200
@@ -312,6 +312,56 @@
                                }},
                        },
                },
+       }, {
+               name: "runtime_keyring name with periods is valid",
+               configuration: types.ImageConfiguration{
+                       Contents: types.ImageContents{
+                               RuntimeKeyring: 
[]types.RuntimeKeyringEntry{{Name: "mirror.rsa.pub", Content: "test"}},
+                       },
+               },
+       }, {
+               name: "runtime_keyring name starting with period",
+               configuration: types.ImageConfiguration{
+                       Contents: types.ImageContents{
+                               RuntimeKeyring: 
[]types.RuntimeKeyringEntry{{Name: ".mirror.rsa.pub", Content: "test"}},
+                       },
+               },
+               expectError: `runtime_keyring entry ".mirror.rsa.pub" has an 
invalid name, it must match ^[a-zA-Z0-9_-]+[a-zA-Z0-9_.-]*$`,
+       }, {
+               name: "runtime_keyring name with path separator",
+               configuration: types.ImageConfiguration{
+                       Contents: types.ImageContents{
+                               RuntimeKeyring: 
[]types.RuntimeKeyringEntry{{Name: "subdir/mirror.rsa.pub", Content: "test"}},
+                       },
+               },
+               expectError: `runtime_keyring entry "subdir/mirror.rsa.pub" has 
an invalid name, it must match ^[a-zA-Z0-9_-]+[a-zA-Z0-9_.-]*$`,
+       }, {
+               name: "duplicate runtime_keyring name",
+               configuration: types.ImageConfiguration{
+                       Contents: types.ImageContents{
+                               RuntimeKeyring: []types.RuntimeKeyringEntry{
+                                       {Name: "mirror.rsa.pub", Content: 
"test"},
+                                       {Name: "mirror.rsa.pub", Content: 
"test"},
+                               },
+                       },
+               },
+               expectError: `runtime_keyring entry "mirror.rsa.pub" is a 
duplicate`,
+       }, {
+               name: "runtime_keyring entry without name",
+               configuration: types.ImageConfiguration{
+                       Contents: types.ImageContents{
+                               RuntimeKeyring: 
[]types.RuntimeKeyringEntry{{Content: "test"}},
+                       },
+               },
+               expectError: `configured runtime_keyring entry has no name`,
+       }, {
+               name: "runtime_keyring entry without content",
+               configuration: types.ImageConfiguration{
+                       Contents: types.ImageContents{
+                               RuntimeKeyring: 
[]types.RuntimeKeyringEntry{{Name: "mirror.rsa.pub"}},
+                       },
+               },
+               expectError: `runtime_keyring entry "mirror.rsa.pub" has no 
content`,
        }}
 
        for _, tt := range tests {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/apko-1.2.22/pkg/build/types/runtime_keyring_entry_test.go 
new/apko-1.2.23/pkg/build/types/runtime_keyring_entry_test.go
--- old/apko-1.2.22/pkg/build/types/runtime_keyring_entry_test.go       
1970-01-01 01:00:00.000000000 +0100
+++ new/apko-1.2.23/pkg/build/types/runtime_keyring_entry_test.go       
2026-07-07 16:15:27.000000000 +0200
@@ -0,0 +1,102 @@
+// Copyright 2025 Chainguard, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package types_test
+
+import (
+       "encoding/json"
+       "strings"
+       "testing"
+
+       "github.com/google/go-cmp/cmp"
+       "gopkg.in/yaml.v3"
+
+       "chainguard.dev/apko/pkg/build/types"
+)
+
+// TestRuntimeKeyringEntryStrictDecode mirrors the configuration loader's
+// strict decoding (KnownFields, image_configuration.go): unknown keys inside
+// a runtime_keyring entry must be rejected, and well-formed entries decode.
+func TestRuntimeKeyringEntryStrictDecode(t *testing.T) {
+       type holder struct {
+               RuntimeKeyring []types.RuntimeKeyringEntry 
`yaml:"runtime_keyring"`
+       }
+
+       tests := []struct {
+               name    string
+               yaml    string
+               want    []types.RuntimeKeyringEntry
+               wantErr bool
+       }{{
+               name: "name and content decode",
+               yaml: "runtime_keyring:\n  - name: mirror.rsa.pub\n    content: 
PEM",
+               want: []types.RuntimeKeyringEntry{{Name: "mirror.rsa.pub", 
Content: "PEM"}},
+       }, {
+               name:    "unknown key rejected by strict decoding",
+               yaml:    "runtime_keyring:\n  - name: mirror.rsa.pub\n    
content: PEM\n    uri: https://example.com/k.rsa.pub";,
+               wantErr: true,
+       }, {
+               name:    "scalar entry rejected (no URI form)",
+               yaml:    "runtime_keyring:\n  - https://example.com/k.rsa.pub";,
+               wantErr: true,
+       }}
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       dec := yaml.NewDecoder(strings.NewReader(tt.yaml))
+                       dec.KnownFields(true)
+                       var h holder
+                       err := dec.Decode(&h)
+                       if (err != nil) != tt.wantErr {
+                               t.Fatalf("Decode error = %v, wantErr %v", err, 
tt.wantErr)
+                       }
+                       if tt.wantErr {
+                               return
+                       }
+                       if diff := cmp.Diff(tt.want, h.RuntimeKeyring); diff != 
"" {
+                               t.Errorf("(-want +got):\n%s", diff)
+                       }
+               })
+       }
+}
+
+// The JSON wire shape feeds /etc/apko.json and the locked-config dedup key:
+// entries must serialize as {name, content} objects carrying the key bytes.
+func TestRuntimeKeyringEntryMarshalJSON(t *testing.T) {
+       b, err := json.Marshal(types.ImageContents{
+               RuntimeKeyring: []types.RuntimeKeyringEntry{{Name: "m.rsa.pub", 
Content: "PEM"}},
+       })
+       if err != nil {
+               t.Fatalf("marshal: %v", err)
+       }
+       want := `{"runtime_keyring":[{"name":"m.rsa.pub","content":"PEM"}]}`
+       if got := string(b); got != want {
+               t.Errorf("MarshalJSON\n got: %s\nwant: %s", got, want)
+       }
+}
+
+// MergeInto must concat RuntimeKeyring — it is the path that builds the locked
+// config (via input.MergeInto) and merges includes, so omission silently drops
+// the field from the dedup key and from include-derived builds.
+func TestImageContentsMergeIntoRuntimeKeyring(t *testing.T) {
+       source := types.ImageContents{RuntimeKeyring: 
[]types.RuntimeKeyringEntry{{Name: "a.rsa.pub", Content: "A"}}}
+       target := types.ImageContents{RuntimeKeyring: 
[]types.RuntimeKeyringEntry{{Name: "b.rsa.pub", Content: "B"}}}
+       if err := source.MergeInto(&target); err != nil {
+               t.Fatalf("MergeInto: %v", err)
+       }
+       want := []types.RuntimeKeyringEntry{{Name: "a.rsa.pub", Content: "A"}, 
{Name: "b.rsa.pub", Content: "B"}}
+       if diff := cmp.Diff(want, target.RuntimeKeyring); diff != "" {
+               t.Errorf("RuntimeKeyring not concatenated (-want +got):\n%s", 
diff)
+       }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.22/pkg/build/types/schema.json 
new/apko-1.2.23/pkg/build/types/schema.json
--- old/apko-1.2.22/pkg/build/types/schema.json 2026-07-03 18:11:53.000000000 
+0200
+++ new/apko-1.2.23/pkg/build/types/schema.json 2026-07-07 16:15:27.000000000 
+0200
@@ -207,6 +207,13 @@
           "type": "array",
           "description": "A list of public keys used to verify the desired 
repositories"
         },
+        "runtime_keyring": {
+          "items": {
+            "$ref": "#/$defs/RuntimeKeyringEntry"
+          },
+          "type": "array",
+          "description": "APK signing public keys installed into /etc/apk/keys 
after package\nresolution, so runtime `apk add` against runtime_repositories 
can verify\nre-signed packages. A runtime trust anchor only — not consulted 
during\nbuild-time package resolution. Each entry is an inline {name, 
content}\npublic key."
+        },
         "packages": {
           "items": {
             "type": "string"
@@ -292,6 +299,21 @@
       "additionalProperties": false,
       "type": "object"
     },
+    "RuntimeKeyringEntry": {
+      "properties": {
+        "name": {
+          "type": "string",
+          "description": "Required: the filename the key is written to under 
/etc/apk/keys. Must\nmatch the filename the repository's APKINDEX signature 
references\n(.SIGN.RSA256.\u003cname\u003e), or apk will not find the key at 
runtime."
+        },
+        "content": {
+          "type": "string",
+          "description": "Required: the PEM-encoded RSA public key content."
+        }
+      },
+      "additionalProperties": false,
+      "type": "object",
+      "description": "RuntimeKeyringEntry is a single inline APK signing 
public key, mirroring AdditionalCertificateEntry."
+    },
     "User": {
       "properties": {
         "username": {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.22/pkg/build/types/types.go 
new/apko-1.2.23/pkg/build/types/types.go
--- old/apko-1.2.22/pkg/build/types/types.go    2026-07-03 18:11:53.000000000 
+0200
+++ new/apko-1.2.23/pkg/build/types/types.go    2026-07-07 16:15:27.000000000 
+0200
@@ -126,6 +126,12 @@
        Repositories []string `json:"repositories,omitempty" 
yaml:"repositories,omitempty"`
        // A list of public keys used to verify the desired repositories
        Keyring []string `json:"keyring,omitempty" yaml:"keyring,omitempty"`
+       // APK signing public keys installed into /etc/apk/keys after package
+       // resolution, so runtime `apk add` against runtime_repositories can 
verify
+       // re-signed packages. A runtime trust anchor only — not consulted 
during
+       // build-time package resolution. Each entry is an inline {name, 
content}
+       // public key.
+       RuntimeKeyring []RuntimeKeyringEntry `json:"runtime_keyring,omitempty" 
yaml:"runtime_keyring,omitempty"`
        // A list of packages to include in the image
        Packages []string `json:"packages,omitempty" yaml:"packages,omitempty"`
        // Optional: Base image to build on top of. Warning: Experimental.
@@ -467,3 +473,16 @@
        // containing CA certificate files to be assembled into the system CA 
bundle.
        Providers []string `json:"providers,omitempty" 
yaml:"providers,omitempty"`
 }
+
+// RuntimeKeyringEntry is a single inline APK signing public key, mirroring
+// AdditionalCertificateEntry. Keys are content-bearing by design: the key 
bytes
+// live in the configuration itself (no URIs to fetch), so builds stay
+// reproducible and the locked configuration carries the full trust anchor.
+type RuntimeKeyringEntry struct {
+       // Required: the filename the key is written to under /etc/apk/keys. 
Must
+       // match the filename the repository's APKINDEX signature references
+       // (.SIGN.RSA256.<name>), or apk will not find the key at runtime.
+       Name string `json:"name,omitempty" yaml:"name,omitempty"`
+       // Required: the PEM-encoded RSA public key content.
+       Content string `json:"content,omitempty" yaml:"content,omitempty"`
+}

++++++ apko.obsinfo ++++++
--- /var/tmp/diff_new_pack.UeLLrt/_old  2026-07-08 17:41:52.368626171 +0200
+++ /var/tmp/diff_new_pack.UeLLrt/_new  2026-07-08 17:41:52.388626868 +0200
@@ -1,5 +1,5 @@
 name: apko
-version: 1.2.22
-mtime: 1783095113
-commit: f9189c2afb477dd0f7ecab00f3751f0888553c4b
+version: 1.2.23
+mtime: 1783433727
+commit: 9d4b52d913063421165620da75b7926bbe211321
 

++++++ vendor.tar.gz ++++++
/work/SRC/openSUSE:Factory/apko/vendor.tar.gz 
/work/SRC/openSUSE:Factory/.apko.new.1982/vendor.tar.gz differ: char 15, line 1

Reply via email to