Skip to content

Commit 0554020

Browse files
committed
Implement signature validation and signing entity TOFU flow. Need tests
1 parent 8cb9ef9 commit 0554020

File tree

9 files changed

+586
-36
lines changed

9 files changed

+586
-36
lines changed

Sources/PackageModel/PackageIdentity.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public struct PackageIdentity: CustomStringConvertible, Sendable {
7474
self.registry != nil
7575
}
7676

77-
public struct RegistryIdentity: CustomStringConvertible {
77+
public struct RegistryIdentity: Hashable, CustomStringConvertible {
7878
public let scope: PackageIdentity.Scope
7979
public let name: PackageIdentity.Name
8080
public let underlying: PackageIdentity

Sources/PackageRegistry/CMakeLists.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
# See http://swift.org/CONTRIBUTORS.txt for Swift project authors
88

99
add_library(PackageRegistry STATIC
10+
ChecksumTOFU.swift
1011
Registry.swift
1112
RegistryConfiguration.swift
1213
RegistryClient.swift
1314
RegistryDownloadsManager.swift
14-
ChecksumTOFU.swift)
15+
SignatureValidation.swift
16+
SigningEntityTOFU.swift)
1517
target_link_libraries(PackageRegistry PUBLIC
1618
Basics
1719
PackageFingerprint

Sources/PackageRegistry/ChecksumTOFU.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,7 @@ struct PackageVersionChecksumTOFU {
104104
) { result in
105105
switch result {
106106
case .success(let metadata):
107-
guard let sourceArchive = metadata.resources
108-
.first(where: { $0.name == "source-archive" })
109-
else {
107+
guard let sourceArchive = metadata.sourceArchive else {
110108
return completion(.failure(RegistryError.missingSourceArchive))
111109
}
112110

Sources/PackageRegistry/RegistryClient.swift

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,21 +26,26 @@ import struct TSCUtility.Version
2626
public final class RegistryClient: Cancellable {
2727
private static let apiVersion: APIVersion = .v1
2828
private static let availabilityCacheTTL: DispatchTimeInterval = .seconds(5 * 60)
29+
private static let metadataCacheTTL: DispatchTimeInterval = .seconds(60 * 60)
2930

3031
private let configuration: RegistryConfiguration
3132
private let archiverProvider: (FileSystem) -> Archiver
3233
private let httpClient: LegacyHTTPClient
3334
private let authorizationProvider: LegacyHTTPClientConfiguration.AuthorizationProvider?
34-
private let signingEntityStorage: PackageSigningEntityStorage?
35-
private let signingEntityCheckingMode: SigningEntityCheckingMode
3635
private let jsonDecoder: JSONDecoder
3736

3837
private var checksumTOFU: PackageVersionChecksumTOFU!
38+
private let signingEntityTOFU: PackageSigningEntityTOFU
3939

4040
private let availabilityCache = ThreadSafeKeyValueStore<
4141
URL,
4242
(status: Result<AvailabilityStatus, Error>, expires: DispatchTime)
4343
>()
44+
45+
private let metadataCache = ThreadSafeKeyValueStore<
46+
MetadataCacheKey,
47+
(metadata: Serialization.VersionMetadata, expires: DispatchTime)
48+
>()
4449

4550
public init(
4651
configuration: RegistryConfiguration,
@@ -80,10 +85,12 @@ public final class RegistryClient: Cancellable {
8085

8186
self.httpClient = customHTTPClient ?? LegacyHTTPClient()
8287
self.archiverProvider = customArchiverProvider ?? { fileSystem in ZipArchiver(fileSystem: fileSystem) }
83-
self.signingEntityStorage = signingEntityStorage
84-
self.signingEntityCheckingMode = signingEntityCheckingMode
8588
self.jsonDecoder = JSONDecoder.makeWithDefaults()
8689

90+
self.signingEntityTOFU = PackageSigningEntityTOFU(
91+
signingEntityStorage: signingEntityStorage,
92+
signingEntityCheckingMode: signingEntityCheckingMode
93+
)
8794
self.checksumTOFU = PackageVersionChecksumTOFU(
8895
fingerprintStorage: fingerprintStorage,
8996
fingerprintCheckingMode: fingerprintCheckingMode,
@@ -321,7 +328,6 @@ public final class RegistryClient: Cancellable {
321328
}
322329
}
323330

324-
// TODO: add caching
325331
// marked internal for testing
326332
func _getRawPackageVersionMetadata(
327333
registry: Registry,
@@ -333,6 +339,11 @@ public final class RegistryClient: Cancellable {
333339
completion: @escaping (Result<Serialization.VersionMetadata, Error>) -> Void
334340
) {
335341
let completion = self.makeAsync(completion, on: callbackQueue)
342+
343+
let cacheKey = MetadataCacheKey(registry: registry, package: package)
344+
if let cached = self.metadataCache[cacheKey], cached.expires < .now() {
345+
return completion(.success(cached.metadata))
346+
}
336347

337348
guard var components = URLComponents(url: registry.url, resolvingAgainstBaseURL: true) else {
338349
return completion(.failure(RegistryError.invalidURL(registry.url)))
@@ -357,10 +368,12 @@ public final class RegistryClient: Cancellable {
357368
result.tryMap { response in
358369
switch response.statusCode {
359370
case 200:
360-
return try response.parseJSON(
371+
let metadata = try response.parseJSON(
361372
Serialization.VersionMetadata.self,
362373
decoder: self.jsonDecoder
363374
)
375+
self.metadataCache[cacheKey] = (metadata: metadata, expires: .now() + Self.metadataCacheTTL)
376+
return metadata
364377
case 404:
365378
throw RegistryError.packageVersionNotFound
366379
default:
@@ -1182,6 +1195,11 @@ public final class RegistryClient: Cancellable {
11821195
options.authorizationProvider = self.authorizationProvider
11831196
return options
11841197
}
1198+
1199+
private struct MetadataCacheKey: Hashable {
1200+
let registry: Registry
1201+
let package: PackageIdentity.RegistryIdentity
1202+
}
11851203
}
11861204

11871205
public enum RegistryError: Error, CustomStringConvertible {
@@ -1216,6 +1234,18 @@ public enum RegistryError: Error, CustomStringConvertible {
12161234
case registryNotAvailable(Registry)
12171235
case packageNotFound
12181236
case packageVersionNotFound
1237+
case sourceArchiveNotSigned(registry: Registry, package: PackageIdentity, version: Version)
1238+
case failedLoadingSignature
1239+
case failedRetrievingSourceArchiveSignature(registry: Registry, package: PackageIdentity, version: Version, error: Error)
1240+
case missingConfiguration(details: String)
1241+
case missingSignatureFormat
1242+
case unknownSignatureFormat(String)
1243+
case invalidSignature(reason: String)
1244+
case invalidSigningCertificate(reason: String)
1245+
case signerNotTrusted
1246+
case failedToValidateSignature(Error)
1247+
case signingEntityForReleaseHasChanged(package: PackageIdentity, version: Version, latest: SigningEntity?, previous: SigningEntity)
1248+
case signingEntityForPackageHasChanged(package: PackageIdentity, latest: SigningEntity?, previous: SigningEntity)
12191249

12201250
public var description: String {
12211251
switch self {
@@ -1280,11 +1310,35 @@ public enum RegistryError: Error, CustomStringConvertible {
12801310
case .forbidden:
12811311
return "Forbidden"
12821312
case .registryNotAvailable(let registry):
1283-
return "registry at '\(registry.url)' is not available at this time, please try again later"
1313+
return "Registry at '\(registry.url)' is not available at this time, please try again later"
12841314
case .packageNotFound:
1285-
return "package not found on registry"
1315+
return "Package not found on registry"
12861316
case .packageVersionNotFound:
1287-
return "package version not found on registry"
1317+
return "Package version not found on registry"
1318+
case .sourceArchiveNotSigned(let registry, let packageIdentity, let version):
1319+
return "'\(packageIdentity)@\(version)' source archive from '\(registry)' is not signed"
1320+
case .failedLoadingSignature:
1321+
return "Failed loading signature for validation"
1322+
case .failedRetrievingSourceArchiveSignature(let registry, let packageIdentity, let version, let error):
1323+
return "Failed retrieving '\(packageIdentity)@\(version)' source archive signature from '\(registry)': \(error)"
1324+
case .missingConfiguration(let details):
1325+
return "Unable to proceed because of missing configuration: \(details)"
1326+
case .missingSignatureFormat:
1327+
return "Missing signature format"
1328+
case .unknownSignatureFormat(let format):
1329+
return "Unknown signature format: \(format)"
1330+
case .invalidSignature(let reason):
1331+
return "Signature is invalid: \(reason)"
1332+
case .invalidSigningCertificate(let reason):
1333+
return "The signing certificate is invalid: \(reason)"
1334+
case .signerNotTrusted:
1335+
return "The signer is not trusted"
1336+
case .failedToValidateSignature(let error):
1337+
return "Failed to validate signature: \(error)"
1338+
case .signingEntityForReleaseHasChanged(let package, let version, let latest, let previous):
1339+
return "The signing entity '\(String(describing: latest))' for '\(package)@\(version)' is different from the previously recorded value '\(previous)'"
1340+
case .signingEntityForPackageHasChanged(let package, let latest, let previous):
1341+
return "The signing entity '\(String(describing: latest))' for '\(package)' is different from the previously recorded value '\(previous)'"
12881342
}
12891343
}
12901344
}
@@ -1613,6 +1667,10 @@ extension RegistryClient {
16131667
public let version: String
16141668
public let resources: [Resource]
16151669
public let metadata: AdditionalMetadata?
1670+
1671+
var sourceArchive: Resource? {
1672+
self.resources.first(where: { $0.name == "source-archive" })
1673+
}
16161674

16171675
public init(
16181676
id: String,

0 commit comments

Comments
 (0)