Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@ import SwiftASN1
/// eContent [0] EXPLICIT OCTET STRING OPTIONAL }
/// ContentType ::= OBJECT IDENTIFIER
/// ```
@usableFromInline
struct CMSEncapsulatedContentInfo: DERImplicitlyTaggable, BERImplicitlyTaggable, Hashable, Sendable {
public struct CMSEncapsulatedContentInfo: DERImplicitlyTaggable, BERImplicitlyTaggable, Hashable, Sendable {
Copy link
Contributor

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.

@inlinable
static var defaultIdentifier: ASN1Identifier {
public static var defaultIdentifier: ASN1Identifier {
.sequence
}

Expand All @@ -35,13 +34,13 @@ struct CMSEncapsulatedContentInfo: DERImplicitlyTaggable, BERImplicitlyTaggable,
var eContent: ASN1OctetString?

@inlinable
init(eContentType: ASN1ObjectIdentifier, eContent: ASN1OctetString? = nil) {
public init(eContentType: ASN1ObjectIdentifier, eContent: ASN1OctetString? = nil) {
self.eContentType = eContentType
self.eContent = eContent
}

@inlinable
init(derEncoded rootNode: ASN1Node, withIdentifier identifier: ASN1Identifier) throws {
public init(derEncoded rootNode: ASN1Node, withIdentifier identifier: ASN1Identifier) throws {
self = try DER.sequence(rootNode, identifier: identifier) { nodes in
let eContentType = try ASN1ObjectIdentifier(derEncoded: &nodes)
let eContent = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 0, tagClass: .contextSpecific) { node in
Expand All @@ -53,7 +52,7 @@ struct CMSEncapsulatedContentInfo: DERImplicitlyTaggable, BERImplicitlyTaggable,
}

@inlinable
init(berEncoded rootNode: ASN1Node, withIdentifier identifier: ASN1Identifier) throws {
public init(berEncoded rootNode: ASN1Node, withIdentifier identifier: ASN1Identifier) throws {
self = try BER.sequence(rootNode, identifier: identifier) { nodes in
let eContentType = try ASN1ObjectIdentifier(derEncoded: &nodes)
let eContent = try BER.optionalExplicitlyTagged(&nodes, tagNumber: 0, tagClass: .contextSpecific) { node in
Expand All @@ -65,7 +64,7 @@ struct CMSEncapsulatedContentInfo: DERImplicitlyTaggable, BERImplicitlyTaggable,
}

@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
Expand Down
144 changes: 125 additions & 19 deletions Sources/X509/CryptographicMessageSyntax/CMSOperations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The 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()
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions Sources/X509/CryptographicMessageSyntax/CMSSignerInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,9 @@ extension ASN1ObjectIdentifier {
@usableFromInline
static let signingTime: Self = [1, 2, 840, 113549, 1, 9, 5]

@usableFromInline
static let trustedTimestamp: Self = [1, 2, 840, 113549, 1, 9, 16, 2, 14]

@usableFromInline
static let contentType: Self = [1, 2, 840, 113549, 1, 9, 3]
}
22 changes: 10 additions & 12 deletions Sources/X509/CryptographicMessageSyntax/CMSVersion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The 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)"
}
}
8 changes: 3 additions & 5 deletions Sources/X509/Digests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid making this enum public directly? It makes it very hard to evolve. Preferably we'd wrap it in a struct.

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 {
Expand All @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion Sources/X509/SignatureAlgorithm.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ extension AlgorithmIdentifier {
}

@inlinable
init(digestAlgorithmFor signatureAlgorithm: Certificate.SignatureAlgorithm) throws {
public init(digestAlgorithmFor signatureAlgorithm: Certificate.SignatureAlgorithm) throws {
// Per RFC 5754 § 2, we must produce digest algorithm identifiers with
// absent parameters, so we do.
switch signatureAlgorithm {
Expand Down
23 changes: 9 additions & 14 deletions Sources/X509/X509BaseTypes/AlgorithmIdentifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there's any particular reason to make these private(set).


@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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -216,8 +212,7 @@ extension AlgorithmIdentifier {
}

extension AlgorithmIdentifier: CustomStringConvertible {
@usableFromInline
var description: String {
public var description: String {
switch self {
case .p256PublicKey:
return "p256PublicKey"
Expand Down