Skip to content

CMSSignerInfo cannot round-trip unsignedAttrs: parser expects wrong implicit tag #306

Description

@emaloney

Description

CMSSignerInfo serializes unsignedAttrs as [1] IMPLICIT SET OF Attribute
(per RFC 5652 §5.3), but its parser reads the [1]-tagged node and then asks
DER.set/BER.set for an inner identifier of [0] instead of [1]. As a
result, any SignerInfo that actually contains unsignedAttrs (e.g. a trusted
timestamp) fails to parse with:

  ASN1Error.unexpectedFieldType: ASN1Identifier(tagNumber: 1, tagClass: contextSpecific)

The bug is latent today because nothing in the library emits unsignedAttrs,
but it means a SignerInfo produced with unsigned attributes cannot be
round-tripped or verified by swift-certificates itself. The serialized wire
format is correct, so third-party parsers (OpenSSL, etc.) read it fine.

Location

Sources/X509/CryptographicMessageSyntax/CMSSignerInfo.swift

  • DER init: the unsignedAttrs block passes identifier: .init(tagWithNumber: 0, ...)
  • BER init: same
  • serialize(into:withIdentifier:) correctly writes tagWithNumber: 1

Fix

Change the inner DER.set / BER.set identifier for unsignedAttrs from
tagWithNumber: 0 to tagWithNumber: 1, matching the serializer and the
signedAttrs pattern.

Reproduction

A CMSSignerInfo with any unsignedAttrs fails assertRoundTrips:

import XCTest
import SwiftASN1
@testable @_spi(CMS) import X509

final class CMSSignerInfoUnsignedAttributesRoundTripTest: XCTestCase
{
    private func assertRoundTrips<ASN1Object: DERParseable & DERSerializable & Equatable>(_ value: ASN1Object)
        throws
    {
        var serializer = DER.Serializer()
        try serializer.serialize(value)
        let parsed = try ASN1Object(derEncoded: serializer.serializedBytes)
        XCTAssertEqual(parsed, value)
    }

    func testCMSSignerInfoWithUnsignedAttrsRoundTrips()
        throws
    {
        // A SignerInfo carrying unsignedAttrs must survive a serialize/parse round trip.
        // This exercises the `[1] IMPLICIT` unsignedAttrs path.
        let unsignedAttr = CMSAttribute(
            attrType: .contentType,
            attrValues: [try ASN1Any(erasing: ASN1OctetString(contentBytes: [0xDE, 0xAD, 0xBE, 0xEF]))]
        )
        try assertRoundTrips(
            CMSSignerInfo(
                version: .v1,
                signerIdentifier: .issuerAndSerialNumber(
                    .init(
                        issuer: .init {
                            CountryName("US")
                            OrganizationName("Apple Inc.")
                            CommonName("Apple Public EV Server ECC CA 1 - G1")
                        },
                        serialNumber: .init(bytes: [20, 30, 40, 50])
                    )
                ),
                digestAlgorithm: .sha256WithRSAEncryptionUsingNil,
                signatureAlgorithm: .ecdsaWithSHA256,
                signature: .init(contentBytes: [100, 110, 120, 130, 140]),
                unsignedAttrs: [unsignedAttr]
            )
        )
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions