-
Notifications
You must be signed in to change notification settings - Fork 0
Encoding and Decoding Custom Structures
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`:
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
}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 |
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.
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..
}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.
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.