Skip to content

Encoding and Decoding Custom Structures

Wes Bustraan edited this page Jan 5, 2025 · 2 revisions

DERTagOptions

This is a Swift OptionSet representing a single DER tag, including the ability to specify whether a tag is primitive or constructed, and the tag class, such as Universal, Application, Context-specific, and Private

All of the most common tags are available as pre-defined names on DERTagOptions. So, for example, DERTagOptions.INTEGER represents an INTEGER tag.

However, you may need to construct your own tag if your ASN.1 definition has application specific tags. Consider this example from RFC 5280:

TBSCertificate  ::=  SEQUENCE  {
     version         [0]  Version DEFAULT v1,
     serialNumber         CertificateSerialNumber,
     signature            AlgorithmIdentifier,
     issuer               Name,
     validity             Validity,
     subject              Name,
     subjectPublicKeyInfo SubjectPublicKeyInfo,
     issuerUniqueID  [1]  IMPLICIT UniqueIdentifier OPTIONAL,
                          -- If present, version MUST be v2 or v3
     subjectUniqueID [2]  IMPLICIT UniqueIdentifier OPTIONAL,
                          -- If present, version MUST be v2 or v3
     extensions      [3]  Extensions OPTIONAL
                          -- If present, version MUST be v3 --  }

To define the tag [2] for subjectUniqueID, you can use the helper function:

DERTagOptions.contextSpecific(2)

However, in order for the DERDecoder or the 'DEREndoderto figure out which tag to use when, you can supply a customDERTagStrategy`:

DERTagStrategy

A DERTagStrategy is helper class that helps the DERDecoder to figure out how to map from DER tags to Swift data types. It also functions in the reverse, where it helps a DEREncoder to write out the correct tags for the Swift structure.

/// A protocol defining strategies for determining DER tags for various aspects of encoding and decoding operations.
///
/// DER tags identify the type of a value in ASN.1 encoding and can vary based on context, type, or value.
/// Implement this protocol to provide custom tagging logic for DER encoding and decoding.
///
/// This protocol is useful for cases where tagging behavior depends on:
/// - The position of the value in the object hierarchy (`codingPath`).
/// - The Swift type of the value being encoded/decoded.
/// - The actual runtime value being encoded.
public protocol DERTagStrategy {
    
    /// Returns the DER tag  for a specific coding path.
    ///
    /// Use this method to determine the appropriate tag based solely on the `codingPath`,
    /// which represents the hierarchy of keys leading to the current value.
    ///
    /// - Parameter codingPath: The hierarchy of coding keys indicating the position in the object graph.
    /// - Returns: The `DERTagOptions` representing the DER tag for this coding path.
    func tag(forPath codingPath: [CodingKey]) -> DERTagOptions
    
    
    /// Returns the DER tag for a specific Swift type at a given coding path.
    ///
    /// Use this method to determine the appropriate tag for a type while taking the coding path into account.
    /// This can be useful when the tagging depends on both the value's position and its type.
    ///
    /// - Parameters:
    ///   - type: The `Decodable` type for which the tag is being determined.
    ///   - codingPath: The hierarchy of coding keys indicating the position in the object graph.
    /// - Returns: The `DERTagOptions` representing the DER tag for this type at the given coding path.
    func tag(forType type: Decodable.Type, atPath codingPath: [CodingKey]) -> DERTagOptions
    
    
    
    /// Returns the DER tag options for a specific value at a given coding path.
    ///
    /// Use this method to determine the appropriate tag for a specific value,
    /// factoring in its runtime characteristics and its position in the object graph.
    ///
    /// - Parameters:
    ///   - value: The `Encodable` value for which the tag is being determined.
    ///   - codingPath: The hierarchy of coding keys indicating the position in the object graph.
    /// - Returns: The `DERTagOptions` representing the DER tag for this value at the given coding path.
    func tag(forValue value: Encodable, atPath codingPath: [CodingKey]) -> DERTagOptions
}

DefaultDERTagStrategy

ASN1Codable provides a DefaultDERTagStrategy class that will be used, well, by default. This provides the common mappings:

Encoding:

Swift Type DER Tag
Bool BOOLEAN
Int INTEGER
Int8 INTEGER
Int16 INTEGER
Int32 INTEGER
Int64 INTEGER
UInt INTEGER
UInt8 INTEGER
UInt16 INTEGER
UInt32 INTEGER
UInt16 INTEGER
UInt64 INTEGER
BInt INTEGER
Float REAL
Double REAL
Data BIT STRING
Date UTCTime
OID OBJECT IDENTIFIER
nil NULL
Set<> SET
Array<> SEQUENCE

DERTagAware

DefaultDERTagStrategy will also check if data types implement the DERTagAware protocol:

public protocol DERTagAware {
    
    /**
        Specify the custom DER tag for encoding and decoding objects of the implementing type
     */
    static var tag: DERTagOptions? { get }
    
    /**
     Specify a strategy to use for child objects of the implementing type
     */
    static var childTagStrategy: DERTagStrategy? { get }
    
}

In this way, a class or struct can tell the decoder what tag to expect during decoding. It can also be used to specific a custom DERTagStrategy to use while decoding its child data.

Custom DERTagStrategy

There likely will be time where you need to specify a custom DER tag that is not one of the pre-defined ones, such as the tags needed for version, issuerUniqueID, subjectUniqueID, and extensions fields in the TBSCertificate show above.

You can extend the DefaultDERTagStrategy to provide your own. Here the corresponding example code for TBSCertificate:

public struct TBSCertificate : Codable, DERTagAware {

    class TagStrategy : DefaultDERTagStrategy {
        override func tag(forType type: Decodable.Type, atPath codingPath: [CodingKey]) -> DERTagOptions {
            
            if let lastKey = codingPath.last as? CodingKeys {
                switch lastKey {
                case .version:
                    // version [0] EXPLICIT Version DEFAULT v1,
                    return DERTagOptions.contextSpecific(0)
                case .issuerUniqueID:
                    // issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL,
                    return DERTagOptions.contextSpecific(1)
                case .subjectUniqueID:
                    // subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL,
                    return DERTagOptions.contextSpecific(2)
                case .extensions:
                    // extensions [3] EXPLICIT Extensions OPTIONAL
                    return DERTagOptions.contextSpecific(3)
                default:
                    break
                }
            }
            
            return super.tag(forType: type, atPath: codingPath)
        }

       // rest of class..
    }
}

Then, it can be used when implementing DERTagAware:

public struct TBSCertificate : Codable, DERTagAware {
    
    public static var tag: DERTagOptions? = nil
    
    public static var childTagStrategy: DERTagStrategy? = TBSCertificate.TagStrategy()

    // rest of struct..
}

Decoding classes and structs

In general, a Swift class or struct is represented in ASN.1 as a SEQUENCE of child elements and ASN1Codable will default to a SEQUENCE for any data type it doesn't otherwise recognize.

Keyed vs Unkeyed containers

DER doesn't really have the concept of a dictionary or map; everything is generally either a SEQUENCE or SET.

In Swift's Codable, you can use the decoder: Decoder to either access the data as a keyed structure or unkeyed:

public init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
}

or

public init(from decoder: Decoder) throws {
    let container = try decoder.unkeyedContainer()
}

In ASN1Codable, you can use either one. The benefit of using the keyed type is that, as described above, the key will be added to the codingPath and you can use it to determine DER tags to use and the codingPath will be included in an DecodingErrors that may be thrown, allowing you to debug the structure more easily.

However, it's important to note that, unlike a Dictionary in JSON, for example, the order that you decode "keys" matters in a DER structure.