Skip to content

Commit 78e84a9

Browse files
authored
[Collections] Add and wire up models for signed package collections (#3229)
- Introduce new `SignedCollection` as [proposed](https://forums.swift.org/t/package-collection-signing/43855). - Update logic in `JSONPackageCollectionProvider` to check for signature - Adjust tests
1 parent 3bef7ed commit 78e84a9

File tree

8 files changed

+494
-32
lines changed

8 files changed

+494
-32
lines changed
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
{
2+
"name": "Sample Package Collection",
3+
"overview": "This is a sample package collection listing made-up packages.",
4+
"keywords": ["sample package collection"],
5+
"formatVersion": "1.0",
6+
"revision": 3,
7+
"generatedAt": "2020-10-22T06:03:52Z",
8+
"generatedBy": {
9+
"name": "Jane Doe"
10+
},
11+
"packages": [
12+
{
13+
"url": "https://www.example.com/repos/RepoOne.git",
14+
"summary": "Package One",
15+
"keywords": ["sample package"],
16+
"readmeURL": "https://www.example.com/repos/RepoOne/README",
17+
"license": {
18+
"name": "Apache-2.0",
19+
"url": "https://www.example.com/repos/RepoOne/LICENSE"
20+
},
21+
"versions": [
22+
{
23+
"version": "0.1.0",
24+
"packageName": "PackageOne",
25+
"targets": [
26+
{
27+
"name": "Foo",
28+
"moduleName": "Foo"
29+
}
30+
],
31+
"products": [
32+
{
33+
"name": "Foo",
34+
"type": {
35+
"library": ["automatic"]
36+
},
37+
"targets": ["Foo"]
38+
}
39+
],
40+
"toolsVersion": "5.1",
41+
"minimumPlatformVersions": [
42+
{ "name": "macOS", "version": "10.15" }
43+
],
44+
"verifiedCompatibility": [
45+
{
46+
"platform": { "name": "macOS" },
47+
"swiftVersion": "5.1"
48+
},
49+
{
50+
"platform": { "name": "iOS" },
51+
"swiftVersion": "5.1"
52+
},
53+
{
54+
"platform": { "name": "Linux" },
55+
"swiftVersion": "5.1"
56+
}
57+
],
58+
"license": {
59+
"name": "Apache-2.0",
60+
"url": "https://www.example.com/repos/RepoOne/LICENSE"
61+
}
62+
}
63+
]
64+
},
65+
{
66+
"url": "https://www.example.com/repos/RepoTwo.git",
67+
"summary": "Package Two",
68+
"readmeURL": "https://www.example.com/repos/RepoTwo/README",
69+
"versions": [
70+
{
71+
"version": "2.1.0",
72+
"packageName": "PackageTwo",
73+
"targets": [
74+
{
75+
"name": "Bar",
76+
"moduleName": "Bar"
77+
}
78+
],
79+
"products": [
80+
{
81+
"name": "Bar",
82+
"type": {
83+
"library": ["automatic"]
84+
},
85+
"targets": ["Bar"]
86+
}
87+
],
88+
"toolsVersion": "5.2"
89+
},
90+
{
91+
"version": "1.8.3",
92+
"packageName": "PackageTwo",
93+
"targets": [
94+
{
95+
"name": "Bar",
96+
"moduleName": "Bar"
97+
}
98+
],
99+
"products": [
100+
{
101+
"name": "Bar",
102+
"type": {
103+
"library": ["automatic"]
104+
},
105+
"targets": ["Bar"]
106+
}
107+
],
108+
"toolsVersion": "5.0"
109+
}
110+
]
111+
}
112+
],
113+
"signature": {
114+
"signature": "<SIGNATURE>",
115+
"certificate": {
116+
"subject": {
117+
"commonName": "Sample Subject"
118+
},
119+
"issuer": {
120+
"commonName": "Sample Issuer"
121+
}
122+
}
123+
}
124+
}

Sources/PackageCollections/Model/Collection.swift

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,13 @@ extension PackageCollectionsModel {
5454
/// When this collection was last processed locally
5555
public let lastProcessedAt: Date
5656

57+
/// The collection's signature metadata
58+
public let signature: SignatureData?
59+
5760
/// Indicates if the collection is signed
58-
public let isSigned: Bool
61+
public var isSigned: Bool {
62+
self.signature != nil
63+
}
5964

6065
/// Initializes a `Collection`
6166
init(
@@ -66,8 +71,8 @@ extension PackageCollectionsModel {
6671
packages: [Package],
6772
createdAt: Date,
6873
createdBy: Author?,
69-
lastProcessedAt: Date = Date(),
70-
isSigned: Bool
74+
signature: SignatureData?,
75+
lastProcessedAt: Date = Date()
7176
) {
7277
self.identifier = .init(from: source)
7378
self.source = source
@@ -77,8 +82,8 @@ extension PackageCollectionsModel {
7782
self.packages = packages
7883
self.createdAt = createdAt
7984
self.createdBy = createdBy
85+
self.signature = signature
8086
self.lastProcessedAt = lastProcessedAt
81-
self.isSigned = isSigned
8287
}
8388
}
8489
}
@@ -180,3 +185,40 @@ extension PackageCollectionsModel.Collection {
180185
public let name: String
181186
}
182187
}
188+
189+
extension PackageCollectionsModel {
190+
/// Package collection signature metadata
191+
public struct SignatureData: Equatable, Codable {
192+
/// Details about the certificate used to generate the signature
193+
public let certificate: Certificate
194+
195+
public init(certificate: Certificate) {
196+
self.certificate = certificate
197+
}
198+
199+
public struct Certificate: Equatable, Codable {
200+
/// Subject of the certificate
201+
public let subject: Name
202+
203+
/// Issuer of the certificate
204+
public let issuer: Name
205+
206+
/// Creates a `Certificate`
207+
public init(subject: Name, issuer: Name) {
208+
self.subject = subject
209+
self.issuer = issuer
210+
}
211+
212+
/// Generic certificate name (e.g., subject, issuer)
213+
public struct Name: Equatable, Codable {
214+
/// Common name
215+
public let commonName: String
216+
217+
/// Creates a `Name`
218+
public init(commonName: String) {
219+
self.commonName = commonName
220+
}
221+
}
222+
}
223+
}
224+
}

Sources/PackageCollections/Providers/JSONPackageCollectionProvider.swift

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ struct JSONPackageCollectionProvider: PackageCollectionProvider {
5454
throw Errors.invalidJSON(error)
5555
}
5656
}
57-
return callback(self.makeCollection(from: collection, source: source))
57+
return callback(self.makeCollection(from: collection, source: source, signature: nil))
5858
} catch {
5959
return callback(.failure(error))
6060
}
@@ -90,14 +90,28 @@ struct JSONPackageCollectionProvider: PackageCollectionProvider {
9090
guard contentLength < self.configuration.maximumSizeInBytes else {
9191
return callback(.failure(Errors.responseTooLarge(contentLength)))
9292
}
93+
guard let body = response.body else {
94+
return callback(.failure(Errors.invalidResponse("Body is empty")))
95+
}
9396

9497
do {
95-
// parse json
96-
guard let collection = try response.decodeBody(JSONModel.Collection.self, using: self.decoder) else {
97-
return callback(.failure(Errors.invalidResponse("Invalid body")))
98+
// parse json and construct result
99+
do {
100+
// This fails if "signature" is missing
101+
let signature = try JSONModel.SignedCollection.signature(from: body, using: self.decoder)
102+
// TODO: Check collection's signature
103+
// If signature is
104+
// a. valid: process the collection; set isSigned=true
105+
// b. invalid: includes expired cert, untrusted cert, signature-payload mismatch => return error
106+
let collection = try JSONModel.SignedCollection.collection(from: body, using: self.decoder)
107+
callback(self.makeCollection(from: collection, source: source, signature: Model.SignatureData(from: signature)))
108+
} catch {
109+
// Collection is not signed
110+
guard let collection = try response.decodeBody(JSONModel.Collection.self, using: self.decoder) else {
111+
return callback(.failure(Errors.invalidResponse("Invalid body")))
112+
}
113+
callback(self.makeCollection(from: collection, source: source, signature: nil))
98114
}
99-
// construct result
100-
callback(self.makeCollection(from: collection, source: source))
101115
} catch {
102116
callback(.failure(Errors.invalidJSON(error)))
103117
}
@@ -107,14 +121,7 @@ struct JSONPackageCollectionProvider: PackageCollectionProvider {
107121
}
108122
}
109123

110-
private func makeCollection(from collection: JSONModel.Collection, source: Model.CollectionSource) -> Result<Model.Collection, Error> {
111-
// TODO: Check collection's signature
112-
// 1. If signed and signature is
113-
// a. valid: process the collection; set isSigned=true
114-
// b. invalid: includes expired cert, untrusted cert, signature-payload mismatch => return error
115-
// 2. If unsigned, process the collection; set isSigned=false.
116-
let isSigned = true
117-
124+
private func makeCollection(from collection: JSONModel.Collection, source: Model.CollectionSource, signature: Model.SignatureData?) -> Result<Model.Collection, Error> {
118125
var serializationOkay = true
119126
let packages = collection.packages.map { package -> Model.Package in
120127
let versions = package.versions.compactMap { version -> Model.Package.Version? in
@@ -177,8 +184,8 @@ struct JSONPackageCollectionProvider: PackageCollectionProvider {
177184
packages: packages,
178185
createdAt: collection.generatedAt,
179186
createdBy: collection.generatedBy.flatMap { Model.Collection.Author(name: $0.name) },
180-
lastProcessedAt: Date(),
181-
isSigned: isSigned))
187+
signature: signature,
188+
lastProcessedAt: Date()))
182189
}
183190

184191
private func makeRequestOptions(validResponseCodes: [Int]) -> HTTPClientRequest.Options {
@@ -309,3 +316,22 @@ extension Model.License {
309316
self.init(type: Model.LicenseType(string: from.name), url: from.url)
310317
}
311318
}
319+
320+
extension Model.SignatureData {
321+
fileprivate init(from: JSONModel.Signature) {
322+
self.certificate = .init(from: from.certificate)
323+
}
324+
}
325+
326+
extension Model.SignatureData.Certificate {
327+
fileprivate init(from: JSONModel.Signature.Certificate) {
328+
self.subject = .init(from: from.subject)
329+
self.issuer = .init(from: from.issuer)
330+
}
331+
}
332+
333+
extension Model.SignatureData.Certificate.Name {
334+
fileprivate init(from: JSONModel.Signature.Certificate.Name) {
335+
self.commonName = from.commonName
336+
}
337+
}

0 commit comments

Comments
 (0)