Skip to content

[Collections] Signing (apple, 2): signed collections #3264

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Binary file not shown.
Binary file added Fixtures/Collections/Signing/TestRootCA_rsa.cer
Binary file not shown.
5 changes: 5 additions & 0 deletions Fixtures/Collections/Signing/Test_ec_key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIKRNt0dFe1qbIFqyWbpU3dvrdzRqZ18BrQBhIoSzm8K2oAoGCCqGSM49
AwEHoUQDQgAE7TEGQSoJ6YWtocE3GTe/GEXgLayMdIGDe1OL66KLECP1CKm0BsJy
Cz5Ae+Rox51jc8zTUcniBXZRNhoP6+6AhQ==
-----END EC PRIVATE KEY-----
27 changes: 27 additions & 0 deletions Fixtures/Collections/Signing/Test_rsa_key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAtFFRh3JRc3PEWmjJ6iygXdSyIeIrNYgVYQPU9t5QwrFQvWSE
IvLaxmKWY9cHOacYJaOgfK2LKuod72K0xFOKX00Ww9Pt6mileP9SKpCkwcT8WN4Z
BLH1dhUqWt32ZZnay0TDs9XT8JQM8vwZrQ2+TlOvVTJdmU8GQzeponj4iNRvoc6Q
g3CvBtADhJHfKW+TGFfDQf284NQEs/95DRual90yeu/E46uY18PGU2jlhJdyugAy
+dK+bBTvZrg0AGylGGZJEQeqPzmWkwMuRsyRTop606yd53op6MhJCEOrlE0aWvUs
Spd/OLPuh5+AhS0RLCkQ8wz/bYZbR66Z4pABewIDAQABAoIBAQCwJuzFrAkkB0kf
pVTzfqsfXwSyEzdw8UMpZkvq613sBLrCemqXlbXhrjgKyuqVCMaPJp1Gj2bwAoxB
6qR7Ur1PwohlwCihIZ/dZ1fGm01Iun5m9nlsW8lWlPCumj32HWpfvwqMKW0Fjixk
R6FxrIZoEFqtmSlU9p1AlyURwqnRSEsAHGeeIj4owD69p5fegjwOjVsJJdvnrU1Y
6iRH3ywlsasv8vonwWiqo2rY3z9SXXb4Omni6U39sKQfDH002wBtZNL9rt/Yx8CD
ua2iikH1BXOWKHl0Wu/swfkPqscX0nYPucMkcUCwZ+xAxZ8DIc1Y/yzgtNaiYEox
GsIMbzMpAoGBANmq1kMzf594jNmb8a23mB50viZYjgLQ8esBZVNHERRK8gAl4feH
uoNvkBdhmT3BtaQCl7RFP315I1LUGctjaWbs12xc2L+5t7kWVfAyHFo5n6eiXoQC
zIretNBzmILp1IJ76atKyhWuH0YWh+UWL6S8rr9K0m6ZWUqSOrvTioPPAoGBANQS
omXglhKBtZGRXiaZpZt2Qz/nPNY95NLEK3yN6lwvI192KEjEkulqJyFELmFoeK4P
uAq5yuXp6qB7BlqxZYGj6/qnsTomeJZb0dwimISHXM46WoSq5sJ0srn8ln/N30PK
8NysaCLaIz13Jonll5D2kCvvZ4Ia8WJq+LkaapaVAoGAZC/K4TGR+3/MLNknW1MW
9GW9o/68lrU/tHB3B+a9CL8aNlE5eeqCQb8W7nwgwZkolu4Oj44UFBeu15ACs2f1
esdmvFzb8xtzYgDS23TlMe41+z20DUUQipbJWOzr9M3V351TR2FsNKBpiqQSNrKI
iWXDdQ7mXru8qqM134AV0GcCgYAKLiLRlShfFw7qP/ovDC0g+1pbFPScrDfxzizw
O7fGWRTvnjJs29LZlZjvReCcGHHCmUqSaTzOMJ5subsiW2WuBXpse+RMEFC1lw7J
7Hc51W2lELQLrlCJgSSbPP7Uf8N586IAVd5h3erXJoMZF4ZhFRTypvlnC3gO62ep
KxV2yQKBgF5nM2fs31xPsHN3Q/iGiQQQG//PIUttCl47XcUnxoabgebIq+lp5UmQ
ArYcBO4+cBZSbNjmUdUOSyM8fAUrWmy4QyvZXNy15V7W6qVzxfa0hT8T6tFUVmKG
qseI/cG+CoygHw9OqBcffl1d8LVAHmF8mkfzJ2CnQs9CLFxS1+f9
-----END RSA PRIVATE KEY-----
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ let package = Package(
.target(
/** Package collections signing */
name: "PackageCollectionsSigning",
dependencies: ["Crypto"]),
dependencies: ["PackageCollectionsModel", "Crypto"]),

