Skip to content

Verify kernel archive integrity - #1703

Merged
katiewasnothere merged 7 commits into
apple:mainfrom
haoruilee:feat/kernelintegrity
Jul 13, 2026
Merged

Verify kernel archive integrity#1703
katiewasnothere merged 7 commits into
apple:mainfrom
haoruilee:feat/kernelintegrity

Conversation

@haoruilee

@haoruileehaoruilee commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Motivation and Context

Closes#1687

The default kernel archive is downloaded from a remote release URL during first-run setup and via container system kernel set --recommended. Previously, the archive contents were not verified after download, so integrity depended on HTTPS and the release artifact remaining unchanged.

This change adds digest verification for kernel archives. The recommended/default kernel now has pinned digest metadata using an algorithm-prefixed value such as sha256:<hex>. container system kernel set --tar accepts --digest; remote tar URLs require it, and local tar archives can also be verified before unpacking and installation.

The system config also supports kernel.digest, and a custom kernel.url must provide a digest for that archive.

Testing

  • Tested locally
  • Added/updated tests
  • Added/updated docs

Validated locally:

  • git diff --check upstream/main...HEAD
  • swift test --filter KernelServiceTests
  • swift test --filter ConfigurationLoaderTests

@kasc0206kasc0206 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查意见 / Review

This is a well-crafted security improvement. The SHA-256 digest verification for kernel downloads is an important addition.

优点 / Strengths

  1. End-to-end implementation: From CLI flag → config → XPC message → server-side verification, the full pipeline is covered
  2. Backward compatible: sha256 is String? with default nil, so existing configs work unchanged
  3. Smart defaults: Auto-populates the default SHA-256 when using the default kernel URL, while allowing custom values for custom URLs
  4. No breaking API changes: All new parameters have sensible defaults
  5. Proper error messaging: The --sha256 can only be used with --tar

Suggestion

  • Consider adding a --sha256-verify flag with --sha256-verify=false to allow users to explicitly skip verification, rather than relying on nil meaning "no verification". But this is optional — current behavior is reasonable.

已验证 / Verified

  • Code compiles with swift build
  • Follows existing project patterns (XPC keys, Config struct, ProgressBar)

Great contribution!

@kasc0206

Copy link
Copy Markdown

Code Review Summary / 代码审查摘要

Strengths / 优点

  1. End-to-end implementation: From CLI flag (--sha256) → Config (sha256 in TOML) → XPC message → server-side verification, the full pipeline is covered
    端到端实现:从 CLI 参数到配置到 XPC 消息到服务端校验,完整链路覆盖
  2. Backward compatible: sha256 is String? with default nil — existing configs work unchanged
    向后兼容:sha256 为可选的 String? 类型,现有配置无需修改
  3. Smart defaults: Auto-populates the default SHA-256 when using the default kernel URL, while allowing custom values for custom URLs
    智能默认值:使用默认内核 URL 时自动填充默认 SHA-256,自定义 URL 可自行指定
  4. No breaking API changes to callers: All new parameters have sensible defaults
    无破坏性 API 变更:所有新参数都有合理的默认值
  5. CLI validation: --sha256 correctly rejected when used without --tar
    CLI 参数校验--sha256 不带 --tar 时正确报错

Suggestion / 建议

Consider adding a --sha256-verify / --sha256-verify=false flag to let users explicitly skip verification. Currently, nil means "no verification", which is reasonable but implicit.
考虑增加 --sha256-verify 标志,让用户能显式关闭校验。当前 nil 表示"不校验"是合理的但较隐晦。

Verification / 验证

  • Code compiles with swift build
  • Follows existing project patterns (XPC keys config, ProgressBar, Config struct)

Great contribution! / 优秀的贡献!

@katiewasnothere

Copy link
Copy Markdown
Contributor

@haoruilee Thank you for the contribution! Could you add commit signatures to your commits? See https://github.com/apple/containerization/blob/main/CONTRIBUTING.md#pull-requests

We definitely want this change, but I think we should change the flags, config fields, and XPC key names to exclude the name of the algorithm so we can support other hashing algorithms in the future. Maybe we should call this just checksum instead? What do you think?

@briansmith

Copy link
Copy Markdown

Perhaps then the values should be prefixed with the digest algorithm.

Perhaps it is worth just copying Subresource Integrity syntax, using "integrity" instead of "checksum" and prefixing the digest algorithm name with a dash in the values, e.g. integrity: "sha256-f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91" instead of sha256 = "f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91" that the PR currently implements.

(Note that using a dash instead of colon prevents any ambiguity with content-addressible-by-digest URI schemes like sha256: that.)

@haoruilee

Copy link
Copy Markdown
ContributorAuthor

Hi @katiewasnothere@briansmith ,

Thanks, that makes sense. I read these suggestions as complementary, the external names should avoid hard coding SHA-256, while the value itself should still carry the digest algorithm.

I’ll update the PR to archive this and add commit signatures and revise the tests and docs accordingly.

@haoruilee
haoruileeforce-pushed the feat/kernelintegrity branch from d24095e to 76e26b9CompareJune 30, 2026 08:14
@haoruileehaoruilee changed the title Verify kernel archive SHA-256 digestsVerify kernel archive integrityJun 30, 2026
@haoruilee
haoruileeforce-pushed the feat/kernelintegrity branch from 76e26b9 to ea5ccebCompareJune 30, 2026 08:17
@haoruilee

Copy link
Copy Markdown
ContributorAuthor

Hi @katiewasnothere

Updated, thanks for your suggestion. I renamed the API surface from sha256 to integrity, with values using sha256-<hex> syntax, and updated tests/docs/PR description accordingly.

Could you please take another look when you have a chance?

@katiewasnothere

Copy link
Copy Markdown
Contributor

@haoruilee Thanks for the update! @briansmith this is a good suggestion, but naming the flag integrity and using a algorithm-hex format is inconsistent with other similar cases in this repo. Thinking about it some more, I think we should name the flag digest and use the format algorithm:hex.

@haoruilee

Copy link
Copy Markdown
ContributorAuthor

Hi @katiewasnothere ,

Thanks, I’ve updated the PR to rename the flag field to digest and use the algorithm:hex format. Docs and tests are updated as well.

Comment threaddocs/container-system-config.md Outdated
|--------------|-----------|--------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| `binaryPath` | `String` | `"opt/kata/share/kata-containers/vmlinux-6.18.15-186"` | Path **inside** the downloaded kernel archive that points to the kernel binary. |
| `url` | `URL` | `"https://github.com/kata-containers/kata-containers/releases/download/3.28.0/kata-static-3.28.0-arm64.tar.zst"` | Archive to download when no kernel is installed. Encoded and decoded as a plain string in TOML. |
| `digest` | `String?` | `"sha256:f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91"` | Expected digest for the archive, for example `sha256:<hex>`. When unset for a custom URL, remote kernel downloads are not verified. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should require that a digest is provided

@github-actions

github-actionsBot commented Jul 1, 2026

Copy link
Copy Markdown

Code Coverage

TierLine Coverage
Unit23.42%
Integration66.54%
Combined75.48%

}

