UpdateManifest.swift
165 lines 5.4 KB
Raw
sha256:bd1eb24434d281fe65142038c1bf217493421f5a86880c22dff7e6037d2d4700 feat(companion): add production macOS distribution path Human minor ⚠ breaking 24 days ago
1 import CryptoKit
2 import Foundation
3
4 public struct SignedUpdateManifest: Codable, Equatable, Sendable {
5 public struct SourceRange: Codable, Equatable, Sendable {
6 public let minimumBuild: Int
7 public let maximumBuild: Int
8
9 public init(minimumBuild: Int, maximumBuild: Int) {
10 self.minimumBuild = minimumBuild
11 self.maximumBuild = maximumBuild
12 }
13 }
14
15 public struct Target: Codable, Equatable, Sendable {
16 public let version: String
17 public let build: Int
18 public let packageURL: String
19 public let packageSHA256: String
20 public let packageBytes: UInt64
21
22 public init(version: String, build: Int, packageURL: String, packageSHA256: String, packageBytes: UInt64) {
23 self.version = version
24 self.build = build
25 self.packageURL = packageURL
26 self.packageSHA256 = packageSHA256
27 self.packageBytes = packageBytes
28 }
29 }
30
31 public let schema: Int
32 public let channel: String
33 public let platform: String
34 public let architecture: String
35 public let keyID: String
36 public let issuedAt: Int64
37 public let expiresAt: Int64
38 public let source: SourceRange
39 public let target: Target
40 public let minimumAcceptedBuild: Int
41
42 public init(
43 schema: Int,
44 channel: String,
45 platform: String,
46 architecture: String,
47 keyID: String,
48 issuedAt: Int64,
49 expiresAt: Int64,
50 source: SourceRange,
51 target: Target,
52 minimumAcceptedBuild: Int
53 ) {
54 self.schema = schema
55 self.channel = channel
56 self.platform = platform
57 self.architecture = architecture
58 self.keyID = keyID
59 self.issuedAt = issuedAt
60 self.expiresAt = expiresAt
61 self.source = source
62 self.target = target
63 self.minimumAcceptedBuild = minimumAcceptedBuild
64 }
65 }
66
67 public struct UpdateEvaluationContext: Equatable, Sendable {
68 public let currentBuild: Int
69 public let persistedFloor: Int
70 public let now: Int64
71 public let channel: String
72 public let platform: String
73 public let architecture: String
74
75 public init(
76 currentBuild: Int,
77 persistedFloor: Int,
78 now: Int64,
79 channel: String = "stable",
80 platform: String = "macos",
81 architecture: String = "arm64"
82 ) {
83 self.currentBuild = currentBuild
84 self.persistedFloor = persistedFloor
85 self.now = now
86 self.channel = channel
87 self.platform = platform
88 self.architecture = architecture
89 }
90 }
91
92 public enum UpdateRejection: Error, Equatable {
93 case signatureInvalid
94 case malformed
95 case unsupportedSchema
96 case wrongChannel
97 case wrongPlatform
98 case wrongArchitecture
99 case wrongKey
100 case notYetValid
101 case expired
102 case sourceMismatch
103 case targetNotNewer
104 case rollback
105 case invalidPackage
106 }
107
108 public enum UpdateManifestVerifier {
109 public static func verifySignature(
110 manifestData: Data,
111 signature: Data,
112 publicKey: Data
113 ) throws {
114 let key: Curve25519.Signing.PublicKey
115 do {
116 key = try Curve25519.Signing.PublicKey(rawRepresentation: publicKey)
117 } catch {
118 throw UpdateRejection.signatureInvalid
119 }
120 guard key.isValidSignature(signature, for: manifestData) else {
121 throw UpdateRejection.signatureInvalid
122 }
123 }
124
125 public static func decode(_ data: Data) throws -> SignedUpdateManifest {
126 do {
127 return try JSONDecoder().decode(SignedUpdateManifest.self, from: data)
128 } catch {
129 throw UpdateRejection.malformed
130 }
131 }
132
133 @discardableResult
134 public static func evaluate(
135 _ manifest: SignedUpdateManifest,
136 expectedKeyID: String,
137 context: UpdateEvaluationContext
138 ) throws -> SignedUpdateManifest.Target {
139 guard manifest.schema == 1 else { throw UpdateRejection.unsupportedSchema }
140 guard manifest.channel == context.channel else { throw UpdateRejection.wrongChannel }
141 guard manifest.platform == context.platform else { throw UpdateRejection.wrongPlatform }
142 guard manifest.architecture == context.architecture else { throw UpdateRejection.wrongArchitecture }
143 guard manifest.keyID == expectedKeyID else { throw UpdateRejection.wrongKey }
144 guard manifest.issuedAt <= context.now else { throw UpdateRejection.notYetValid }
145 guard manifest.expiresAt >= context.now, manifest.expiresAt > manifest.issuedAt else {
146 throw UpdateRejection.expired
147 }
148 guard context.currentBuild >= manifest.source.minimumBuild,
149 context.currentBuild <= manifest.source.maximumBuild else {
150 throw UpdateRejection.sourceMismatch
151 }
152 guard manifest.target.build > context.currentBuild else {
153 throw UpdateRejection.targetNotNewer
154 }
155 let floor = max(context.persistedFloor, manifest.minimumAcceptedBuild)
156 guard manifest.target.build >= floor else { throw UpdateRejection.rollback }
157 guard manifest.target.packageBytes > 0,
158 manifest.target.packageSHA256.range(of: "^[0-9a-f]{64}$", options: .regularExpression) != nil,
159 let url = URL(string: manifest.target.packageURL),
160 url.scheme == "https" else {
161 throw UpdateRejection.invalidPackage
162 }
163 return manifest.target
164 }
165 }
File History 1 commit
sha256:bd1eb24434d281fe65142038c1bf217493421f5a86880c22dff7e6037d2d4700 feat(companion): add production macOS distribution path Human minor ⚠ 24 days ago