Skip to content
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

Cryptographic Message Syntax ASN.1 de/serialisation #14

Merged
merged 4 commits into from
Feb 2, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/apple/swift-crypto.git", from: "2.2.1"),
// swift-asn1 repo is private, so we can't access it anonymously yet
// .package(url: "https://github.com/apple/swift-asn1.git", .upToNextMinor(from: "0.3.0")),
.package(url: "git@github.com:apple/swift-asn1.git", .upToNextMinor(from: "0.3.0")),
// .package(url: "https://github.com/apple/swift-asn1.git", .upToNextMinor(from: "0.4.0")),
.package(url: "git@github.com:apple/swift-asn1.git", .upToNextMinor(from: "0.4.0")),
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
],
targets: [
Expand Down
52 changes: 48 additions & 4 deletions Sources/X509/CryptographicMessageSyntax/CMSContentInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,70 @@

import SwiftASN1

extension ASN1ObjectIdentifier {
/// Cryptographic Message Syntax (CMS) Signed Data.
///
/// ASN.1 definition:
/// ```
/// id-signedData OBJECT IDENTIFIER ::= { iso(1) member-body(2)
/// us(840) rsadsi(113549) pkcs(1) pkcs7(7) 2 }
/// ```
static let cmsSignedData: ASN1ObjectIdentifier = [1, 2, 840, 113549, 1, 7, 2]
}

/// ``ContentInfo`` is defined in ASN.1 as:
/// ```
/// ContentInfo ::= SEQUENCE {
/// contentType ContentType,
/// content [0] EXPLICIT ANY DEFINED BY contentType }
/// ContentType ::= OBJECT IDENTIFIER
/// ```
struct CMSContentInfo {
struct CMSContentInfo: DERImplicitlyTaggable, Hashable {
static var defaultIdentifier: ASN1Identifier {
.sequence
}

var contentType: ASN1ObjectIdentifier
var content: ASN1Any

init(contentType: ASN1ObjectIdentifier, content: ASN1Any) {
self.contentType = contentType
self.content = content
}

init(derEncoded rootNode: ASN1Node, withIdentifier identifier: ASN1Identifier) throws {
self = try DER.sequence(rootNode, identifier: identifier) { nodes in
let contentType = try ASN1ObjectIdentifier(derEncoded: &nodes)

let content = try DER.explicitlyTagged(&nodes, tagNumber: 0, tagClass: .contextSpecific) { node in
ASN1Any(derEncoded: node)
}
return .init(contentType: contentType, content: content)
}
}

func serialize(into coder: inout DER.Serializer, withIdentifier identifier: ASN1Identifier) throws {
try coder.appendConstructedNode(identifier: identifier) { coder in
try coder.serialize(contentType)
try coder.serialize(explicitlyTaggedWithTagNumber: 0, tagClass: .contextSpecific) { coder in
try coder.serialize(content)
}
}
}
}

extension CMSContentInfo {
init(_ signedData: CMSSignedData) {
fatalError("TODO: not implemented")
init(_ signedData: CMSSignedData) throws {
self.contentType = .cmsSignedData
self.content = try ASN1Any(erasing: signedData)
}

var signedData: CMSSignedData? {
get throws {
fatalError("TODO: not implemented")
guard contentType == .cmsSignedData else {
return nil
}
return try CMSSignedData(asn1Any: content)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,38 @@ import SwiftASN1
/// eContent [0] EXPLICIT OCTET STRING OPTIONAL }
/// ContentType ::= OBJECT IDENTIFIER
/// ```
struct CMSEncapsulatedContentInfo {
struct CMSEncapsulatedContentInfo: DERImplicitlyTaggable, Hashable {
static var defaultIdentifier: ASN1Identifier {
.sequence
}

var eContentType: ASN1ObjectIdentifier
var eContent: ASN1OctetString?

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

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
try ASN1OctetString(derEncoded: node)
}

return .init(eContentType: eContentType, eContent: eContent)
}
}

func serialize(into coder: inout DER.Serializer, withIdentifier identifier: ASN1Identifier) throws {
try coder.appendConstructedNode(identifier: identifier, { coder in
try coder.serialize(eContentType)
if let eContent {
try coder.serialize(explicitlyTaggedWithTagNumber: 0, tagClass: .contextSpecific) { coder in
try coder.serialize(eContent)
}
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,34 @@ 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].
struct CMSIssuerAndSerialNumber {
struct CMSIssuerAndSerialNumber: DERImplicitlyTaggable, Hashable {
static var defaultIdentifier: ASN1Identifier {
.sequence
}

var issuer: DistinguishedName
var serialNumber: Certificate.SerialNumber

init(
issuer: DistinguishedName,
serialNumber: Certificate.SerialNumber
) {
self.issuer = issuer
self.serialNumber = serialNumber
}

init(derEncoded rootNode: ASN1Node, withIdentifier identifier: ASN1Identifier) throws {
self = try DER.sequence(rootNode, identifier: identifier) { nodes in
let issuer = try DistinguishedName(derEncoded: &nodes)
let serialNumber = try ArraySlice<UInt8>(derEncoded: &nodes)
return .init(issuer: issuer, serialNumber: .init(bytes: serialNumber))
}
}

func serialize(into coder: inout DER.Serializer, withIdentifier identifier: ASN1Identifier) throws {
try coder.appendConstructedNode(identifier: identifier) { coder in
try coder.serialize(self.issuer)
try coder.serialize(self.serialNumber.bytes)
}
}
}
81 changes: 80 additions & 1 deletion Sources/X509/CryptographicMessageSyntax/CMSSignedData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import SwiftASN1
/// DigestAlgorithmIdentifiers ::= SET OF DigestAlgorithmIdentifier
/// DigestAlgorithmIdentifier ::= AlgorithmIdentifier
/// SignerInfos ::= SET OF SignerInfo
/// CertificateSet ::= SET OF CertificateChoices
///
/// CertificateChoices ::= CHOICE {
/// certificate Certificate,
Expand All @@ -40,10 +41,88 @@ import SwiftASN1
/// otherCert ANY DEFINED BY otherCertFormat }
/// ```
/// - Note: At the moment we don't support `crls` (`RevocationInfoChoices`)
struct CMSSignedData {
struct CMSSignedData: DERImplicitlyTaggable, Hashable {
private enum Error: Swift.Error {
case multipleDigestAlgorithmsAreNotSupportedYet
case multipleSignerInfosAreNotSupportedYet
}
static var defaultIdentifier: ASN1Identifier {
.sequence
}

var version: CMSVersion
var digestAlgorithms: [AlgorithmIdentifier]
var encapContentInfo: CMSEncapsulatedContentInfo
var certificates: [Certificate]?
var signerInfos: [CMSSignerInfo]

init(
version: CMSVersion,
digestAlgorithms: [AlgorithmIdentifier],
encapContentInfo: CMSEncapsulatedContentInfo,
certificates: [Certificate]?,
signerInfos: [CMSSignerInfo]
) {
self.version = version
self.digestAlgorithms = digestAlgorithms
self.encapContentInfo = encapContentInfo
self.certificates = certificates
self.signerInfos = signerInfos
}

init(derEncoded: ASN1Node, withIdentifier identifier: ASN1Identifier) throws {
self = try DER.sequence(derEncoded, identifier: identifier) { nodes in
let version = try CMSVersion(rawValue: Int.init(derEncoded: &nodes))
let digestAlgorithms = try DER.sequence(of: AlgorithmIdentifier.self, identifier: .set, nodes: &nodes)
// TODO: support multiple digest algorithms. For this we need to validate that the binary representation of each element is lexicographically sorted.
guard digestAlgorithms.count <= 1 else {
throw Error.multipleDigestAlgorithmsAreNotSupportedYet
}

let encapContentInfo = try CMSEncapsulatedContentInfo(derEncoded: &nodes)
let certificates = try DER.optionalImplicitlyTagged(&nodes, tagNumber: 0, tagClass: .contextSpecific) { node in
// TODO: this is actually a SET OF so we need to verify that the binary representation of each element is lexicographically sorted.
try DER.sequence(of: Certificate.self, identifier: .init(tagWithNumber: 0, tagClass: .contextSpecific), rootNode: node)
}

// we need to skip this node even though we don't support it
_ = DER.optionalImplicitlyTagged(&nodes, tagNumber: 1, tagClass: .contextSpecific) { _ in }

let signerInfos = try DER.sequence(of: CMSSignerInfo.self, identifier: .set, nodes: &nodes)

// TODO: support multiple signer infos. For this we need to validate that the binary representation of each element is lexicographically sorted.
guard signerInfos.count <= 1 else {
throw Error.multipleDigestAlgorithmsAreNotSupportedYet
Copy link
Contributor

Choose a reason for hiding this comment

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

Presumably this was meant to be the signer infos error.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch! Thanks. Fixed in c33a826

}

return .init(
version: version,
digestAlgorithms: digestAlgorithms,
encapContentInfo: encapContentInfo,
certificates: certificates,
signerInfos: signerInfos
)
}
}

func serialize(into coder: inout DER.Serializer, withIdentifier identifier: ASN1Identifier) throws {
try coder.appendConstructedNode(identifier: identifier) { coder in
try coder.serialize(version.rawValue)
guard self.digestAlgorithms.count <= 1 else {
throw Error.multipleDigestAlgorithmsAreNotSupportedYet
}
// TODO: this is actually a SET OF. We need to sort the binary representation of each element lexicographically before encoding.
try coder.serializeSequenceOf(self.digestAlgorithms, identifier: .set)
try coder.serialize(self.encapContentInfo)
if let certificates {
// TODO: this is actually a SET OF. We need to sort the binary representation of each element lexicographically before encoding.
try coder.serializeSequenceOf(certificates, identifier: .init(tagWithNumber: 0, tagClass: .contextSpecific))
}
guard self.signerInfos.count <= 1 else {
throw Error.multipleSignerInfosAreNotSupportedYet
}
// TODO: this is actually a SET OF. We need to sort the binary representation of each element lexicographically before encoding.
try coder.serializeSequenceOf(self.signerInfos, identifier: .set)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,34 @@ import SwiftASN1
/// issuerAndSerialNumber IssuerAndSerialNumber,
/// subjectKeyIdentifier [0] SubjectKeyIdentifier }
/// ```
enum CMSSignerIdentifier {
enum CMSSignerIdentifier: DERParseable, DERSerializable, Hashable {

private static let subjectKeyIdentifierIdentifier = ASN1Identifier(tagWithNumber: 0, tagClass: .contextSpecific)
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe call this skiIdentifier, repeating the word identifier feels weird here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Agree, changed in c33a826


case issuerAndSerialNumber(CMSIssuerAndSerialNumber)
case subjectKeyIdentifier(Certificate.Extensions.SubjectKeyIdentifier)

init(derEncoded node: ASN1Node) throws {
switch node.identifier {
case CMSIssuerAndSerialNumber.defaultIdentifier:
self = try .issuerAndSerialNumber(.init(derEncoded: node))

case Self.subjectKeyIdentifierIdentifier:
self = try .subjectKeyIdentifier(.init(keyIdentifier: .init(derEncoded: node, withIdentifier: Self.subjectKeyIdentifierIdentifier)))

default:
throw ASN1Error.invalidASN1Object
}
}

func serialize(into coder: inout DER.Serializer) throws {
switch self {
case .issuerAndSerialNumber(let issuerAndSerialNumber):
try issuerAndSerialNumber.serialize(into: &coder)

case .subjectKeyIdentifier(let subjectKeyIdentifier):
try subjectKeyIdentifier.keyIdentifier.serialize(into: &coder, withIdentifier: Self.subjectKeyIdentifierIdentifier)

}
}
}
87 changes: 85 additions & 2 deletions Sources/X509/CryptographicMessageSyntax/CMSSignerInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,94 @@ import SwiftASN1
/// then the `version` MUST be 1. If the `SignerIdentifier` is `subjectKeyIdentifier`,
/// then the `version` MUST be 3.
/// - Note: At the moment we neither support `signedAttrs` (`SignedAttributes`) nor `unsignedAttrs` (`UnsignedAttributes`)
struct CMSSignerInfo {
struct CMSSignerInfo: DERImplicitlyTaggable, Hashable {
enum Error: Swift.Error {
case versionAndSignerIdentifierMismatch(String)
}
static var defaultIdentifier: ASN1Identifier {
.sequence
}

var version: CMSVersion
var signerIdentifier: CMSSignerIdentifier
var digestAlgorithm: AlgorithmIdentifier
var signatureAlgorithm: AlgorithmIdentifier
var signature: ASN1OctetString

init(
signerIdentifier: CMSSignerIdentifier,
digestAlgorithm: AlgorithmIdentifier,
signatureAlgorithm: AlgorithmIdentifier,
signature: ASN1OctetString
) {
switch signerIdentifier {
case .issuerAndSerialNumber:
self.version = .v1
case .subjectKeyIdentifier:
self.version = .v3
}
self.signerIdentifier = signerIdentifier
self.digestAlgorithm = digestAlgorithm
self.signatureAlgorithm = signatureAlgorithm
self.signature = signature
}

init(
version: CMSVersion,
signerIdentifier: CMSSignerIdentifier,
digestAlgorithm: AlgorithmIdentifier,
signatureAlgorithm: AlgorithmIdentifier,
signature: ASN1OctetString
) {
self.version = version
self.signerIdentifier = signerIdentifier
self.digestAlgorithm = digestAlgorithm
self.signatureAlgorithm = signatureAlgorithm
self.signature = signature
}

init(derEncoded rootNode: ASN1Node, withIdentifier identifier: ASN1Identifier) throws {
self = try DER.sequence(rootNode, identifier: identifier) { nodes in
let version = try CMSVersion(rawValue: Int(derEncoded: &nodes))
let signerIdentifier = try CMSSignerIdentifier(derEncoded: &nodes)
switch signerIdentifier {
case .issuerAndSerialNumber:
guard version == .v1 else {
throw Error.versionAndSignerIdentifierMismatch("expected \(CMSVersion.v1) but got \(version) where signerIdentifier is \(signerIdentifier)")
}
case .subjectKeyIdentifier:
guard version == .v3 else {
throw Error.versionAndSignerIdentifierMismatch("expected \(CMSVersion.v3) but got \(version) where signerIdentifier is \(signerIdentifier)")
}
}
let digestAlgorithm = try AlgorithmIdentifier(derEncoded: &nodes)

// we don't support signedAttrs yet but we still need to skip them
_ = DER.optionalImplicitlyTagged(&nodes, tagNumber: 0, tagClass: .contextSpecific) { _ in }

let signatureAlgorithm = try AlgorithmIdentifier(derEncoded: &nodes)
let signature = try ASN1OctetString(derEncoded: &nodes)

// we don't support unsignedAttrs yet but we still need to skip them
_ = DER.optionalImplicitlyTagged(&nodes, tagNumber: 1, tagClass: .contextSpecific) { _ in }

return .init(
version: version,
signerIdentifier: signerIdentifier,
digestAlgorithm: digestAlgorithm,
signatureAlgorithm: signatureAlgorithm,
signature: signature
)
}
}

func serialize(into coder: inout DER.Serializer, withIdentifier identifier: ASN1Identifier) throws {
try coder.appendConstructedNode(identifier: identifier) { coder in
try coder.serialize(self.version.rawValue)
try coder.serialize(self.signerIdentifier)
try coder.serialize(self.digestAlgorithm)
try coder.serialize(self.signatureAlgorithm)
try coder.serialize(self.signature)
}
}
}

Loading