Skip to content

Commit 636536d

Browse files
committed
Add signing tool
This is **experimental**!!! Depends on swiftlang/swift-package-manager#3275
1 parent 478175a commit 636536d

File tree

18 files changed

+327
-35
lines changed

18 files changed

+327
-35
lines changed

Package.swift

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ let package = Package(
99
products: [
1010
.library(name: "PackageCollectionGenerator", targets: ["PackageCollectionGenerator"]),
1111
.executable(name: "package-collection-generate", targets: ["PackageCollectionGeneratorExecutable"]),
12+
.library(name: "PackageCollectionSigner", targets: ["PackageCollectionSigner"]),
13+
.executable(name: "package-collection-sign", targets: ["PackageCollectionSignerExecutable"]),
1214
.library(name: "PackageCollectionValidator", targets: ["PackageCollectionValidator"]),
1315
.executable(name: "package-collection-validate", targets: ["PackageCollectionValidatorExecutable"]),
1416
.library(name: "PackageCollectionDiff", targets: ["PackageCollectionDiff"]),
@@ -21,33 +23,40 @@ let package = Package(
2123
],
2224
targets: [
2325
.target(name: "Utilities", dependencies: [
24-
.product(name: "SwiftPMDataModel", package: "SwiftPM"),
26+
.product(name: "SwiftPMPackageCollections", package: "SwiftPM"),
2527
]),
2628

2729
.target(name: "PackageCollectionGenerator", dependencies: [
2830
"Utilities",
29-
.product(name: "SwiftPMDataModel", package: "SwiftPM"),
31+
.product(name: "SwiftPMPackageCollections", package: "SwiftPM"),
3032
.product(name: "ArgumentParser", package: "swift-argument-parser"),
3133
]),
3234
.target(name: "PackageCollectionGeneratorExecutable", dependencies: ["PackageCollectionGenerator"]),
3335

36+
.target(name: "PackageCollectionSigner", dependencies: [
37+
"Utilities",
38+
.product(name: "SwiftPMPackageCollections", package: "SwiftPM"),
39+
.product(name: "ArgumentParser", package: "swift-argument-parser"),
40+
]),
41+
.target(name: "PackageCollectionSignerExecutable", dependencies: ["PackageCollectionSigner"]),
42+
3443
.target(name: "PackageCollectionValidator", dependencies: [
3544
"Utilities",
36-
.product(name: "SwiftPMDataModel", package: "SwiftPM"),
45+
.product(name: "SwiftPMPackageCollections", package: "SwiftPM"),
3746
.product(name: "ArgumentParser", package: "swift-argument-parser"),
3847
]),
3948
.target(name: "PackageCollectionValidatorExecutable", dependencies: ["PackageCollectionValidator"]),
4049

4150
.target(name: "PackageCollectionDiff", dependencies: [
4251
"Utilities",
43-
.product(name: "SwiftPMDataModel", package: "SwiftPM"),
52+
.product(name: "SwiftPMPackageCollections", package: "SwiftPM"),
4453
.product(name: "ArgumentParser", package: "swift-argument-parser"),
4554
]),
4655
.target(name: "PackageCollectionDiffExecutable", dependencies: ["PackageCollectionDiff"]),
4756

4857
.target(name: "TestUtilities", dependencies: [
4958
.product(name: "ArgumentParser", package: "swift-argument-parser"),
50-
.product(name: "SwiftPMDataModel", package: "SwiftPM"),
59+
.product(name: "SwiftPMPackageCollections", package: "SwiftPM"),
5160
]),
5261

5362
.testTarget(name: "PackageCollectionGeneratorTests", dependencies: ["PackageCollectionGenerator"]),
@@ -56,6 +65,11 @@ let package = Package(
5665
"TestUtilities",
5766
]),
5867

68+
.testTarget(name: "PackageCollectionSignerExecutableTests", dependencies: [
69+
"PackageCollectionSignerExecutable",
70+
"TestUtilities",
71+
]),
72+
5973
.testTarget(name: "PackageCollectionValidatorExecutableTests", dependencies: [
6074
"PackageCollectionValidatorExecutable",
6175
"TestUtilities",

Sources/PackageCollectionDiff/PackageCollectionDiff.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
import ArgumentParser
1616
import Foundation
17+
18+
import Basics
1719
import PackageCollectionsModel
1820
import TSCBasic
1921
import Utilities
@@ -41,8 +43,7 @@ public struct PackageCollectionDiff: ParsableCommand {
4143

4244
print("Comparing collections located at \(self.collectionOnePath) and \(self.collectionTwoPath)", inColor: .cyan, verbose: self.verbose)
4345

44-
let jsonDecoder = JSONDecoder()
45-
jsonDecoder.dateDecodingStrategy = .iso8601
46+
let jsonDecoder = JSONDecoder.makeWithDefaults()
4647

4748
let collectionOne = try self.parsePackageCollection(at: self.collectionOnePath, using: jsonDecoder)
4849
let collectionTwo = try self.parsePackageCollection(at: self.collectionTwoPath, using: jsonDecoder)

Sources/PackageCollectionGenerator/Models/PackageCollectionGeneratorInput.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
import Foundation
16+
1617
import PackageCollectionsModel
1718

1819
/// Input for the `package-collection-generate` command

Sources/PackageCollectionGenerator/Models/PackageManifest.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
import Foundation
16+
1617
import PackageModel
1718

1819
// Note: Not using SwiftPM's `PackageModel.Manifest` to avoid issues with

Sources/PackageCollectionGenerator/PackageCollectionGenerate.swift

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
import ArgumentParser
1616
import Foundation
17+
18+
import Basics
1719
import PackageCollectionsModel
1820
import PackageModel
1921
import TSCBasic
@@ -58,7 +60,7 @@ public struct PackageCollectionGenerate: ParsableCommand {
5860
print("Using input file located at \(self.inputPath)", inColor: .cyan, verbose: self.verbose)
5961

6062
// Get the list of packages to process
61-
let jsonDecoder = JSONDecoder()
63+
let jsonDecoder = JSONDecoder.makeWithDefaults()
6264
let input = try jsonDecoder.decode(PackageCollectionGeneratorInput.self, from: Data(contentsOf: URL(fileURLWithPath: self.inputPath)))
6365
print("\(input)", verbose: self.verbose)
6466

@@ -85,16 +87,6 @@ public struct PackageCollectionGenerate: ParsableCommand {
8587
generatedBy: input.author
8688
)
8789

88-
let jsonEncoder = JSONEncoder()
89-
jsonEncoder.dateEncodingStrategy = .iso8601
90-
if #available(macOS 10.15, *) {
91-
#if os(macOS)
92-
jsonEncoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
93-
#else
94-
jsonEncoder.outputFormatting = [.sortedKeys]
95-
#endif
96-
}
97-
9890
// Make sure the output directory exists
9991
let outputAbsolutePath: AbsolutePath
10092
do {
@@ -106,6 +98,7 @@ public struct PackageCollectionGenerate: ParsableCommand {
10698
try localFileSystem.createDirectory(outputDirectory, recursive: true)
10799

108100
// Write the package collection
101+
let jsonEncoder = JSONEncoder.makeWithDefaults(prettified: false)
109102
let jsonData = try jsonEncoder.encode(packageCollection)
110103
try jsonData.write(to: URL(fileURLWithPath: outputAbsolutePath.pathString))
111104
print("Package collection saved to \(outputAbsolutePath)", inColor: .cyan, verbose: self.verbose)
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift Package Collection Generator open source project
4+
//
5+
// Copyright (c) 2021 Apple Inc. and the Swift Package Collection Generator project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift Package Collection Generator project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import PackageCollectionsSigning
16+
17+
import ArgumentParser
18+
import Foundation
19+
20+
import Basics
21+
import PackageCollectionsModel
22+
import PackageCollectionsSigning
23+
24+
import TSCBasic
25+
import Utilities
26+
27+
public struct PackageCollectionSign: ParsableCommand {
28+
public static let configuration = CommandConfiguration(
29+
abstract: "Sign a package collection."
30+
)
31+
32+
@Argument(help: "The path to the package collection file to be signed")
33+
var inputPath: String
34+
35+
@Argument(help: "The path to write the signed package collection to")
36+
var outputPath: String
37+
38+
@Argument(help: "The path to certificate's private key (PEM encoded)")
39+
var privateKeyPath: String
40+
41+
@Argument(help: "Paths to all certificates (DER encoded) in the chain. The certificate used for signing must be first and the root certificate last.")
42+
var certChainPaths: [String]
43+
44+
@Flag(name: .long, help: "Show extra logging for debugging purposes")
45+
var verbose: Bool = false
46+
47+
typealias Model = PackageCollectionModel.V1
48+
49+
public init() {}
50+
51+
public func run() throws {
52+
try self._run(signer: nil)
53+
}
54+
55+
internal func _run(signer: PackageCollectionSigner?) throws {
56+
guard !self.certChainPaths.isEmpty else {
57+
printError("Certificate chain cannot be empty")
58+
throw PackageCollectionSigningError.emptyCertChain
59+
}
60+
61+
Process.verbose = self.verbose
62+
63+
print("Signing package collection located at \(self.inputPath)", inColor: .cyan, verbose: self.verbose)
64+
65+
let jsonDecoder = JSONDecoder.makeWithDefaults()
66+
let collection = try jsonDecoder.decode(Model.Collection.self, from: Data(contentsOf: URL(fileURLWithPath: self.inputPath)))
67+
68+
let privateKeyURL = URL(fileURLWithPath: self.privateKeyPath)
69+
let certChainURLs = self.certChainPaths.map(URL.init(fileURLWithPath:))
70+
71+
try withTemporaryDirectory(removeTreeOnDeinit: true) { tmpDir in
72+
// The last item in the array is the root certificate and we want to trust it, so here we
73+
// create a temp directory, copy the root certificate to it, and make it the trustedRootCertsDir.
74+
let rootCertPath = AbsolutePath(certChainPaths.last!) // !-safe since certChain cannot be empty at this point
75+
let rootCertFilename = rootCertPath.components.last!
76+
try localFileSystem.copy(from: rootCertPath, to: tmpDir.appending(component: rootCertFilename))
77+
78+
// Sign the collection
79+
let signer = signer ?? PackageCollectionSigning(trustedRootCertsDir: tmpDir.asURL)
80+
let signedCollection = try tsc_await { callback in
81+
signer.sign(collection: collection, certChainPaths: certChainURLs, certPrivateKeyPath: privateKeyURL, certPolicyKey: .default, callback: callback)
82+
}
83+
84+
// Make sure the output directory exists
85+
let outputAbsolutePath: AbsolutePath
86+
do {
87+
outputAbsolutePath = try AbsolutePath(validating: self.outputPath)
88+
} catch {
89+
outputAbsolutePath = AbsolutePath(self.outputPath, relativeTo: AbsolutePath(FileManager.default.currentDirectoryPath))
90+
}
91+
let outputDirectory = outputAbsolutePath.parentDirectory
92+
try localFileSystem.createDirectory(outputDirectory, recursive: true)
93+
94+
// Write the signed collection
95+
let jsonEncoder = JSONEncoder.makeWithDefaults(prettified: false)
96+
let jsonData = try jsonEncoder.encode(signedCollection)
97+
try jsonData.write(to: URL(fileURLWithPath: outputAbsolutePath.pathString))
98+
print("Signed package collection saved to \(outputAbsolutePath)", inColor: .cyan, verbose: self.verbose)
99+
}
100+
}
101+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Package Collection Signer
2+
3+
This tool is used for signing a package collection.
4+
5+
```
6+
> swift run package-collection-sign --help
7+
OVERVIEW: Sign a package collection.
8+
9+
USAGE: package-collection-sign <input-path> <output-path> <private-key-path> [<cert-chain-paths> ...] [--verbose]
10+
11+
ARGUMENTS:
12+
<input-path> The path to the package collection file to be signed
13+
<output-path> The path to write the signed package collection to
14+
<private-key-path> The path to certificate's private key (PEM encoded)
15+
<cert-chain-paths> Paths to all certificates (DER encoded) in the chain. The certificate used for signing must be first and the root
16+
certificate last.
17+
18+
OPTIONS:
19+
--verbose Show extra logging for debugging purposes
20+
-h, --help Show help information.
21+
```
22+
23+
### Sample Usage
24+
25+
```
26+
> swift run package-collection-sign \
27+
my-collection.json \
28+
my-signed-collection.json \
29+
priivate-key.pem \
30+
certificate.cer intermediate_ca.cer root_ca.cer
31+
```
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift Package Collection Generator open source project
4+
//
5+
// Copyright (c) 2020 Apple Inc. and the Swift Package Collection Generator project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift Package Collection Generator project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import PackageCollectionSigner
16+
17+
PackageCollectionSign.main()

Sources/PackageCollectionValidator/PackageCollectionValidate.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
import ArgumentParser
1616
import Foundation
17+
18+
import Basics
1719
import enum PackageCollections.ValidationError
1820
import struct PackageCollections.ValidationMessage
1921
import enum PackageCollectionsModel.PackageCollectionModel
@@ -45,8 +47,7 @@ public struct PackageCollectionValidate: ParsableCommand {
4547

4648
let validator = Model.Validator()
4749

48-
let jsonDecoder = JSONDecoder()
49-
jsonDecoder.dateDecodingStrategy = .iso8601
50+
let jsonDecoder = JSONDecoder.makeWithDefaults()
5051

5152
let collection: Model.Collection
5253
do {

Tests/PackageCollectionDiffExecutableTests/PackageCollectionDiffTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15+
import XCTest
16+
1517
@testable import PackageCollectionDiff
1618
@testable import TestUtilities
1719
import TSCBasic
18-
import XCTest
1920

2021
final class PackageCollectionDiffTests: XCTestCase {
2122
func test_help() throws {

Tests/PackageCollectionGeneratorExecutableTests/PackageCollectionGenerateTests.swift

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
import Foundation
16+
import XCTest
17+
18+
import Basics
1619
@testable import PackageCollectionGenerator
1720
import PackageCollectionsModel
1821
@testable import TestUtilities
1922
import TSCBasic
2023
import TSCUtility
21-
import XCTest
2224

2325
final class PackageCollectionGenerateTests: XCTestCase {
2426
typealias Model = PackageCollectionModel.V1
@@ -42,15 +44,6 @@ final class PackageCollectionGenerateTests: XCTestCase {
4244
let repoThreeArchivePath = AbsolutePath(#file).parentDirectory.appending(components: "Inputs", "TestRepoThree.tgz")
4345
try systemQuietly(["tar", "-x", "-v", "-C", tmpDir.pathString, "-f", repoThreeArchivePath.pathString])
4446

45-
let jsonEncoder = JSONEncoder()
46-
if #available(macOS 10.15, *) {
47-
#if os(macOS)
48-
jsonEncoder.outputFormatting = [.withoutEscapingSlashes]
49-
#else
50-
jsonEncoder.outputFormatting = [.sortedKeys]
51-
#endif
52-
}
53-
5447
// Prepare input.json
5548
let input = PackageCollectionGeneratorInput(
5649
name: "Test Package Collection",
@@ -72,6 +65,7 @@ final class PackageCollectionGenerateTests: XCTestCase {
7265
),
7366
]
7467
)
68+
let jsonEncoder = JSONEncoder.makeWithDefaults()
7569
let inputData = try jsonEncoder.encode(input)
7670
let inputFilePath = tmpDir.appending(component: "input.json")
7771
try localFileSystem.writeFileContents(inputFilePath, bytes: ByteString(inputData))
@@ -194,8 +188,7 @@ final class PackageCollectionGenerateTests: XCTestCase {
194188
),
195189
]
196190

197-
let jsonDecoder = JSONDecoder()
198-
jsonDecoder.dateDecodingStrategy = .iso8601
191+
let jsonDecoder = JSONDecoder.makeWithDefaults()
199192

200193
// Assert the generated package collection
201194
let collectionData = try localFileSystem.readFileContents(outputFilePath).contents

Tests/PackageCollectionGeneratorTests/PackageCollectionGeneratorInputTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
import Foundation
16-
import TSCBasic
1716
import XCTest
1817

1918
@testable import PackageCollectionGenerator
19+
import TSCBasic
2020

2121
class PackageCollectionGeneratorInputTests: XCTestCase {
2222
func testLoadFromFile() throws {
Binary file not shown.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-----BEGIN EC PRIVATE KEY-----
2+
MHcCAQEEIKRNt0dFe1qbIFqyWbpU3dvrdzRqZ18BrQBhIoSzm8K2oAoGCCqGSM49
3+
AwEHoUQDQgAE7TEGQSoJ6YWtocE3GTe/GEXgLayMdIGDe1OL66KLECP1CKm0BsJy
4+
Cz5Ae+Rox51jc8zTUcniBXZRNhoP6+6AhQ==
5+
-----END EC PRIVATE KEY-----

0 commit comments

Comments
 (0)