Verify kernel archive integrity#1703
Conversation
kasc0206
left a comment
There was a problem hiding this comment.
审查意见 / Review
This is a well-crafted security improvement. The SHA-256 digest verification for kernel downloads is an important addition.
优点 / Strengths
- End-to-end implementation: From CLI flag → config → XPC message → server-side verification, the full pipeline is covered
- Backward compatible:
sha256isString?with defaultnil, so existing configs work unchanged - Smart defaults: Auto-populates the default SHA-256 when using the default kernel URL, while allowing custom values for custom URLs
- No breaking API changes: All new parameters have sensible defaults
- Proper error messaging: The
--sha256can only be used with--tar
Suggestion
- Consider adding a
--sha256-verifyflag with--sha256-verify=falseto allow users to explicitly skip verification, rather than relying onnilmeaning "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!
Code Review Summary / 代码审查摘要Strengths / 优点
Suggestion / 建议Consider adding a Verification / 验证
Great contribution! / 优秀的贡献! |
|
@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 |
|
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. (Note that using a dash instead of colon prevents any ambiguity with content-addressible-by-digest URI schemes like |
|
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. |
d24095e to
76e26b9
Compare
76e26b9 to
ea5cceb
Compare
|
Updated, thanks for your suggestion. I renamed the API surface from Could you please take another look when you have a chance? |
|
@haoruilee Thanks for the update! @briansmith this is a good suggestion, but naming the flag |
…rity # Conflicts: # Tests/CLITests/Subcommands/System/TestKernelSet.swift
|
Hi @katiewasnothere , Thanks, I’ve updated the PR to rename the flag field to |
| |--------------|-----------|--------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------| | ||
| | `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. | |
There was a problem hiding this comment.
I think we should require that a digest is provided
Code Coverage
|
| } | ||
|
|
||
| @Test func customKernelURLWithoutDigestLeavesDigestUnset() async throws { | ||
| @Test func customKernelURLWithoutDigestThrows() async throws { |
There was a problem hiding this comment.
How about a test where the digest doesn't match the file contents?
There was a problem hiding this comment.
How about tests where:
- The digest algorithm is "sha1" (should fail).
- The digest algorithm is "sha256" and the digest is correct but truncated by one byte (should fail).
- The digest algorithm is "sha256" but the digest value is a correct SHA-1 (should fail).
There was a problem hiding this comment.
👍🏻 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) { |
There was a problem hiding this comment.
Why do we want this init?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
url and digest should have their default values set here
| 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" |
There was a problem hiding this comment.
We could check for this in the decoder instead of having a custom validation function. What do you think?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Do we need this function when we have the one below on line 195?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
Type of Change
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 --taraccepts--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 customkernel.urlmust provide a digest for that archive.Testing
Validated locally:
git diff --check upstream/main...HEADswift test --filter KernelServiceTestsswift test --filter ConfigurationLoaderTests