.target(
/** Data structures and support for package collections */
Expand Down
12 changes: 10 additions & 2 deletions Sources/PackageCollectionsSigning/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

add_library(PackageCollectionsSigning
Certificate/Certificate.swift
Certificate/CertificatePolicy.swift
Key/ASN1/ASN1.swift
Key/ASN1/ASN1Error.swift
Key/ASN1/PEMDocument.swift
Expand All @@ -21,10 +22,17 @@ add_library(PackageCollectionsSigning
Key/Key.swift
Key/Key+EC.swift
Key/Key+RSA.swift
PackageCollectionSigning.swift
Signing/Signature.swift
Signing/Signing.swift
Signing/Signing+ECKey.swift
Signing/Signing+RSAKey.swift
Utilities/Base64URL.swift
Utilities/Utilities.swift)
target_link_libraries(PackageCollectionsSigning PUBLIC
$<$<NOT:$<PLATFORM_ID:Darwin>>:dispatch>
$<$<NOT:$<PLATFORM_ID:Darwin>>:Foundation>)
PackageCollectionsModel
$<$<NOT:$<PLATFORM_ID:Darwin>>:dispatch>
$<$<NOT:$<PLATFORM_ID:Darwin>>:Foundation>)
target_link_libraries(PackageCollectionsSigning PRIVATE
Crypto)
# NOTE(compnerd) workaround for CMake not setting up include flags yet
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
This source file is part of the Swift.org open source project

Copyright (c) 2021 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See http://swift.org/LICENSE.txt for license information
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

protocol CertificatePolicy {
/// Validates the given certificate chain.
///
/// - Parameters:
/// - certChainPaths: Paths to each certificate in the chain. The certificate being verified must be the first element of the array,
/// with its issuer the next element and so on, and the root CA certificate is last.
/// - callback: The callback to invoke when the result is available.
func validate(certChain: [Certificate], callback: @escaping (Result<Void, Error>) -> Void)
}

// TODO: actual cert policies to be implemented later
struct NoopCertificatePolicy: CertificatePolicy {
func validate(certChain: [Certificate], callback: @escaping (Result<Void, Error>) -> Void) {
callback(.success(()))
}
}
16 changes: 16 additions & 0 deletions Sources/PackageCollectionsSigning/Key/Key+RSA.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ typealias RSAPrivateKey = BoringSSLRSAPrivateKey
struct CoreRSAPrivateKey: PrivateKey {
let underlying: SecKey

var sizeInBits: Int {
toBits(bytes: SecKeyGetBlockSize(self.underlying))
}

init<Data>(pem data: Data) throws where Data: DataProtocol {
let pemString = String(decoding: data, as: UTF8.self)
let pemDocument = try ASN1.PEMDocument(pemString: pemString)
Expand All @@ -52,6 +56,10 @@ struct CoreRSAPrivateKey: PrivateKey {
struct CoreRSAPublicKey: PublicKey {
let underlying: SecKey

var sizeInBits: Int {
toBits(bytes: SecKeyGetBlockSize(self.underlying))
}

/// `data` should be in PKCS #1 format
init(data: Data) throws {
let options: [String: Any] = [
Expand Down Expand Up @@ -80,12 +88,20 @@ struct CoreRSAPublicKey: PublicKey {

#else
final class BoringSSLRSAPrivateKey: PrivateKey {
var sizeInBits: Int {
fatalError("Not implemented")
}

init<Data>(pem data: Data) throws where Data: DataProtocol {
fatalError("Not implemented: \(#function)")
}
}

final class BoringSSLRSAPublicKey: PublicKey {
var sizeInBits: Int {
fatalError("Not implemented")
}

/// `data` should be in the PKCS #1 format
init(data: Data) throws {
fatalError("Not implemented: \(#function)")
Expand Down
8 changes: 6 additions & 2 deletions Sources/PackageCollectionsSigning/Key/Key.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@

import Foundation

protocol PrivateKey {
protocol PrivateKey: MessageSigner {
/// Creates a private key from PEM.
///
/// - Parameters:
/// - pem: The key in PEM format, including the `-----BEGIN` and `-----END` lines.
init<Data>(pem data: Data) throws where Data: DataProtocol
}

protocol PublicKey {
protocol PublicKey: MessageValidator {
/// Creates a public key from raw bytes.
///
/// Refer to implementation for details on what representation the raw bytes should be.
Expand All @@ -40,3 +40,7 @@ enum KeyType {
case RSA
case EC
}

func toBits(bytes: Int) -> Int {
bytes * 8
}
173 changes: 173 additions & 0 deletions Sources/PackageCollectionsSigning/PackageCollectionSigning.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*
This source file is part of the Swift.org open source project

Copyright (c) 2021 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See http://swift.org/LICENSE.txt for license information
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import Foundation

import PackageCollectionsModel

public struct PackageCollectionSigning {
public typealias Model = PackageCollectionModel.V1

private static let minimumRSAKeySizeInBits = 2048

let certPolicy: CertificatePolicy

public init() {
self.init(certPolicy: NoopCertificatePolicy())
}

init(certPolicy: CertificatePolicy) {
self.certPolicy = certPolicy
}

/// Signs package collection using the given certificate and key.
///
/// - Parameters:
/// - collection: The package collection to be signed
/// - certChainPaths: Paths to all DER-encoded certificates in the chain. The certificate used for signing
/// must be the first in the array.
/// - certPrivateKeyPath: Path to the private key (*.pem) of the certificate
/// - jsonEncoder: The `JSONEncoder` to use
/// - callback: The callback to invoke when the signed collection is available.
public func sign(collection: Model.Collection,
certChainPaths: [URL],
certPrivateKeyPath: URL,
jsonEncoder: JSONEncoder = JSONEncoder(),
callback: @escaping (Result<Model.SignedCollection, Error>) -> Void) {
do {
let certChainData = try certChainPaths.map { try Data(contentsOf: $0) }
// Check that the certificate is valid
self.validateCertChain(certChainData) { result in
switch result {
case .failure(let error):
return callback(.failure(error))
case .success(let certChain):
do {
let certificate = certChain.first! // !-safe because certChain cannot be empty at this point
let keyType = try certificate.keyType()

// Signature header
let signatureAlgorithm = Signature.Algorithm.from(keyType: keyType)
let header = Signature.Header(
algorithm: signatureAlgorithm,
certChain: certChainData.map { $0.base64EncodedString() }
)

// Key for signing
let privateKeyPEM = try Data(contentsOf: certPrivateKeyPath)

let privateKey: PrivateKey
switch keyType {
case .RSA:
privateKey = try RSAPrivateKey(pem: privateKeyPEM)
case .EC:
privateKey = try ECPrivateKey(pem: privateKeyPEM)
}
try self.validateKey(privateKey)

// Generate the signature
let signatureData = try Signature.generate(for: collection, with: header, using: privateKey, jsonEncoder: jsonEncoder)

guard let signature = String(bytes: signatureData, encoding: .utf8) else {
return callback(.failure(PackageCollectionSigningError.invalidSignature))
}

let collectionSignature = Model.Signature(
signature: signature,
certificate: Model.Signature.Certificate(
subject: Model.Signature.Certificate.Name(from: try certificate.subject()),
issuer: Model.Signature.Certificate.Name(from: try certificate.issuer())
)
)
callback(.success(Model.SignedCollection(collection: collection, signature: collectionSignature)))
} catch {
callback(.failure(error))
}
}
}
} catch {
callback(.failure(error))
}
}

/// Validates a signed package collection.
///
/// - Parameters:
/// - signedCollection: The signed package collection
/// - jsonDecoder: The `JSONDecoder` to use
/// - callback: The callback to invoke when the result is available.
public func validate(signedCollection: Model.SignedCollection,
jsonDecoder: JSONDecoder = JSONDecoder(),
callback: @escaping (Result<Void, Error>) -> Void) {
guard let signature = signedCollection.signature.signature.data(using: .utf8)?.copyBytes() else {
return callback(.failure(PackageCollectionSigningError.invalidSignature))
}

// Parse the signature
Signature.parse(signature, certChainValidate: self.validateCertChain, jsonDecoder: jsonDecoder) { result in
switch result {
case .failure(let error):
callback(.failure(error))
case .success(let signature):
// Verify the collection embedded in the signature is the same as received
// i.e., the signature is associated with the given collection and not another
guard let collectionFromSignature = try? jsonDecoder.decode(Model.Collection.self, from: signature.payload),
signedCollection.collection == collectionFromSignature else {
return callback(.failure(PackageCollectionSigningError.invalidSignature))
}
callback(.success(()))
}
}
}

private func validateCertChain(_ certChainData: [Data], callback: @escaping (Result<[Certificate], Error>) -> Void) {
guard !certChainData.isEmpty else {
return callback(.failure(PackageCollectionSigningError.emptyCertChain))
}

do {
let certChain = try certChainData.map { try Certificate(derEncoded: $0) }
self.certPolicy.validate(certChain: certChain) { result in
switch result {
case .failure:
// TODO: emit error with DiagnosticsEngine
callback(.failure(PackageCollectionSigningError.invalidCertChain))
case .success:
callback(.success(certChain))
}
}
} catch {
// TODO: emit error with DiagnosticsEngine
callback(.failure(PackageCollectionSigningError.invalidCertChain))
}
}

private func validateKey(_ privateKey: PrivateKey) throws {
if let rsaKey = privateKey as? RSAPrivateKey {
guard rsaKey.sizeInBits >= Self.minimumRSAKeySizeInBits else {
throw PackageCollectionSigningError.invalidKeySize(minimumBits: Self.minimumRSAKeySizeInBits)
}
}
}
}

enum PackageCollectionSigningError: Error, Equatable {
case emptyCertChain
case invalidCertChain
case invalidSignature
case missingCertInfo
case invalidKeySize(minimumBits: Int)
}

private extension PackageCollectionModel.V1.Signature.Certificate.Name {
init(from name: CertificateName) {
self.init(userID: name.userID, commonName: name.commonName, organizationalUnit: name.organizationalUnit, organization: name.organization)
}
}
Loading