在Swift中解码ASN.1 DER序列

我正在使用的API返回使用椭圆曲线数字签名算法(ECDSA)签名的敏感数据。 签名本身是一个ASN.1 DER序列-因为我在网上找不到Swift或Objective-C DER解析器,所以我需要自己编写。

ECDSA签名可以分为两个整数。 我们可以使用openssl检查此整数。

  $ echo -en“ 3045022100a85e76ff1f18e52d4eccde774aa46e3ec2891068ebef89a7cb6e0653eb0dffb202204cd76e19bcb18d76c13af0ea061117cbd6d3c8fbd1d9fc9aac265266371c3a3f” 
xxd -pu -r>签名
$ openssl asn1parse-通知der-输入签名
。 0:d = 0。 hl = 2 l =。 69弊:SEQUENCE。
。 2:d = 1。 hl = 2 l =。 33 prim:整数。 :A85E76FF1F18E52D4ECCDE774AA46E3EC2891068EBEF89A7CB6E0653EB0DFFB2
。 37:d = 1。 hl = 2 l =。 32素数:整数。 :4CD76E19BCB18D76C13AF0EA061117CBD6D3C8FBD1D9FC9AAC265266371C3A3F

如您所见,这两个整数在签名的十六进制编码版本中可见,但是我们如何使用Swift将它们取出呢?

维基百科上有一些DER编码数据的很好的例子。 事实证明,整个十六进制字符串都位于类型-长度-值三元组中。 这意味着数据所保存的每个可变长度值都以其类型和长度为开头,每个值都为1个字节。 我发现此页面描述了ASN1解码器应该期望的类型标签。 让我们将其表示为一个enum

  struct ASN1DERDecoder { 

枚举DERCode:UInt8 {

//所有序列应以此开头
情况序列= 0x30

//类型标记-在此处添加更多!
大小写整数= 0x02

//一种方便的方法,可用于枚举所有数据类型
静态函数allTypes()-> [DERCode] {
返回[
。整数,
]
}
}
}

请注意,在本文中,我将只使用整数,但是您可以根据需要添加更多类型。

在我们的签名中,通过查看十六进制编码版本,我们可以看到第一个八位位组是30 ,表示DER序列。 第二个八位位组为45 ,表示随后的值的长度,其值的类型为02 ,如第三个八位位组所示。 如果Swift的Scanner使用Data会很棒,但是事实并非如此,因此我们需要实现自己的。

我的扫描仪具有最简单的实现方式:可以按照给定的步骤向前遍历Data对象。 如果有的话,您可以用自己的替代。

 类SimpleScanner { 
让数据:数据
私有(设置)变量位置= 0

初始化(数据:数据){
self.data =数据
}

var isComplete:Bool {
返回位置> = data.count
}

func rollback(距离:整数){
位置=位置-距离

如果位置<0 {
位置= 0
}
}

func scan(distance:Int)->数据? {
返回popByte(s:距离)
}

func scanToEnd()->数据? {
返回扫描(距离:data.count-位置)
}

私人函数popByte(s:Int = 1)->数据? {

后卫s> 0 else {return nil}
后卫位置<=(data.count-s)else {return nil}

推迟{
位置=位置+ s
}

返回data.subdata(in:data.startIndex.advanced(by:position).. <data.startIndex.advanced(by:position + s))
}
}

要解码我们的序列,我们需要一个对象将数据序列化为该对象,以及一个方便的方法来检查Data对象的第一个字节。

  struct ASN1Object { 
让类型:ASN1DERDecoder.DERCode
让数据:数据
}
扩展数据{
var firstByte:UInt8 {
var字节:UInt8 = 0
copyBytes(to:&byte,count:MemoryLayout .size)
返回字节
}
}

最后,我们可以在ASN1DERDecoder结构中添加一个方法,将我们的数据扫描到ASN1Object

 静态函数解码(数据:数据)-> [ASN1Object]?  { 

让扫描仪= SimpleScanner(数据:数据)

//验证这实际上是DER序列
保护扫描器.scan(距离:1)?. firstByte == DERCode.Sequence.rawValue else {
返回零
}

//第二个字节应等于数据的长度,减去自身和序列类型
后卫让ExpectLength =扫描器.scan(距离:1)?. firstByte,Int(expectedLength)== data.count-2其他{
返回零
}

//我们可以用来附加输出的对象
var输出:[ASN1Object] = []

//遍历所有数据
而!scanner.isComplete {

//搜索序列的当前位置以查找已知类型
var dataType:DERCode?
用于DERCode.allTypes(){
如果scan.scan(distance:1)?. firstByte == type.rawValue {
dataType =类型
}其他{
scan.rollback(距离:1)
}
}

警卫队让类型= dataType其他{
//不受支持的类型-将其添加到`DERCode.all()`
返回零
}

警戒线长度= scan.scan(distance:1)else {
//期望一个字节描述行进数据的长度
返回零
}

让lengthInt = length.firstByte

警卫让actualData = scan.scan(distance:Int(lengthInt))其他{
//预计将能够扫描`lengthInt`字节
返回零
}

让对象= ASN1Object(类型:类型,数据:ActualData)
output.append(对象)
}

返回输出
}

现在,我们可以编写一个函数以将单个序列中的整数作为单个Data对象返回。 我正在使用GMEllipticCurveCrytpo进行繁重的实际验证签名。 理查德·摩尔(Richard Moore)的这个单类库期望签名为64个字节—我发现我正在使用的API有时每个整数返回33个字节,第一个字节是某种填充。 我们需要在整数解析函数中对此加以考虑。

  func integerData(fromBase64String string:String)->数据{ 

让数据=数据(base64Encoded:字符串,选项:[])!
让integerData = ASN1DERDecoder.decode(data:data)

return integerData!.reduce(Data(),{(sum,next)->数据输入

让过滤器= SimpleScanner(data:next.data)
如果filter.scan(distance:1)?. firstByte == 0x0 {
返回总和+ filter.scanToEnd()!
}其他{
返回总和+ next.data
}
})
}

就是这样! 不幸的是,简单地从integerData(fromBase64String:)打印输出会导致Swift 3打印64 bytes (不要以为我会错过NSData的格式),因此要将数据与openssl打印的两个整数进行比较,我们需要将整数映射到十六进制字符串。

  //此字符串只是我们作为base64的签名。 您可以通过将其复制到剪贴板并运行`pbpaste来进行验证 
base64 -D
xxd -pu -c 999`,以使其与本文开头的八位位组字符串匹配。
让整数= integerData(fromBase64String:“ MEUCIQCoXnb / HxjlLU7M3ndKpG4 + wokQaOvviafLbgZT6w3 / sgIgTNduGbyxjXbBOvDqBhEXy9bTyPvR2fyarCZSZjccOj8 =”)
let hex = integer.reduce(“”){$ 0 + String(格式:“%02x”,$ 1)}
//打印“ a85e76ff1f18e52d4eccde774aa46e3ec2891068ebef89a7cb6e0653eb0dffb24cd76e19bcb18d76c13af0ea061117cbd6d3c8fbd1d9fc9aac265266371c3a3f“
打印(十六进制)

如您所见,out与本文开头的openssl输出中两个整数的串联相同。

最后,您可以使用此工具快速生成ECC签名进行测试。