Files
container/Tests/ContainerAPIServiceTests/KernelServiceTests.swift
T
0x4C33 57b07fa2ac Verify kernel archive integrity (#1703)
Closes https://github.com/apple/container/issues/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.
2026-07-13 13:04:39 -07:00

163 lines
6.7 KiB
Swift

//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import Containerization
import ContainerizationArchive
import ContainerizationError
import CryptoKit
import Foundation
import Logging
import Testing
@testable import ContainerAPIService
struct KernelServiceTests {
@Test func installKernelFromLocalTarVerifiesDigest() async throws {
try await withTempDir { tempDir in
let kernelPath = "boot/vmlinux"
let kernelData = Data("kernel binary".utf8)
let tarFile = try Self.writeTar(
at: tempDir.appendingPathComponent("kernel.tar"),
path: kernelPath,
data: kernelData)
let service = try KernelService(
log: Logger(label: "com.apple.container.test.kernel-service"),
appRoot: tempDir.appendingPathComponent("app"))
let digest = try KernelService.sha256Hex(of: tarFile)
try await service.installKernelFrom(
tar: URL(string: tarFile.path)!,
kernelFilePath: kernelPath,
platform: .linuxArm,
progressUpdate: nil,
expectedDigest: "sha256:\(digest)",
force: false)
let kernel = try await service.getDefaultKernel(platform: .linuxArm)
#expect(try Data(contentsOf: kernel.path) == kernelData)
}
}
@Test func installKernelFromLocalTarRejectsDigestMismatchWithoutInstalling() async throws {
try await withTempDir { tempDir in
let kernelPath = "boot/vmlinux"
let kernelData = Data("kernel binary".utf8)
let tarFile = try Self.writeTar(
at: tempDir.appendingPathComponent("kernel.tar"),
path: kernelPath,
data: kernelData)
let service = try KernelService(
log: Logger(label: "com.apple.container.test.kernel-service"),
appRoot: tempDir.appendingPathComponent("app"))
let wrongDigest = String(repeating: "0", count: 64)
await #expect(throws: ContainerizationError.self) {
try await service.installKernelFrom(
tar: URL(fileURLWithPath: tarFile.path),
kernelFilePath: kernelPath,
platform: .linuxArm,
progressUpdate: nil,
expectedDigest: "sha256:\(wrongDigest)",
force: false)
}
await #expect(throws: ContainerizationError.self) {
_ = try await service.getDefaultKernel(platform: .linuxArm)
}
}
}
@Test func installKernelFromLocalTarRejectsInvalidDigestValues() async throws {
try await withTempDir { tempDir in
let kernelPath = "boot/vmlinux"
let kernelData = Data("kernel binary".utf8)
let tarFile = try Self.writeTar(
at: tempDir.appendingPathComponent("kernel.tar"),
path: kernelPath,
data: kernelData)
let service = try KernelService(
log: Logger(label: "com.apple.container.test.kernel-service"),
appRoot: tempDir.appendingPathComponent("app"))
let sha256 = try KernelService.sha256Hex(of: tarFile)
let sha1 = try Self.sha1Hex(of: tarFile)
let invalidDigests = [
"sha256-not-a-digest",
"sha1:\(sha1)",
"sha256:not-a-digest",
String(repeating: "0", count: 64),
"sha256:\(String(sha256.dropLast(2)))",
"sha256:\(sha1)",
]
for digest in invalidDigests {
await #expect(throws: ContainerizationError.self) {
try await service.installKernelFrom(
tar: URL(fileURLWithPath: tarFile.path),
kernelFilePath: kernelPath,
platform: .linuxArm,
progressUpdate: nil,
expectedDigest: digest,
force: false)
}
}
await #expect(throws: ContainerizationError.self) {
_ = try await service.getDefaultKernel(platform: .linuxArm)
}
}
}
@Test func installKernelFromRemoteTarRequiresDigest() async throws {
try await withTempDir { tempDir in
let service = try KernelService(
log: Logger(label: "com.apple.container.test.kernel-service"),
appRoot: tempDir.appendingPathComponent("app"))
await #expect(throws: ContainerizationError.self) {
try await service.installKernelFrom(
tar: URL(string: "https://example.com/kernel.tar")!,
kernelFilePath: "boot/vmlinux",
platform: .linuxArm,
progressUpdate: nil,
expectedDigest: nil,
force: false)
}
}
}
private static func writeTar(at tarFile: URL, path: String, data: Data) throws -> URL {
let archiver = try ArchiveWriter(format: .paxRestricted, filter: .none, file: tarFile)
let entry = WriteEntry()
entry.path = path
entry.fileType = .regular
entry.permissions = 0o644
entry.size = numericCast(data.count)
try archiver.writeEntry(entry: entry, data: data)
try archiver.finishEncoding()
return tarFile
}
private static func sha1Hex(of file: URL) throws -> String {
let data = try Data(contentsOf: file)
return Insecure.SHA1.hash(data: data).map { String(format: "%02x", $0) }.joined()
}
private func withTempDir(body: (URL) async throws -> Void) async throws {
let dir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: dir) }
try await body(dir)
}
}