Skip to content

Commit

Permalink
Add support for attached signatures (#201) (#206)
Browse files Browse the repository at this point in the history
* Add support for attached signatures (#201)

* Relay detachment through with signing time

* Factor attachment into generateSignedData

* Adjust test coverage

* Test verifying an attached signature

* Test verifying an attached signature and timestamp

* Test failing detached signature as attached

* Test failing detached timed signature as attached

* Test accepting attached signature as detached

* Test accepting attached timed signature as detached
  • Loading branch information
jonct authored Nov 22, 2024
1 parent 3705c35 commit d4d5646
Show file tree
Hide file tree
Showing 2 changed files with 196 additions and 25 deletions.
42 changes: 35 additions & 7 deletions Sources/X509/CryptographicMessageSyntax/CMSOperations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,17 @@ public enum CMS {
additionalIntermediateCertificates: [Certificate] = [],
certificate: Certificate,
privateKey: Certificate.PrivateKey,
signingTime: Date? = nil
signingTime: Date? = nil,
detached: Bool = true
) throws -> [UInt8] {
if let signingTime = signingTime {
return try self.signWithSigningTime(
bytes,
signatureAlgorithm: signatureAlgorithm,
certificate: certificate,
privateKey: privateKey,
signingTime: signingTime
signingTime: signingTime,
detached: detached
)
}

Expand All @@ -42,7 +44,8 @@ public enum CMS {
signatureBytes: ASN1OctetString(signature),
signatureAlgorithm: signatureAlgorithm,
additionalIntermediateCertificates: additionalIntermediateCertificates,
certificate: certificate
certificate: certificate,
withContent: detached ? nil : bytes
)

return try self.serializeSignedData(signedData)
Expand All @@ -55,7 +58,8 @@ public enum CMS {
additionalIntermediateCertificates: [Certificate] = [],
certificate: Certificate,
privateKey: Certificate.PrivateKey,
signingTime: Date
signingTime: Date,
detached: Bool = true
) throws -> [UInt8] {
var signedAttrs: [CMSAttribute] = []
// As specified in RFC 5652 section 11 when including signedAttrs we need to include a minimum of:
Expand Down Expand Up @@ -91,7 +95,8 @@ public enum CMS {
signatureAlgorithm: signatureAlgorithm,
additionalIntermediateCertificates: additionalIntermediateCertificates,
certificate: certificate,
signedAttrs: signedAttrs
signedAttrs: signedAttrs,
withContent: detached ? nil : bytes
)
return try self.serializeSignedData(signedData)
}
Expand All @@ -108,7 +113,8 @@ public enum CMS {
signatureBytes: signatureBytes,
signatureAlgorithm: signatureAlgorithm,
additionalIntermediateCertificates: additionalIntermediateCertificates,
certificate: certificate
certificate: certificate,
withContent: nil as Data?
)

return try serializeSignedData(signedData)
Expand All @@ -130,9 +136,31 @@ public enum CMS {
additionalIntermediateCertificates: [Certificate],
certificate: Certificate,
signedAttrs: [CMSAttribute]? = nil
) throws -> CMSContentInfo {
return try generateSignedData(
signatureBytes: signatureBytes,
signatureAlgorithm: signatureAlgorithm,
additionalIntermediateCertificates: additionalIntermediateCertificates,
certificate: certificate,
signedAttrs: signedAttrs,
withContent: nil as Data?
)
}

@inlinable
static func generateSignedData<Bytes: DataProtocol>(
signatureBytes: ASN1OctetString,
signatureAlgorithm: Certificate.SignatureAlgorithm,
additionalIntermediateCertificates: [Certificate],
certificate: Certificate,
signedAttrs: [CMSAttribute]? = nil,
withContent content: Bytes? = nil
) throws -> CMSContentInfo {
let digestAlgorithm = try AlgorithmIdentifier(digestAlgorithmFor: signatureAlgorithm)
let contentInfo = CMSEncapsulatedContentInfo(eContentType: .cmsData)
var contentInfo = CMSEncapsulatedContentInfo(eContentType: .cmsData)
if let content {
contentInfo.eContent = ASN1OctetString(contentBytes: Array(content)[...])
}

let signerInfo = CMSSignerInfo(
signerIdentifier: .init(issuerAndSerialNumber: certificate),
Expand Down
179 changes: 161 additions & 18 deletions Tests/X509Tests/CMSTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,76 @@ final class CMSTests: XCTestCase {
)
}

func testAttachedSigningVerifying() async throws {
let data: [UInt8] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

let signature = try CMS.sign(
data,
signatureAlgorithm: .ecdsaWithSHA256,
certificate: Self.leaf1Cert,
privateKey: Self.leaf1Key,
detached: false
)
let log = DiagnosticsLog()
let isValidSignature = await CMS.isValidSignature(
dataBytes: data,
signatureBytes: signature,
trustRoots: CertificateStore([Self.rootCert]),
diagnosticCallback: log.append(_:),
allowAttachedContent: true
) { Self.defaultPolicies }
XCTAssertValidSignature(isValidSignature)

XCTAssertEqual(
log,
[
.searchingForIssuerOfPartialChain([Self.leaf1Cert]),
.foundCandidateIssuersOfPartialChainInRootStore([Self.leaf1Cert], issuers: [Self.rootCert]),
.foundValidCertificateChain([Self.leaf1Cert, Self.rootCert]),
]
)
}

func testForbidsDetachedSignatureVerifyingAsAttached() async throws {
let data: [UInt8] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

let signature = try CMS.sign(
data,
signatureAlgorithm: .ecdsaWithSHA256,
certificate: Self.leaf1Cert,
privateKey: Self.leaf1Key,
detached: true
)
let log = DiagnosticsLog()
let isValidAttachedSignature = await CMS.isValidAttachedSignature(
signatureBytes: signature,
trustRoots: CertificateStore([Self.rootCert]),
diagnosticCallback: log.append(_:)
) { Self.defaultPolicies }
XCTAssertInvalidCMSBlock(isValidAttachedSignature)
}

func testToleratesAttachedSignatureVerifyingAsDetached() async throws {
let data: [UInt8] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

let signature = try CMS.sign(
data,
signatureAlgorithm: .ecdsaWithSHA256,
certificate: Self.leaf1Cert,
privateKey: Self.leaf1Key,
detached: false
)
let log = DiagnosticsLog()
let isValidDetachedSignature = await CMS.isValidSignature(
dataBytes: data,
signatureBytes: signature,
trustRoots: CertificateStore([Self.rootCert]),
diagnosticCallback: log.append(_:),
allowAttachedContent: true
) { Self.defaultPolicies }
XCTAssertValidSignature(isValidDetachedSignature)
}

func testParsingSimpleSignature() async throws {
let data: [UInt8] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Expand Down Expand Up @@ -697,18 +767,14 @@ final class CMSTests: XCTestCase {

func testCMSAttachedSignature() async throws {
let data: [UInt8] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
var cmsData = try CMS.generateSignedTestData(
let cmsData = try CMS.generateSignedTestData(
data,
signatureAlgorithm: .ecdsaWithSHA256,
certificate: Self.leaf1Cert,
privateKey: Self.leaf1Key
privateKey: Self.leaf1Key,
detached: false
)

// Let's add the signed data in here!
var signedData = try CMSSignedData(asn1Any: cmsData.content)
signedData.encapContentInfo.eContent = ASN1OctetString(contentBytes: data[...])
cmsData.content = try ASN1Any(erasing: signedData)

let isValidSignature = try await CMS.isValidAttachedSignature(
signatureBytes: cmsData.encodedBytes,
trustRoots: CertificateStore([Self.rootCert])
Expand Down Expand Up @@ -760,19 +826,32 @@ final class CMSTests: XCTestCase {
XCTAssertInvalidCMSBlock(isValidSignature)
}

func testRequireDetachedSignature() async throws {
func testRequireAttachedSignature() async throws {
let data: [UInt8] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
var cmsData = try CMS.generateSignedTestData(
let cmsData = try CMS.generateSignedTestData(
data,
signatureAlgorithm: .ecdsaWithSHA256,
certificate: Self.leaf1Cert,
privateKey: Self.leaf1Key
privateKey: Self.leaf1Key,
detached: true
)

// Let's add the signed data in here!
var signedData = try CMSSignedData(asn1Any: cmsData.content)
signedData.encapContentInfo.eContent = ASN1OctetString(contentBytes: data[...])
cmsData.content = try ASN1Any(erasing: signedData)
let isValidSignature = try await CMS.isValidAttachedSignature(
signatureBytes: cmsData.encodedBytes,
trustRoots: CertificateStore([Self.rootCert])
) {}
XCTAssertInvalidCMSBlock(isValidSignature)
}

func testRequireDetachedSignature() async throws {
let data: [UInt8] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
let cmsData = try CMS.generateSignedTestData(
data,
signatureAlgorithm: .ecdsaWithSHA256,
certificate: Self.leaf1Cert,
privateKey: Self.leaf1Key,
detached: false
)

let isValidSignature = try await CMS.isValidSignature(
dataBytes: data,
Expand All @@ -791,7 +870,7 @@ final class CMSTests: XCTestCase {
privateKey: Self.leaf1Key
)

// Let's add the signed data in here!
// Let's add data not matching the signature
var signedData = try CMSSignedData(asn1Any: cmsData.content)
signedData.encapContentInfo.eContent = ASN1OctetString(contentBytes: [0xba, 0xd])
cmsData.content = try ASN1Any(erasing: signedData)
Expand Down Expand Up @@ -973,6 +1052,67 @@ final class CMSTests: XCTestCase {
XCTAssertValidSignature(isValidSignature)
}

func testSigningAttachedWithSigningTimeSignedAttr() async throws {
let data: [UInt8] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
let signature = try CMS.sign(
data,
signatureAlgorithm: .ecdsaWithSHA256,
certificate: Self.leaf1Cert,
privateKey: Self.leaf1Key,
signingTime: Date(),
detached: false
)
let isValidSignature = await CMS.isValidSignature(
dataBytes: data,
signatureBytes: signature,
trustRoots: CertificateStore([Self.rootCert]),
allowAttachedContent: true
) {
Self.defaultPolicies
}
XCTAssertValidSignature(isValidSignature)
}

func testToleratesAttachedSignatureWithSigningTimeSignedAttrVerifyingAsDetached() async throws {
let data: [UInt8] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
let signature = try CMS.sign(
data,
signatureAlgorithm: .ecdsaWithSHA256,
certificate: Self.leaf1Cert,
privateKey: Self.leaf1Key,
signingTime: Date(),
detached: false
)
let isValidDetachedSignature = await CMS.isValidSignature(
dataBytes: data,
signatureBytes: signature,
trustRoots: CertificateStore([Self.rootCert]),
allowAttachedContent: true
) {
Self.defaultPolicies
}
XCTAssertValidSignature(isValidDetachedSignature)
}

func testForbidsDetachedSignatureWithSigningTimeSignedAttrVerifyingAsAttached() async throws {
let data: [UInt8] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
let signature = try CMS.sign(
data,
signatureAlgorithm: .ecdsaWithSHA256,
certificate: Self.leaf1Cert,
privateKey: Self.leaf1Key,
signingTime: Date(),
detached: true
)
let isValidAttachedSignature = await CMS.isValidAttachedSignature(
signatureBytes: signature,
trustRoots: CertificateStore([Self.rootCert])
) {
Self.defaultPolicies
}
XCTAssertInvalidCMSBlock(isValidAttachedSignature)
}

func testSigningContentBytesWithSigningTimeSignedAttrsIsInvalidSignature() async throws {
let data: [UInt8] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Expand Down Expand Up @@ -1045,14 +1185,16 @@ extension CMS {
signatureAlgorithm: Certificate.SignatureAlgorithm,
additionalIntermediateCertificates: [Certificate] = [],
certificate: Certificate,
privateKey: Certificate.PrivateKey
privateKey: Certificate.PrivateKey,
detached: Bool = true
) throws -> CMSContentInfo {
let signature = try privateKey.sign(bytes: bytes, signatureAlgorithm: signatureAlgorithm)
return try generateSignedData(
signatureBytes: ASN1OctetString(signature),
signatureAlgorithm: signatureAlgorithm,
additionalIntermediateCertificates: additionalIntermediateCertificates,
certificate: certificate
certificate: certificate,
withContent: detached ? nil : bytes
)
}
static func generateInvalidSignedTestDataWithSignedAttrs<Bytes: DataProtocol>(
Expand Down Expand Up @@ -1097,7 +1239,8 @@ extension CMS {
signatureAlgorithm: signatureAlgorithm,
additionalIntermediateCertificates: additionalIntermediateCertificates,
certificate: certificate,
signedAttrs: signedAttrs
signedAttrs: signedAttrs,
withContent: nil as Data?
)
}
}

0 comments on commit d4d5646

Please sign in to comment.