Skip to content

Commit 20e6d63

Browse files
committed
[Collections] Signing (part 2): signed collections
This is part 2 of a series of PRs to support package collection signing. Depends on [part 1](swiftlang#3241) Modifications: Add support for signing with EC and RSA keys
1 parent ce1432b commit 20e6d63

File tree

8 files changed

+316
-2
lines changed

8 files changed

+316
-2
lines changed

Sources/PackageCollectionsSigning/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ add_library(PackageCollectionsSigning
1212
Key/Key.swift
1313
Key/Key+EC.swift
1414
Key/Key+RSA.swift
15+
Signing/BoringSSLSigning.swift
16+
Signing/Signing.swift
17+
Signing/Signing+ECKey.swift
18+
Signing/Signing+RSAKey.swift
1519
Utilities/Utilities.swift)
1620
target_link_libraries(PackageCollectionsSigning PUBLIC
1721
$<$<NOT:$<PLATFORM_ID:Darwin>>:dispatch>

Sources/PackageCollectionsSigning/Key/Key.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@
1010

1111
import Foundation
1212

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

21-
protocol PublicKey {
21+
protocol PublicKey: MessageValidator {
2222
/// Creates a public key from raw bytes.
2323
///
2424
/// Refer to implementation for details on what representation the raw bytes should be.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2021 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See http://swift.org/LICENSE.txt for license information
8+
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
//===----------------------------------------------------------------------===//
12+
//
13+
// This source file is part of the Vapor open source project
14+
//
15+
// Copyright (c) 2017-2020 Vapor project authors
16+
// Licensed under MIT
17+
//
18+
// See LICENSE for license information
19+
//
20+
// SPDX-License-Identifier: MIT
21+
//
22+
//===----------------------------------------------------------------------===//
23+
24+
#if !(os(macOS) || os(iOS) || os(watchOS) || os(tvOS))
25+
import Foundation
26+
27+
@_implementationOnly import CCryptoBoringSSL
28+
29+
protocol BoringSSLSigning {}
30+
31+
extension BoringSSLSigning {
32+
// Source: https://github.com/vapor/jwt-kit/blob/master/Sources/JWTKit/Utilities/OpenSSLSigner.swift
33+
func digest<Message>(_ message: Message, algorithm: OpaquePointer) throws -> [UInt8] where Message: DataProtocol {
34+
let context = CCryptoBoringSSL_EVP_MD_CTX_new()
35+
defer { CCryptoBoringSSL_EVP_MD_CTX_free(context) }
36+
37+
guard CCryptoBoringSSL_EVP_DigestInit_ex(context, algorithm, nil) == 1 else {
38+
throw BoringSSLSigningError.digestInitializationFailure
39+
}
40+
41+
let message = message.copyBytes()
42+
43+
guard CCryptoBoringSSL_EVP_DigestUpdate(context, message, numericCast(message.count)) == 1 else {
44+
throw BoringSSLSigningError.digestUpdateFailure
45+
}
46+
47+
var digest: [UInt8] = .init(repeating: 0, count: Int(EVP_MAX_MD_SIZE))
48+
var digestLength: UInt32 = 0
49+
50+
guard CCryptoBoringSSL_EVP_DigestFinal_ex(context, &digest, &digestLength) == 1 else {
51+
throw BoringSSLSigningError.digestFinalizationFailure
52+
}
53+
54+
return .init(digest[0 ..< Int(digestLength)])
55+
}
56+
}
57+
58+
enum BoringSSLSigningError: Error {
59+
case digestInitializationFailure
60+
case digestUpdateFailure
61+
case digestFinalizationFailure
62+
}
63+
#endif
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2021 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See http://swift.org/LICENSE.txt for license information
8+
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import struct Foundation.Data
12+
13+
import Crypto
14+
15+
// MARK: - MessageSigner and MessageValidator conformance
16+
17+
extension ECPrivateKey {
18+
func sign(message: Data) throws -> Data {
19+
let signature = try self.underlying.signature(for: SHA256.hash(data: message))
20+
return signature.rawRepresentation
21+
}
22+
}
23+
24+
extension ECPublicKey {
25+
func isValidSignature(_ signature: Data, for message: Data) throws -> Bool {
26+
return try self.underlying.isValidSignature(.init(rawRepresentation: signature), for: SHA256.hash(data: message))
27+
}
28+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2021 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See http://swift.org/LICENSE.txt for license information
8+
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
//===----------------------------------------------------------------------===//
12+
//
13+
// This source file is part of the Vapor open source project
14+
//
15+
// Copyright (c) 2017-2020 Vapor project authors
16+
// Licensed under MIT
17+
//
18+
// See LICENSE for license information
19+
//
20+
// SPDX-License-Identifier: MIT
21+
//
22+
//===----------------------------------------------------------------------===//
23+
24+
import struct Foundation.Data
25+
26+
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
27+
import Security
28+
#else
29+
@_implementationOnly import CCryptoBoringSSL
30+
#endif
31+
32+
// MARK: - MessageSigner and MessageValidator conformance using the Security framework
33+
34+
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
35+
extension CoreRSAPrivateKey {
36+
func sign(message: Data) throws -> Data {
37+
var error: Unmanaged<CFError>?
38+
guard let signature = SecKeyCreateSignature(self.underlying,
39+
.rsaSignatureMessagePKCS1v15SHA256,
40+
message as CFData,
41+
&error) as Data? else {
42+
throw error.map { $0.takeRetainedValue() as Error } ?? SigningError.signFailure
43+
}
44+
return signature
45+
}
46+
}
47+
48+
extension CoreRSAPublicKey {
49+
func isValidSignature(_ signature: Data, for message: Data) throws -> Bool {
50+
SecKeyVerifySignature(
51+
self.underlying,
52+
.rsaSignatureMessagePKCS1v15SHA256,
53+
message as CFData,
54+
signature as CFData,
55+
nil // no-match is considered an error as well so we would rather not trap it
56+
)
57+
}
58+
}
59+
60+
// MARK: - MessageSigner and MessageValidator conformance using BoringSSL
61+
62+
#else
63+
// Reference: https://github.com/vapor/jwt-kit/blob/master/Sources/JWTKit/RSA/RSASigner.swift
64+
extension BoringSSLRSAPrivateKey: BoringSSLSigning {
65+
func sign(message: Data) throws -> Data {
66+
guard let algorithm = CCryptoBoringSSL_EVP_sha256() else {
67+
throw SigningError.algorithmFailure
68+
}
69+
70+
let digest = try self.digest(message, algorithm: algorithm)
71+
72+
var signatureLength: UInt32 = 0
73+
var signature = [UInt8](
74+
repeating: 0,
75+
count: Int(CCryptoBoringSSL_RSA_size(self.underlying))
76+
)
77+
78+
guard CCryptoBoringSSL_RSA_sign(
79+
CCryptoBoringSSL_EVP_MD_type(algorithm),
80+
digest,
81+
numericCast(digest.count),
82+
&signature,
83+
&signatureLength,
84+
self.underlying
85+
) == 1 else {
86+
throw SigningError.signFailure
87+
}
88+
89+
return Data(signature[0 ..< numericCast(signatureLength)])
90+
}
91+
}
92+
93+
extension BoringSSLRSAPublicKey: BoringSSLSigning {
94+
func isValidSignature(_ signature: Data, for message: Data) throws -> Bool {
95+
guard let algorithm = CCryptoBoringSSL_EVP_sha256() else {
96+
throw SigningError.algorithmFailure
97+
}
98+
99+
let digest = try self.digest(message, algorithm: algorithm)
100+
let signature = signature.copyBytes()
101+
102+
return CCryptoBoringSSL_RSA_verify(
103+
CCryptoBoringSSL_EVP_MD_type(algorithm),
104+
digest,
105+
numericCast(digest.count),
106+
signature,
107+
numericCast(signature.count),
108+
self.underlying
109+
) == 1
110+
}
111+
}
112+
#endif
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2021 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See http://swift.org/LICENSE.txt for license information
8+
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import struct Foundation.Data
12+
13+
protocol MessageSigner {
14+
/// Signs a message.
15+
///
16+
/// - Returns:The message's signature.
17+
///
18+
/// - Parameters:
19+
/// - message: The message to sign.
20+
func sign(message: Data) throws -> Data
21+
}
22+
23+
protocol MessageValidator {
24+
/// Checks if a signature is valid for a message.
25+
///
26+
/// - Parameters:
27+
/// - signature: The signature to verify.
28+
/// - message: The message to check signature for.
29+
func isValidSignature(_ signature: Data, for message: Data) throws -> Bool
30+
}
31+
32+
enum SigningError: Error {
33+
case signFailure
34+
case algorithmFailure
35+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2021 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See http://swift.org/LICENSE.txt for license information
8+
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import Foundation
12+
import XCTest
13+
14+
@testable import PackageCollectionsSigning
15+
16+
class ECKeySigningTests: XCTestCase {
17+
func test_signAndValidate_happyCase() throws {
18+
let privateKey = try ECPrivateKey(pem: ecPrivateKey.bytes)
19+
let publicKey = try ECPublicKey(pem: ecPublicKey.bytes)
20+
21+
let message = try JSONEncoder().encode(["foo": "bar"])
22+
let signature = try privateKey.sign(message: message)
23+
XCTAssertTrue(try publicKey.isValidSignature(signature, for: message))
24+
}
25+
26+
func test_signAndValidate_mismatch() throws {
27+
let privateKey = try ECPrivateKey(pem: ecPrivateKey.bytes)
28+
let publicKey = try ECPublicKey(pem: ecPublicKey.bytes)
29+
30+
let jsonEncoder = JSONEncoder()
31+
let message = try jsonEncoder.encode(["foo": "bar"])
32+
let otherMessage = try jsonEncoder.encode(["foo": "baz"])
33+
let signature = try privateKey.sign(message: message)
34+
XCTAssertFalse(try publicKey.isValidSignature(signature, for: otherMessage))
35+
}
36+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2021 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See http://swift.org/LICENSE.txt for license information
8+
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import Foundation
12+
import XCTest
13+
14+
@testable import PackageCollectionsSigning
15+
16+
class RSAKeySigningTests: XCTestCase {
17+
func test_signAndValidate_happyCase() throws {
18+
let privateKey = try RSAPrivateKey(pem: rsaPrivateKey.bytes)
19+
let publicKey = try RSAPublicKey(pem: rsaPublicKey.bytes)
20+
21+
let message = try JSONEncoder().encode(["foo": "bar"])
22+
let signature = try privateKey.sign(message: message)
23+
XCTAssertTrue(try publicKey.isValidSignature(signature, for: message))
24+
}
25+
26+
func test_signAndValidate_mismatch() throws {
27+
let privateKey = try RSAPrivateKey(pem: rsaPrivateKey.bytes)
28+
let publicKey = try RSAPublicKey(pem: rsaPublicKey.bytes)
29+
30+
let jsonEncoder = JSONEncoder()
31+
let message = try jsonEncoder.encode(["foo": "bar"])
32+
let otherMessage = try jsonEncoder.encode(["foo": "baz"])
33+
let signature = try privateKey.sign(message: message)
34+
XCTAssertFalse(try publicKey.isValidSignature(signature, for: otherMessage))
35+
}
36+
}

0 commit comments

Comments
 (0)