在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签名进行测试。