Skip to content

Commit

Permalink
Expose a CMSSignature type (#65)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Lukasa authored Apr 13, 2023
1 parent d0c71eb commit 678756a
Show file tree
Hide file tree
Showing 11 changed files with 146 additions and 7 deletions.
1 change: 1 addition & 0 deletions Sources/X509/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ public enum CMS {
@_spi(CMS)
public enum Error: Swift.Error {
case incorrectCMSVersionUsed
case unexpectedCMSType
}

@_spi(CMS)
Expand Down
79 changes: 79 additions & 0 deletions Sources/X509/CryptographicMessageSyntax/CMSSignature.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion Sources/X509/CryptographicMessageSyntax/CMSVersion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
58 changes: 58 additions & 0 deletions Tests/X509Tests/CMSTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down

0 comments on commit 678756a

Please sign in to comment.