Skip to content

Commit 57b07fa

Browse files
authored
Verify kernel archive integrity (apple#1703)
Closes apple#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.
1 parent 5f277a9 commit 57b07fa

15 files changed

Lines changed: 460 additions & 45 deletions

File tree

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ let package = Package(
211211
name: "ContainerAPIServiceTests",
212212
dependencies: [
213213
.product(name: "Containerization", package: "containerization"),
214+
"ContainerAPIService",
214215
"ContainerResource",
215216
"ContainerRuntimeLinuxClient",
216217
"ContainerRuntimeClient",

Sources/ContainerCommands/System/Kernel/KernelSet.swift

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,18 +47,25 @@ extension Application {
4747
@Option(name: .customLong("tar"), help: "Filesystem path or remote URL to a tar archive containing a kernel file")
4848
var tarPath: String? = nil
4949

50+
@Option(name: .long, help: "Expected digest for the tar archive, for example sha256:<hex>. Required when --tar is a remote URL.")
51+
var digest: String? = nil
52+
5053
@OptionGroup
5154
public var logOptions: Flags.Logging
5255

5356
public init() {}
5457

5558
public func run() async throws {
56-
let containerSystemConfig: ContainerSystemConfig = try await Application.loadContainerSystemConfig()
5759
if recommended {
60+
let containerSystemConfig: ContainerSystemConfig = try await Application.loadContainerSystemConfig()
5861
let url = containerSystemConfig.kernel.url
5962
let path: String = containerSystemConfig.kernel.binaryPath
6063
log.info("Installing the recommended kernel from \(url)...")
61-
try await Self.downloadAndInstallWithProgressBar(tarRemoteURL: url, kernelFilePath: path, force: force)
64+
try await Self.downloadAndInstallWithProgressBar(
65+
tarRemoteURL: url,
66+
kernelFilePath: path,
67+
expectedDigest: containerSystemConfig.kernel.digest,
68+
force: force)
6269
return
6370
}
6471
guard tarPath != nil else {
@@ -68,6 +75,9 @@ extension Application {
6875
}
6976

7077
private func setKernelFromBinary() async throws {
78+
guard digest == nil else {
79+
throw ArgumentParser.ValidationError("'--digest' can only be used with '--tar'")
80+
}
7181
guard let binaryPath else {
7282
throw ArgumentParser.ValidationError("missing argument '--binary'")
7383
}
@@ -84,16 +94,32 @@ extension Application {
8494
throw ArgumentParser.ValidationError("missing argument '--tar")
8595
}
8696
let platform = try getSystemPlatform()
97+
let remoteURL = URL(string: tarPath)
98+
let remoteScheme = remoteURL?.scheme?.lowercased()
99+
let isHTTPURL = remoteScheme == "http" || remoteScheme == "https"
87100
let localTarPath = URL(fileURLWithPath: tarPath, relativeTo: .currentDirectory()).path
88101
let fm = FileManager.default
89-
if fm.fileExists(atPath: localTarPath) {
90-
try await ClientKernel.installKernelFromTar(tarFile: localTarPath, kernelFilePath: binaryPath, platform: platform, force: force)
102+
if !isHTTPURL && fm.fileExists(atPath: localTarPath) {
103+
try await ClientKernel.installKernelFromTar(
104+
tarFile: localTarPath,
105+
kernelFilePath: binaryPath,
106+
platform: platform,
107+
expectedDigest: digest,
108+
force: force)
91109
return
92110
}
93-
guard let remoteURL = URL(string: tarPath) else {
111+
guard let remoteURL else {
94112
throw ContainerizationError(.invalidArgument, message: "invalid remote URL '\(tarPath)' for argument '--tar'. Missing protocol?")
95113
}
96-
try await Self.downloadAndInstallWithProgressBar(tarRemoteURL: remoteURL, kernelFilePath: binaryPath, platform: platform, force: force)
114+
guard let digest else {
115+
throw ArgumentParser.ValidationError("'--digest' is required when '--tar' is a remote URL")
116+
}
117+
try await Self.downloadAndInstallWithProgressBar(
118+
tarRemoteURL: remoteURL,
119+
kernelFilePath: binaryPath,
120+
platform: platform,
121+
expectedDigest: digest,
122+
force: force)
97123
}
98124

99125
private func getSystemPlatform() throws -> SystemPlatform {
@@ -107,18 +133,29 @@ extension Application {
107133
}
108134
}
109135

110-
static func downloadAndInstallWithProgressBar(tarRemoteURL: URL, kernelFilePath: String, platform: SystemPlatform = .current, force: Bool) async throws {
136+
static func downloadAndInstallWithProgressBar(
137+
tarRemoteURL: URL,
138+
kernelFilePath: String,
139+
platform: SystemPlatform = .current,
140+
expectedDigest: String,
141+
force: Bool
142+
) async throws {
111143
let progressConfig = try ProgressConfig(
112144
showTasks: true,
113-
totalTasks: 2
145+
totalTasks: 3
114146
)
115147
let progress = ProgressBar(config: progressConfig)
116148
defer {
117149
progress.finish()
118150
}
119151
progress.start()
120152
try await ClientKernel.installKernelFromTar(
121-
tarFile: tarRemoteURL.absoluteString, kernelFilePath: kernelFilePath, platform: platform, progressUpdate: progress.handler, force: force)
153+
tarFile: tarRemoteURL.absoluteString,
154+
kernelFilePath: kernelFilePath,
155+
platform: platform,
156+
progressUpdate: progress.handler,
157+
expectedDigest: expectedDigest,
158+
force: force)
122159
progress.finish()
123160
}
124161

Sources/ContainerCommands/System/SystemStart.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,10 @@ extension Application {
157157
guard await !kernelExists() else {
158158
return
159159
}
160-
try await installDefaultKernel(kernelURL: containerSystemConfig.kernel.url, kernelBinaryPath: containerSystemConfig.kernel.binaryPath)
160+
try await installDefaultKernel(
161+
kernelURL: containerSystemConfig.kernel.url,
162+
kernelBinaryPath: containerSystemConfig.kernel.binaryPath,
163+
kernelDigest: containerSystemConfig.kernel.digest)
161164
}
162165

163166
private func installInitialFilesystem(initImage: String) async throws {
@@ -171,7 +174,7 @@ extension Application {
171174
}
172175
}
173176

174-
private func installDefaultKernel(kernelURL: URL, kernelBinaryPath: String) async throws {
177+
private func installDefaultKernel(kernelURL: URL, kernelBinaryPath: String, kernelDigest: String) async throws {
175178
var shouldInstallKernel = false
176179
if kernelInstall == nil {
177180
print("No default kernel configured.")
@@ -191,7 +194,11 @@ extension Application {
191194
return
192195
}
193196
log.info("Installing kernel...")
194-
try await KernelSet.downloadAndInstallWithProgressBar(tarRemoteURL: kernelURL, kernelFilePath: kernelBinaryPath, force: true)
197+
try await KernelSet.downloadAndInstallWithProgressBar(
198+
tarRemoteURL: kernelURL,
199+
kernelFilePath: kernelBinaryPath,
200+
expectedDigest: kernelDigest,
201+
force: true)
195202
}
196203

197204
private func initImageExists(containerSystemConfig: ContainerSystemConfig) async -> Bool {

Sources/ContainerPersistence/ContainerSystemConfig.swift

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -168,35 +168,50 @@ final public class KernelConfig: Codable, Sendable {
168168
public static let defaultBinaryPath = "opt/kata/share/kata-containers/vmlinux-6.18.15-186"
169169
public static let defaultURL: URL =
170170
URL(string: "https://github.com/kata-containers/kata-containers/releases/download/3.28.0/kata-static-3.28.0-arm64.tar.zst")!
171+
public static let defaultDigest = "sha256:f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91"
171172

172173
private enum CodingKeys: String, CodingKey {
173174
case binaryPath
174175
case url
176+
case digest
175177
}
176178

177179
public let binaryPath: String
178180
public let url: URL
181+
public let digest: String
179182

180-
public init(
181-
binaryPath: String = defaultBinaryPath,
182-
url: URL = defaultURL
183-
) {
183+
public init(binaryPath: String = defaultBinaryPath, url: URL = defaultURL, digest: String = defaultDigest) {
184184
self.binaryPath = binaryPath
185185
self.url = url
186+
self.digest = digest
186187
}
187188

188189
public init(from decoder: any Decoder) throws {
189190
let container = try decoder.container(keyedBy: CodingKeys.self)
190191
self.binaryPath =
191192
try container.decodeIfPresent(String.self, forKey: .binaryPath)
192193
?? Self.defaultBinaryPath
193-
if let urlString = try container.decodeIfPresent(String.self, forKey: .url),
194-
let parsed = URL(string: urlString)
195-
{
194+
if let urlString = try container.decodeIfPresent(String.self, forKey: .url) {
195+
guard let parsed = URL(string: urlString) else {
196+
throw DecodingError.dataCorruptedError(
197+
forKey: .url,
198+
in: container,
199+
debugDescription: "invalid kernel URL '\(urlString)'")
200+
}
196201
self.url = parsed
197202
} else {
198203
self.url = Self.defaultURL
199204
}
205+
if let digest = try container.decodeIfPresent(String.self, forKey: .digest) {
206+
self.digest = digest
207+
} else if self.url.absoluteString == Self.defaultURL.absoluteString {
208+
self.digest = Self.defaultDigest
209+
} else {
210+
throw DecodingError.dataCorruptedError(
211+
forKey: .digest,
212+
in: container,
213+
debugDescription: "kernel.digest is required when kernel.url is not the default URL")
214+
}
200215
}
201216

202217
// JSONEncoder special-cases URL to encode as absoluteString, but third-party
@@ -209,6 +224,7 @@ final public class KernelConfig: Codable, Sendable {
209224
var container = encoder.container(keyedBy: CodingKeys.self)
210225
try container.encode(binaryPath, forKey: .binaryPath)
211226
try container.encode(url.absoluteString, forKey: .url)
227+
try container.encode(digest, forKey: .digest)
212228
}
213229
}
214230

Sources/Services/ContainerAPIService/Client/ClientKernel.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,23 @@ extension ClientKernel {
4242
try await client.send(message)
4343
}
4444

45-
public static func installKernelFromTar(tarFile: String, kernelFilePath: String, platform: SystemPlatform, progressUpdate: ProgressUpdateHandler? = nil, force: Bool)
46-
async throws
47-
{
45+
public static func installKernelFromTar(
46+
tarFile: String,
47+
kernelFilePath: String,
48+
platform: SystemPlatform,
49+
progressUpdate: ProgressUpdateHandler? = nil,
50+
expectedDigest: String? = nil,
51+
force: Bool
52+
) async throws {
4853
let client = newClient()
4954
let message = XPCMessage(route: .installKernel)
5055

5156
message.set(key: .kernelTarURL, value: tarFile)
5257
message.set(key: .kernelFilePath, value: kernelFilePath)
5358
message.set(key: .kernelForce, value: force)
59+
if let expectedDigest {
60+
message.set(key: .kernelDigest, value: expectedDigest)
61+
}
5462

5563
let platformData = try JSONEncoder().encode(platform)
5664
message.set(key: .systemPlatform, value: platformData)

Sources/Services/ContainerAPIService/Client/XPC+.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ public enum XPCKeys: String {
112112
case kernelFilePath
113113
case systemPlatform
114114
case kernelForce
115+
case kernelDigest
115116

116117
/// Init image reference
117118
case initImage

Sources/Services/ContainerAPIService/Server/Kernel/KernelHarness.swift

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public struct KernelHarness: Sendable {
3535
let kernelFilePath = try message.kernelFilePath()
3636
let platform = try message.platform()
3737
let force = try message.kernelForce()
38+
let expectedDigest = message.kernelDigest()
3839

3940
guard let kernelTarUrl = try message.kernelTarURL() else {
4041
// We have been given a path to a kernel binary on disk
@@ -47,7 +48,12 @@ public struct KernelHarness: Sendable {
4748

4849
let progressUpdateService = ProgressUpdateService(message: message)
4950
try await self.service.installKernelFrom(
50-
tar: kernelTarUrl, kernelFilePath: kernelFilePath, platform: platform, progressUpdate: progressUpdateService?.handler, force: force)
51+
tar: kernelTarUrl,
52+
kernelFilePath: kernelFilePath,
53+
platform: platform,
54+
progressUpdate: progressUpdateService?.handler,
55+
expectedDigest: expectedDigest,
56+
force: force)
5157
return message.reply()
5258
}
5359

@@ -85,13 +91,17 @@ extension XPCMessage {
8591
guard let kernelTarURLString = self.string(key: .kernelTarURL) else {
8692
return nil
8793
}
88-
guard let k = URL(string: kernelTarURLString) else {
89-
throw ContainerizationError(.invalidArgument, message: "cannot parse URL from \(kernelTarURLString)")
94+
if let k = URL(string: kernelTarURLString), k.scheme != nil {
95+
return k
9096
}
91-
return k
97+
return URL(fileURLWithPath: kernelTarURLString)
9298
}
9399

94100
fileprivate func kernelForce() throws -> Bool {
95101
self.bool(key: .kernelForce)
96102
}
103+
104+
fileprivate func kernelDigest() -> String? {
105+
self.string(key: .kernelDigest)
106+
}
97107
}

0 commit comments

Comments
 (0)