mirror of
https://github.com/apple/container.git
synced 2026-08-27 10:56:32 +00:00
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.
163 lines
6.7 KiB
Swift
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)
|
|
}
|
|
}
|