From 678756a6590c8fc30ab85a01442152bab9b83928 Mon Sep 17 00:00:00 2001 From: Cory Benfield Date: Thu, 13 Apr 2023 18:30:47 +0100 Subject: [PATCH] Expose a CMSSignature type (#65) There are some use-cases where interrogating the structure of a CMS signature is necessary. This patch adds an SPI for looking at a CMS signature. As with the rest of our CMS operations, we're not willing to commit to an API shape yet, so this patch doesn't provide one. Instead, users need to opt-in to using the unstable API. --- Sources/X509/CMakeLists.txt | 1 + .../CMSContentInfo.swift | 2 +- .../CMSEncapsulatedContentInfo.swift | 2 +- .../CMSIssuerAndSerialNumber.swift | 2 +- .../CMSOperations.swift | 1 + .../CMSSignature.swift | 79 +++++++++++++++++++ .../CMSSignedData.swift | 2 +- .../CMSSignerIdentifier.swift | 2 +- .../CMSSignerInfo.swift | 2 +- .../CMSVersion.swift | 2 +- Tests/X509Tests/CMSTests.swift | 58 ++++++++++++++ 11 files changed, 146 insertions(+), 7 deletions(-) create mode 100644 Sources/X509/CryptographicMessageSyntax/CMSSignature.swift diff --git a/Sources/X509/CMakeLists.txt b/Sources/X509/CMakeLists.txt index 87f65a6a..6ecba21a 100644 --- a/Sources/X509/CMakeLists.txt +++ b/Sources/X509/CMakeLists.txt @@ -22,6 +22,7 @@ add_library(X509 "CryptographicMessageSyntax/CMSEncapsulatedContentInfo.swift" "CryptographicMessageSyntax/CMSIssuerAndSerialNumber.swift" "CryptographicMessageSyntax/CMSOperations.swift" + "CryptographicMessageSyntax/CMSSignature.swift" "CryptographicMessageSyntax/CMSSignedData.swift" "CryptographicMessageSyntax/CMSSignerIdentifier.swift" "CryptographicMessageSyntax/CMSSignerInfo.swift" diff --git a/Sources/X509/CryptographicMessageSyntax/CMSContentInfo.swift b/Sources/X509/CryptographicMessageSyntax/CMSContentInfo.swift index 77443f7b..6d68a525 100644 --- a/Sources/X509/CryptographicMessageSyntax/CMSContentInfo.swift +++ b/Sources/X509/CryptographicMessageSyntax/CMSContentInfo.swift @@ -44,7 +44,7 @@ extension ASN1ObjectIdentifier { /// ContentType ::= OBJECT IDENTIFIER /// ``` @usableFromInline -struct CMSContentInfo: DERImplicitlyTaggable, Hashable { +struct CMSContentInfo: DERImplicitlyTaggable, Hashable, Sendable { @inlinable static var defaultIdentifier: ASN1Identifier { .sequence diff --git a/Sources/X509/CryptographicMessageSyntax/CMSEncapsulatedContentInfo.swift b/Sources/X509/CryptographicMessageSyntax/CMSEncapsulatedContentInfo.swift index f934ca6c..f81219df 100644 --- a/Sources/X509/CryptographicMessageSyntax/CMSEncapsulatedContentInfo.swift +++ b/Sources/X509/CryptographicMessageSyntax/CMSEncapsulatedContentInfo.swift @@ -22,7 +22,7 @@ import SwiftASN1 /// ContentType ::= OBJECT IDENTIFIER /// ``` @usableFromInline -struct CMSEncapsulatedContentInfo: DERImplicitlyTaggable, Hashable { +struct CMSEncapsulatedContentInfo: DERImplicitlyTaggable, Hashable, Sendable { @inlinable static var defaultIdentifier: ASN1Identifier { .sequence diff --git a/Sources/X509/CryptographicMessageSyntax/CMSIssuerAndSerialNumber.swift b/Sources/X509/CryptographicMessageSyntax/CMSIssuerAndSerialNumber.swift index 98d47dd4..1e6dd766 100644 --- a/Sources/X509/CryptographicMessageSyntax/CMSIssuerAndSerialNumber.swift +++ b/Sources/X509/CryptographicMessageSyntax/CMSIssuerAndSerialNumber.swift @@ -23,7 +23,7 @@ import SwiftASN1 /// The definition of `Name` is taken from X.501 [X.501-88], and the /// definition of `CertificateSerialNumber` is taken from X.509 [X.509-97]. @usableFromInline -struct CMSIssuerAndSerialNumber: DERImplicitlyTaggable, Hashable { +struct CMSIssuerAndSerialNumber: DERImplicitlyTaggable, Hashable, Sendable { @inlinable static var defaultIdentifier: ASN1Identifier { .sequence diff --git a/Sources/X509/CryptographicMessageSyntax/CMSOperations.swift b/Sources/X509/CryptographicMessageSyntax/CMSOperations.swift index 0eab6af2..78da80e8 100644 --- a/Sources/X509/CryptographicMessageSyntax/CMSOperations.swift +++ b/Sources/X509/CryptographicMessageSyntax/CMSOperations.swift @@ -168,6 +168,7 @@ public enum CMS { @_spi(CMS) public enum Error: Swift.Error { case incorrectCMSVersionUsed + case unexpectedCMSType } @_spi(CMS) diff --git a/Sources/X509/CryptographicMessageSyntax/CMSSignature.swift b/Sources/X509/CryptographicMessageSyntax/CMSSignature.swift new file mode 100644 index 00000000..453deeab --- /dev/null +++ b/Sources/X509/CryptographicMessageSyntax/CMSSignature.swift @@ -0,0 +1,79 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCertificates open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftCertificates project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftCertificates project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftASN1 + +/// A representation of a CMS signature over some data. +/// +/// This type hides the specifics of how CMS represents data, instead offering a limited +/// view over a CMS signed-data payload. It also abstracts the specific ASN.1 layout of the +/// signature. +@_spi(CMS) +public struct CMSSignature: Sendable, Hashable { + @usableFromInline + let base: CMSSignedData + + /// Returns the certificates associated with the signers + @inlinable + public var signers: [Signer] { + get throws { + try self.base.signerInfos.compactMap { signerInfo in + try self.base.certificates?.certificate(signerInfo: signerInfo).map { Signer(certificate: $0) } + } + } + } + + /// The certificates in the signature. + @inlinable + public var certificates: [Certificate] { + self.base.certificates ?? [] + } +} + +extension CMSSignature: DERImplicitlyTaggable { + @inlinable + public static var defaultIdentifier: ASN1Identifier { + CMSContentInfo.defaultIdentifier + } + + @inlinable + public init(derEncoded rootNode: ASN1Node, withIdentifier identifier: ASN1Identifier) throws { + guard let base = try CMSContentInfo(derEncoded: rootNode, withIdentifier: identifier).signedData, base.version == .v1 else { + throw CMS.Error.unexpectedCMSType + } + + self.base = base + } + + @inlinable + public func serialize(into coder: inout DER.Serializer, withIdentifier identifier: ASN1Identifier) throws { + try CMSContentInfo(self.base).serialize(into: &coder, withIdentifier: identifier) + } +} + +extension CMSSignature { + /// One of the "signers" that produced a given CMS block. + /// + /// Note that the signer has not been validated, so it is possible that the signer did not actually + /// sign the block in question. + @_spi(CMS) + public struct Signer: Sendable, Hashable { + public let certificate: Certificate + + @inlinable + init(certificate: Certificate) { + self.certificate = certificate + } + } +} diff --git a/Sources/X509/CryptographicMessageSyntax/CMSSignedData.swift b/Sources/X509/CryptographicMessageSyntax/CMSSignedData.swift index afb38a36..48a69cf2 100644 --- a/Sources/X509/CryptographicMessageSyntax/CMSSignedData.swift +++ b/Sources/X509/CryptographicMessageSyntax/CMSSignedData.swift @@ -42,7 +42,7 @@ import SwiftASN1 /// ``` /// - Note: At the moment we don't support `crls` (`RevocationInfoChoices`) @usableFromInline -struct CMSSignedData: DERImplicitlyTaggable, Hashable { +struct CMSSignedData: DERImplicitlyTaggable, Hashable, Sendable { @inlinable static var defaultIdentifier: ASN1Identifier { .sequence diff --git a/Sources/X509/CryptographicMessageSyntax/CMSSignerIdentifier.swift b/Sources/X509/CryptographicMessageSyntax/CMSSignerIdentifier.swift index 6cb4b789..d16ea5bf 100644 --- a/Sources/X509/CryptographicMessageSyntax/CMSSignerIdentifier.swift +++ b/Sources/X509/CryptographicMessageSyntax/CMSSignerIdentifier.swift @@ -21,7 +21,7 @@ import SwiftASN1 /// subjectKeyIdentifier [0] SubjectKeyIdentifier } /// ``` @usableFromInline -enum CMSSignerIdentifier: DERParseable, DERSerializable, Hashable { +enum CMSSignerIdentifier: DERParseable, DERSerializable, Hashable, Sendable { @usableFromInline static let skiIdentifier = ASN1Identifier(tagWithNumber: 0, tagClass: .contextSpecific) diff --git a/Sources/X509/CryptographicMessageSyntax/CMSSignerInfo.swift b/Sources/X509/CryptographicMessageSyntax/CMSSignerInfo.swift index 7bc17a4b..a0cf0903 100644 --- a/Sources/X509/CryptographicMessageSyntax/CMSSignerInfo.swift +++ b/Sources/X509/CryptographicMessageSyntax/CMSSignerInfo.swift @@ -34,7 +34,7 @@ import SwiftASN1 /// then the `version` MUST be 3. /// - Note: At the moment we neither support `signedAttrs` (`SignedAttributes`) nor `unsignedAttrs` (`UnsignedAttributes`) @usableFromInline -struct CMSSignerInfo: DERImplicitlyTaggable, Hashable { +struct CMSSignerInfo: DERImplicitlyTaggable, Hashable, Sendable { @usableFromInline enum Error: Swift.Error { case versionAndSignerIdentifierMismatch(String) diff --git a/Sources/X509/CryptographicMessageSyntax/CMSVersion.swift b/Sources/X509/CryptographicMessageSyntax/CMSVersion.swift index b9b485d3..4815df28 100644 --- a/Sources/X509/CryptographicMessageSyntax/CMSVersion.swift +++ b/Sources/X509/CryptographicMessageSyntax/CMSVersion.swift @@ -20,7 +20,7 @@ import SwiftASN1 /// { v0(0), v1(1), v2(2), v3(3), v4(4), v5(5) } /// ``` @usableFromInline -struct CMSVersion: RawRepresentable, Hashable { +struct CMSVersion: RawRepresentable, Hashable, Sendable { @usableFromInline var rawValue: Int diff --git a/Tests/X509Tests/CMSTests.swift b/Tests/X509Tests/CMSTests.swift index 5a5d1a25..a7022e28 100644 --- a/Tests/X509Tests/CMSTests.swift +++ b/Tests/X509Tests/CMSTests.swift @@ -317,6 +317,64 @@ final class CMSTests: XCTestCase { XCTAssertValidSignature(isValidSignature) } + func testParsingSimpleSignature() async throws { + let data: [UInt8] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + + let signatureBytes = try CMS.sign(data, signatureAlgorithm: .ecdsaWithSHA256, certificate: Self.leaf1Cert, privateKey: Self.leaf1Key) + let signature = try CMSSignature(derEncoded: signatureBytes) + + XCTAssertEqual(try signature.signers, [CMSSignature.Signer(certificate: Self.leaf1Cert)]) + XCTAssertEqual(signature.certificates, [Self.leaf1Cert]) + + XCTAssertEqual(signatureBytes, try signature.encodedBytes) + } + + func testParsingSignatureWithIntermediates() async throws { + let data: [UInt8] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + + let signatureBytes = try CMS.sign( + data, + signatureAlgorithm: .ecdsaWithSHA256, + additionalIntermediateCertificates: [Self.intermediateCert], + certificate: Self.leaf2Cert, + privateKey: Self.leaf2Key + ) + let signature = try CMSSignature(derEncoded: signatureBytes) + + XCTAssertEqual(try signature.signers, [CMSSignature.Signer(certificate: Self.leaf2Cert)]) + XCTAssertEqual(signature.certificates, [Self.intermediateCert, Self.leaf2Cert]) + + XCTAssertEqual(signatureBytes, try signature.encodedBytes) + } + + func testToleratesAdditionalSignerInfos() async throws { + let data: [UInt8] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + var cmsData = try CMS.generateSignedTestData(data, signatureAlgorithm: .ecdsaWithSHA256, certificate: Self.leaf1Cert, privateKey: Self.leaf1Key) + + // Add a second, identical, signer info. + var signedData = try CMSSignedData(asn1Any: cmsData.content) + signedData.signerInfos.append(signedData.signerInfos[0]) + cmsData.content = try ASN1Any(erasing: signedData) + + let signature = try CMSSignature(derEncoded: cmsData.encodedBytes) + XCTAssertEqual(try signature.signers, [CMSSignature.Signer(certificate: Self.leaf1Cert), CMSSignature.Signer(certificate: Self.leaf1Cert)]) + XCTAssertEqual(signature.certificates, [Self.leaf1Cert]) + + XCTAssertEqual(try cmsData.encodedBytes, try signature.encodedBytes) + } + + func testRequireCMSV1SignatureOnCMSSignatureType() async throws { + let data: [UInt8] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + var cmsData = try CMS.generateSignedTestData(data, signatureAlgorithm: .ecdsaWithSHA256, certificate: Self.leaf1Cert, privateKey: Self.leaf1Key) + + // Change the version number to v3 in both places. + var signedData = try CMSSignedData(asn1Any: cmsData.content) + signedData.version = .v3 + cmsData.content = try ASN1Any(erasing: signedData) + + XCTAssertThrowsError(try CMSSignature(derEncoded: cmsData.encodedBytes)) + } + func testRejectsSignatureWithoutRoot() async throws { let data: [UInt8] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]