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

@haoruilee haoruilee 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

@kasc0206 kasc0206 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! / 优秀的贡献!

@jglogan jglogan requested a review from katiewasnothere June 15, 2026 17:16
@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
Contributor Author

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 haoruilee force-pushed the feat/kernelintegrity branch from d24095e to 76e26b9 Compare June 30, 2026 08:14
@haoruilee haoruilee changed the title Verify kernel archive SHA-256 digests Verify kernel archive integrity Jun 30, 2026
@haoruilee haoruilee force-pushed the feat/kernelintegrity branch from 76e26b9 to ea5cceb Compare June 30, 2026 08:17
@haoruilee

Copy link
Copy Markdown
Contributor Author

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
Contributor Author

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 thread docs/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-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

Code Coverage

Tier Line Coverage
Unit 23.42%
Integration 66.54%
Combined 75.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
Contributor Author

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
Contributor Author

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
Contributor Author

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
Contributor Author

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
Contributor Author

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
Contributor Author

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.

@katiewasnothere katiewasnothere 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:main Jul 13, 2026
3 checks passed
Sign up for free to 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