-
Notifications
You must be signed in to change notification settings - Fork 89
Support generating CMS with trusted signing time #288
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -78,39 +78,106 @@ public enum CMS: Sendable { | |
| } | ||
|
|
||
| @inlinable | ||
| static func signWithSigningTime<Bytes: DataProtocol>( | ||
| _ bytes: Bytes, | ||
| static func buildSignedAttributes<Bytes: DataProtocol>( | ||
| for bytes: Bytes, | ||
| signatureAlgorithm: Certificate.SignatureAlgorithm, | ||
| additionalIntermediateCertificates: [Certificate] = [], | ||
| certificate: Certificate, | ||
| privateKey: Certificate.PrivateKey, | ||
| 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: | ||
| // 1. content-type | ||
| // 2. message-digest | ||
|
|
||
| // add content-type signedAttr cms data | ||
| signingTime: Date | ||
| ) throws -> [CMSAttribute] { | ||
| // 1. content-type attribute | ||
| let contentTypeVal = try ASN1Any(erasing: ASN1ObjectIdentifier.cmsData) | ||
| let contentTypeAttribute = CMSAttribute(attrType: .contentType, attrValues: [contentTypeVal]) | ||
| signedAttrs.append(contentTypeAttribute) | ||
|
|
||
| // add message-digest of provided content bytes | ||
| // 2. message-digest attribute | ||
| let digestAlgorithm = try AlgorithmIdentifier(digestAlgorithmFor: signatureAlgorithm) | ||
| let computedDigest = try Digest.computeDigest(for: bytes, using: digestAlgorithm) | ||
| let messageDigest = ASN1OctetString(contentBytes: ArraySlice(computedDigest)) | ||
| let messageDigestVal = try ASN1Any(erasing: messageDigest) | ||
| let messageDigestAttr = CMSAttribute(attrType: .messageDigest, attrValues: [messageDigestVal]) | ||
| signedAttrs.append(messageDigestAttr) | ||
| let messageDigestAttribute = CMSAttribute(attrType: .messageDigest, attrValues: [messageDigestVal]) | ||
|
|
||
| // add signing time utc time in 'YYMMDDHHMMSSZ' format as specificed in `UTCTime` | ||
| let utcTime = try UTCTime(signingTime.utcDate) | ||
| let signingTimeAttrVal = try ASN1Any(erasing: utcTime) | ||
| let signingTimeAttribute = CMSAttribute(attrType: .signingTime, attrValues: [signingTimeAttrVal]) | ||
| signedAttrs.append(signingTimeAttribute) | ||
| return [contentTypeAttribute, messageDigestAttribute, signingTimeAttribute] | ||
| } | ||
|
|
||
| @_spi(CMS) | ||
| @inlinable | ||
| public static func createSigningTimeASN1(signingTime: Date) throws -> Data { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this particular function pulling its weight? It isn't used by anything inside this module, and it does something quite specific, so I'm inclined to want to pull it out of this module and keep it in the code that uses it. |
||
| let utcTime = try UTCTime(signingTime.utcDate) | ||
| let signingTimeAttrVal = try ASN1Any(erasing: utcTime) | ||
| var coder = DER.Serializer() | ||
| try coder.serialize(signingTimeAttrVal) | ||
| return Data(coder.serializedBytes) | ||
| } | ||
|
|
||
| @_spi(CMS) | ||
| @inlinable | ||
| public static func getSignedAttributesBytes<Data: DataProtocol>( | ||
| digest: Data, | ||
| signatureAlgorithm: Certificate.SignatureAlgorithm, | ||
| signingTime: Date | ||
| ) throws -> [UInt8] { | ||
| let signedAttrs: [CMSAttribute] = try buildSignedAttributes( | ||
| for: digest, | ||
| signatureAlgorithm: signatureAlgorithm, | ||
| signingTime: signingTime | ||
| ) | ||
| var coder = DER.Serializer() | ||
| try coder.serializeSetOf(signedAttrs) | ||
| return coder.serializedBytes | ||
| } | ||
|
|
||
| @_spi(CMS) | ||
| @inlinable | ||
| public static func signWithTrustedTimestamp<Data: DataProtocol>( | ||
| _ bytes: Data, | ||
| signatureBytes: ASN1OctetString, | ||
| signatureAlgorithm: Certificate.SignatureAlgorithm, | ||
| additionalIntermediateCertificates: [Certificate] = [], | ||
| certificate: Certificate, | ||
| signingTime: Date, | ||
| detached: Bool = true, | ||
| trustedTimestampBytes: [UInt8] | ||
| ) throws -> [UInt8] { | ||
| let signedAttrs: [CMSAttribute] = try buildSignedAttributes( | ||
| for: bytes, | ||
| signatureAlgorithm: signatureAlgorithm, | ||
| signingTime: signingTime | ||
| ) | ||
| // Adding trusted timestamp to unsignedAttrs | ||
| let tsrWrapped = try ASN1Any(derEncoded: trustedTimestampBytes) | ||
| let timestampAttr = CMSAttribute(attrType: .trustedTimestamp, attrValues: [tsrWrapped]) | ||
| let unsignedAttrs: [CMSAttribute] = [timestampAttr] | ||
|
|
||
| // Generating CMS | ||
| let signedData = try self.generateSignedDataWithUnsignedAttrs( | ||
| signatureBytes: signatureBytes, | ||
| signatureAlgorithm: signatureAlgorithm, | ||
| additionalIntermediateCertificates: additionalIntermediateCertificates, | ||
| certificate: certificate, | ||
| signedAttrs: signedAttrs, | ||
| withContent: detached ? nil : bytes, | ||
| unsignedAttrs: unsignedAttrs | ||
| ) | ||
| return try self.serializeSignedData(signedData) | ||
| } | ||
|
|
||
| @inlinable | ||
| static func signWithSigningTime<Bytes: DataProtocol>( | ||
| _ bytes: Bytes, | ||
| signatureAlgorithm: Certificate.SignatureAlgorithm, | ||
| additionalIntermediateCertificates: [Certificate] = [], | ||
| certificate: Certificate, | ||
| privateKey: Certificate.PrivateKey, | ||
| signingTime: Date, | ||
| detached: Bool = true | ||
| ) throws -> [UInt8] { | ||
| let signedAttrs: [CMSAttribute] = try buildSignedAttributes( | ||
| for: bytes, | ||
| signatureAlgorithm: signatureAlgorithm, | ||
| signingTime: signingTime | ||
| ) | ||
| // As specified in RFC 5652 section 5.4: | ||
| // When the [signedAttrs] field is present, however, the result is the message digest of the complete DER encoding of the SignedAttrs value contained in the signedAttrs field. | ||
| var coder = DER.Serializer() | ||
|
|
@@ -174,6 +241,45 @@ public enum CMS: Sendable { | |
| ) | ||
| } | ||
|
|
||
| @inlinable | ||
| static func generateSignedDataWithUnsignedAttrs<Bytes: DataProtocol>( | ||
| signatureBytes: ASN1OctetString, | ||
| signatureAlgorithm: Certificate.SignatureAlgorithm, | ||
| additionalIntermediateCertificates: [Certificate], | ||
| certificate: Certificate, | ||
| signedAttrs: [CMSAttribute]? = nil, | ||
| withContent content: Bytes? = nil, | ||
| unsignedAttrs: [CMSAttribute]? = nil | ||
| ) throws -> CMSContentInfo { | ||
| let digestAlgorithm = try AlgorithmIdentifier(digestAlgorithmFor: signatureAlgorithm) | ||
| var contentInfo = CMSEncapsulatedContentInfo(eContentType: .cmsData) | ||
| if let content { | ||
| contentInfo.eContent = ASN1OctetString(contentBytes: Array(content)[...]) | ||
| } | ||
|
|
||
| let signerInfo = CMSSignerInfo( | ||
| signerIdentifier: .init(issuerAndSerialNumber: certificate), | ||
| digestAlgorithm: digestAlgorithm, | ||
| signedAttrs: signedAttrs, | ||
| signatureAlgorithm: AlgorithmIdentifier(signatureAlgorithm), | ||
| signature: signatureBytes, | ||
| unsignedAttrs: unsignedAttrs | ||
| ) | ||
|
|
||
|
|
||
| var certificates = [certificate] | ||
| certificates.append(contentsOf: additionalIntermediateCertificates) | ||
|
|
||
| let signedData = CMSSignedData( | ||
| version: .v3, // Signed Data should be v3 | ||
| digestAlgorithms: [digestAlgorithm], | ||
| encapContentInfo: contentInfo, | ||
| certificates: certificates, | ||
| signerInfos: [signerInfo] | ||
| ) | ||
| return try CMSContentInfo(signedData) | ||
| } | ||
|
|
||
| @inlinable | ||
| static func generateSignedData<Bytes: DataProtocol>( | ||
| signatureBytes: ASN1OctetString, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,27 +19,25 @@ import SwiftASN1 | |
| /// CMSVersion ::= INTEGER | ||
| /// { v0(0), v1(1), v2(2), v3(3), v4(4), v5(5) } | ||
| /// ``` | ||
| @usableFromInline | ||
| struct CMSVersion: RawRepresentable, Hashable, Sendable { | ||
| @usableFromInline | ||
| var rawValue: Int | ||
| public struct CMSVersion: RawRepresentable, Hashable, Sendable { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's keep this behind the same SPI as the rest of the CMS stuff. |
||
| public var rawValue: Int | ||
|
|
||
| @inlinable | ||
| init(rawValue: Int) { | ||
| public init(rawValue: Int) { | ||
| self.rawValue = rawValue | ||
| } | ||
|
|
||
| @usableFromInline static let v0 = Self(rawValue: 0) | ||
| @usableFromInline static let v1 = Self(rawValue: 1) | ||
| @usableFromInline static let v2 = Self(rawValue: 2) | ||
| @usableFromInline static let v3 = Self(rawValue: 3) | ||
| @usableFromInline static let v4 = Self(rawValue: 4) | ||
| @usableFromInline static let v5 = Self(rawValue: 5) | ||
| public static let v0 = Self(rawValue: 0) | ||
| public static let v1 = Self(rawValue: 1) | ||
| public static let v2 = Self(rawValue: 2) | ||
| public static let v3 = Self(rawValue: 3) | ||
| public static let v4 = Self(rawValue: 4) | ||
| public static let v5 = Self(rawValue: 5) | ||
| } | ||
|
|
||
| extension CMSVersion: CustomStringConvertible { | ||
| @inlinable | ||
| var description: String { | ||
| public var description: String { | ||
| "CMSv\(rawValue)" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,16 +19,15 @@ import Foundation | |
| #endif | ||
| @preconcurrency import Crypto | ||
|
|
||
| @usableFromInline | ||
| @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *) | ||
| enum Digest: Sendable { | ||
| public enum Digest: Sendable { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we avoid making this |
||
| case insecureSHA1(Insecure.SHA1Digest) | ||
| case sha256(SHA256Digest) | ||
| case sha384(SHA384Digest) | ||
| case sha512(SHA512Digest) | ||
|
|
||
| @inlinable | ||
| static func computeDigest<Bytes: DataProtocol>( | ||
| public static func computeDigest<Bytes: DataProtocol>( | ||
| for bytes: Bytes, | ||
| using digestIdentifier: AlgorithmIdentifier | ||
| ) throws -> Digest { | ||
|
|
@@ -49,8 +48,7 @@ enum Digest: Sendable { | |
|
|
||
| @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *) | ||
| extension Digest: Sequence { | ||
| @usableFromInline | ||
| func makeIterator() -> some IteratorProtocol<UInt8> { | ||
| public func makeIterator() -> some IteratorProtocol<UInt8> { | ||
| switch self { | ||
| case .insecureSHA1(let sha1): | ||
| return sha1.makeIterator() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,27 +14,23 @@ | |
|
|
||
| import SwiftASN1 | ||
|
|
||
| @usableFromInline | ||
| struct AlgorithmIdentifier: DERImplicitlyTaggable, BERImplicitlyTaggable, Hashable, Sendable { | ||
| public struct AlgorithmIdentifier: DERImplicitlyTaggable, BERImplicitlyTaggable, Hashable, Sendable { | ||
| @inlinable | ||
| static var defaultIdentifier: ASN1Identifier { | ||
| public static var defaultIdentifier: ASN1Identifier { | ||
| .sequence | ||
| } | ||
|
|
||
| @usableFromInline | ||
| var algorithm: ASN1ObjectIdentifier | ||
| public private(set) var algorithm: ASN1ObjectIdentifier | ||
|
|
||
| @usableFromInline | ||
| var parameters: ASN1Any? | ||
| public private(set) var parameters: ASN1Any? | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think there's any particular reason to make these |
||
|
|
||
| @inlinable | ||
| init(algorithm: ASN1ObjectIdentifier, parameters: ASN1Any?) { | ||
| public init(algorithm: ASN1ObjectIdentifier, parameters: ASN1Any?) { | ||
| self.algorithm = algorithm | ||
| self.parameters = parameters | ||
| } | ||
|
|
||
| @inlinable | ||
| init(derEncoded rootNode: ASN1Node, withIdentifier identifier: ASN1Identifier) throws { | ||
| public init(derEncoded rootNode: ASN1Node, withIdentifier identifier: ASN1Identifier) throws { | ||
| // The AlgorithmIdentifier block looks like this. | ||
| // | ||
| // AlgorithmIdentifier ::= SEQUENCE { | ||
|
|
@@ -51,12 +47,12 @@ struct AlgorithmIdentifier: DERImplicitlyTaggable, BERImplicitlyTaggable, Hashab | |
| } | ||
|
|
||
| @inlinable | ||
| init(berEncoded rootNode: ASN1Node, withIdentifier identifier: ASN1Identifier) throws { | ||
| public init(berEncoded rootNode: ASN1Node, withIdentifier identifier: ASN1Identifier) throws { | ||
| self = try .init(derEncoded: rootNode, withIdentifier: identifier) | ||
| } | ||
|
|
||
| @inlinable | ||
| func serialize(into coder: inout DER.Serializer, withIdentifier identifier: ASN1Identifier) throws { | ||
| public func serialize(into coder: inout DER.Serializer, withIdentifier identifier: ASN1Identifier) throws { | ||
| try coder.appendConstructedNode(identifier: identifier) { coder in | ||
| try coder.serialize(self.algorithm) | ||
| if let parameters = self.parameters { | ||
|
|
@@ -216,8 +212,7 @@ extension AlgorithmIdentifier { | |
| } | ||
|
|
||
| extension AlgorithmIdentifier: CustomStringConvertible { | ||
| @usableFromInline | ||
| var description: String { | ||
| public var description: String { | ||
| switch self { | ||
| case .p256PublicKey: | ||
| return "p256PublicKey" | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's keep this type behind the same SPI as the rest of the CMS stuff.