From d4d564670a0bf0aa155459db87be6e7507c50a7a Mon Sep 17 00:00:00 2001 From: "Jon C. Thomason" <2807816+jonct@users.noreply.github.com> Date: Fri, 22 Nov 2024 05:54:27 -0500 Subject: [PATCH] Add support for attached signatures (#201) (#206) * 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 --- .../CMSOperations.swift | 42 +++- Tests/X509Tests/CMSTests.swift | 179 ++++++++++++++++-- 2 files changed, 196 insertions(+), 25 deletions(-) diff --git a/Sources/X509/CryptographicMessageSyntax/CMSOperations.swift b/Sources/X509/CryptographicMessageSyntax/CMSOperations.swift index af92be9..be2315c 100644 --- a/Sources/X509/CryptographicMessageSyntax/CMSOperations.swift +++ b/Sources/X509/CryptographicMessageSyntax/CMSOperations.swift @@ -24,7 +24,8 @@ 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( @@ -32,7 +33,8 @@ public enum CMS { signatureAlgorithm: signatureAlgorithm, certificate: certificate, privateKey: privateKey, - signingTime: signingTime + signingTime: signingTime, + detached: detached ) } @@ -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) @@ -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: @@ -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) } @@ -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) @@ -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( + 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), diff --git a/Tests/X509Tests/CMSTests.swift b/Tests/X509Tests/CMSTests.swift index def4637..e2ed8b2 100644 --- a/Tests/X509Tests/CMSTests.swift +++ b/Tests/X509Tests/CMSTests.swift @@ -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] @@ -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]) @@ -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, @@ -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) @@ -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] @@ -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( @@ -1097,7 +1239,8 @@ extension CMS { signatureAlgorithm: signatureAlgorithm, additionalIntermediateCertificates: additionalIntermediateCertificates, certificate: certificate, - signedAttrs: signedAttrs + signedAttrs: signedAttrs, + withContent: nil as Data? ) } }