@Test func customKernelURLWithoutDigestLeavesDigestUnset() async throws {
@Test func customKernelURLWithoutDigestThrows() async throws {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about a test where the digest doesn't match the file contents?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about tests where:

  1. The digest algorithm is "sha1" (should fail).
  2. The digest algorithm is "sha256" and the digest is correct but truncated by one byte (should fail).
  3. The digest algorithm is "sha256" but the digest value is a correct SHA-1 (should fail).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻 The archive content mismatch case is covered by installKernelFromLocalTarRejectsDigestMismatchWithoutInstalling, and I added coverage for sha1, truncated sha256, and a valid SHA-1 value passed as sha256.

binaryPath: String = defaultBinaryPath,
url: URL = defaultURL
) {
public init(binaryPath: String = defaultBinaryPath) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we want this init?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, the extra overload isn’t needed. I collapsed this into a single initializer with defaults for binaryPath, url, and digest.

self.digest = Self.defaultDigest
}

public init(binaryPath: String = defaultBinaryPath, url: URL, digest: String) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

url and digest should have their default values set here

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 💯

Comment on lines +211 to +215
let digestResult = try provider.value(forKey: AbsoluteConfigKey(ConfigKey("kernel.digest")), type: .string)
guard digestResult.value != nil else {
throw ContainerizationError(
.invalidArgument,
message: "kernel.digest is required in '\(path)' when kernel.url configures a custom archive"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could check for this in the decoder instead of having a custom validation function. What do you think?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the decoder check is still useful, but not sufficient for the layered config case.

The rule I’m trying to enforce is: if a config layer overrides kernel.url with a non-default archive, that same layer must also provide the digest for that archive. The digest is tied to the archive contents, so it should not be inherited from another layer.

The decoder only sees the merged snapshot, so it can tell whether the final config has both kernel.url and kernel.digest, but it can’t tell which file each value came from. That means it would accept a custom URL from a higher precedence file paired with the default digest from a lower precedence file, which is the case this validation is meant to reject.

I kept this as a premerge loader validation and added a comment to make that layering requirement clearer.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rule I’m trying to enforce is: if a config layer overrides kernel.url with a non-default archive, that same layer must also provide the digest for that archive.

In my opinion it doesn't matter if we get the kernel url and kernel digest from different layers in a layered config case. If the digest does not match the kernel url's contents when we go to fetch the kernel, the user will get an error. I could see having the values in separate layers being useful. Is there a specific scenario you want to prevent with this?

let fileIOThreadPool = NIOThreadPool(numberOfThreads: 1)
fileIOThreadPool.start()

let delegate = try HashingFileDownloadDelegate(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A follow up to this PR or future work could be to look into if we can do something similar to what is done for fetching image blobs here

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, that seems worth exploring as follow up work. I’d keep it separate from this PR since this one is focused on kernel archive integrity.

}
}

static func verifyDigest(of file: URL, expected: String) throws {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this function when we have the one below on line 195?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I removed the extra string overload and updated the tests.


// Mirrors AsyncHTTPClient's file download delegate while updating an optional
// SHA-256 hasher from the same response chunks that are written to disk.
private final class HashingFileDownloadDelegate: @unchecked Sendable, HTTPClientResponseDelegate {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After talking with others, I think for now we should just remove this in favor of downloading the whole tar in the kernel service and then calling verifyDigest(of file: URL, expected: ExpectedDigest) to incrementally compute the hash like we do if there's a local tar. This will make this PR more scoped and we can optimize further later by looking into the suggestion here. After removing this part, I'm ready to approve.

@katiewasnotherekatiewasnothere left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thank you!

@katiewasnothere
katiewasnothere merged commit 57b07fa into apple:mainJul 13, 2026
3 checks passed
saehejkang pushed a commit to saehejkang/container that referenced this pull request Jul 16, 2026
Closesapple#1687
The default kernel archive is downloaded from a remote release URL
during first-run setup and via `container system kernel set
--recommended`. Previously, the archive contents were not verified after
download, so integrity depended on HTTPS and the release artifact
remaining unchanged.
This change adds digest verification for kernel archives. The
recommended/default kernel now has pinned digest metadata using an
algorithm-prefixed value such as `sha256:<hex>`. `container system
kernel set --tar` accepts `--digest`; remote tar URLs require it, and
local tar archives can also be verified before unpacking and
installation.
The system config also supports `kernel.digest`, and a custom
`kernel.url` must provide a digest for that archive.
andrewkomkov added a commit to getgantry/gantry that referenced this pull request Aug 1, 2026
…ot args (#14)
apple/container **1.2.0** is out (previously tracked: `1.1.0`).
Upstream notes: https://github.com/apple/container/releases/tag/1.2.0 —
mirrored in `docs/upstream/apple-container-1.2.0.md`.
## Review checklist
- [ ] New or changed CLI flags Gantry should surface (`container
run/create/machine/build`)
- [ ] Changed `--format json` shapes the DockerKit apple transport
decodes
- [ ] Fixed upstream bugs Gantry currently works around
- [ ] `ContainerTooling.recommendedVersion` / feature gates need moving
to `1.2.0`
- [ ] MCP tools and App Intents that expose the affected commands
- [ ] README and CHANGELOG entries for whatever is adopted
Merging records the version as reviewed. Implement the adopted parts on
this branch, or merge as-is and open follow-ups.
---
<details><summary>Upstream release notes</summary>
## What's Changed
* Add TestCLISystemLogs and TestCLITermIO integration tests in new
integration test suite by @katiewasnothere in
apple/container#1879
* Restore reverted migrations, migrate last tests. by @jglogan in
apple/container#1880
* Removes obsolete CLITests directory. by @jglogan in
apple/container#1886
* Integration coverage xpc helpers by @noah-thor in
apple/container#1551
* Upgrade grpc-swift-nio-transport to 2.9.0 and remove HTTP2ConnectBuff…
by @adityabagchi24 in apple/container#1790
* Updates containerization to 0.36.0. by @jglogan in
apple/container#1912
* Use containerization version 0.37.0 by @adityaramani in
apple/container#1932
* Verify kernel archive integrity by @haoruilee in
apple/container#1703
* Add commit/issue alert to PR template. by @jglogan in
apple/container#1945
* Remove `--skip-build` from test Makefile target. by @jglogan in
apple/container#1951
* Restore `--skip-build`, enable `import testable` for release builds.
by @jglogan in apple/container#1955
* [package]: bump container-builder-shim to 0.13.0 by @saehejkang in
apple/container#1953
* Validate container ID from XPC requests by @katiewasnothere in
apple/container#1956
* Remove force unwraps on XPC error set/get by @katiewasnothere in
apple/container#1958
* Do not follow destination symlink when copying user configuration by
@katiewasnothere in apple/container#1957
* Fix machine ID length test. by @jglogan in
apple/container#1971
* Address flaky TestCLIKernelSetSerial suite. by @jglogan in
apple/container#1976
* [gitignore]: ignore vscode workspace files by @saehejkang in
apple/container#1966
* Update containerization dependency with new EXT4Unpacker func
definition by @katiewasnothere in
apple/container#1973
* Periodic dependency updates. by @jglogan in
apple/container#1981
* Use ordered journal mode for unpacked images. by @jglogan in
apple/container#1974
* Reword DNS container name resolution doc information by
@katiewasnothere in apple/container#1960
* ci: bump the github-actions group across 1 directory with 3 updates by
@dependabot[bot] in apple/container#1983
* Pass build config in when building protoc dependencies by
@katiewasnothere in apple/container#1972
* Container test fixture package by @katiewasnothere in
apple/container#1887
* Downgrade swift-collections to 1.5.1. by @jglogan in
apple/container#1984
* Use `enum` for warmup images. by @jglogan in
apple/container#1990
* Add missing dependencies to new ContainerTestSupport package by
@katiewasnothere in apple/container#1994
* Add OCI maskedPaths and readonlyPaths support to Container API. by
@jglogan in apple/container#1996
* Integration test - miscellaneous fixture and test refinements. by
@jglogan in apple/container#1993
* Use log instead of print for system start status messages by
@adityabagchi24 in apple/container#1889
* Fix BuilderStart race, parallelize `container build` tests. by
@jglogan in apple/container#2002
* Allow custom kernel boot args via --kernel-arg by @arirubinstein in
apple/container#1744
* fix: Increase XPC timeout for Machine API operations by @dev-kvt in
apple/container#2006
* Update containerization import to latest 0.40.0 by @katiewasnothere in
apple/container#2028
* Fix image env vars, build context checks, TCP/UDP port forward buffer,
and validate plugin name by @katiewasnothere in
apple/container#2027
* Update containerization import to 0.40.1 by @katiewasnothere in
apple/container#2038
## New Contributors
* @haoruilee made their first contribution in
apple/container#1703
* @arirubinstein made their first contribution in
apple/container#1744
* @dev-kvt made their first contribution in
apple/container#2006
**Full Changelog**:
apple/container@1.1.0...1.2.0
</details>
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Andrew <Andrew.Komkov@gmail.com>
henrywang added a commit to henrywang/Berthly that referenced this pull request Aug 4, 2026
## Summary
- container 1.2.0 added digest verification for kernel archives
(apple/container#1703): `kernel set --tar` accepts `--digest`, remote
tar URLs require one, and the recommended default kernel now ships
pinned sha256 digest metadata. containerization's `KernelConfig` gained
a required `digest` field, and `ClientKernel.installKernelFromTar` an
`expectedDigest` parameter — both reachable from Berthly's native XPC
calls.
- `KernelSetOptions` gained `digest: String?`, threaded into
`setKernel(options:)`'s `installKernelFromTar` call.
- `installDefaultKernelIfNeeded` now passes `expectedDigest:
containerSystemConfig.kernel.digest` — the default kernel already
carries a pinned digest from upstream.
- New "Digest" field on the Set Kernel sheet's Tar Archive section,
required only for remote URLs (matching upstream's exact rule),
prefilled by "Use Recommended" alongside the URL.
- `PARITY.md`'s System `kernel` row updated in the same commit.
## Why
Part of the container 1.2.0 milestone (#79). Mirrors the security
posture Berthly already applies elsewhere (pkg signature verification
against a pinned Apple identity) — kernel archives were the one download
path left unverified.
Closes#79
## Test plan
- [x] `xcodebuild build` succeeds
- [x] `xcodebuild test -only-testing:BerthlyTests` — full suite passes,
including new `kernelDigest` coverage in `SystemConfigMappingTests`
- [x] `swiftlint lint --strict` — 0 violations
- [x] Verified visually via Xcode's live preview renderer — confirmed
the Tar Archive section (including the recommended-URL prefill) renders
correctly
henrywang added a commit to henrywang/Berthly that referenced this pull request Aug 4, 2026
## Summary
- `resolvedSystemConfig()` used `try?` around
`ConfigurationLoader.load()`, so any decode failure silently fell back
to an all-defaults `ContainerSystemConfig` — not just the field that
failed, the user's entire `config.toml` (DNS, build settings, kernel,
everything).
- `ConfigurationLoader.load()` only throws when a config file exists on
disk but fails to parse/decode; it already returns cleanly with defaults
when no file is present at all — so every error caught here represents
real discarded user data, not an absent-file no-op.
- container 1.2.0 made this concrete: `KernelConfig`'s decoder now
throws when `kernel.url` is customized without a paired `kernel.digest`
(apple/container#1703). Any Berthly user who ran `container system
kernel set --tar <url>` pre-1.2.0 has exactly that config shape, and
would silently lose their whole system config on next load once the
daemon is upgraded.
- Extracts the load-outcome handling into a pure, testable
`mapSystemConfigLoadResult(_:)` and surfaces a failure through the
existing `lastStartupWarning` mechanism instead of swallowing it.
## Why
Found while implementing #79 (kernel digest verification) — this
milestone's own version bump is what triggers the failure for affected
users, so it needs fixing in the same milestone.
Closes#87
## Test plan
- [x] `xcodebuild build` succeeds
- [x] `xcodebuild test -only-testing:BerthlyTests` — full suite passes,
including new `SystemConfigLoadResultMappingTests` covering both the
success and failure paths
- [x] `swiftlint lint --strict` — 0 violations
- [x] No View files touched — no UI test needed for this change (tracked
separately in #89: the sidebar's warning-state rendering itself has no
UI coverage yet, pre-existing gap this PR surfaces a second producer
into)
jianliang00 pushed a commit to jianliang00/container that referenced this pull request Aug 28, 2026
Closesapple#1687
The default kernel archive is downloaded from a remote release URL
during first-run setup and via `container system kernel set
--recommended`. Previously, the archive contents were not verified after
download, so integrity depended on HTTPS and the release artifact
remaining unchanged.
This change adds digest verification for kernel archives. The
recommended/default kernel now has pinned digest metadata using an
algorithm-prefixed value such as `sha256:<hex>`. `container system
kernel set --tar` accepts `--digest`; remote tar URLs require it, and
local tar archives can also be verified before unpacking and
installation.
The system config also supports `kernel.digest`, and a custom
`kernel.url` must provide a digest for that archive.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Request]: Check kernel download integrity when downloading

5 participants

@haoruilee@kasc0206@katiewasnothere@briansmith@jglogan
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Verify kernel archive integrity by haoruilee · Pull Request #1703 · apple/container · GitHub
Skip to content

Verify kernel archive integrity - #1703

Merged
katiewasnothere merged 7 commits into
apple:mainfrom
haoruilee:feat/kernelintegrity
Jul 13, 2026
Merged

Verify kernel archive integrity#1703
katiewasnothere merged 7 commits into
apple:mainfrom
haoruilee:feat/kernelintegrity

Conversation

@haoruilee

@haoruileehaoruilee commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Motivation and Context

Closes#1687

The default kernel archive is downloaded from a remote release URL during first-run setup and via container system kernel set --recommended. Previously, the archive contents were not verified after download, so integrity depended on HTTPS and the release artifact remaining unchanged.

This change adds digest verification for kernel archives. The recommended/default kernel now has pinned digest metadata using an algorithm-prefixed value such as sha256:<hex>. container system kernel set --tar accepts --digest; remote tar URLs require it, and local tar archives can also be verified before unpacking and installation.

The system config also supports kernel.digest, and a custom kernel.url must provide a digest for that archive.

Testing

  • Tested locally
  • Added/updated tests
  • Added/updated docs

Validated locally:

  • git diff --check upstream/main...HEAD
  • swift test --filter KernelServiceTests
  • swift test --filter ConfigurationLoaderTests

@kasc0206kasc0206 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查意见 / Review

This is a well-crafted security improvement. The SHA-256 digest verification for kernel downloads is an important addition.

优点 / Strengths

  1. End-to-end implementation: From CLI flag → config → XPC message → server-side verification, the full pipeline is covered
  2. Backward compatible: sha256 is String? with default nil, so existing configs work unchanged
  3. Smart defaults: Auto-populates the default SHA-256 when using the default kernel URL, while allowing custom values for custom URLs
  4. No breaking API changes: All new parameters have sensible defaults
  5. Proper error messaging: The --sha256 can only be used with --tar

Suggestion

  • Consider adding a --sha256-verify flag with --sha256-verify=false to allow users to explicitly skip verification, rather than relying on nil meaning "no verification". But this is optional — current behavior is reasonable.

已验证 / Verified

  • Code compiles with swift build
  • Follows existing project patterns (XPC keys, Config struct, ProgressBar)

Great contribution!

@kasc0206

Copy link
Copy Markdown

Code Review Summary / 代码审查摘要

Strengths / 优点

  1. End-to-end implementation: From CLI flag (--sha256) → Config (sha256 in TOML) → XPC message → server-side verification, the full pipeline is covered
    端到端实现:从 CLI 参数到配置到 XPC 消息到服务端校验,完整链路覆盖
  2. Backward compatible: sha256 is String? with default nil — existing configs work unchanged
    向后兼容:sha256 为可选的 String? 类型,现有配置无需修改
  3. Smart defaults: Auto-populates the default SHA-256 when using the default kernel URL, while allowing custom values for custom URLs
    智能默认值:使用默认内核 URL 时自动填充默认 SHA-256,自定义 URL 可自行指定
  4. No breaking API changes to callers: All new parameters have sensible defaults
    无破坏性 API 变更:所有新参数都有合理的默认值
  5. CLI validation: --sha256 correctly rejected when used without --tar
    CLI 参数校验--sha256 不带 --tar 时正确报错

Suggestion / 建议

Consider adding a --sha256-verify / --sha256-verify=false flag to let users explicitly skip verification. Currently, nil means "no verification", which is reasonable but implicit.
考虑增加 --sha256-verify 标志,让用户能显式关闭校验。当前 nil 表示"不校验"是合理的但较隐晦。

Verification / 验证

  • Code compiles with swift build
  • Follows existing project patterns (XPC keys config, ProgressBar, Config struct)

Great contribution! / 优秀的贡献!

@katiewasnothere

Copy link
Copy Markdown
Contributor

@haoruilee Thank you for the contribution! Could you add commit signatures to your commits? See https://github.com/apple/containerization/blob/main/CONTRIBUTING.md#pull-requests

We definitely want this change, but I think we should change the flags, config fields, and XPC key names to exclude the name of the algorithm so we can support other hashing algorithms in the future. Maybe we should call this just checksum instead? What do you think?

@briansmith

Copy link
Copy Markdown

Perhaps then the values should be prefixed with the digest algorithm.

Perhaps it is worth just copying Subresource Integrity syntax, using "integrity" instead of "checksum" and prefixing the digest algorithm name with a dash in the values, e.g. integrity: "sha256-f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91" instead of sha256 = "f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91" that the PR currently implements.

(Note that using a dash instead of colon prevents any ambiguity with content-addressible-by-digest URI schemes like sha256: that.)

@haoruilee

Copy link
Copy Markdown
ContributorAuthor

Hi @katiewasnothere@briansmith ,

Thanks, that makes sense. I read these suggestions as complementary, the external names should avoid hard coding SHA-256, while the value itself should still carry the digest algorithm.

I’ll update the PR to archive this and add commit signatures and revise the tests and docs accordingly.

@haoruilee
haoruileeforce-pushed the feat/kernelintegrity branch from d24095e to 76e26b9CompareJune 30, 2026 08:14
@haoruileehaoruilee changed the title Verify kernel archive SHA-256 digestsVerify kernel archive integrityJun 30, 2026
@haoruilee
haoruileeforce-pushed the feat/kernelintegrity branch from 76e26b9 to ea5ccebCompareJune 30, 2026 08:17
@haoruilee

Copy link
Copy Markdown
ContributorAuthor

Hi @katiewasnothere

Updated, thanks for your suggestion. I renamed the API surface from sha256 to integrity, with values using sha256-<hex> syntax, and updated tests/docs/PR description accordingly.

Could you please take another look when you have a chance?

@katiewasnothere

Copy link
Copy Markdown
Contributor

@haoruilee Thanks for the update! @briansmith this is a good suggestion, but naming the flag integrity and using a algorithm-hex format is inconsistent with other similar cases in this repo. Thinking about it some more, I think we should name the flag digest and use the format algorithm:hex.

@haoruilee

Copy link
Copy Markdown
ContributorAuthor

Hi @katiewasnothere ,

Thanks, I’ve updated the PR to rename the flag field to digest and use the algorithm:hex format. Docs and tests are updated as well.

Comment threaddocs/container-system-config.md Outdated
|--------------|-----------|--------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| `binaryPath` | `String` | `"opt/kata/share/kata-containers/vmlinux-6.18.15-186"` | Path **inside** the downloaded kernel archive that points to the kernel binary. |
| `url` | `URL` | `"https://github.com/kata-containers/kata-containers/releases/download/3.28.0/kata-static-3.28.0-arm64.tar.zst"` | Archive to download when no kernel is installed. Encoded and decoded as a plain string in TOML. |
| `digest` | `String?` | `"sha256:f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91"` | Expected digest for the archive, for example `sha256:<hex>`. When unset for a custom URL, remote kernel downloads are not verified. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should require that a digest is provided

@github-actions

github-actionsBot commented Jul 1, 2026

Copy link
Copy Markdown

Code Coverage

TierLine Coverage
Unit23.42%
Integration66.54%
Combined75.48%

}

@Test func customKernelURLWithoutDigestLeavesDigestUnset() async throws {
@Test func customKernelURLWithoutDigestThrows() async throws {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about a test where the digest doesn't match the file contents?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about tests where:

  1. The digest algorithm is "sha1" (should fail).
  2. The digest algorithm is "sha256" and the digest is correct but truncated by one byte (should fail).
  3. The digest algorithm is "sha256" but the digest value is a correct SHA-1 (should fail).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻 The archive content mismatch case is covered by installKernelFromLocalTarRejectsDigestMismatchWithoutInstalling, and I added coverage for sha1, truncated sha256, and a valid SHA-1 value passed as sha256.

binaryPath: String = defaultBinaryPath,
url: URL = defaultURL
) {
public init(binaryPath: String = defaultBinaryPath) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we want this init?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, the extra overload isn’t needed. I collapsed this into a single initializer with defaults for binaryPath, url, and digest.

self.digest = Self.defaultDigest
}

public init(binaryPath: String = defaultBinaryPath, url: URL, digest: String) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

url and digest should have their default values set here

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 💯

Comment on lines +211 to +215
let digestResult = try provider.value(forKey: AbsoluteConfigKey(ConfigKey("kernel.digest")), type: .string)
guard digestResult.value != nil else {
throw ContainerizationError(
.invalidArgument,
message: "kernel.digest is required in '\(path)' when kernel.url configures a custom archive"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could check for this in the decoder instead of having a custom validation function. What do you think?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the decoder check is still useful, but not sufficient for the layered config case.

The rule I’m trying to enforce is: if a config layer overrides kernel.url with a non-default archive, that same layer must also provide the digest for that archive. The digest is tied to the archive contents, so it should not be inherited from another layer.

The decoder only sees the merged snapshot, so it can tell whether the final config has both kernel.url and kernel.digest, but it can’t tell which file each value came from. That means it would accept a custom URL from a higher precedence file paired with the default digest from a lower precedence file, which is the case this validation is meant to reject.

I kept this as a premerge loader validation and added a comment to make that layering requirement clearer.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rule I’m trying to enforce is: if a config layer overrides kernel.url with a non-default archive, that same layer must also provide the digest for that archive.

In my opinion it doesn't matter if we get the kernel url and kernel digest from different layers in a layered config case. If the digest does not match the kernel url's contents when we go to fetch the kernel, the user will get an error. I could see having the values in separate layers being useful. Is there a specific scenario you want to prevent with this?

let fileIOThreadPool = NIOThreadPool(numberOfThreads: 1)
fileIOThreadPool.start()

let delegate = try HashingFileDownloadDelegate(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A follow up to this PR or future work could be to look into if we can do something similar to what is done for fetching image blobs here

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, that seems worth exploring as follow up work. I’d keep it separate from this PR since this one is focused on kernel archive integrity.

}
}

static func verifyDigest(of file: URL, expected: String) throws {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this function when we have the one below on line 195?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I removed the extra string overload and updated the tests.


// Mirrors AsyncHTTPClient's file download delegate while updating an optional
// SHA-256 hasher from the same response chunks that are written to disk.
private final class HashingFileDownloadDelegate: @unchecked Sendable, HTTPClientResponseDelegate {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After talking with others, I think for now we should just remove this in favor of downloading the whole tar in the kernel service and then calling verifyDigest(of file: URL, expected: ExpectedDigest) to incrementally compute the hash like we do if there's a local tar. This will make this PR more scoped and we can optimize further later by looking into the suggestion here. After removing this part, I'm ready to approve.

@katiewasnotherekatiewasnothere left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thank you!

@katiewasnothere
katiewasnothere merged commit 57b07fa into apple:mainJul 13, 2026
3 checks passed
saehejkang pushed a commit to saehejkang/container that referenced this pull request Jul 16, 2026
Closesapple#1687
The default kernel archive is downloaded from a remote release URL
during first-run setup and via `container system kernel set
--recommended`. Previously, the archive contents were not verified after
download, so integrity depended on HTTPS and the release artifact
remaining unchanged.
This change adds digest verification for kernel archives. The
recommended/default kernel now has pinned digest metadata using an
algorithm-prefixed value such as `sha256:<hex>`. `container system
kernel set --tar` accepts `--digest`; remote tar URLs require it, and
local tar archives can also be verified before unpacking and
installation.
The system config also supports `kernel.digest`, and a custom
`kernel.url` must provide a digest for that archive.
andrewkomkov added a commit to getgantry/gantry that referenced this pull request Aug 1, 2026
…ot args (#14)
apple/container **1.2.0** is out (previously tracked: `1.1.0`).
Upstream notes: https://github.com/apple/container/releases/tag/1.2.0 —
mirrored in `docs/upstream/apple-container-1.2.0.md`.
## Review checklist
- [ ] New or changed CLI flags Gantry should surface (`container
run/create/machine/build`)
- [ ] Changed `--format json` shapes the DockerKit apple transport
decodes
- [ ] Fixed upstream bugs Gantry currently works around
- [ ] `ContainerTooling.recommendedVersion` / feature gates need moving
to `1.2.0`
- [ ] MCP tools and App Intents that expose the affected commands
- [ ] README and CHANGELOG entries for whatever is adopted
Merging records the version as reviewed. Implement the adopted parts on
this branch, or merge as-is and open follow-ups.
---
<details><summary>Upstream release notes</summary>
## What's Changed
* Add TestCLISystemLogs and TestCLITermIO integration tests in new
integration test suite by @katiewasnothere in
apple/container#1879
* Restore reverted migrations, migrate last tests. by @jglogan in
apple/container#1880
* Removes obsolete CLITests directory. by @jglogan in
apple/container#1886
* Integration coverage xpc helpers by @noah-thor in
apple/container#1551
* Upgrade grpc-swift-nio-transport to 2.9.0 and remove HTTP2ConnectBuff…
by @adityabagchi24 in apple/container#1790
* Updates containerization to 0.36.0. by @jglogan in
apple/container#1912
* Use containerization version 0.37.0 by @adityaramani in
apple/container#1932
* Verify kernel archive integrity by @haoruilee in
apple/container#1703
* Add commit/issue alert to PR template. by @jglogan in
apple/container#1945
* Remove `--skip-build` from test Makefile target. by @jglogan in
apple/container#1951
* Restore `--skip-build`, enable `import testable` for release builds.
by @jglogan in apple/container#1955
* [package]: bump container-builder-shim to 0.13.0 by @saehejkang in
apple/container#1953
* Validate container ID from XPC requests by @katiewasnothere in
apple/container#1956
* Remove force unwraps on XPC error set/get by @katiewasnothere in
apple/container#1958
* Do not follow destination symlink when copying user configuration by
@katiewasnothere in apple/container#1957
* Fix machine ID length test. by @jglogan in
apple/container#1971
* Address flaky TestCLIKernelSetSerial suite. by @jglogan in
apple/container#1976
* [gitignore]: ignore vscode workspace files by @saehejkang in
apple/container#1966
* Update containerization dependency with new EXT4Unpacker func
definition by @katiewasnothere in
apple/container#1973
* Periodic dependency updates. by @jglogan in
apple/container#1981
* Use ordered journal mode for unpacked images. by @jglogan in
apple/container#1974
* Reword DNS container name resolution doc information by
@katiewasnothere in apple/container#1960
* ci: bump the github-actions group across 1 directory with 3 updates by
@dependabot[bot] in apple/container#1983
* Pass build config in when building protoc dependencies by
@katiewasnothere in apple/container#1972
* Container test fixture package by @katiewasnothere in
apple/container#1887
* Downgrade swift-collections to 1.5.1. by @jglogan in
apple/container#1984
* Use `enum` for warmup images. by @jglogan in
apple/container#1990
* Add missing dependencies to new ContainerTestSupport package by
@katiewasnothere in apple/container#1994
* Add OCI maskedPaths and readonlyPaths support to Container API. by
@jglogan in apple/container#1996
* Integration test - miscellaneous fixture and test refinements. by
@jglogan in apple/container#1993
* Use log instead of print for system start status messages by
@adityabagchi24 in apple/container#1889
* Fix BuilderStart race, parallelize `container build` tests. by
@jglogan in apple/container#2002
* Allow custom kernel boot args via --kernel-arg by @arirubinstein in
apple/container#1744
* fix: Increase XPC timeout for Machine API operations by @dev-kvt in
apple/container#2006
* Update containerization import to latest 0.40.0 by @katiewasnothere in
apple/container#2028
* Fix image env vars, build context checks, TCP/UDP port forward buffer,
and validate plugin name by @katiewasnothere in
apple/container#2027
* Update containerization import to 0.40.1 by @katiewasnothere in
apple/container#2038
## New Contributors
* @haoruilee made their first contribution in
apple/container#1703
* @arirubinstein made their first contribution in
apple/container#1744
* @dev-kvt made their first contribution in
apple/container#2006
**Full Changelog**:
apple/container@1.1.0...1.2.0
</details>
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Andrew <Andrew.Komkov@gmail.com>
henrywang added a commit to henrywang/Berthly that referenced this pull request Aug 4, 2026
## Summary
- container 1.2.0 added digest verification for kernel archives
(apple/container#1703): `kernel set --tar` accepts `--digest`, remote
tar URLs require one, and the recommended default kernel now ships
pinned sha256 digest metadata. containerization's `KernelConfig` gained
a required `digest` field, and `ClientKernel.installKernelFromTar` an
`expectedDigest` parameter — both reachable from Berthly's native XPC
calls.
- `KernelSetOptions` gained `digest: String?`, threaded into
`setKernel(options:)`'s `installKernelFromTar` call.
- `installDefaultKernelIfNeeded` now passes `expectedDigest:
containerSystemConfig.kernel.digest` — the default kernel already
carries a pinned digest from upstream.
- New "Digest" field on the Set Kernel sheet's Tar Archive section,
required only for remote URLs (matching upstream's exact rule),
prefilled by "Use Recommended" alongside the URL.
- `PARITY.md`'s System `kernel` row updated in the same commit.
## Why
Part of the container 1.2.0 milestone (#79). Mirrors the security
posture Berthly already applies elsewhere (pkg signature verification
against a pinned Apple identity) — kernel archives were the one download
path left unverified.
Closes#79
## Test plan
- [x] `xcodebuild build` succeeds
- [x] `xcodebuild test -only-testing:BerthlyTests` — full suite passes,
including new `kernelDigest` coverage in `SystemConfigMappingTests`
- [x] `swiftlint lint --strict` — 0 violations
- [x] Verified visually via Xcode's live preview renderer — confirmed
the Tar Archive section (including the recommended-URL prefill) renders
correctly
henrywang added a commit to henrywang/Berthly that referenced this pull request Aug 4, 2026
## Summary
- `resolvedSystemConfig()` used `try?` around
`ConfigurationLoader.load()`, so any decode failure silently fell back
to an all-defaults `ContainerSystemConfig` — not just the field that
failed, the user's entire `config.toml` (DNS, build settings, kernel,
everything).
- `ConfigurationLoader.load()` only throws when a config file exists on
disk but fails to parse/decode; it already returns cleanly with defaults
when no file is present at all — so every error caught here represents
real discarded user data, not an absent-file no-op.
- container 1.2.0 made this concrete: `KernelConfig`'s decoder now
throws when `kernel.url` is customized without a paired `kernel.digest`
(apple/container#1703). Any Berthly user who ran `container system
kernel set --tar <url>` pre-1.2.0 has exactly that config shape, and
would silently lose their whole system config on next load once the
daemon is upgraded.
- Extracts the load-outcome handling into a pure, testable
`mapSystemConfigLoadResult(_:)` and surfaces a failure through the
existing `lastStartupWarning` mechanism instead of swallowing it.
## Why
Found while implementing #79 (kernel digest verification) — this
milestone's own version bump is what triggers the failure for affected
users, so it needs fixing in the same milestone.
Closes#87
## Test plan
- [x] `xcodebuild build` succeeds
- [x] `xcodebuild test -only-testing:BerthlyTests` — full suite passes,
including new `SystemConfigLoadResultMappingTests` covering both the
success and failure paths
- [x] `swiftlint lint --strict` — 0 violations
- [x] No View files touched — no UI test needed for this change (tracked
separately in #89: the sidebar's warning-state rendering itself has no
UI coverage yet, pre-existing gap this PR surfaces a second producer
into)
jianliang00 pushed a commit to jianliang00/container that referenced this pull request Aug 28, 2026
Closesapple#1687
The default kernel archive is downloaded from a remote release URL
during first-run setup and via `container system kernel set
--recommended`. Previously, the archive contents were not verified after
download, so integrity depended on HTTPS and the release artifact
remaining unchanged.
This change adds digest verification for kernel archives. The
recommended/default kernel now has pinned digest metadata using an
algorithm-prefixed value such as `sha256:<hex>`. `container system
kernel set --tar` accepts `--digest`; remote tar URLs require it, and
local tar archives can also be verified before unpacking and
installation.
The system config also supports `kernel.digest`, and a custom
`kernel.url` must provide a digest for that archive.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Request]: Check kernel download integrity when downloading

5 participants

@haoruilee@kasc0206@katiewasnothere@briansmith@jglogan
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Verify kernel archive integrity by haoruilee · Pull Request #1703 · apple/container · GitHub
Skip to content

Verify kernel archive integrity - #1703

Merged
katiewasnothere merged 7 commits into
apple:mainfrom
haoruilee:feat/kernelintegrity
Jul 13, 2026
Merged

Verify kernel archive integrity#1703
katiewasnothere merged 7 commits into
apple:mainfrom
haoruilee:feat/kernelintegrity

Conversation

@haoruilee

@haoruileehaoruilee commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Motivation and Context

Closes#1687

The default kernel archive is downloaded from a remote release URL during first-run setup and via container system kernel set --recommended. Previously, the archive contents were not verified after download, so integrity depended on HTTPS and the release artifact remaining unchanged.

This change adds digest verification for kernel archives. The recommended/default kernel now has pinned digest metadata using an algorithm-prefixed value such as sha256:<hex>. container system kernel set --tar accepts --digest; remote tar URLs require it, and local tar archives can also be verified before unpacking and installation.

The system config also supports kernel.digest, and a custom kernel.url must provide a digest for that archive.

Testing

  • Tested locally
  • Added/updated tests
  • Added/updated docs

Validated locally:

  • git diff --check upstream/main...HEAD
  • swift test --filter KernelServiceTests
  • swift test --filter ConfigurationLoaderTests

@kasc0206kasc0206 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查意见 / Review

This is a well-crafted security improvement. The SHA-256 digest verification for kernel downloads is an important addition.

优点 / Strengths

  1. End-to-end implementation: From CLI flag → config → XPC message → server-side verification, the full pipeline is covered
  2. Backward compatible: sha256 is String? with default nil, so existing configs work unchanged
  3. Smart defaults: Auto-populates the default SHA-256 when using the default kernel URL, while allowing custom values for custom URLs
  4. No breaking API changes: All new parameters have sensible defaults
  5. Proper error messaging: The --sha256 can only be used with --tar

Suggestion

  • Consider adding a --sha256-verify flag with --sha256-verify=false to allow users to explicitly skip verification, rather than relying on nil meaning "no verification". But this is optional — current behavior is reasonable.

已验证 / Verified

  • Code compiles with swift build
  • Follows existing project patterns (XPC keys, Config struct, ProgressBar)

Great contribution!

@kasc0206

Copy link
Copy Markdown

Code Review Summary / 代码审查摘要

Strengths / 优点

  1. End-to-end implementation: From CLI flag (--sha256) → Config (sha256 in TOML) → XPC message → server-side verification, the full pipeline is covered
    端到端实现:从 CLI 参数到配置到 XPC 消息到服务端校验,完整链路覆盖
  2. Backward compatible: sha256 is String? with default nil — existing configs work unchanged
    向后兼容:sha256 为可选的 String? 类型,现有配置无需修改
  3. Smart defaults: Auto-populates the default SHA-256 when using the default kernel URL, while allowing custom values for custom URLs
    智能默认值:使用默认内核 URL 时自动填充默认 SHA-256,自定义 URL 可自行指定
  4. No breaking API changes to callers: All new parameters have sensible defaults
    无破坏性 API 变更:所有新参数都有合理的默认值
  5. CLI validation: --sha256 correctly rejected when used without --tar
    CLI 参数校验--sha256 不带 --tar 时正确报错

Suggestion / 建议

Consider adding a --sha256-verify / --sha256-verify=false flag to let users explicitly skip verification. Currently, nil means "no verification", which is reasonable but implicit.
考虑增加 --sha256-verify 标志,让用户能显式关闭校验。当前 nil 表示"不校验"是合理的但较隐晦。

Verification / 验证

  • Code compiles with swift build
  • Follows existing project patterns (XPC keys config, ProgressBar, Config struct)

Great contribution! / 优秀的贡献!

@katiewasnothere

Copy link
Copy Markdown
Contributor

@haoruilee Thank you for the contribution! Could you add commit signatures to your commits? See https://github.com/apple/containerization/blob/main/CONTRIBUTING.md#pull-requests

We definitely want this change, but I think we should change the flags, config fields, and XPC key names to exclude the name of the algorithm so we can support other hashing algorithms in the future. Maybe we should call this just checksum instead? What do you think?

@briansmith

Copy link
Copy Markdown

Perhaps then the values should be prefixed with the digest algorithm.

Perhaps it is worth just copying Subresource Integrity syntax, using "integrity" instead of "checksum" and prefixing the digest algorithm name with a dash in the values, e.g. integrity: "sha256-f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91" instead of sha256 = "f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91" that the PR currently implements.

(Note that using a dash instead of colon prevents any ambiguity with content-addressible-by-digest URI schemes like sha256: that.)

@haoruilee

Copy link
Copy Markdown
ContributorAuthor

Hi @katiewasnothere@briansmith ,

Thanks, that makes sense. I read these suggestions as complementary, the external names should avoid hard coding SHA-256, while the value itself should still carry the digest algorithm.

I’ll update the PR to archive this and add commit signatures and revise the tests and docs accordingly.

@haoruilee
haoruileeforce-pushed the feat/kernelintegrity branch from d24095e to 76e26b9CompareJune 30, 2026 08:14
@haoruileehaoruilee changed the title Verify kernel archive SHA-256 digestsVerify kernel archive integrityJun 30, 2026
@haoruilee
haoruileeforce-pushed the feat/kernelintegrity branch from 76e26b9 to ea5ccebCompareJune 30, 2026 08:17
@haoruilee

Copy link
Copy Markdown
ContributorAuthor

Hi @katiewasnothere

Updated, thanks for your suggestion. I renamed the API surface from sha256 to integrity, with values using sha256-<hex> syntax, and updated tests/docs/PR description accordingly.

Could you please take another look when you have a chance?

@katiewasnothere

Copy link
Copy Markdown
Contributor

@haoruilee Thanks for the update! @briansmith this is a good suggestion, but naming the flag integrity and using a algorithm-hex format is inconsistent with other similar cases in this repo. Thinking about it some more, I think we should name the flag digest and use the format algorithm:hex.

@haoruilee

Copy link
Copy Markdown
ContributorAuthor

Hi @katiewasnothere ,

Thanks, I’ve updated the PR to rename the flag field to digest and use the algorithm:hex format. Docs and tests are updated as well.

Comment threaddocs/container-system-config.md Outdated
|--------------|-----------|--------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| `binaryPath` | `String` | `"opt/kata/share/kata-containers/vmlinux-6.18.15-186"` | Path **inside** the downloaded kernel archive that points to the kernel binary. |
| `url` | `URL` | `"https://github.com/kata-containers/kata-containers/releases/download/3.28.0/kata-static-3.28.0-arm64.tar.zst"` | Archive to download when no kernel is installed. Encoded and decoded as a plain string in TOML. |
| `digest` | `String?` | `"sha256:f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91"` | Expected digest for the archive, for example `sha256:<hex>`. When unset for a custom URL, remote kernel downloads are not verified. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should require that a digest is provided

@github-actions

github-actionsBot commented Jul 1, 2026

Copy link
Copy Markdown

Code Coverage

TierLine Coverage
Unit23.42%
Integration66.54%
Combined75.48%

}

@Test func customKernelURLWithoutDigestLeavesDigestUnset() async throws {
@Test func customKernelURLWithoutDigestThrows() async throws {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about a test where the digest doesn't match the file contents?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about tests where:

  1. The digest algorithm is "sha1" (should fail).
  2. The digest algorithm is "sha256" and the digest is correct but truncated by one byte (should fail).
  3. The digest algorithm is "sha256" but the digest value is a correct SHA-1 (should fail).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻 The archive content mismatch case is covered by installKernelFromLocalTarRejectsDigestMismatchWithoutInstalling, and I added coverage for sha1, truncated sha256, and a valid SHA-1 value passed as sha256.

binaryPath: String = defaultBinaryPath,
url: URL = defaultURL
) {
public init(binaryPath: String = defaultBinaryPath) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we want this init?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, the extra overload isn’t needed. I collapsed this into a single initializer with defaults for binaryPath, url, and digest.

self.digest = Self.defaultDigest
}

public init(binaryPath: String = defaultBinaryPath, url: URL, digest: String) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

url and digest should have their default values set here

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 💯

Comment on lines +211 to +215
let digestResult = try provider.value(forKey: AbsoluteConfigKey(ConfigKey("kernel.digest")), type: .string)
guard digestResult.value != nil else {
throw ContainerizationError(
.invalidArgument,
message: "kernel.digest is required in '\(path)' when kernel.url configures a custom archive"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could check for this in the decoder instead of having a custom validation function. What do you think?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the decoder check is still useful, but not sufficient for the layered config case.

The rule I’m trying to enforce is: if a config layer overrides kernel.url with a non-default archive, that same layer must also provide the digest for that archive. The digest is tied to the archive contents, so it should not be inherited from another layer.

The decoder only sees the merged snapshot, so it can tell whether the final config has both kernel.url and kernel.digest, but it can’t tell which file each value came from. That means it would accept a custom URL from a higher precedence file paired with the default digest from a lower precedence file, which is the case this validation is meant to reject.

I kept this as a premerge loader validation and added a comment to make that layering requirement clearer.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rule I’m trying to enforce is: if a config layer overrides kernel.url with a non-default archive, that same layer must also provide the digest for that archive.

In my opinion it doesn't matter if we get the kernel url and kernel digest from different layers in a layered config case. If the digest does not match the kernel url's contents when we go to fetch the kernel, the user will get an error. I could see having the values in separate layers being useful. Is there a specific scenario you want to prevent with this?

let fileIOThreadPool = NIOThreadPool(numberOfThreads: 1)
fileIOThreadPool.start()

let delegate = try HashingFileDownloadDelegate(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A follow up to this PR or future work could be to look into if we can do something similar to what is done for fetching image blobs here

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, that seems worth exploring as follow up work. I’d keep it separate from this PR since this one is focused on kernel archive integrity.

}
}

static func verifyDigest(of file: URL, expected: String) throws {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this function when we have the one below on line 195?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I removed the extra string overload and updated the tests.


// Mirrors AsyncHTTPClient's file download delegate while updating an optional
// SHA-256 hasher from the same response chunks that are written to disk.
private final class HashingFileDownloadDelegate: @unchecked Sendable, HTTPClientResponseDelegate {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After talking with others, I think for now we should just remove this in favor of downloading the whole tar in the kernel service and then calling verifyDigest(of file: URL, expected: ExpectedDigest) to incrementally compute the hash like we do if there's a local tar. This will make this PR more scoped and we can optimize further later by looking into the suggestion here. After removing this part, I'm ready to approve.

@katiewasnotherekatiewasnothere left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thank you!

@katiewasnothere
katiewasnothere merged commit 57b07fa into apple:mainJul 13, 2026
3 checks passed
saehejkang pushed a commit to saehejkang/container that referenced this pull request Jul 16, 2026
Closesapple#1687
The default kernel archive is downloaded from a remote release URL
during first-run setup and via `container system kernel set
--recommended`. Previously, the archive contents were not verified after
download, so integrity depended on HTTPS and the release artifact
remaining unchanged.
This change adds digest verification for kernel archives. The
recommended/default kernel now has pinned digest metadata using an
algorithm-prefixed value such as `sha256:<hex>`. `container system
kernel set --tar` accepts `--digest`; remote tar URLs require it, and
local tar archives can also be verified before unpacking and
installation.
The system config also supports `kernel.digest`, and a custom
`kernel.url` must provide a digest for that archive.
andrewkomkov added a commit to getgantry/gantry that referenced this pull request Aug 1, 2026
…ot args (#14)
apple/container **1.2.0** is out (previously tracked: `1.1.0`).
Upstream notes: https://github.com/apple/container/releases/tag/1.2.0 —
mirrored in `docs/upstream/apple-container-1.2.0.md`.
## Review checklist
- [ ] New or changed CLI flags Gantry should surface (`container
run/create/machine/build`)
- [ ] Changed `--format json` shapes the DockerKit apple transport
decodes
- [ ] Fixed upstream bugs Gantry currently works around
- [ ] `ContainerTooling.recommendedVersion` / feature gates need moving
to `1.2.0`
- [ ] MCP tools and App Intents that expose the affected commands
- [ ] README and CHANGELOG entries for whatever is adopted
Merging records the version as reviewed. Implement the adopted parts on
this branch, or merge as-is and open follow-ups.
---
<details><summary>Upstream release notes</summary>
## What's Changed
* Add TestCLISystemLogs and TestCLITermIO integration tests in new
integration test suite by @katiewasnothere in
apple/container#1879
* Restore reverted migrations, migrate last tests. by @jglogan in
apple/container#1880
* Removes obsolete CLITests directory. by @jglogan in
apple/container#1886
* Integration coverage xpc helpers by @noah-thor in
apple/container#1551
* Upgrade grpc-swift-nio-transport to 2.9.0 and remove HTTP2ConnectBuff…
by @adityabagchi24 in apple/container#1790
* Updates containerization to 0.36.0. by @jglogan in
apple/container#1912
* Use containerization version 0.37.0 by @adityaramani in
apple/container#1932
* Verify kernel archive integrity by @haoruilee in
apple/container#1703
* Add commit/issue alert to PR template. by @jglogan in
apple/container#1945
* Remove `--skip-build` from test Makefile target. by @jglogan in
apple/container#1951
* Restore `--skip-build`, enable `import testable` for release builds.
by @jglogan in apple/container#1955
* [package]: bump container-builder-shim to 0.13.0 by @saehejkang in
apple/container#1953
* Validate container ID from XPC requests by @katiewasnothere in
apple/container#1956
* Remove force unwraps on XPC error set/get by @katiewasnothere in
apple/container#1958
* Do not follow destination symlink when copying user configuration by
@katiewasnothere in apple/container#1957
* Fix machine ID length test. by @jglogan in
apple/container#1971
* Address flaky TestCLIKernelSetSerial suite. by @jglogan in
apple/container#1976
* [gitignore]: ignore vscode workspace files by @saehejkang in
apple/container#1966
* Update containerization dependency with new EXT4Unpacker func
definition by @katiewasnothere in
apple/container#1973
* Periodic dependency updates. by @jglogan in
apple/container#1981
* Use ordered journal mode for unpacked images. by @jglogan in
apple/container#1974
* Reword DNS container name resolution doc information by
@katiewasnothere in apple/container#1960
* ci: bump the github-actions group across 1 directory with 3 updates by
@dependabot[bot] in apple/container#1983
* Pass build config in when building protoc dependencies by
@katiewasnothere in apple/container#1972
* Container test fixture package by @katiewasnothere in
apple/container#1887
* Downgrade swift-collections to 1.5.1. by @jglogan in
apple/container#1984
* Use `enum` for warmup images. by @jglogan in
apple/container#1990
* Add missing dependencies to new ContainerTestSupport package by
@katiewasnothere in apple/container#1994
* Add OCI maskedPaths and readonlyPaths support to Container API. by
@jglogan in apple/container#1996
* Integration test - miscellaneous fixture and test refinements. by
@jglogan in apple/container#1993
* Use log instead of print for system start status messages by
@adityabagchi24 in apple/container#1889
* Fix BuilderStart race, parallelize `container build` tests. by
@jglogan in apple/container#2002
* Allow custom kernel boot args via --kernel-arg by @arirubinstein in
apple/container#1744
* fix: Increase XPC timeout for Machine API operations by @dev-kvt in
apple/container#2006
* Update containerization import to latest 0.40.0 by @katiewasnothere in
apple/container#2028
* Fix image env vars, build context checks, TCP/UDP port forward buffer,
and validate plugin name by @katiewasnothere in
apple/container#2027
* Update containerization import to 0.40.1 by @katiewasnothere in
apple/container#2038
## New Contributors
* @haoruilee made their first contribution in
apple/container#1703
* @arirubinstein made their first contribution in
apple/container#1744
* @dev-kvt made their first contribution in
apple/container#2006
**Full Changelog**:
apple/container@1.1.0...1.2.0
</details>
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Andrew <Andrew.Komkov@gmail.com>
henrywang added a commit to henrywang/Berthly that referenced this pull request Aug 4, 2026
## Summary
- container 1.2.0 added digest verification for kernel archives
(apple/container#1703): `kernel set --tar` accepts `--digest`, remote
tar URLs require one, and the recommended default kernel now ships
pinned sha256 digest metadata. containerization's `KernelConfig` gained
a required `digest` field, and `ClientKernel.installKernelFromTar` an
`expectedDigest` parameter — both reachable from Berthly's native XPC
calls.
- `KernelSetOptions` gained `digest: String?`, threaded into
`setKernel(options:)`'s `installKernelFromTar` call.
- `installDefaultKernelIfNeeded` now passes `expectedDigest:
containerSystemConfig.kernel.digest` — the default kernel already
carries a pinned digest from upstream.
- New "Digest" field on the Set Kernel sheet's Tar Archive section,
required only for remote URLs (matching upstream's exact rule),
prefilled by "Use Recommended" alongside the URL.
- `PARITY.md`'s System `kernel` row updated in the same commit.
## Why
Part of the container 1.2.0 milestone (#79). Mirrors the security
posture Berthly already applies elsewhere (pkg signature verification
against a pinned Apple identity) — kernel archives were the one download
path left unverified.
Closes#79
## Test plan
- [x] `xcodebuild build` succeeds
- [x] `xcodebuild test -only-testing:BerthlyTests` — full suite passes,
including new `kernelDigest` coverage in `SystemConfigMappingTests`
- [x] `swiftlint lint --strict` — 0 violations
- [x] Verified visually via Xcode's live preview renderer — confirmed
the Tar Archive section (including the recommended-URL prefill) renders
correctly
henrywang added a commit to henrywang/Berthly that referenced this pull request Aug 4, 2026
## Summary
- `resolvedSystemConfig()` used `try?` around
`ConfigurationLoader.load()`, so any decode failure silently fell back
to an all-defaults `ContainerSystemConfig` — not just the field that
failed, the user's entire `config.toml` (DNS, build settings, kernel,
everything).
- `ConfigurationLoader.load()` only throws when a config file exists on
disk but fails to parse/decode; it already returns cleanly with defaults
when no file is present at all — so every error caught here represents
real discarded user data, not an absent-file no-op.
- container 1.2.0 made this concrete: `KernelConfig`'s decoder now
throws when `kernel.url` is customized without a paired `kernel.digest`
(apple/container#1703). Any Berthly user who ran `container system
kernel set --tar <url>` pre-1.2.0 has exactly that config shape, and
would silently lose their whole system config on next load once the
daemon is upgraded.
- Extracts the load-outcome handling into a pure, testable
`mapSystemConfigLoadResult(_:)` and surfaces a failure through the
existing `lastStartupWarning` mechanism instead of swallowing it.
## Why
Found while implementing #79 (kernel digest verification) — this
milestone's own version bump is what triggers the failure for affected
users, so it needs fixing in the same milestone.
Closes#87
## Test plan
- [x] `xcodebuild build` succeeds
- [x] `xcodebuild test -only-testing:BerthlyTests` — full suite passes,
including new `SystemConfigLoadResultMappingTests` covering both the
success and failure paths
- [x] `swiftlint lint --strict` — 0 violations
- [x] No View files touched — no UI test needed for this change (tracked
separately in #89: the sidebar's warning-state rendering itself has no
UI coverage yet, pre-existing gap this PR surfaces a second producer
into)
jianliang00 pushed a commit to jianliang00/container that referenced this pull request Aug 28, 2026
Closesapple#1687
The default kernel archive is downloaded from a remote release URL
during first-run setup and via `container system kernel set
--recommended`. Previously, the archive contents were not verified after
download, so integrity depended on HTTPS and the release artifact
remaining unchanged.
This change adds digest verification for kernel archives. The
recommended/default kernel now has pinned digest metadata using an
algorithm-prefixed value such as `sha256:<hex>`. `container system
kernel set --tar` accepts `--digest`; remote tar URLs require it, and
local tar archives can also be verified before unpacking and
installation.
The system config also supports `kernel.digest`, and a custom
`kernel.url` must provide a digest for that archive.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Request]: Check kernel download integrity when downloading

5 participants

@haoruilee@kasc0206@katiewasnothere@briansmith@jglogan
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Verify kernel archive integrity by haoruilee · Pull Request #1703 · apple/container · GitHub
Skip to content

Verify kernel archive integrity - #1703

Merged
katiewasnothere merged 7 commits into
apple:mainfrom
haoruilee:feat/kernelintegrity
Jul 13, 2026
Merged

Verify kernel archive integrity#1703
katiewasnothere merged 7 commits into
apple:mainfrom
haoruilee:feat/kernelintegrity

Conversation

@haoruilee

@haoruileehaoruilee commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Motivation and Context

Closes#1687

The default kernel archive is downloaded from a remote release URL during first-run setup and via container system kernel set --recommended. Previously, the archive contents were not verified after download, so integrity depended on HTTPS and the release artifact remaining unchanged.

This change adds digest verification for kernel archives. The recommended/default kernel now has pinned digest metadata using an algorithm-prefixed value such as sha256:<hex>. container system kernel set --tar accepts --digest; remote tar URLs require it, and local tar archives can also be verified before unpacking and installation.

The system config also supports kernel.digest, and a custom kernel.url must provide a digest for that archive.

Testing

  • Tested locally
  • Added/updated tests
  • Added/updated docs

Validated locally:

  • git diff --check upstream/main...HEAD
  • swift test --filter KernelServiceTests
  • swift test --filter ConfigurationLoaderTests

@kasc0206kasc0206 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查意见 / Review

This is a well-crafted security improvement. The SHA-256 digest verification for kernel downloads is an important addition.

优点 / Strengths

  1. End-to-end implementation: From CLI flag → config → XPC message → server-side verification, the full pipeline is covered
  2. Backward compatible: sha256 is String? with default nil, so existing configs work unchanged
  3. Smart defaults: Auto-populates the default SHA-256 when using the default kernel URL, while allowing custom values for custom URLs
  4. No breaking API changes: All new parameters have sensible defaults
  5. Proper error messaging: The --sha256 can only be used with --tar

Suggestion

  • Consider adding a --sha256-verify flag with --sha256-verify=false to allow users to explicitly skip verification, rather than relying on nil meaning "no verification". But this is optional — current behavior is reasonable.

已验证 / Verified

  • Code compiles with swift build
  • Follows existing project patterns (XPC keys, Config struct, ProgressBar)

Great contribution!

@kasc0206

Copy link
Copy Markdown

Code Review Summary / 代码审查摘要

Strengths / 优点

  1. End-to-end implementation: From CLI flag (--sha256) → Config (sha256 in TOML) → XPC message → server-side verification, the full pipeline is covered
    端到端实现:从 CLI 参数到配置到 XPC 消息到服务端校验,完整链路覆盖
  2. Backward compatible: sha256 is String? with default nil — existing configs work unchanged
    向后兼容:sha256 为可选的 String? 类型,现有配置无需修改
  3. Smart defaults: Auto-populates the default SHA-256 when using the default kernel URL, while allowing custom values for custom URLs
    智能默认值:使用默认内核 URL 时自动填充默认 SHA-256,自定义 URL 可自行指定
  4. No breaking API changes to callers: All new parameters have sensible defaults
    无破坏性 API 变更:所有新参数都有合理的默认值
  5. CLI validation: --sha256 correctly rejected when used without --tar
    CLI 参数校验--sha256 不带 --tar 时正确报错

Suggestion / 建议

Consider adding a --sha256-verify / --sha256-verify=false flag to let users explicitly skip verification. Currently, nil means "no verification", which is reasonable but implicit.
考虑增加 --sha256-verify 标志,让用户能显式关闭校验。当前 nil 表示"不校验"是合理的但较隐晦。

Verification / 验证

  • Code compiles with swift build
  • Follows existing project patterns (XPC keys config, ProgressBar, Config struct)

Great contribution! / 优秀的贡献!

@katiewasnothere

Copy link
Copy Markdown
Contributor

@haoruilee Thank you for the contribution! Could you add commit signatures to your commits? See https://github.com/apple/containerization/blob/main/CONTRIBUTING.md#pull-requests

We definitely want this change, but I think we should change the flags, config fields, and XPC key names to exclude the name of the algorithm so we can support other hashing algorithms in the future. Maybe we should call this just checksum instead? What do you think?

@briansmith

Copy link
Copy Markdown

Perhaps then the values should be prefixed with the digest algorithm.

Perhaps it is worth just copying Subresource Integrity syntax, using "integrity" instead of "checksum" and prefixing the digest algorithm name with a dash in the values, e.g. integrity: "sha256-f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91" instead of sha256 = "f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91" that the PR currently implements.

(Note that using a dash instead of colon prevents any ambiguity with content-addressible-by-digest URI schemes like sha256: that.)

@haoruilee

Copy link
Copy Markdown
ContributorAuthor

Hi @katiewasnothere@briansmith ,

Thanks, that makes sense. I read these suggestions as complementary, the external names should avoid hard coding SHA-256, while the value itself should still carry the digest algorithm.

I’ll update the PR to archive this and add commit signatures and revise the tests and docs accordingly.

@haoruilee
haoruileeforce-pushed the feat/kernelintegrity branch from d24095e to 76e26b9CompareJune 30, 2026 08:14
@haoruileehaoruilee changed the title Verify kernel archive SHA-256 digestsVerify kernel archive integrityJun 30, 2026
@haoruilee
haoruileeforce-pushed the feat/kernelintegrity branch from 76e26b9 to ea5ccebCompareJune 30, 2026 08:17
@haoruilee

Copy link
Copy Markdown
ContributorAuthor

Hi @katiewasnothere

Updated, thanks for your suggestion. I renamed the API surface from sha256 to integrity, with values using sha256-<hex> syntax, and updated tests/docs/PR description accordingly.

Could you please take another look when you have a chance?

@katiewasnothere

Copy link
Copy Markdown
Contributor

@haoruilee Thanks for the update! @briansmith this is a good suggestion, but naming the flag integrity and using a algorithm-hex format is inconsistent with other similar cases in this repo. Thinking about it some more, I think we should name the flag digest and use the format algorithm:hex.

@haoruilee

Copy link
Copy Markdown
ContributorAuthor

Hi @katiewasnothere ,

Thanks, I’ve updated the PR to rename the flag field to digest and use the algorithm:hex format. Docs and tests are updated as well.

Comment threaddocs/container-system-config.md Outdated
|--------------|-----------|--------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| `binaryPath` | `String` | `"opt/kata/share/kata-containers/vmlinux-6.18.15-186"` | Path **inside** the downloaded kernel archive that points to the kernel binary. |
| `url` | `URL` | `"https://github.com/kata-containers/kata-containers/releases/download/3.28.0/kata-static-3.28.0-arm64.tar.zst"` | Archive to download when no kernel is installed. Encoded and decoded as a plain string in TOML. |
| `digest` | `String?` | `"sha256:f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91"` | Expected digest for the archive, for example `sha256:<hex>`. When unset for a custom URL, remote kernel downloads are not verified. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should require that a digest is provided

@github-actions

github-actionsBot commented Jul 1, 2026

Copy link
Copy Markdown

Code Coverage

TierLine Coverage
Unit23.42%
Integration66.54%
Combined75.48%

}

@Test func customKernelURLWithoutDigestLeavesDigestUnset() async throws {
@Test func customKernelURLWithoutDigestThrows() async throws {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about a test where the digest doesn't match the file contents?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about tests where:

  1. The digest algorithm is "sha1" (should fail).
  2. The digest algorithm is "sha256" and the digest is correct but truncated by one byte (should fail).
  3. The digest algorithm is "sha256" but the digest value is a correct SHA-1 (should fail).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻 The archive content mismatch case is covered by installKernelFromLocalTarRejectsDigestMismatchWithoutInstalling, and I added coverage for sha1, truncated sha256, and a valid SHA-1 value passed as sha256.

binaryPath: String = defaultBinaryPath,
url: URL = defaultURL
) {
public init(binaryPath: String = defaultBinaryPath) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we want this init?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, the extra overload isn’t needed. I collapsed this into a single initializer with defaults for binaryPath, url, and digest.

self.digest = Self.defaultDigest
}

public init(binaryPath: String = defaultBinaryPath, url: URL, digest: String) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

url and digest should have their default values set here

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 💯

Comment on lines +211 to +215
let digestResult = try provider.value(forKey: AbsoluteConfigKey(ConfigKey("kernel.digest")), type: .string)
guard digestResult.value != nil else {
throw ContainerizationError(
.invalidArgument,
message: "kernel.digest is required in '\(path)' when kernel.url configures a custom archive"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could check for this in the decoder instead of having a custom validation function. What do you think?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the decoder check is still useful, but not sufficient for the layered config case.

The rule I’m trying to enforce is: if a config layer overrides kernel.url with a non-default archive, that same layer must also provide the digest for that archive. The digest is tied to the archive contents, so it should not be inherited from another layer.

The decoder only sees the merged snapshot, so it can tell whether the final config has both kernel.url and kernel.digest, but it can’t tell which file each value came from. That means it would accept a custom URL from a higher precedence file paired with the default digest from a lower precedence file, which is the case this validation is meant to reject.

I kept this as a premerge loader validation and added a comment to make that layering requirement clearer.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rule I’m trying to enforce is: if a config layer overrides kernel.url with a non-default archive, that same layer must also provide the digest for that archive.

In my opinion it doesn't matter if we get the kernel url and kernel digest from different layers in a layered config case. If the digest does not match the kernel url's contents when we go to fetch the kernel, the user will get an error. I could see having the values in separate layers being useful. Is there a specific scenario you want to prevent with this?

let fileIOThreadPool = NIOThreadPool(numberOfThreads: 1)
fileIOThreadPool.start()

let delegate = try HashingFileDownloadDelegate(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A follow up to this PR or future work could be to look into if we can do something similar to what is done for fetching image blobs here

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, that seems worth exploring as follow up work. I’d keep it separate from this PR since this one is focused on kernel archive integrity.

}
}

static func verifyDigest(of file: URL, expected: String) throws {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this function when we have the one below on line 195?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I removed the extra string overload and updated the tests.


// Mirrors AsyncHTTPClient's file download delegate while updating an optional
// SHA-256 hasher from the same response chunks that are written to disk.
private final class HashingFileDownloadDelegate: @unchecked Sendable, HTTPClientResponseDelegate {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After talking with others, I think for now we should just remove this in favor of downloading the whole tar in the kernel service and then calling verifyDigest(of file: URL, expected: ExpectedDigest) to incrementally compute the hash like we do if there's a local tar. This will make this PR more scoped and we can optimize further later by looking into the suggestion here. After removing this part, I'm ready to approve.

@katiewasnotherekatiewasnothere left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thank you!

@katiewasnothere
katiewasnothere merged commit 57b07fa into apple:mainJul 13, 2026
3 checks passed
saehejkang pushed a commit to saehejkang/container that referenced this pull request Jul 16, 2026
Closesapple#1687
The default kernel archive is downloaded from a remote release URL
during first-run setup and via `container system kernel set
--recommended`. Previously, the archive contents were not verified after
download, so integrity depended on HTTPS and the release artifact
remaining unchanged.
This change adds digest verification for kernel archives. The
recommended/default kernel now has pinned digest metadata using an
algorithm-prefixed value such as `sha256:<hex>`. `container system
kernel set --tar` accepts `--digest`; remote tar URLs require it, and
local tar archives can also be verified before unpacking and
installation.
The system config also supports `kernel.digest`, and a custom
`kernel.url` must provide a digest for that archive.
andrewkomkov added a commit to getgantry/gantry that referenced this pull request Aug 1, 2026
…ot args (#14)
apple/container **1.2.0** is out (previously tracked: `1.1.0`).
Upstream notes: https://github.com/apple/container/releases/tag/1.2.0 —
mirrored in `docs/upstream/apple-container-1.2.0.md`.
## Review checklist
- [ ] New or changed CLI flags Gantry should surface (`container
run/create/machine/build`)
- [ ] Changed `--format json` shapes the DockerKit apple transport
decodes
- [ ] Fixed upstream bugs Gantry currently works around
- [ ] `ContainerTooling.recommendedVersion` / feature gates need moving
to `1.2.0`
- [ ] MCP tools and App Intents that expose the affected commands
- [ ] README and CHANGELOG entries for whatever is adopted
Merging records the version as reviewed. Implement the adopted parts on
this branch, or merge as-is and open follow-ups.
---
<details><summary>Upstream release notes</summary>
## What's Changed
* Add TestCLISystemLogs and TestCLITermIO integration tests in new
integration test suite by @katiewasnothere in
apple/container#1879
* Restore reverted migrations, migrate last tests. by @jglogan in
apple/container#1880
* Removes obsolete CLITests directory. by @jglogan in
apple/container#1886
* Integration coverage xpc helpers by @noah-thor in
apple/container#1551
* Upgrade grpc-swift-nio-transport to 2.9.0 and remove HTTP2ConnectBuff…
by @adityabagchi24 in apple/container#1790
* Updates containerization to 0.36.0. by @jglogan in
apple/container#1912
* Use containerization version 0.37.0 by @adityaramani in
apple/container#1932
* Verify kernel archive integrity by @haoruilee in
apple/container#1703
* Add commit/issue alert to PR template. by @jglogan in
apple/container#1945
* Remove `--skip-build` from test Makefile target. by @jglogan in
apple/container#1951
* Restore `--skip-build`, enable `import testable` for release builds.
by @jglogan in apple/container#1955
* [package]: bump container-builder-shim to 0.13.0 by @saehejkang in
apple/container#1953
* Validate container ID from XPC requests by @katiewasnothere in
apple/container#1956
* Remove force unwraps on XPC error set/get by @katiewasnothere in
apple/container#1958
* Do not follow destination symlink when copying user configuration by
@katiewasnothere in apple/container#1957
* Fix machine ID length test. by @jglogan in
apple/container#1971
* Address flaky TestCLIKernelSetSerial suite. by @jglogan in
apple/container#1976
* [gitignore]: ignore vscode workspace files by @saehejkang in
apple/container#1966
* Update containerization dependency with new EXT4Unpacker func
definition by @katiewasnothere in
apple/container#1973
* Periodic dependency updates. by @jglogan in
apple/container#1981
* Use ordered journal mode for unpacked images. by @jglogan in
apple/container#1974
* Reword DNS container name resolution doc information by
@katiewasnothere in apple/container#1960
* ci: bump the github-actions group across 1 directory with 3 updates by
@dependabot[bot] in apple/container#1983
* Pass build config in when building protoc dependencies by
@katiewasnothere in apple/container#1972
* Container test fixture package by @katiewasnothere in
apple/container#1887
* Downgrade swift-collections to 1.5.1. by @jglogan in
apple/container#1984
* Use `enum` for warmup images. by @jglogan in
apple/container#1990
* Add missing dependencies to new ContainerTestSupport package by
@katiewasnothere in apple/container#1994
* Add OCI maskedPaths and readonlyPaths support to Container API. by
@jglogan in apple/container#1996
* Integration test - miscellaneous fixture and test refinements. by
@jglogan in apple/container#1993
* Use log instead of print for system start status messages by
@adityabagchi24 in apple/container#1889
* Fix BuilderStart race, parallelize `container build` tests. by
@jglogan in apple/container#2002
* Allow custom kernel boot args via --kernel-arg by @arirubinstein in
apple/container#1744
* fix: Increase XPC timeout for Machine API operations by @dev-kvt in
apple/container#2006
* Update containerization import to latest 0.40.0 by @katiewasnothere in
apple/container#2028
* Fix image env vars, build context checks, TCP/UDP port forward buffer,
and validate plugin name by @katiewasnothere in
apple/container#2027
* Update containerization import to 0.40.1 by @katiewasnothere in
apple/container#2038
## New Contributors
* @haoruilee made their first contribution in
apple/container#1703
* @arirubinstein made their first contribution in
apple/container#1744
* @dev-kvt made their first contribution in
apple/container#2006
**Full Changelog**:
apple/container@1.1.0...1.2.0
</details>
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Andrew <Andrew.Komkov@gmail.com>
henrywang added a commit to henrywang/Berthly that referenced this pull request Aug 4, 2026
## Summary
- container 1.2.0 added digest verification for kernel archives
(apple/container#1703): `kernel set --tar` accepts `--digest`, remote
tar URLs require one, and the recommended default kernel now ships
pinned sha256 digest metadata. containerization's `KernelConfig` gained
a required `digest` field, and `ClientKernel.installKernelFromTar` an
`expectedDigest` parameter — both reachable from Berthly's native XPC
calls.
- `KernelSetOptions` gained `digest: String?`, threaded into
`setKernel(options:)`'s `installKernelFromTar` call.
- `installDefaultKernelIfNeeded` now passes `expectedDigest:
containerSystemConfig.kernel.digest` — the default kernel already
carries a pinned digest from upstream.
- New "Digest" field on the Set Kernel sheet's Tar Archive section,
required only for remote URLs (matching upstream's exact rule),
prefilled by "Use Recommended" alongside the URL.
- `PARITY.md`'s System `kernel` row updated in the same commit.
## Why
Part of the container 1.2.0 milestone (#79). Mirrors the security
posture Berthly already applies elsewhere (pkg signature verification
against a pinned Apple identity) — kernel archives were the one download
path left unverified.
Closes#79
## Test plan
- [x] `xcodebuild build` succeeds
- [x] `xcodebuild test -only-testing:BerthlyTests` — full suite passes,
including new `kernelDigest` coverage in `SystemConfigMappingTests`
- [x] `swiftlint lint --strict` — 0 violations
- [x] Verified visually via Xcode's live preview renderer — confirmed
the Tar Archive section (including the recommended-URL prefill) renders
correctly
henrywang added a commit to henrywang/Berthly that referenced this pull request Aug 4, 2026
## Summary
- `resolvedSystemConfig()` used `try?` around
`ConfigurationLoader.load()`, so any decode failure silently fell back
to an all-defaults `ContainerSystemConfig` — not just the field that
failed, the user's entire `config.toml` (DNS, build settings, kernel,
everything).
- `ConfigurationLoader.load()` only throws when a config file exists on
disk but fails to parse/decode; it already returns cleanly with defaults
when no file is present at all — so every error caught here represents
real discarded user data, not an absent-file no-op.
- container 1.2.0 made this concrete: `KernelConfig`'s decoder now
throws when `kernel.url` is customized without a paired `kernel.digest`
(apple/container#1703). Any Berthly user who ran `container system
kernel set --tar <url>` pre-1.2.0 has exactly that config shape, and
would silently lose their whole system config on next load once the
daemon is upgraded.
- Extracts the load-outcome handling into a pure, testable
`mapSystemConfigLoadResult(_:)` and surfaces a failure through the
existing `lastStartupWarning` mechanism instead of swallowing it.
## Why
Found while implementing #79 (kernel digest verification) — this
milestone's own version bump is what triggers the failure for affected
users, so it needs fixing in the same milestone.
Closes#87
## Test plan
- [x] `xcodebuild build` succeeds
- [x] `xcodebuild test -only-testing:BerthlyTests` — full suite passes,
including new `SystemConfigLoadResultMappingTests` covering both the
success and failure paths
- [x] `swiftlint lint --strict` — 0 violations
- [x] No View files touched — no UI test needed for this change (tracked
separately in #89: the sidebar's warning-state rendering itself has no
UI coverage yet, pre-existing gap this PR surfaces a second producer
into)
jianliang00 pushed a commit to jianliang00/container that referenced this pull request Aug 28, 2026
Closesapple#1687
The default kernel archive is downloaded from a remote release URL
during first-run setup and via `container system kernel set
--recommended`. Previously, the archive contents were not verified after
download, so integrity depended on HTTPS and the release artifact
remaining unchanged.
This change adds digest verification for kernel archives. The
recommended/default kernel now has pinned digest metadata using an
algorithm-prefixed value such as `sha256:<hex>`. `container system
kernel set --tar` accepts `--digest`; remote tar URLs require it, and
local tar archives can also be verified before unpacking and
installation.
The system config also supports `kernel.digest`, and a custom
`kernel.url` must provide a digest for that archive.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Request]: Check kernel download integrity when downloading

5 participants

@haoruilee@kasc0206@katiewasnothere@briansmith@jglogan
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Verify kernel archive integrity by haoruilee · Pull Request #1703 · apple/container · GitHub
Skip to content

Verify kernel archive integrity - #1703

Merged
katiewasnothere merged 7 commits into
apple:mainfrom
haoruilee:feat/kernelintegrity
Jul 13, 2026
Merged

Verify kernel archive integrity#1703
katiewasnothere merged 7 commits into
apple:mainfrom
haoruilee:feat/kernelintegrity

Conversation

@haoruilee

@haoruileehaoruilee commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Motivation and Context

Closes#1687

The default kernel archive is downloaded from a remote release URL during first-run setup and via container system kernel set --recommended. Previously, the archive contents were not verified after download, so integrity depended on HTTPS and the release artifact remaining unchanged.

This change adds digest verification for kernel archives. The recommended/default kernel now has pinned digest metadata using an algorithm-prefixed value such as sha256:<hex>. container system kernel set --tar accepts --digest; remote tar URLs require it, and local tar archives can also be verified before unpacking and installation.

The system config also supports kernel.digest, and a custom kernel.url must provide a digest for that archive.

Testing

  • Tested locally
  • Added/updated tests
  • Added/updated docs

Validated locally:

  • git diff --check upstream/main...HEAD
  • swift test --filter KernelServiceTests
  • swift test --filter ConfigurationLoaderTests

@kasc0206kasc0206 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查意见 / Review

This is a well-crafted security improvement. The SHA-256 digest verification for kernel downloads is an important addition.

优点 / Strengths

  1. End-to-end implementation: From CLI flag → config → XPC message → server-side verification, the full pipeline is covered
  2. Backward compatible: sha256 is String? with default nil, so existing configs work unchanged
  3. Smart defaults: Auto-populates the default SHA-256 when using the default kernel URL, while allowing custom values for custom URLs
  4. No breaking API changes: All new parameters have sensible defaults
  5. Proper error messaging: The --sha256 can only be used with --tar

Suggestion

  • Consider adding a --sha256-verify flag with --sha256-verify=false to allow users to explicitly skip verification, rather than relying on nil meaning "no verification". But this is optional — current behavior is reasonable.

已验证 / Verified

  • Code compiles with swift build
  • Follows existing project patterns (XPC keys, Config struct, ProgressBar)

Great contribution!

@kasc0206

Copy link
Copy Markdown

Code Review Summary / 代码审查摘要

Strengths / 优点

  1. End-to-end implementation: From CLI flag (--sha256) → Config (sha256 in TOML) → XPC message → server-side verification, the full pipeline is covered
    端到端实现:从 CLI 参数到配置到 XPC 消息到服务端校验,完整链路覆盖
  2. Backward compatible: sha256 is String? with default nil — existing configs work unchanged
    向后兼容:sha256 为可选的 String? 类型,现有配置无需修改
  3. Smart defaults: Auto-populates the default SHA-256 when using the default kernel URL, while allowing custom values for custom URLs
    智能默认值:使用默认内核 URL 时自动填充默认 SHA-256,自定义 URL 可自行指定
  4. No breaking API changes to callers: All new parameters have sensible defaults
    无破坏性 API 变更:所有新参数都有合理的默认值
  5. CLI validation: --sha256 correctly rejected when used without --tar
    CLI 参数校验--sha256 不带 --tar 时正确报错

Suggestion / 建议

Consider adding a --sha256-verify / --sha256-verify=false flag to let users explicitly skip verification. Currently, nil means "no verification", which is reasonable but implicit.
考虑增加 --sha256-verify 标志,让用户能显式关闭校验。当前 nil 表示"不校验"是合理的但较隐晦。

Verification / 验证

  • Code compiles with swift build
  • Follows existing project patterns (XPC keys config, ProgressBar, Config struct)

Great contribution! / 优秀的贡献!

@katiewasnothere

Copy link
Copy Markdown
Contributor

@haoruilee Thank you for the contribution! Could you add commit signatures to your commits? See https://github.com/apple/containerization/blob/main/CONTRIBUTING.md#pull-requests

We definitely want this change, but I think we should change the flags, config fields, and XPC key names to exclude the name of the algorithm so we can support other hashing algorithms in the future. Maybe we should call this just checksum instead? What do you think?

@briansmith

Copy link
Copy Markdown

Perhaps then the values should be prefixed with the digest algorithm.

Perhaps it is worth just copying Subresource Integrity syntax, using "integrity" instead of "checksum" and prefixing the digest algorithm name with a dash in the values, e.g. integrity: "sha256-f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91" instead of sha256 = "f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91" that the PR currently implements.

(Note that using a dash instead of colon prevents any ambiguity with content-addressible-by-digest URI schemes like sha256: that.)

@haoruilee

Copy link
Copy Markdown
ContributorAuthor

Hi @katiewasnothere@briansmith ,

Thanks, that makes sense. I read these suggestions as complementary, the external names should avoid hard coding SHA-256, while the value itself should still carry the digest algorithm.

I’ll update the PR to archive this and add commit signatures and revise the tests and docs accordingly.

@haoruilee
haoruileeforce-pushed the feat/kernelintegrity branch from d24095e to 76e26b9CompareJune 30, 2026 08:14
@haoruileehaoruilee changed the title Verify kernel archive SHA-256 digestsVerify kernel archive integrityJun 30, 2026
@haoruilee
haoruileeforce-pushed the feat/kernelintegrity branch from 76e26b9 to ea5ccebCompareJune 30, 2026 08:17
@haoruilee

Copy link
Copy Markdown
ContributorAuthor

Hi @katiewasnothere

Updated, thanks for your suggestion. I renamed the API surface from sha256 to integrity, with values using sha256-<hex> syntax, and updated tests/docs/PR description accordingly.

Could you please take another look when you have a chance?

@katiewasnothere

Copy link
Copy Markdown
Contributor

@haoruilee Thanks for the update! @briansmith this is a good suggestion, but naming the flag integrity and using a algorithm-hex format is inconsistent with other similar cases in this repo. Thinking about it some more, I think we should name the flag digest and use the format algorithm:hex.

@haoruilee

Copy link
Copy Markdown
ContributorAuthor

Hi @katiewasnothere ,

Thanks, I’ve updated the PR to rename the flag field to digest and use the algorithm:hex format. Docs and tests are updated as well.

Comment threaddocs/container-system-config.md Outdated
|--------------|-----------|--------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| `binaryPath` | `String` | `"opt/kata/share/kata-containers/vmlinux-6.18.15-186"` | Path **inside** the downloaded kernel archive that points to the kernel binary. |
| `url` | `URL` | `"https://github.com/kata-containers/kata-containers/releases/download/3.28.0/kata-static-3.28.0-arm64.tar.zst"` | Archive to download when no kernel is installed. Encoded and decoded as a plain string in TOML. |
| `digest` | `String?` | `"sha256:f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91"` | Expected digest for the archive, for example `sha256:<hex>`. When unset for a custom URL, remote kernel downloads are not verified. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should require that a digest is provided

@github-actions

github-actionsBot commented Jul 1, 2026

Copy link
Copy Markdown

Code Coverage

TierLine Coverage
Unit23.42%
Integration66.54%
Combined75.48%

}

@Test func customKernelURLWithoutDigestLeavesDigestUnset() async throws {
@Test func customKernelURLWithoutDigestThrows() async throws {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about a test where the digest doesn't match the file contents?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about tests where:

  1. The digest algorithm is "sha1" (should fail).
  2. The digest algorithm is "sha256" and the digest is correct but truncated by one byte (should fail).
  3. The digest algorithm is "sha256" but the digest value is a correct SHA-1 (should fail).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻 The archive content mismatch case is covered by installKernelFromLocalTarRejectsDigestMismatchWithoutInstalling, and I added coverage for sha1, truncated sha256, and a valid SHA-1 value passed as sha256.

binaryPath: String = defaultBinaryPath,
url: URL = defaultURL
) {
public init(binaryPath: String = defaultBinaryPath) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we want this init?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, the extra overload isn’t needed. I collapsed this into a single initializer with defaults for binaryPath, url, and digest.

self.digest = Self.defaultDigest
}

public init(binaryPath: String = defaultBinaryPath, url: URL, digest: String) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

url and digest should have their default values set here

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 💯

Comment on lines +211 to +215
let digestResult = try provider.value(forKey: AbsoluteConfigKey(ConfigKey("kernel.digest")), type: .string)
guard digestResult.value != nil else {
throw ContainerizationError(
.invalidArgument,
message: "kernel.digest is required in '\(path)' when kernel.url configures a custom archive"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could check for this in the decoder instead of having a custom validation function. What do you think?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the decoder check is still useful, but not sufficient for the layered config case.

The rule I’m trying to enforce is: if a config layer overrides kernel.url with a non-default archive, that same layer must also provide the digest for that archive. The digest is tied to the archive contents, so it should not be inherited from another layer.

The decoder only sees the merged snapshot, so it can tell whether the final config has both kernel.url and kernel.digest, but it can’t tell which file each value came from. That means it would accept a custom URL from a higher precedence file paired with the default digest from a lower precedence file, which is the case this validation is meant to reject.

I kept this as a premerge loader validation and added a comment to make that layering requirement clearer.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rule I’m trying to enforce is: if a config layer overrides kernel.url with a non-default archive, that same layer must also provide the digest for that archive.

In my opinion it doesn't matter if we get the kernel url and kernel digest from different layers in a layered config case. If the digest does not match the kernel url's contents when we go to fetch the kernel, the user will get an error. I could see having the values in separate layers being useful. Is there a specific scenario you want to prevent with this?

let fileIOThreadPool = NIOThreadPool(numberOfThreads: 1)
fileIOThreadPool.start()

let delegate = try HashingFileDownloadDelegate(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A follow up to this PR or future work could be to look into if we can do something similar to what is done for fetching image blobs here

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, that seems worth exploring as follow up work. I’d keep it separate from this PR since this one is focused on kernel archive integrity.

}
}

static func verifyDigest(of file: URL, expected: String) throws {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this function when we have the one below on line 195?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I removed the extra string overload and updated the tests.


// Mirrors AsyncHTTPClient's file download delegate while updating an optional
// SHA-256 hasher from the same response chunks that are written to disk.
private final class HashingFileDownloadDelegate: @unchecked Sendable, HTTPClientResponseDelegate {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After talking with others, I think for now we should just remove this in favor of downloading the whole tar in the kernel service and then calling verifyDigest(of file: URL, expected: ExpectedDigest) to incrementally compute the hash like we do if there's a local tar. This will make this PR more scoped and we can optimize further later by looking into the suggestion here. After removing this part, I'm ready to approve.

@katiewasnotherekatiewasnothere left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thank you!

@katiewasnothere
katiewasnothere merged commit 57b07fa into apple:mainJul 13, 2026
3 checks passed
saehejkang pushed a commit to saehejkang/container that referenced this pull request Jul 16, 2026
Closesapple#1687
The default kernel archive is downloaded from a remote release URL
during first-run setup and via `container system kernel set
--recommended`. Previously, the archive contents were not verified after
download, so integrity depended on HTTPS and the release artifact
remaining unchanged.
This change adds digest verification for kernel archives. The
recommended/default kernel now has pinned digest metadata using an
algorithm-prefixed value such as `sha256:<hex>`. `container system
kernel set --tar` accepts `--digest`; remote tar URLs require it, and
local tar archives can also be verified before unpacking and
installation.
The system config also supports `kernel.digest`, and a custom
`kernel.url` must provide a digest for that archive.
andrewkomkov added a commit to getgantry/gantry that referenced this pull request Aug 1, 2026
…ot args (#14)
apple/container **1.2.0** is out (previously tracked: `1.1.0`).
Upstream notes: https://github.com/apple/container/releases/tag/1.2.0 —
mirrored in `docs/upstream/apple-container-1.2.0.md`.
## Review checklist
- [ ] New or changed CLI flags Gantry should surface (`container
run/create/machine/build`)
- [ ] Changed `--format json` shapes the DockerKit apple transport
decodes
- [ ] Fixed upstream bugs Gantry currently works around
- [ ] `ContainerTooling.recommendedVersion` / feature gates need moving
to `1.2.0`
- [ ] MCP tools and App Intents that expose the affected commands
- [ ] README and CHANGELOG entries for whatever is adopted
Merging records the version as reviewed. Implement the adopted parts on
this branch, or merge as-is and open follow-ups.
---
<details><summary>Upstream release notes</summary>
## What's Changed
* Add TestCLISystemLogs and TestCLITermIO integration tests in new
integration test suite by @katiewasnothere in
apple/container#1879
* Restore reverted migrations, migrate last tests. by @jglogan in
apple/container#1880
* Removes obsolete CLITests directory. by @jglogan in
apple/container#1886
* Integration coverage xpc helpers by @noah-thor in
apple/container#1551
* Upgrade grpc-swift-nio-transport to 2.9.0 and remove HTTP2ConnectBuff…
by @adityabagchi24 in apple/container#1790
* Updates containerization to 0.36.0. by @jglogan in
apple/container#1912
* Use containerization version 0.37.0 by @adityaramani in
apple/container#1932
* Verify kernel archive integrity by @haoruilee in
apple/container#1703
* Add commit/issue alert to PR template. by @jglogan in
apple/container#1945
* Remove `--skip-build` from test Makefile target. by @jglogan in
apple/container#1951
* Restore `--skip-build`, enable `import testable` for release builds.
by @jglogan in apple/container#1955
* [package]: bump container-builder-shim to 0.13.0 by @saehejkang in
apple/container#1953
* Validate container ID from XPC requests by @katiewasnothere in
apple/container#1956
* Remove force unwraps on XPC error set/get by @katiewasnothere in
apple/container#1958
* Do not follow destination symlink when copying user configuration by
@katiewasnothere in apple/container#1957
* Fix machine ID length test. by @jglogan in
apple/container#1971
* Address flaky TestCLIKernelSetSerial suite. by @jglogan in
apple/container#1976
* [gitignore]: ignore vscode workspace files by @saehejkang in
apple/container#1966
* Update containerization dependency with new EXT4Unpacker func
definition by @katiewasnothere in
apple/container#1973
* Periodic dependency updates. by @jglogan in
apple/container#1981
* Use ordered journal mode for unpacked images. by @jglogan in
apple/container#1974
* Reword DNS container name resolution doc information by
@katiewasnothere in apple/container#1960
* ci: bump the github-actions group across 1 directory with 3 updates by
@dependabot[bot] in apple/container#1983
* Pass build config in when building protoc dependencies by
@katiewasnothere in apple/container#1972
* Container test fixture package by @katiewasnothere in
apple/container#1887
* Downgrade swift-collections to 1.5.1. by @jglogan in
apple/container#1984
* Use `enum` for warmup images. by @jglogan in
apple/container#1990
* Add missing dependencies to new ContainerTestSupport package by
@katiewasnothere in apple/container#1994
* Add OCI maskedPaths and readonlyPaths support to Container API. by
@jglogan in apple/container#1996
* Integration test - miscellaneous fixture and test refinements. by
@jglogan in apple/container#1993
* Use log instead of print for system start status messages by
@adityabagchi24 in apple/container#1889
* Fix BuilderStart race, parallelize `container build` tests. by
@jglogan in apple/container#2002
* Allow custom kernel boot args via --kernel-arg by @arirubinstein in
apple/container#1744
* fix: Increase XPC timeout for Machine API operations by @dev-kvt in
apple/container#2006
* Update containerization import to latest 0.40.0 by @katiewasnothere in
apple/container#2028
* Fix image env vars, build context checks, TCP/UDP port forward buffer,
and validate plugin name by @katiewasnothere in
apple/container#2027
* Update containerization import to 0.40.1 by @katiewasnothere in
apple/container#2038
## New Contributors
* @haoruilee made their first contribution in
apple/container#1703
* @arirubinstein made their first contribution in
apple/container#1744
* @dev-kvt made their first contribution in
apple/container#2006
**Full Changelog**:
apple/container@1.1.0...1.2.0
</details>
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Andrew <Andrew.Komkov@gmail.com>
henrywang added a commit to henrywang/Berthly that referenced this pull request Aug 4, 2026
## Summary
- container 1.2.0 added digest verification for kernel archives
(apple/container#1703): `kernel set --tar` accepts `--digest`, remote
tar URLs require one, and the recommended default kernel now ships
pinned sha256 digest metadata. containerization's `KernelConfig` gained
a required `digest` field, and `ClientKernel.installKernelFromTar` an
`expectedDigest` parameter — both reachable from Berthly's native XPC
calls.
- `KernelSetOptions` gained `digest: String?`, threaded into
`setKernel(options:)`'s `installKernelFromTar` call.
- `installDefaultKernelIfNeeded` now passes `expectedDigest:
containerSystemConfig.kernel.digest` — the default kernel already
carries a pinned digest from upstream.
- New "Digest" field on the Set Kernel sheet's Tar Archive section,
required only for remote URLs (matching upstream's exact rule),
prefilled by "Use Recommended" alongside the URL.
- `PARITY.md`'s System `kernel` row updated in the same commit.
## Why
Part of the container 1.2.0 milestone (#79). Mirrors the security
posture Berthly already applies elsewhere (pkg signature verification
against a pinned Apple identity) — kernel archives were the one download
path left unverified.
Closes#79
## Test plan
- [x] `xcodebuild build` succeeds
- [x] `xcodebuild test -only-testing:BerthlyTests` — full suite passes,
including new `kernelDigest` coverage in `SystemConfigMappingTests`
- [x] `swiftlint lint --strict` — 0 violations
- [x] Verified visually via Xcode's live preview renderer — confirmed
the Tar Archive section (including the recommended-URL prefill) renders
correctly
henrywang added a commit to henrywang/Berthly that referenced this pull request Aug 4, 2026
## Summary
- `resolvedSystemConfig()` used `try?` around
`ConfigurationLoader.load()`, so any decode failure silently fell back
to an all-defaults `ContainerSystemConfig` — not just the field that
failed, the user's entire `config.toml` (DNS, build settings, kernel,
everything).
- `ConfigurationLoader.load()` only throws when a config file exists on
disk but fails to parse/decode; it already returns cleanly with defaults
when no file is present at all — so every error caught here represents
real discarded user data, not an absent-file no-op.
- container 1.2.0 made this concrete: `KernelConfig`'s decoder now
throws when `kernel.url` is customized without a paired `kernel.digest`
(apple/container#1703). Any Berthly user who ran `container system
kernel set --tar <url>` pre-1.2.0 has exactly that config shape, and
would silently lose their whole system config on next load once the
daemon is upgraded.
- Extracts the load-outcome handling into a pure, testable
`mapSystemConfigLoadResult(_:)` and surfaces a failure through the
existing `lastStartupWarning` mechanism instead of swallowing it.
## Why
Found while implementing #79 (kernel digest verification) — this
milestone's own version bump is what triggers the failure for affected
users, so it needs fixing in the same milestone.
Closes#87
## Test plan
- [x] `xcodebuild build` succeeds
- [x] `xcodebuild test -only-testing:BerthlyTests` — full suite passes,
including new `SystemConfigLoadResultMappingTests` covering both the
success and failure paths
- [x] `swiftlint lint --strict` — 0 violations
- [x] No View files touched — no UI test needed for this change (tracked
separately in #89: the sidebar's warning-state rendering itself has no
UI coverage yet, pre-existing gap this PR surfaces a second producer
into)
jianliang00 pushed a commit to jianliang00/container that referenced this pull request Aug 28, 2026
Closesapple#1687
The default kernel archive is downloaded from a remote release URL
during first-run setup and via `container system kernel set
--recommended`. Previously, the archive contents were not verified after
download, so integrity depended on HTTPS and the release artifact
remaining unchanged.
This change adds digest verification for kernel archives. The
recommended/default kernel now has pinned digest metadata using an
algorithm-prefixed value such as `sha256:<hex>`. `container system
kernel set --tar` accepts `--digest`; remote tar URLs require it, and
local tar archives can also be verified before unpacking and
installation.
The system config also supports `kernel.digest`, and a custom
`kernel.url` must provide a digest for that archive.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Request]: Check kernel download integrity when downloading

5 participants

@haoruilee@kasc0206@katiewasnothere@briansmith@jglogan
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Verify kernel archive integrity by haoruilee · Pull Request #1703 · apple/container · GitHub
Skip to content

Verify kernel archive integrity - #1703

Merged
katiewasnothere merged 7 commits into
apple:mainfrom
haoruilee:feat/kernelintegrity
Jul 13, 2026
Merged

Verify kernel archive integrity#1703
katiewasnothere merged 7 commits into
apple:mainfrom
haoruilee:feat/kernelintegrity

Conversation

@haoruilee

@haoruileehaoruilee commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Motivation and Context

Closes#1687

The default kernel archive is downloaded from a remote release URL during first-run setup and via container system kernel set --recommended. Previously, the archive contents were not verified after download, so integrity depended on HTTPS and the release artifact remaining unchanged.

This change adds digest verification for kernel archives. The recommended/default kernel now has pinned digest metadata using an algorithm-prefixed value such as sha256:<hex>. container system kernel set --tar accepts --digest; remote tar URLs require it, and local tar archives can also be verified before unpacking and installation.

The system config also supports kernel.digest, and a custom kernel.url must provide a digest for that archive.

Testing

  • Tested locally
  • Added/updated tests
  • Added/updated docs

Validated locally:

  • git diff --check upstream/main...HEAD
  • swift test --filter KernelServiceTests
  • swift test --filter ConfigurationLoaderTests

@kasc0206kasc0206 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查意见 / Review

This is a well-crafted security improvement. The SHA-256 digest verification for kernel downloads is an important addition.

优点 / Strengths

  1. End-to-end implementation: From CLI flag → config → XPC message → server-side verification, the full pipeline is covered
  2. Backward compatible: sha256 is String? with default nil, so existing configs work unchanged
  3. Smart defaults: Auto-populates the default SHA-256 when using the default kernel URL, while allowing custom values for custom URLs
  4. No breaking API changes: All new parameters have sensible defaults
  5. Proper error messaging: The --sha256 can only be used with --tar

Suggestion

  • Consider adding a --sha256-verify flag with --sha256-verify=false to allow users to explicitly skip verification, rather than relying on nil meaning "no verification". But this is optional — current behavior is reasonable.

已验证 / Verified

  • Code compiles with swift build
  • Follows existing project patterns (XPC keys, Config struct, ProgressBar)

Great contribution!

@kasc0206

Copy link
Copy Markdown

Code Review Summary / 代码审查摘要

Strengths / 优点

  1. End-to-end implementation: From CLI flag (--sha256) → Config (sha256 in TOML) → XPC message → server-side verification, the full pipeline is covered
    端到端实现:从 CLI 参数到配置到 XPC 消息到服务端校验,完整链路覆盖
  2. Backward compatible: sha256 is String? with default nil — existing configs work unchanged
    向后兼容:sha256 为可选的 String? 类型,现有配置无需修改
  3. Smart defaults: Auto-populates the default SHA-256 when using the default kernel URL, while allowing custom values for custom URLs
    智能默认值:使用默认内核 URL 时自动填充默认 SHA-256,自定义 URL 可自行指定
  4. No breaking API changes to callers: All new parameters have sensible defaults
    无破坏性 API 变更:所有新参数都有合理的默认值
  5. CLI validation: --sha256 correctly rejected when used without --tar
    CLI 参数校验--sha256 不带 --tar 时正确报错

Suggestion / 建议

Consider adding a --sha256-verify / --sha256-verify=false flag to let users explicitly skip verification. Currently, nil means "no verification", which is reasonable but implicit.
考虑增加 --sha256-verify 标志,让用户能显式关闭校验。当前 nil 表示"不校验"是合理的但较隐晦。

Verification / 验证

  • Code compiles with swift build
  • Follows existing project patterns (XPC keys config, ProgressBar, Config struct)

Great contribution! / 优秀的贡献!

@katiewasnothere

Copy link
Copy Markdown
Contributor

@haoruilee Thank you for the contribution! Could you add commit signatures to your commits? See https://github.com/apple/containerization/blob/main/CONTRIBUTING.md#pull-requests

We definitely want this change, but I think we should change the flags, config fields, and XPC key names to exclude the name of the algorithm so we can support other hashing algorithms in the future. Maybe we should call this just checksum instead? What do you think?

@briansmith

Copy link
Copy Markdown

Perhaps then the values should be prefixed with the digest algorithm.

Perhaps it is worth just copying Subresource Integrity syntax, using "integrity" instead of "checksum" and prefixing the digest algorithm name with a dash in the values, e.g. integrity: "sha256-f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91" instead of sha256 = "f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91" that the PR currently implements.

(Note that using a dash instead of colon prevents any ambiguity with content-addressible-by-digest URI schemes like sha256: that.)

@haoruilee

Copy link
Copy Markdown
ContributorAuthor

Hi @katiewasnothere@briansmith ,

Thanks, that makes sense. I read these suggestions as complementary, the external names should avoid hard coding SHA-256, while the value itself should still carry the digest algorithm.

I’ll update the PR to archive this and add commit signatures and revise the tests and docs accordingly.

@haoruilee
haoruileeforce-pushed the feat/kernelintegrity branch from d24095e to 76e26b9CompareJune 30, 2026 08:14
@haoruileehaoruilee changed the title Verify kernel archive SHA-256 digestsVerify kernel archive integrityJun 30, 2026
@haoruilee
haoruileeforce-pushed the feat/kernelintegrity branch from 76e26b9 to ea5ccebCompareJune 30, 2026 08:17
@haoruilee

Copy link
Copy Markdown
ContributorAuthor

Hi @katiewasnothere

Updated, thanks for your suggestion. I renamed the API surface from sha256 to integrity, with values using sha256-<hex> syntax, and updated tests/docs/PR description accordingly.

Could you please take another look when you have a chance?

@katiewasnothere

Copy link
Copy Markdown
Contributor

@haoruilee Thanks for the update! @briansmith this is a good suggestion, but naming the flag integrity and using a algorithm-hex format is inconsistent with other similar cases in this repo. Thinking about it some more, I think we should name the flag digest and use the format algorithm:hex.

@haoruilee

Copy link
Copy Markdown
ContributorAuthor

Hi @katiewasnothere ,

Thanks, I’ve updated the PR to rename the flag field to digest and use the algorithm:hex format. Docs and tests are updated as well.

Comment threaddocs/container-system-config.md Outdated
|--------------|-----------|--------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| `binaryPath` | `String` | `"opt/kata/share/kata-containers/vmlinux-6.18.15-186"` | Path **inside** the downloaded kernel archive that points to the kernel binary. |
| `url` | `URL` | `"https://github.com/kata-containers/kata-containers/releases/download/3.28.0/kata-static-3.28.0-arm64.tar.zst"` | Archive to download when no kernel is installed. Encoded and decoded as a plain string in TOML. |
| `digest` | `String?` | `"sha256:f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91"` | Expected digest for the archive, for example `sha256:<hex>`. When unset for a custom URL, remote kernel downloads are not verified. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should require that a digest is provided

@github-actions

github-actionsBot commented Jul 1, 2026

Copy link
Copy Markdown

Code Coverage

TierLine Coverage
Unit23.42%
Integration66.54%
Combined75.48%

}

@Test func customKernelURLWithoutDigestLeavesDigestUnset() async throws {
@Test func customKernelURLWithoutDigestThrows() async throws {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about a test where the digest doesn't match the file contents?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about tests where:

  1. The digest algorithm is "sha1" (should fail).
  2. The digest algorithm is "sha256" and the digest is correct but truncated by one byte (should fail).
  3. The digest algorithm is "sha256" but the digest value is a correct SHA-1 (should fail).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻 The archive content mismatch case is covered by installKernelFromLocalTarRejectsDigestMismatchWithoutInstalling, and I added coverage for sha1, truncated sha256, and a valid SHA-1 value passed as sha256.

binaryPath: String = defaultBinaryPath,
url: URL = defaultURL
) {
public init(binaryPath: String = defaultBinaryPath) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we want this init?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, the extra overload isn’t needed. I collapsed this into a single initializer with defaults for binaryPath, url, and digest.

self.digest = Self.defaultDigest
}

public init(binaryPath: String = defaultBinaryPath, url: URL, digest: String) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

url and digest should have their default values set here

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 💯

Comment on lines +211 to +215
let digestResult = try provider.value(forKey: AbsoluteConfigKey(ConfigKey("kernel.digest")), type: .string)
guard digestResult.value != nil else {
throw ContainerizationError(
.invalidArgument,
message: "kernel.digest is required in '\(path)' when kernel.url configures a custom archive"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could check for this in the decoder instead of having a custom validation function. What do you think?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the decoder check is still useful, but not sufficient for the layered config case.

The rule I’m trying to enforce is: if a config layer overrides kernel.url with a non-default archive, that same layer must also provide the digest for that archive. The digest is tied to the archive contents, so it should not be inherited from another layer.

The decoder only sees the merged snapshot, so it can tell whether the final config has both kernel.url and kernel.digest, but it can’t tell which file each value came from. That means it would accept a custom URL from a higher precedence file paired with the default digest from a lower precedence file, which is the case this validation is meant to reject.

I kept this as a premerge loader validation and added a comment to make that layering requirement clearer.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rule I’m trying to enforce is: if a config layer overrides kernel.url with a non-default archive, that same layer must also provide the digest for that archive.

In my opinion it doesn't matter if we get the kernel url and kernel digest from different layers in a layered config case. If the digest does not match the kernel url's contents when we go to fetch the kernel, the user will get an error. I could see having the values in separate layers being useful. Is there a specific scenario you want to prevent with this?

let fileIOThreadPool = NIOThreadPool(numberOfThreads: 1)
fileIOThreadPool.start()

let delegate = try HashingFileDownloadDelegate(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A follow up to this PR or future work could be to look into if we can do something similar to what is done for fetching image blobs here

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, that seems worth exploring as follow up work. I’d keep it separate from this PR since this one is focused on kernel archive integrity.

}
}

static func verifyDigest(of file: URL, expected: String) throws {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this function when we have the one below on line 195?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I removed the extra string overload and updated the tests.


// Mirrors AsyncHTTPClient's file download delegate while updating an optional
// SHA-256 hasher from the same response chunks that are written to disk.
private final class HashingFileDownloadDelegate: @unchecked Sendable, HTTPClientResponseDelegate {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After talking with others, I think for now we should just remove this in favor of downloading the whole tar in the kernel service and then calling verifyDigest(of file: URL, expected: ExpectedDigest) to incrementally compute the hash like we do if there's a local tar. This will make this PR more scoped and we can optimize further later by looking into the suggestion here. After removing this part, I'm ready to approve.

@katiewasnotherekatiewasnothere left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thank you!

@katiewasnothere
katiewasnothere merged commit 57b07fa into apple:mainJul 13, 2026
3 checks passed
saehejkang pushed a commit to saehejkang/container that referenced this pull request Jul 16, 2026
Closesapple#1687
The default kernel archive is downloaded from a remote release URL
during first-run setup and via `container system kernel set
--recommended`. Previously, the archive contents were not verified after
download, so integrity depended on HTTPS and the release artifact
remaining unchanged.
This change adds digest verification for kernel archives. The
recommended/default kernel now has pinned digest metadata using an
algorithm-prefixed value such as `sha256:<hex>`. `container system
kernel set --tar` accepts `--digest`; remote tar URLs require it, and
local tar archives can also be verified before unpacking and
installation.
The system config also supports `kernel.digest`, and a custom
`kernel.url` must provide a digest for that archive.
andrewkomkov added a commit to getgantry/gantry that referenced this pull request Aug 1, 2026
…ot args (#14)
apple/container **1.2.0** is out (previously tracked: `1.1.0`).
Upstream notes: https://github.com/apple/container/releases/tag/1.2.0 —
mirrored in `docs/upstream/apple-container-1.2.0.md`.
## Review checklist
- [ ] New or changed CLI flags Gantry should surface (`container
run/create/machine/build`)
- [ ] Changed `--format json` shapes the DockerKit apple transport
decodes
- [ ] Fixed upstream bugs Gantry currently works around
- [ ] `ContainerTooling.recommendedVersion` / feature gates need moving
to `1.2.0`
- [ ] MCP tools and App Intents that expose the affected commands
- [ ] README and CHANGELOG entries for whatever is adopted
Merging records the version as reviewed. Implement the adopted parts on
this branch, or merge as-is and open follow-ups.
---
<details><summary>Upstream release notes</summary>
## What's Changed
* Add TestCLISystemLogs and TestCLITermIO integration tests in new
integration test suite by @katiewasnothere in
apple/container#1879
* Restore reverted migrations, migrate last tests. by @jglogan in
apple/container#1880
* Removes obsolete CLITests directory. by @jglogan in
apple/container#1886
* Integration coverage xpc helpers by @noah-thor in
apple/container#1551
* Upgrade grpc-swift-nio-transport to 2.9.0 and remove HTTP2ConnectBuff…
by @adityabagchi24 in apple/container#1790
* Updates containerization to 0.36.0. by @jglogan in
apple/container#1912
* Use containerization version 0.37.0 by @adityaramani in
apple/container#1932
* Verify kernel archive integrity by @haoruilee in
apple/container#1703
* Add commit/issue alert to PR template. by @jglogan in
apple/container#1945
* Remove `--skip-build` from test Makefile target. by @jglogan in
apple/container#1951
* Restore `--skip-build`, enable `import testable` for release builds.
by @jglogan in apple/container#1955
* [package]: bump container-builder-shim to 0.13.0 by @saehejkang in
apple/container#1953
* Validate container ID from XPC requests by @katiewasnothere in
apple/container#1956
* Remove force unwraps on XPC error set/get by @katiewasnothere in
apple/container#1958
* Do not follow destination symlink when copying user configuration by
@katiewasnothere in apple/container#1957
* Fix machine ID length test. by @jglogan in
apple/container#1971
* Address flaky TestCLIKernelSetSerial suite. by @jglogan in
apple/container#1976
* [gitignore]: ignore vscode workspace files by @saehejkang in
apple/container#1966
* Update containerization dependency with new EXT4Unpacker func
definition by @katiewasnothere in
apple/container#1973
* Periodic dependency updates. by @jglogan in
apple/container#1981
* Use ordered journal mode for unpacked images. by @jglogan in
apple/container#1974
* Reword DNS container name resolution doc information by
@katiewasnothere in apple/container#1960
* ci: bump the github-actions group across 1 directory with 3 updates by
@dependabot[bot] in apple/container#1983
* Pass build config in when building protoc dependencies by
@katiewasnothere in apple/container#1972
* Container test fixture package by @katiewasnothere in
apple/container#1887
* Downgrade swift-collections to 1.5.1. by @jglogan in
apple/container#1984
* Use `enum` for warmup images. by @jglogan in
apple/container#1990
* Add missing dependencies to new ContainerTestSupport package by
@katiewasnothere in apple/container#1994
* Add OCI maskedPaths and readonlyPaths support to Container API. by
@jglogan in apple/container#1996
* Integration test - miscellaneous fixture and test refinements. by
@jglogan in apple/container#1993
* Use log instead of print for system start status messages by
@adityabagchi24 in apple/container#1889
* Fix BuilderStart race, parallelize `container build` tests. by
@jglogan in apple/container#2002
* Allow custom kernel boot args via --kernel-arg by @arirubinstein in
apple/container#1744
* fix: Increase XPC timeout for Machine API operations by @dev-kvt in
apple/container#2006
* Update containerization import to latest 0.40.0 by @katiewasnothere in
apple/container#2028
* Fix image env vars, build context checks, TCP/UDP port forward buffer,
and validate plugin name by @katiewasnothere in
apple/container#2027
* Update containerization import to 0.40.1 by @katiewasnothere in
apple/container#2038
## New Contributors
* @haoruilee made their first contribution in
apple/container#1703
* @arirubinstein made their first contribution in
apple/container#1744
* @dev-kvt made their first contribution in
apple/container#2006
**Full Changelog**:
apple/container@1.1.0...1.2.0
</details>
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Andrew <Andrew.Komkov@gmail.com>
henrywang added a commit to henrywang/Berthly that referenced this pull request Aug 4, 2026
## Summary
- container 1.2.0 added digest verification for kernel archives
(apple/container#1703): `kernel set --tar` accepts `--digest`, remote
tar URLs require one, and the recommended default kernel now ships
pinned sha256 digest metadata. containerization's `KernelConfig` gained
a required `digest` field, and `ClientKernel.installKernelFromTar` an
`expectedDigest` parameter — both reachable from Berthly's native XPC
calls.
- `KernelSetOptions` gained `digest: String?`, threaded into
`setKernel(options:)`'s `installKernelFromTar` call.
- `installDefaultKernelIfNeeded` now passes `expectedDigest:
containerSystemConfig.kernel.digest` — the default kernel already
carries a pinned digest from upstream.
- New "Digest" field on the Set Kernel sheet's Tar Archive section,
required only for remote URLs (matching upstream's exact rule),
prefilled by "Use Recommended" alongside the URL.
- `PARITY.md`'s System `kernel` row updated in the same commit.
## Why
Part of the container 1.2.0 milestone (#79). Mirrors the security
posture Berthly already applies elsewhere (pkg signature verification
against a pinned Apple identity) — kernel archives were the one download
path left unverified.
Closes#79
## Test plan
- [x] `xcodebuild build` succeeds
- [x] `xcodebuild test -only-testing:BerthlyTests` — full suite passes,
including new `kernelDigest` coverage in `SystemConfigMappingTests`
- [x] `swiftlint lint --strict` — 0 violations
- [x] Verified visually via Xcode's live preview renderer — confirmed
the Tar Archive section (including the recommended-URL prefill) renders
correctly
henrywang added a commit to henrywang/Berthly that referenced this pull request Aug 4, 2026
## Summary
- `resolvedSystemConfig()` used `try?` around
`ConfigurationLoader.load()`, so any decode failure silently fell back
to an all-defaults `ContainerSystemConfig` — not just the field that
failed, the user's entire `config.toml` (DNS, build settings, kernel,
everything).
- `ConfigurationLoader.load()` only throws when a config file exists on
disk but fails to parse/decode; it already returns cleanly with defaults
when no file is present at all — so every error caught here represents
real discarded user data, not an absent-file no-op.
- container 1.2.0 made this concrete: `KernelConfig`'s decoder now
throws when `kernel.url` is customized without a paired `kernel.digest`
(apple/container#1703). Any Berthly user who ran `container system
kernel set --tar <url>` pre-1.2.0 has exactly that config shape, and
would silently lose their whole system config on next load once the
daemon is upgraded.
- Extracts the load-outcome handling into a pure, testable
`mapSystemConfigLoadResult(_:)` and surfaces a failure through the
existing `lastStartupWarning` mechanism instead of swallowing it.
## Why
Found while implementing #79 (kernel digest verification) — this
milestone's own version bump is what triggers the failure for affected
users, so it needs fixing in the same milestone.
Closes#87
## Test plan
- [x] `xcodebuild build` succeeds
- [x] `xcodebuild test -only-testing:BerthlyTests` — full suite passes,
including new `SystemConfigLoadResultMappingTests` covering both the
success and failure paths
- [x] `swiftlint lint --strict` — 0 violations
- [x] No View files touched — no UI test needed for this change (tracked
separately in #89: the sidebar's warning-state rendering itself has no
UI coverage yet, pre-existing gap this PR surfaces a second producer
into)
jianliang00 pushed a commit to jianliang00/container that referenced this pull request Aug 28, 2026
Closesapple#1687
The default kernel archive is downloaded from a remote release URL
during first-run setup and via `container system kernel set
--recommended`. Previously, the archive contents were not verified after
download, so integrity depended on HTTPS and the release artifact
remaining unchanged.
This change adds digest verification for kernel archives. The
recommended/default kernel now has pinned digest metadata using an
algorithm-prefixed value such as `sha256:<hex>`. `container system
kernel set --tar` accepts `--digest`; remote tar URLs require it, and
local tar archives can also be verified before unpacking and
installation.
The system config also supports `kernel.digest`, and a custom
`kernel.url` must provide a digest for that archive.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Request]: Check kernel download integrity when downloading

5 participants

@haoruilee@kasc0206@katiewasnothere@briansmith@jglogan
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Verify kernel archive integrity by haoruilee · Pull Request #1703 · apple/container · GitHub
Skip to content

Verify kernel archive integrity - #1703

Merged
katiewasnothere merged 7 commits into
apple:mainfrom
haoruilee:feat/kernelintegrity
Jul 13, 2026
Merged

Verify kernel archive integrity#1703
katiewasnothere merged 7 commits into
apple:mainfrom
haoruilee:feat/kernelintegrity

Conversation

@haoruilee

@haoruileehaoruilee commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Motivation and Context

Closes#1687

The default kernel archive is downloaded from a remote release URL during first-run setup and via container system kernel set --recommended. Previously, the archive contents were not verified after download, so integrity depended on HTTPS and the release artifact remaining unchanged.

This change adds digest verification for kernel archives. The recommended/default kernel now has pinned digest metadata using an algorithm-prefixed value such as sha256:<hex>. container system kernel set --tar accepts --digest; remote tar URLs require it, and local tar archives can also be verified before unpacking and installation.

The system config also supports kernel.digest, and a custom kernel.url must provide a digest for that archive.

Testing

  • Tested locally
  • Added/updated tests
  • Added/updated docs

Validated locally:

  • git diff --check upstream/main...HEAD
  • swift test --filter KernelServiceTests
  • swift test --filter ConfigurationLoaderTests

@kasc0206kasc0206 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查意见 / Review

This is a well-crafted security improvement. The SHA-256 digest verification for kernel downloads is an important addition.

优点 / Strengths

  1. End-to-end implementation: From CLI flag → config → XPC message → server-side verification, the full pipeline is covered
  2. Backward compatible: sha256 is String? with default nil, so existing configs work unchanged
  3. Smart defaults: Auto-populates the default SHA-256 when using the default kernel URL, while allowing custom values for custom URLs
  4. No breaking API changes: All new parameters have sensible defaults
  5. Proper error messaging: The --sha256 can only be used with --tar

Suggestion

  • Consider adding a --sha256-verify flag with --sha256-verify=false to allow users to explicitly skip verification, rather than relying on nil meaning "no verification". But this is optional — current behavior is reasonable.

已验证 / Verified

  • Code compiles with swift build
  • Follows existing project patterns (XPC keys, Config struct, ProgressBar)

Great contribution!

@kasc0206

Copy link
Copy Markdown

Code Review Summary / 代码审查摘要

Strengths / 优点

  1. End-to-end implementation: From CLI flag (--sha256) → Config (sha256 in TOML) → XPC message → server-side verification, the full pipeline is covered
    端到端实现:从 CLI 参数到配置到 XPC 消息到服务端校验,完整链路覆盖
  2. Backward compatible: sha256 is String? with default nil — existing configs work unchanged
    向后兼容:sha256 为可选的 String? 类型,现有配置无需修改
  3. Smart defaults: Auto-populates the default SHA-256 when using the default kernel URL, while allowing custom values for custom URLs
    智能默认值:使用默认内核 URL 时自动填充默认 SHA-256,自定义 URL 可自行指定
  4. No breaking API changes to callers: All new parameters have sensible defaults
    无破坏性 API 变更:所有新参数都有合理的默认值
  5. CLI validation: --sha256 correctly rejected when used without --tar
    CLI 参数校验--sha256 不带 --tar 时正确报错

Suggestion / 建议

Consider adding a --sha256-verify / --sha256-verify=false flag to let users explicitly skip verification. Currently, nil means "no verification", which is reasonable but implicit.
考虑增加 --sha256-verify 标志,让用户能显式关闭校验。当前 nil 表示"不校验"是合理的但较隐晦。

Verification / 验证

  • Code compiles with swift build
  • Follows existing project patterns (XPC keys config, ProgressBar, Config struct)

Great contribution! / 优秀的贡献!

@katiewasnothere

Copy link
Copy Markdown
Contributor

@haoruilee Thank you for the contribution! Could you add commit signatures to your commits? See https://github.com/apple/containerization/blob/main/CONTRIBUTING.md#pull-requests

We definitely want this change, but I think we should change the flags, config fields, and XPC key names to exclude the name of the algorithm so we can support other hashing algorithms in the future. Maybe we should call this just checksum instead? What do you think?

@briansmith

Copy link
Copy Markdown

Perhaps then the values should be prefixed with the digest algorithm.

Perhaps it is worth just copying Subresource Integrity syntax, using "integrity" instead of "checksum" and prefixing the digest algorithm name with a dash in the values, e.g. integrity: "sha256-f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91" instead of sha256 = "f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91" that the PR currently implements.

(Note that using a dash instead of colon prevents any ambiguity with content-addressible-by-digest URI schemes like sha256: that.)

@haoruilee

Copy link
Copy Markdown
ContributorAuthor

Hi @katiewasnothere@briansmith ,

Thanks, that makes sense. I read these suggestions as complementary, the external names should avoid hard coding SHA-256, while the value itself should still carry the digest algorithm.

I’ll update the PR to archive this and add commit signatures and revise the tests and docs accordingly.

@haoruilee
haoruileeforce-pushed the feat/kernelintegrity branch from d24095e to 76e26b9CompareJune 30, 2026 08:14
@haoruileehaoruilee changed the title Verify kernel archive SHA-256 digestsVerify kernel archive integrityJun 30, 2026
@haoruilee
haoruileeforce-pushed the feat/kernelintegrity branch from 76e26b9 to ea5ccebCompareJune 30, 2026 08:17
@haoruilee

Copy link
Copy Markdown
ContributorAuthor

Hi @katiewasnothere

Updated, thanks for your suggestion. I renamed the API surface from sha256 to integrity, with values using sha256-<hex> syntax, and updated tests/docs/PR description accordingly.

Could you please take another look when you have a chance?

@katiewasnothere

Copy link
Copy Markdown
Contributor

@haoruilee Thanks for the update! @briansmith this is a good suggestion, but naming the flag integrity and using a algorithm-hex format is inconsistent with other similar cases in this repo. Thinking about it some more, I think we should name the flag digest and use the format algorithm:hex.

@haoruilee

Copy link
Copy Markdown
ContributorAuthor

Hi @katiewasnothere ,

Thanks, I’ve updated the PR to rename the flag field to digest and use the algorithm:hex format. Docs and tests are updated as well.

Comment threaddocs/container-system-config.md Outdated
|--------------|-----------|--------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| `binaryPath` | `String` | `"opt/kata/share/kata-containers/vmlinux-6.18.15-186"` | Path **inside** the downloaded kernel archive that points to the kernel binary. |
| `url` | `URL` | `"https://github.com/kata-containers/kata-containers/releases/download/3.28.0/kata-static-3.28.0-arm64.tar.zst"` | Archive to download when no kernel is installed. Encoded and decoded as a plain string in TOML. |
| `digest` | `String?` | `"sha256:f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91"` | Expected digest for the archive, for example `sha256:<hex>`. When unset for a custom URL, remote kernel downloads are not verified. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should require that a digest is provided

@github-actions

github-actionsBot commented Jul 1, 2026

Copy link
Copy Markdown

Code Coverage

TierLine Coverage
Unit23.42%
Integration66.54%
Combined75.48%

}

@Test func customKernelURLWithoutDigestLeavesDigestUnset() async throws {
@Test func customKernelURLWithoutDigestThrows() async throws {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about a test where the digest doesn't match the file contents?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about tests where:

  1. The digest algorithm is "sha1" (should fail).
  2. The digest algorithm is "sha256" and the digest is correct but truncated by one byte (should fail).
  3. The digest algorithm is "sha256" but the digest value is a correct SHA-1 (should fail).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻 The archive content mismatch case is covered by installKernelFromLocalTarRejectsDigestMismatchWithoutInstalling, and I added coverage for sha1, truncated sha256, and a valid SHA-1 value passed as sha256.

binaryPath: String = defaultBinaryPath,
url: URL = defaultURL
) {
public init(binaryPath: String = defaultBinaryPath) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we want this init?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, the extra overload isn’t needed. I collapsed this into a single initializer with defaults for binaryPath, url, and digest.

self.digest = Self.defaultDigest
}

public init(binaryPath: String = defaultBinaryPath, url: URL, digest: String) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

url and digest should have their default values set here

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 💯

Comment on lines +211 to +215
let digestResult = try provider.value(forKey: AbsoluteConfigKey(ConfigKey("kernel.digest")), type: .string)
guard digestResult.value != nil else {
throw ContainerizationError(
.invalidArgument,
message: "kernel.digest is required in '\(path)' when kernel.url configures a custom archive"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could check for this in the decoder instead of having a custom validation function. What do you think?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the decoder check is still useful, but not sufficient for the layered config case.

The rule I’m trying to enforce is: if a config layer overrides kernel.url with a non-default archive, that same layer must also provide the digest for that archive. The digest is tied to the archive contents, so it should not be inherited from another layer.

The decoder only sees the merged snapshot, so it can tell whether the final config has both kernel.url and kernel.digest, but it can’t tell which file each value came from. That means it would accept a custom URL from a higher precedence file paired with the default digest from a lower precedence file, which is the case this validation is meant to reject.

I kept this as a premerge loader validation and added a comment to make that layering requirement clearer.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rule I’m trying to enforce is: if a config layer overrides kernel.url with a non-default archive, that same layer must also provide the digest for that archive.

In my opinion it doesn't matter if we get the kernel url and kernel digest from different layers in a layered config case. If the digest does not match the kernel url's contents when we go to fetch the kernel, the user will get an error. I could see having the values in separate layers being useful. Is there a specific scenario you want to prevent with this?

let fileIOThreadPool = NIOThreadPool(numberOfThreads: 1)
fileIOThreadPool.start()

let delegate = try HashingFileDownloadDelegate(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A follow up to this PR or future work could be to look into if we can do something similar to what is done for fetching image blobs here

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, that seems worth exploring as follow up work. I’d keep it separate from this PR since this one is focused on kernel archive integrity.

}
}

static func verifyDigest(of file: URL, expected: String) throws {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this function when we have the one below on line 195?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I removed the extra string overload and updated the tests.


// Mirrors AsyncHTTPClient's file download delegate while updating an optional
// SHA-256 hasher from the same response chunks that are written to disk.
private final class HashingFileDownloadDelegate: @unchecked Sendable, HTTPClientResponseDelegate {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After talking with others, I think for now we should just remove this in favor of downloading the whole tar in the kernel service and then calling verifyDigest(of file: URL, expected: ExpectedDigest) to incrementally compute the hash like we do if there's a local tar. This will make this PR more scoped and we can optimize further later by looking into the suggestion here. After removing this part, I'm ready to approve.

@katiewasnotherekatiewasnothere left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thank you!

@katiewasnothere
katiewasnothere merged commit 57b07fa into apple:mainJul 13, 2026
3 checks passed
saehejkang pushed a commit to saehejkang/container that referenced this pull request Jul 16, 2026
Closesapple#1687
The default kernel archive is downloaded from a remote release URL
during first-run setup and via `container system kernel set
--recommended`. Previously, the archive contents were not verified after
download, so integrity depended on HTTPS and the release artifact
remaining unchanged.
This change adds digest verification for kernel archives. The
recommended/default kernel now has pinned digest metadata using an
algorithm-prefixed value such as `sha256:<hex>`. `container system
kernel set --tar` accepts `--digest`; remote tar URLs require it, and
local tar archives can also be verified before unpacking and
installation.
The system config also supports `kernel.digest`, and a custom
`kernel.url` must provide a digest for that archive.
andrewkomkov added a commit to getgantry/gantry that referenced this pull request Aug 1, 2026
…ot args (#14)
apple/container **1.2.0** is out (previously tracked: `1.1.0`).
Upstream notes: https://github.com/apple/container/releases/tag/1.2.0 —
mirrored in `docs/upstream/apple-container-1.2.0.md`.
## Review checklist
- [ ] New or changed CLI flags Gantry should surface (`container
run/create/machine/build`)
- [ ] Changed `--format json` shapes the DockerKit apple transport
decodes
- [ ] Fixed upstream bugs Gantry currently works around
- [ ] `ContainerTooling.recommendedVersion` / feature gates need moving
to `1.2.0`
- [ ] MCP tools and App Intents that expose the affected commands
- [ ] README and CHANGELOG entries for whatever is adopted
Merging records the version as reviewed. Implement the adopted parts on
this branch, or merge as-is and open follow-ups.
---
<details><summary>Upstream release notes</summary>
## What's Changed
* Add TestCLISystemLogs and TestCLITermIO integration tests in new
integration test suite by @katiewasnothere in
apple/container#1879
* Restore reverted migrations, migrate last tests. by @jglogan in
apple/container#1880
* Removes obsolete CLITests directory. by @jglogan in
apple/container#1886
* Integration coverage xpc helpers by @noah-thor in
apple/container#1551
* Upgrade grpc-swift-nio-transport to 2.9.0 and remove HTTP2ConnectBuff…
by @adityabagchi24 in apple/container#1790
* Updates containerization to 0.36.0. by @jglogan in
apple/container#1912
* Use containerization version 0.37.0 by @adityaramani in
apple/container#1932
* Verify kernel archive integrity by @haoruilee in
apple/container#1703
* Add commit/issue alert to PR template. by @jglogan in
apple/container#1945
* Remove `--skip-build` from test Makefile target. by @jglogan in
apple/container#1951
* Restore `--skip-build`, enable `import testable` for release builds.
by @jglogan in apple/container#1955
* [package]: bump container-builder-shim to 0.13.0 by @saehejkang in
apple/container#1953
* Validate container ID from XPC requests by @katiewasnothere in
apple/container#1956
* Remove force unwraps on XPC error set/get by @katiewasnothere in
apple/container#1958
* Do not follow destination symlink when copying user configuration by
@katiewasnothere in apple/container#1957
* Fix machine ID length test. by @jglogan in
apple/container#1971
* Address flaky TestCLIKernelSetSerial suite. by @jglogan in
apple/container#1976
* [gitignore]: ignore vscode workspace files by @saehejkang in
apple/container#1966
* Update containerization dependency with new EXT4Unpacker func
definition by @katiewasnothere in
apple/container#1973
* Periodic dependency updates. by @jglogan in
apple/container#1981
* Use ordered journal mode for unpacked images. by @jglogan in
apple/container#1974
* Reword DNS container name resolution doc information by
@katiewasnothere in apple/container#1960
* ci: bump the github-actions group across 1 directory with 3 updates by
@dependabot[bot] in apple/container#1983
* Pass build config in when building protoc dependencies by
@katiewasnothere in apple/container#1972
* Container test fixture package by @katiewasnothere in
apple/container#1887
* Downgrade swift-collections to 1.5.1. by @jglogan in
apple/container#1984
* Use `enum` for warmup images. by @jglogan in
apple/container#1990
* Add missing dependencies to new ContainerTestSupport package by
@katiewasnothere in apple/container#1994
* Add OCI maskedPaths and readonlyPaths support to Container API. by
@jglogan in apple/container#1996
* Integration test - miscellaneous fixture and test refinements. by
@jglogan in apple/container#1993
* Use log instead of print for system start status messages by
@adityabagchi24 in apple/container#1889
* Fix BuilderStart race, parallelize `container build` tests. by
@jglogan in apple/container#2002
* Allow custom kernel boot args via --kernel-arg by @arirubinstein in
apple/container#1744
* fix: Increase XPC timeout for Machine API operations by @dev-kvt in
apple/container#2006
* Update containerization import to latest 0.40.0 by @katiewasnothere in
apple/container#2028
* Fix image env vars, build context checks, TCP/UDP port forward buffer,
and validate plugin name by @katiewasnothere in
apple/container#2027
* Update containerization import to 0.40.1 by @katiewasnothere in
apple/container#2038
## New Contributors
* @haoruilee made their first contribution in
apple/container#1703
* @arirubinstein made their first contribution in
apple/container#1744
* @dev-kvt made their first contribution in
apple/container#2006
**Full Changelog**:
apple/container@1.1.0...1.2.0
</details>
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Andrew <Andrew.Komkov@gmail.com>
henrywang added a commit to henrywang/Berthly that referenced this pull request Aug 4, 2026
## Summary
- container 1.2.0 added digest verification for kernel archives
(apple/container#1703): `kernel set --tar` accepts `--digest`, remote
tar URLs require one, and the recommended default kernel now ships
pinned sha256 digest metadata. containerization's `KernelConfig` gained
a required `digest` field, and `ClientKernel.installKernelFromTar` an
`expectedDigest` parameter — both reachable from Berthly's native XPC
calls.
- `KernelSetOptions` gained `digest: String?`, threaded into
`setKernel(options:)`'s `installKernelFromTar` call.
- `installDefaultKernelIfNeeded` now passes `expectedDigest:
containerSystemConfig.kernel.digest` — the default kernel already
carries a pinned digest from upstream.
- New "Digest" field on the Set Kernel sheet's Tar Archive section,
required only for remote URLs (matching upstream's exact rule),
prefilled by "Use Recommended" alongside the URL.
- `PARITY.md`'s System `kernel` row updated in the same commit.
## Why
Part of the container 1.2.0 milestone (#79). Mirrors the security
posture Berthly already applies elsewhere (pkg signature verification
against a pinned Apple identity) — kernel archives were the one download
path left unverified.
Closes#79
## Test plan
- [x] `xcodebuild build` succeeds
- [x] `xcodebuild test -only-testing:BerthlyTests` — full suite passes,
including new `kernelDigest` coverage in `SystemConfigMappingTests`
- [x] `swiftlint lint --strict` — 0 violations
- [x] Verified visually via Xcode's live preview renderer — confirmed
the Tar Archive section (including the recommended-URL prefill) renders
correctly
henrywang added a commit to henrywang/Berthly that referenced this pull request Aug 4, 2026
## Summary
- `resolvedSystemConfig()` used `try?` around
`ConfigurationLoader.load()`, so any decode failure silently fell back
to an all-defaults `ContainerSystemConfig` — not just the field that
failed, the user's entire `config.toml` (DNS, build settings, kernel,
everything).
- `ConfigurationLoader.load()` only throws when a config file exists on
disk but fails to parse/decode; it already returns cleanly with defaults
when no file is present at all — so every error caught here represents
real discarded user data, not an absent-file no-op.
- container 1.2.0 made this concrete: `KernelConfig`'s decoder now
throws when `kernel.url` is customized without a paired `kernel.digest`
(apple/container#1703). Any Berthly user who ran `container system
kernel set --tar <url>` pre-1.2.0 has exactly that config shape, and
would silently lose their whole system config on next load once the
daemon is upgraded.
- Extracts the load-outcome handling into a pure, testable
`mapSystemConfigLoadResult(_:)` and surfaces a failure through the
existing `lastStartupWarning` mechanism instead of swallowing it.
## Why
Found while implementing #79 (kernel digest verification) — this
milestone's own version bump is what triggers the failure for affected
users, so it needs fixing in the same milestone.
Closes#87
## Test plan
- [x] `xcodebuild build` succeeds
- [x] `xcodebuild test -only-testing:BerthlyTests` — full suite passes,
including new `SystemConfigLoadResultMappingTests` covering both the
success and failure paths
- [x] `swiftlint lint --strict` — 0 violations
- [x] No View files touched — no UI test needed for this change (tracked
separately in #89: the sidebar's warning-state rendering itself has no
UI coverage yet, pre-existing gap this PR surfaces a second producer
into)
jianliang00 pushed a commit to jianliang00/container that referenced this pull request Aug 28, 2026
Closesapple#1687
The default kernel archive is downloaded from a remote release URL
during first-run setup and via `container system kernel set
--recommended`. Previously, the archive contents were not verified after
download, so integrity depended on HTTPS and the release artifact
remaining unchanged.
This change adds digest verification for kernel archives. The
recommended/default kernel now has pinned digest metadata using an
algorithm-prefixed value such as `sha256:<hex>`. `container system
kernel set --tar` accepts `--digest`; remote tar URLs require it, and
local tar archives can also be verified before unpacking and
installation.
The system config also supports `kernel.digest`, and a custom
`kernel.url` must provide a digest for that archive.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Request]: Check kernel download integrity when downloading

5 participants

@haoruilee@kasc0206@katiewasnothere@briansmith@jglogan
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Verify kernel archive integrity by haoruilee · Pull Request #1703 · apple/container · GitHub
Skip to content

Verify kernel archive integrity - #1703

Merged
katiewasnothere merged 7 commits into
apple:mainfrom
haoruilee:feat/kernelintegrity
Jul 13, 2026
Merged

Verify kernel archive integrity#1703
katiewasnothere merged 7 commits into
apple:mainfrom
haoruilee:feat/kernelintegrity

Conversation

@haoruilee

@haoruileehaoruilee commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Motivation and Context

Closes#1687

The default kernel archive is downloaded from a remote release URL during first-run setup and via container system kernel set --recommended. Previously, the archive contents were not verified after download, so integrity depended on HTTPS and the release artifact remaining unchanged.

This change adds digest verification for kernel archives. The recommended/default kernel now has pinned digest metadata using an algorithm-prefixed value such as sha256:<hex>. container system kernel set --tar accepts --digest; remote tar URLs require it, and local tar archives can also be verified before unpacking and installation.

The system config also supports kernel.digest, and a custom kernel.url must provide a digest for that archive.

Testing

  • Tested locally
  • Added/updated tests
  • Added/updated docs

Validated locally:

  • git diff --check upstream/main...HEAD
  • swift test --filter KernelServiceTests
  • swift test --filter ConfigurationLoaderTests

@kasc0206kasc0206 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查意见 / Review

This is a well-crafted security improvement. The SHA-256 digest verification for kernel downloads is an important addition.

优点 / Strengths

  1. End-to-end implementation: From CLI flag → config → XPC message → server-side verification, the full pipeline is covered
  2. Backward compatible: sha256 is String? with default nil, so existing configs work unchanged
  3. Smart defaults: Auto-populates the default SHA-256 when using the default kernel URL, while allowing custom values for custom URLs
  4. No breaking API changes: All new parameters have sensible defaults
  5. Proper error messaging: The --sha256 can only be used with --tar

Suggestion

  • Consider adding a --sha256-verify flag with --sha256-verify=false to allow users to explicitly skip verification, rather than relying on nil meaning "no verification". But this is optional — current behavior is reasonable.

已验证 / Verified

  • Code compiles with swift build
  • Follows existing project patterns (XPC keys, Config struct, ProgressBar)

Great contribution!

@kasc0206

Copy link
Copy Markdown

Code Review Summary / 代码审查摘要

Strengths / 优点

  1. End-to-end implementation: From CLI flag (--sha256) → Config (sha256 in TOML) → XPC message → server-side verification, the full pipeline is covered
    端到端实现:从 CLI 参数到配置到 XPC 消息到服务端校验,完整链路覆盖
  2. Backward compatible: sha256 is String? with default nil — existing configs work unchanged
    向后兼容:sha256 为可选的 String? 类型,现有配置无需修改
  3. Smart defaults: Auto-populates the default SHA-256 when using the default kernel URL, while allowing custom values for custom URLs
    智能默认值:使用默认内核 URL 时自动填充默认 SHA-256,自定义 URL 可自行指定
  4. No breaking API changes to callers: All new parameters have sensible defaults
    无破坏性 API 变更:所有新参数都有合理的默认值
  5. CLI validation: --sha256 correctly rejected when used without --tar
    CLI 参数校验--sha256 不带 --tar 时正确报错

Suggestion / 建议

Consider adding a --sha256-verify / --sha256-verify=false flag to let users explicitly skip verification. Currently, nil means "no verification", which is reasonable but implicit.
考虑增加 --sha256-verify 标志,让用户能显式关闭校验。当前 nil 表示"不校验"是合理的但较隐晦。

Verification / 验证

  • Code compiles with swift build
  • Follows existing project patterns (XPC keys config, ProgressBar, Config struct)

Great contribution! / 优秀的贡献!

@katiewasnothere

Copy link
Copy Markdown
Contributor

@haoruilee Thank you for the contribution! Could you add commit signatures to your commits? See https://github.com/apple/containerization/blob/main/CONTRIBUTING.md#pull-requests

We definitely want this change, but I think we should change the flags, config fields, and XPC key names to exclude the name of the algorithm so we can support other hashing algorithms in the future. Maybe we should call this just checksum instead? What do you think?

@briansmith

Copy link
Copy Markdown

Perhaps then the values should be prefixed with the digest algorithm.

Perhaps it is worth just copying Subresource Integrity syntax, using "integrity" instead of "checksum" and prefixing the digest algorithm name with a dash in the values, e.g. integrity: "sha256-f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91" instead of sha256 = "f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91" that the PR currently implements.

(Note that using a dash instead of colon prevents any ambiguity with content-addressible-by-digest URI schemes like sha256: that.)

@haoruilee

Copy link
Copy Markdown
ContributorAuthor

Hi @katiewasnothere@briansmith ,

Thanks, that makes sense. I read these suggestions as complementary, the external names should avoid hard coding SHA-256, while the value itself should still carry the digest algorithm.

I’ll update the PR to archive this and add commit signatures and revise the tests and docs accordingly.

@haoruilee
haoruileeforce-pushed the feat/kernelintegrity branch from d24095e to 76e26b9CompareJune 30, 2026 08:14
@haoruileehaoruilee changed the title Verify kernel archive SHA-256 digestsVerify kernel archive integrityJun 30, 2026
@haoruilee
haoruileeforce-pushed the feat/kernelintegrity branch from 76e26b9 to ea5ccebCompareJune 30, 2026 08:17
@haoruilee

Copy link
Copy Markdown
ContributorAuthor

Hi @katiewasnothere

Updated, thanks for your suggestion. I renamed the API surface from sha256 to integrity, with values using sha256-<hex> syntax, and updated tests/docs/PR description accordingly.

Could you please take another look when you have a chance?

@katiewasnothere

Copy link
Copy Markdown
Contributor

@haoruilee Thanks for the update! @briansmith this is a good suggestion, but naming the flag integrity and using a algorithm-hex format is inconsistent with other similar cases in this repo. Thinking about it some more, I think we should name the flag digest and use the format algorithm:hex.

@haoruilee

Copy link
Copy Markdown
ContributorAuthor

Hi @katiewasnothere ,

Thanks, I’ve updated the PR to rename the flag field to digest and use the algorithm:hex format. Docs and tests are updated as well.

Comment threaddocs/container-system-config.md Outdated
|--------------|-----------|--------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| `binaryPath` | `String` | `"opt/kata/share/kata-containers/vmlinux-6.18.15-186"` | Path **inside** the downloaded kernel archive that points to the kernel binary. |
| `url` | `URL` | `"https://github.com/kata-containers/kata-containers/releases/download/3.28.0/kata-static-3.28.0-arm64.tar.zst"` | Archive to download when no kernel is installed. Encoded and decoded as a plain string in TOML. |
| `digest` | `String?` | `"sha256:f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91"` | Expected digest for the archive, for example `sha256:<hex>`. When unset for a custom URL, remote kernel downloads are not verified. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should require that a digest is provided

@github-actions

github-actionsBot commented Jul 1, 2026

Copy link
Copy Markdown

Code Coverage

TierLine Coverage
Unit23.42%
Integration66.54%
Combined75.48%

}

@Test func customKernelURLWithoutDigestLeavesDigestUnset() async throws {
@Test func customKernelURLWithoutDigestThrows() async throws {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about a test where the digest doesn't match the file contents?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about tests where:

  1. The digest algorithm is "sha1" (should fail).
  2. The digest algorithm is "sha256" and the digest is correct but truncated by one byte (should fail).
  3. The digest algorithm is "sha256" but the digest value is a correct SHA-1 (should fail).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻 The archive content mismatch case is covered by installKernelFromLocalTarRejectsDigestMismatchWithoutInstalling, and I added coverage for sha1, truncated sha256, and a valid SHA-1 value passed as sha256.

binaryPath: String = defaultBinaryPath,
url: URL = defaultURL
) {
public init(binaryPath: String = defaultBinaryPath) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we want this init?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, the extra overload isn’t needed. I collapsed this into a single initializer with defaults for binaryPath, url, and digest.

self.digest = Self.defaultDigest
}

public init(binaryPath: String = defaultBinaryPath, url: URL, digest: String) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

url and digest should have their default values set here

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 💯

Comment on lines +211 to +215
let digestResult = try provider.value(forKey: AbsoluteConfigKey(ConfigKey("kernel.digest")), type: .string)
guard digestResult.value != nil else {
throw ContainerizationError(
.invalidArgument,
message: "kernel.digest is required in '\(path)' when kernel.url configures a custom archive"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could check for this in the decoder instead of having a custom validation function. What do you think?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the decoder check is still useful, but not sufficient for the layered config case.

The rule I’m trying to enforce is: if a config layer overrides kernel.url with a non-default archive, that same layer must also provide the digest for that archive. The digest is tied to the archive contents, so it should not be inherited from another layer.

The decoder only sees the merged snapshot, so it can tell whether the final config has both kernel.url and kernel.digest, but it can’t tell which file each value came from. That means it would accept a custom URL from a higher precedence file paired with the default digest from a lower precedence file, which is the case this validation is meant to reject.

I kept this as a premerge loader validation and added a comment to make that layering requirement clearer.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rule I’m trying to enforce is: if a config layer overrides kernel.url with a non-default archive, that same layer must also provide the digest for that archive.

In my opinion it doesn't matter if we get the kernel url and kernel digest from different layers in a layered config case. If the digest does not match the kernel url's contents when we go to fetch the kernel, the user will get an error. I could see having the values in separate layers being useful. Is there a specific scenario you want to prevent with this?

let fileIOThreadPool = NIOThreadPool(numberOfThreads: 1)
fileIOThreadPool.start()

let delegate = try HashingFileDownloadDelegate(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A follow up to this PR or future work could be to look into if we can do something similar to what is done for fetching image blobs here

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, that seems worth exploring as follow up work. I’d keep it separate from this PR since this one is focused on kernel archive integrity.

}
}

static func verifyDigest(of file: URL, expected: String) throws {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this function when we have the one below on line 195?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I removed the extra string overload and updated the tests.


// Mirrors AsyncHTTPClient's file download delegate while updating an optional
// SHA-256 hasher from the same response chunks that are written to disk.
private final class HashingFileDownloadDelegate: @unchecked Sendable, HTTPClientResponseDelegate {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After talking with others, I think for now we should just remove this in favor of downloading the whole tar in the kernel service and then calling verifyDigest(of file: URL, expected: ExpectedDigest) to incrementally compute the hash like we do if there's a local tar. This will make this PR more scoped and we can optimize further later by looking into the suggestion here. After removing this part, I'm ready to approve.

@katiewasnotherekatiewasnothere left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thank you!

@katiewasnothere
katiewasnothere merged commit 57b07fa into apple:mainJul 13, 2026
3 checks passed
saehejkang pushed a commit to saehejkang/container that referenced this pull request Jul 16, 2026
Closesapple#1687
The default kernel archive is downloaded from a remote release URL
during first-run setup and via `container system kernel set
--recommended`. Previously, the archive contents were not verified after
download, so integrity depended on HTTPS and the release artifact
remaining unchanged.
This change adds digest verification for kernel archives. The
recommended/default kernel now has pinned digest metadata using an
algorithm-prefixed value such as `sha256:<hex>`. `container system
kernel set --tar` accepts `--digest`; remote tar URLs require it, and
local tar archives can also be verified before unpacking and
installation.
The system config also supports `kernel.digest`, and a custom
`kernel.url` must provide a digest for that archive.
andrewkomkov added a commit to getgantry/gantry that referenced this pull request Aug 1, 2026
…ot args (#14)
apple/container **1.2.0** is out (previously tracked: `1.1.0`).
Upstream notes: https://github.com/apple/container/releases/tag/1.2.0 —
mirrored in `docs/upstream/apple-container-1.2.0.md`.
## Review checklist
- [ ] New or changed CLI flags Gantry should surface (`container
run/create/machine/build`)
- [ ] Changed `--format json` shapes the DockerKit apple transport
decodes
- [ ] Fixed upstream bugs Gantry currently works around
- [ ] `ContainerTooling.recommendedVersion` / feature gates need moving
to `1.2.0`
- [ ] MCP tools and App Intents that expose the affected commands
- [ ] README and CHANGELOG entries for whatever is adopted
Merging records the version as reviewed. Implement the adopted parts on
this branch, or merge as-is and open follow-ups.
---
<details><summary>Upstream release notes</summary>
## What's Changed
* Add TestCLISystemLogs and TestCLITermIO integration tests in new
integration test suite by @katiewasnothere in
apple/container#1879
* Restore reverted migrations, migrate last tests. by @jglogan in
apple/container#1880
* Removes obsolete CLITests directory. by @jglogan in
apple/container#1886
* Integration coverage xpc helpers by @noah-thor in
apple/container#1551
* Upgrade grpc-swift-nio-transport to 2.9.0 and remove HTTP2ConnectBuff…
by @adityabagchi24 in apple/container#1790
* Updates containerization to 0.36.0. by @jglogan in
apple/container#1912
* Use containerization version 0.37.0 by @adityaramani in
apple/container#1932
* Verify kernel archive integrity by @haoruilee in
apple/container#1703
* Add commit/issue alert to PR template. by @jglogan in
apple/container#1945
* Remove `--skip-build` from test Makefile target. by @jglogan in
apple/container#1951
* Restore `--skip-build`, enable `import testable` for release builds.
by @jglogan in apple/container#1955
* [package]: bump container-builder-shim to 0.13.0 by @saehejkang in
apple/container#1953
* Validate container ID from XPC requests by @katiewasnothere in
apple/container#1956
* Remove force unwraps on XPC error set/get by @katiewasnothere in
apple/container#1958
* Do not follow destination symlink when copying user configuration by
@katiewasnothere in apple/container#1957
* Fix machine ID length test. by @jglogan in
apple/container#1971
* Address flaky TestCLIKernelSetSerial suite. by @jglogan in
apple/container#1976
* [gitignore]: ignore vscode workspace files by @saehejkang in
apple/container#1966
* Update containerization dependency with new EXT4Unpacker func
definition by @katiewasnothere in
apple/container#1973
* Periodic dependency updates. by @jglogan in
apple/container#1981
* Use ordered journal mode for unpacked images. by @jglogan in
apple/container#1974
* Reword DNS container name resolution doc information by
@katiewasnothere in apple/container#1960
* ci: bump the github-actions group across 1 directory with 3 updates by
@dependabot[bot] in apple/container#1983
* Pass build config in when building protoc dependencies by
@katiewasnothere in apple/container#1972
* Container test fixture package by @katiewasnothere in
apple/container#1887
* Downgrade swift-collections to 1.5.1. by @jglogan in
apple/container#1984
* Use `enum` for warmup images. by @jglogan in
apple/container#1990
* Add missing dependencies to new ContainerTestSupport package by
@katiewasnothere in apple/container#1994
* Add OCI maskedPaths and readonlyPaths support to Container API. by
@jglogan in apple/container#1996
* Integration test - miscellaneous fixture and test refinements. by
@jglogan in apple/container#1993
* Use log instead of print for system start status messages by
@adityabagchi24 in apple/container#1889
* Fix BuilderStart race, parallelize `container build` tests. by
@jglogan in apple/container#2002
* Allow custom kernel boot args via --kernel-arg by @arirubinstein in
apple/container#1744
* fix: Increase XPC timeout for Machine API operations by @dev-kvt in
apple/container#2006
* Update containerization import to latest 0.40.0 by @katiewasnothere in
apple/container#2028
* Fix image env vars, build context checks, TCP/UDP port forward buffer,
and validate plugin name by @katiewasnothere in
apple/container#2027
* Update containerization import to 0.40.1 by @katiewasnothere in
apple/container#2038
## New Contributors
* @haoruilee made their first contribution in
apple/container#1703
* @arirubinstein made their first contribution in
apple/container#1744
* @dev-kvt made their first contribution in
apple/container#2006
**Full Changelog**:
apple/container@1.1.0...1.2.0
</details>
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Andrew <Andrew.Komkov@gmail.com>
henrywang added a commit to henrywang/Berthly that referenced this pull request Aug 4, 2026
## Summary
- container 1.2.0 added digest verification for kernel archives
(apple/container#1703): `kernel set --tar` accepts `--digest`, remote
tar URLs require one, and the recommended default kernel now ships
pinned sha256 digest metadata. containerization's `KernelConfig` gained
a required `digest` field, and `ClientKernel.installKernelFromTar` an
`expectedDigest` parameter — both reachable from Berthly's native XPC
calls.
- `KernelSetOptions` gained `digest: String?`, threaded into
`setKernel(options:)`'s `installKernelFromTar` call.
- `installDefaultKernelIfNeeded` now passes `expectedDigest:
containerSystemConfig.kernel.digest` — the default kernel already
carries a pinned digest from upstream.
- New "Digest" field on the Set Kernel sheet's Tar Archive section,
required only for remote URLs (matching upstream's exact rule),
prefilled by "Use Recommended" alongside the URL.
- `PARITY.md`'s System `kernel` row updated in the same commit.
## Why
Part of the container 1.2.0 milestone (#79). Mirrors the security
posture Berthly already applies elsewhere (pkg signature verification
against a pinned Apple identity) — kernel archives were the one download
path left unverified.
Closes#79
## Test plan
- [x] `xcodebuild build` succeeds
- [x] `xcodebuild test -only-testing:BerthlyTests` — full suite passes,
including new `kernelDigest` coverage in `SystemConfigMappingTests`
- [x] `swiftlint lint --strict` — 0 violations
- [x] Verified visually via Xcode's live preview renderer — confirmed
the Tar Archive section (including the recommended-URL prefill) renders
correctly
henrywang added a commit to henrywang/Berthly that referenced this pull request Aug 4, 2026
## Summary
- `resolvedSystemConfig()` used `try?` around
`ConfigurationLoader.load()`, so any decode failure silently fell back
to an all-defaults `ContainerSystemConfig` — not just the field that
failed, the user's entire `config.toml` (DNS, build settings, kernel,
everything).
- `ConfigurationLoader.load()` only throws when a config file exists on
disk but fails to parse/decode; it already returns cleanly with defaults
when no file is present at all — so every error caught here represents
real discarded user data, not an absent-file no-op.
- container 1.2.0 made this concrete: `KernelConfig`'s decoder now
throws when `kernel.url` is customized without a paired `kernel.digest`
(apple/container#1703). Any Berthly user who ran `container system
kernel set --tar <url>` pre-1.2.0 has exactly that config shape, and
would silently lose their whole system config on next load once the
daemon is upgraded.
- Extracts the load-outcome handling into a pure, testable
`mapSystemConfigLoadResult(_:)` and surfaces a failure through the
existing `lastStartupWarning` mechanism instead of swallowing it.
## Why
Found while implementing #79 (kernel digest verification) — this
milestone's own version bump is what triggers the failure for affected
users, so it needs fixing in the same milestone.
Closes#87
## Test plan
- [x] `xcodebuild build` succeeds
- [x] `xcodebuild test -only-testing:BerthlyTests` — full suite passes,
including new `SystemConfigLoadResultMappingTests` covering both the
success and failure paths
- [x] `swiftlint lint --strict` — 0 violations
- [x] No View files touched — no UI test needed for this change (tracked
separately in #89: the sidebar's warning-state rendering itself has no
UI coverage yet, pre-existing gap this PR surfaces a second producer
into)
jianliang00 pushed a commit to jianliang00/container that referenced this pull request Aug 28, 2026
Closesapple#1687
The default kernel archive is downloaded from a remote release URL
during first-run setup and via `container system kernel set
--recommended`. Previously, the archive contents were not verified after
download, so integrity depended on HTTPS and the release artifact
remaining unchanged.
This change adds digest verification for kernel archives. The
recommended/default kernel now has pinned digest metadata using an
algorithm-prefixed value such as `sha256:<hex>`. `container system
kernel set --tar` accepts `--digest`; remote tar URLs require it, and
local tar archives can also be verified before unpacking and
installation.
The system config also supports `kernel.digest`, and a custom
`kernel.url` must provide a digest for that archive.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Request]: Check kernel download integrity when downloading

5 participants

@haoruilee@kasc0206@katiewasnothere@briansmith@jglogan