SwiftASN1

repository·main·Indexed 41 days ago

https://github.com/apple/swift-asn1

An implementation of ASN.1 types and DER (Distinguished Encoding Rules) serialization for Swift, primarily designed for cryptographic use cases requiring canonical encoding. It provides tools for parsing DER-encoded bytes into ASN1Node trees, serializing nodes back to bytes, and handling PEM (Privacy-Enhanced Mail) documents via PEMParseable and PEMSerializable protocols.

Tokens
3.6K
Snippets
7
Records
16
Agent score
52%

What's inside SwiftASN1

  1. Overview of SwiftASN1

    main

    SwiftASN1 is an implementation of ASN.1 types and DER (Distinguished Encoding Rules) serialization. It is designed for use cases where deterministic serialization is required, such as cryptography.

    The library provides several core components:

    • ASN1Node: A high-level tree representation of an ASN.1 object.
    • DER Parser: Functions to construct an ASN1Node tree from serialized bytes.
    • DER Serializer: Tools to construct serialized bytes from an ASN1Node tree.
    • Built-in Types: Implementations of common ASN.1 constructs (e.g., Integers, Bit Strings, Object Identifiers).
    • PEM Support: Parsers and serializers for Privacy-Enhanced Mail (PEM) format.
  2. Implement Implicit Tagging with DERImplicitlyTaggable

    main

    Implicit tagging replaces an object's usual tag with a new one, saving space (at least 2 bytes) but making the encoding non-self-describing.

    To support implicit tagging, a type should conform to DERImplicitlyTaggable. This requires implementing:

    • init(derEncoded:withIdentifier:)
    • serialize(into:withIdentifier:)

    Warning: Do not use implicit tagging for CHOICE types. Because CHOICE types encode as the chosen value itself, losing the original tag makes it impossible to determine which case was selected during decoding.

    When decoding or encoding, you must manually construct the ASN1Identifier with the correct tagClass (usually .contextSpecific) and tagNumber.

    // Example: Decoding an implicitly tagged INTEGER within a SEQUENCE
    init(derEncoded rootNode: ASN1Node) throws {
        self = try DER.sequence(rootNode, identifier: .sequence) { nodes in
            let value = try Int64(derEncoded: &nodes, withIdentifier: .init(tagWithNumber: 0, tagClass: .contextSpecific))
            return ContainsImplicitTag(value: value)
        }
    }
    
    // Example: Encoding an implicitly tagged INTEGER within a SEQUENCE
    func serialize(into coder: inout DER.Serializer) throws {
        try coder.appendConstructedNode(identifier: .sequence) { coder in
            try self.value.serialize(into: &coder, withIdentifier: .init(tagWithNumber: 0, tagClass: .contextSpecific))
        }
    }
  3. Parse and Serialize PEM documents

    main

    For handling data wrapped in PEM (Privacy-Enhanced Mail) format, use the following protocols and types:

    • PEMParseable: For types that can be parsed from a PEM document.
    • PEMSerializable: For types that can be serialized into a PEM document.
    • PEMRepresentable: A general protocol for PEM handling.
    • PEMDocument: The structure representing a PEM-encoded file or block.
  4. Understand PEM-related protocols and types

    main

    The PEM functionality in SwiftASN1 is built around several key protocols and types:

    • PEMParseable: A protocol for types that can be initialized from a PEM-encoded string using init(pemEncoded:).
    • PEMSerializable: A protocol for types that can be converted into a PEMDocument using serializeAsPEM().
    • PEMDocument: A container for the resulting PEM data, providing access to the encoded string via the pemString property.
    • PEMRepresentable: (Referenced as a related type for PEM operations).
  5. Represent ASN.1 types using ASN1Node

    main
    The library uses ASN1Node as the primary abstraction for representing the structure of an ASN.1 object. An object is modeled as a tree where nodes can be primitive values or composite (constructed) types. Related nodes can be managed via ASN1NodeCollection, and each node is identified by an ASN1Identifier.
  6. Decoding ASN.1 INTEGER values using ArraySlice<UInt8>

    main

    ASN.1 INTEGER types can be of arbitrary size (e.g., 32 bytes or more for ECDSA signatures), meaning they may not fit into a standard Swift Int.

    To handle large integers without implementing a full arbitrary-precision math library, you can use the ASN1IntegerRepresentable protocol. SwiftASN1 provides a built-in conformance for ArraySlice<UInt8>, allowing you to store and decode the raw big-endian bytes of an INTEGER directly.

    // Using ArraySlice<UInt8> to store large ASN.1 INTEGERs
    struct ECDSASignature {
        var r: ArraySlice<UInt8>
        var s: ArraySlice<UInt8>
    
        init(r: ArraySlice<UInt8>, s: ArraySlice<UInt8>) {
            self.r = r
            self.s = s
        }
    }
  7. How SwiftASN1 works: Nodes, Parsing, and Serialization

    main

    SwiftASN1 provides a workflow for handling ASN.1 objects using the Distinguished Encoding Rules (DER) format. The library is composed of four main components that work together:

    1. ASN1Node: A high-level tree representation of an ASN.1 object.
    2. DER.parse(_:): A parser that converts serialized bytes into an ASN1Node tree.
    3. DER.Serializer: A serializer that converts an ASN1Node tree back into serialized bytes.
    4. Built-in Types: A collection of common ASN.1 constructs ready for use.

    This architecture allows you to parse DER-encoded bytes into a traversable tree, manipulate the nodes, and then re-serialize them back into a canonical DER format suitable for cryptographic operations.

  8. Implement Explicit Tagging

    main

    Explicit tagging wraps an object in a new constructed object with the specified tag. Unlike implicit tagging, the underlying object retains its original tag, keeping the encoding self-describing. This method can be applied to any node.

    To decode an explicitly tagged value, use ASN1.explicitlyTagged. To encode it, use serialize(..., explicitlyTaggedWithTagNumber:tagClass:) on the value.

    // Example: Decoding an explicitly tagged INTEGER within a SEQUENCE
    init(derEncoded rootNode: ASN1Node) throws {
        self = try DER.sequence(rootNode, identifier: .sequence) { nodes in
            let value = try ASN1.explicitlyTagged(&nodes, tagNumber: 0, tagClass: .contextSpecific) { 
                try Int64(derEncoded: $0) 
            }
            return ContainsExplicitTag(value: value)
        }
    }
    
    // Example: Encoding an explicitly tagged INTEGER within a SEQUENCE
    func serialize(into coder: inout DER.Serializer) throws {
        try coder.appendConstructedNode(identifier: .sequence) { coder in
            try coder.serialize(self.value, explicitlyTaggedWithTagNumber: 0, tagClass: .contextSpecific)
        }
    }
  9. How to implement DER encoding and decoding for custom types

    main

    SwiftASN1 does not provide an ASN.1 compiler to automatically generate serialization code. Instead, you must manually implement encoding and decoding by conforming your types to DERImplicitlyTaggable (for SEQUENCE types) or DERParseable (for individual objects).

    Decoding Pattern

    1. Conform to DERImplicitlyTaggable: This is the standard approach for SEQUENCE types. Implement init(derEncoded:withIdentifier:).
    2. Unwrap the Sequence: Use DER.sequence(_:identifier:_:) to validate the root node's identifier and access its children via an ASN1NodeCollection.Iterator.
    3. Recursive Decoding: Use the DERParseable initializer init(derEncoded:) on the child nodes. This method automatically consumes the next node from the iterator.

    Encoding Pattern

    1. Implement serialize(into:withIdentifier:): This is required by DERImplicitlyTaggable.
    2. Append Constructed Nodes: For sequences, use coder.appendConstructedNode(identifier:_:). This method provides a closure with a new inout reference to the serializer.
    3. Recursive Serialization: Use coder.serialize(_:) on your fields, which invokes the DERSerializable implementation for those types.
    struct ECDSASignature: DERImplicitlyTaggable {
        static var defaultIdentifier: ASN1Identifier { .sequence }
    
        var r: ArraySlice<UInt8>
        var s: ArraySlice<UInt8>
    
        init(r: ArraySlice<UInt8>, s: ArraySlice<UInt8>) {
            self.r = r
            self.s = s
        }
    
        // Decoding
        init(derEncoded rootNode: ASN1Node, withIdentifier identifier: ASN1Identifier) throws {
            self = try DER.sequence(rootNode, identifier: identifier) { nodes in
                let r = try ArraySlice<UInt8>(derEncoded: &nodes)
                let s = try ArraySlice<UInt8>(derEncoded: &nodes)
                return ECDSASignature(r: r, s: s)
            }
        }
    
        // Encoding
        func serialize(into coder: inout DER.Serializer, withIdentifier identifier: ASN1Identifier) throws {
            try coder.appendConstructedNode(identifier: identifier) { coder in
                try coder.serialize(self.r)
                try coder.serialize(self.s)
            }
        }
    }
  10. Add SwiftASN1 to your Swift Package Manager project

    main

    To use swift-asn1 in your project, add the repository as a dependency in your Package.swift file and then include the SwiftASN1 product in your target's dependencies.

    // Add the package dependency
    dependencies: [
        .package(url: "https://github.com/apple/swift-asn1.git", .upToNextMajor(from: "1.0.0"))
    ]
    
    // Add the product to your target
    target: [
        .target(
            name: "YourTarget",
            dependencies: [
                .product(name: "SwiftASN1", package: "swift-asn1"),
            ]
        )
    ]