使用Swift创建数字签名

数字签名的主要目的是验证某些信息的完整性。 举个简单的例子,假设您有一个通过网络传输的文件,并且您想检查整个文件是否正确传输。 在这种情况下,您将使用校验和。

“校验和是从数字数据块中提取的小型数据,目的是检测可能在其传输或存储过程中引入的错误” – Wikipedia

我们如何得出该校验和? 最好的选择是使用哈希。 哈希函数将获取可变数量的数据,并输出固定长度的签名。 例如,我们可以在线发布文件及其哈希值。 当某人下载文件时,他们然后可以在其文件版本上运行相同的哈希函数并比较结果。 如果哈希值相同,则复制或下载的文件与原始文件相同。

哈希也是一种单向函数。 给定结果输出,没有计算上可行的方法来反转该哈希值以显示原始输入是什么。 SHA(Secure Hash Algorithm)是一种众所周知的标准,它引用一组具有此属性和某些其他属性的哈希函数,这些哈希函数使它们可用于数字签名。

关于SHA

自从首次发布以来,SHA经历了许多迭代。 现在已知第一次和第二次迭代SHA-0和SHA-1具有主要缺点 。 它们不再被批准用于安全性实施:通常不应将它们用于依赖安全性的应用程序。 但是,SHA-2系列包含称为SHA-256和SHA-512的版本,它们被认为是安全的。 “ 256”和“ 512”仅指产生的结果位数。 在本教程中,我们将使用SHA-512。

注意:另一个较旧的流行哈希算法是MD5。 还发现它具有重大缺陷。

使用SHA可以很好地检查数据是否意外损坏,但这不能防止恶意用户篡改数据。 假设哈希输出大小是固定的,那么攻击者所需要做的就是找出给定输出大小使用了哪种算法,更改数据并重新计算哈希。 我们需要的是在对数据进行散列时将一些秘密信息添加到混合中,以使攻击者无法在不知道秘密的情况下重新计算散列。 这称为哈希消息验证码(HMAC)。

HMAC

HMAC可以对一条信息或消息进行身份验证,以确保该信息或消息源自正确的发送者,并且该信息没有被更改。 一种常见的情况是,当您使用带有应用程序后端API的服务器进行通信时。 进行身份验证以确保仅允许您的应用程序与API对话可能很重要。 API将具有对特定资源(例如/ register_user端点)的访问控制 。 客户端需要将其请求签名/ register_user端点才能成功使用。

在签署请求时,通常的做法是获取请求的选定部分(例如POST参数和URL),然后将它们连接在一起成为一个字符串。 采取商定的元素并将其按特定顺序排列称为规范化 。 在HMAC中,将连接的字符串与密钥一起进行哈希处理以生成签名 。 我们不使用哈希来称呼哈希,而是使用签名一词,就像在现实生活中使用一个人的签名来验证身份或完整性一样。 签名作为请求标头(通常也称为“签名”)添加回客户端的请求中。 签名有时称为消息摘要,但两个术语可以互换使用。

在API方面,服务器重复连接字符串和创建签名的过程。 如果签名匹配,则证明该应用程序必须拥有该机密。 这证明了该应用程序的身份。 由于请求的特定参数也是要签名的字符串的一部分,因此它也保证了请求的完整性。 例如,它可以防止攻击者进行中间人攻击,并根据自己的喜好更改请求参数。

class func hmacExample()
{
    //Some URL example...
    let urlString = "https://example.com"
    let postString = "id=123"
    let url = URL.init(string: urlString)
    var request = URLRequest(url: url!)
    request.httpMethod = "POST"
    request.httpBody = postString.data(using: .utf8)
    let session = URLSession.shared

    //Create a signature
    let stringToSign = request.httpMethod! + "&" + urlString + "&" + postString
    print("The string to sign is : ", stringToSign)
    if let dataToSign = stringToSign.data(using: .utf8)
    {
        let signingSecret = "4kDfjgQhcw4dG6J80QnvRFbtuJfkgitH6phkLN90"
        if let signingSecretData = signingSecret.data(using: .utf8)
        {
            let digestLength = Int(CC_SHA512_DIGEST_LENGTH)
            let digestBytes = UnsafeMutablePointer<UInt8>.allocate(capacity:digestLength)
            
            CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA512), [UInt8](signingSecretData), signingSecretData.count, [UInt8](dataToSign), dataToSign.count, digestBytes)
            
            //base64 output
            let hmacData = Data(bytes: digestBytes, count: digestLength)
            let signature = hmacData.base64EncodedString()
            print("The HMAC signature in base64 is " + signature)
            
            //or HEX output
            let hexString = NSMutableString()
            for i in 0..<digestLength
            {
                hexString.appendFormat("%02x", digestBytes[i])
            }
            print("The HMAC signature in HEX is", hexString)
            
            //Set the Signature header
            request.setValue(signature, forHTTPHeaderField: "Signature")
        }
    }
    
    //Start your request...
    //let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
        // ...
    //})
    //task.resume()
    
}

在此代码中, CCHmac函数采用要使用的哈希函数类型的参数,以及两个字节字符串及其长度(消息和密钥)。 为了获得最佳安全性,请至少使用从加密安全的随机数生成器生成的256位(32字节)密钥。 要验证另一端是否一切正常,请运行示例,然后在此远程服务器上输入密钥和消息,并验证输出是否相同。

您还可以在请求和签名字符串中添加时间戳标头,以使请求更具唯一性。 这可以帮助API清除重播攻击。 例如,如果时间戳记是过时的10分钟,则API可能会丢弃该请求。

坚持使用安全的SHA版本固然很好,但事实证明,不安全的SHA版本的许多漏洞都不适用于HMAC。 因此,您可能会在生产代码中看到SHA1。 但是,从公共关系的角度来看,如果您必须解释为什么从密码学角度讲,在这种情况下可以使用SHA1可能看起来很糟糕。 SHA1的许多弱点归因于所谓的碰撞攻击 。 无论上下文如何,代码审核员或安全研究人员都可能希望您的代码具有抗冲突性。 另外,如果您编写模块化代码,以后可以在其中交换签名函数以换用另一个签名函数,则可能会忘记更新不安全的哈希函数。 因此,我们仍然会选择SHA-512作为算法。

HMAC CPU操作速度很快,但是缺点之一是密钥交换问题。 我们如何让对方知道什么是秘密密钥而不被拦截? 例如,您的API可能需要动态地从白名单中添加或删除多个应用程序或平台。 在这种情况下,将要求应用程序进行注册,并且成功注册后,必须将机密传递给应用程序。 您可以通过HTTPS发送密钥并使用SSL固定 ,但是即使如此,也始终担心在交换过程中密钥会被盗。 解决密钥交换问题的方法是生成不需要首先将设备留在原处的密钥。 这可以使用公钥密码术来完成,RSA是一种非常流行且被接受的标准。

RSA

RSA代表Rivest-Shamir-Adleman(密码系统的作者)。 它涉及利用将两个非常大的质数乘积分解的困难。 RSA可以用于加密或身份验证,尽管在本示例中,我们将仅将其用于身份验证。 RSA生成两个密钥,即公共密钥和私有密钥,我们可以使用SecKeyGeneratePair函数来完成。 当用于身份验证时,私钥用于创建签名,而公钥则用于验证签名。 给定公用密钥,在计算上无法导出专用密钥。

下一个示例演示了Apple和所有流行的游戏机公司在分发软件时使用的内容。 假设您的公司定期创建并交付一个文件,用户将其拖到iTunes中您应用程序文件共享部分中 。 您要确保在应用程序中解析发送的文件之前,不要对其进行篡改。 您的公司将保留并保护用于签名文件的私钥。 该应用程序包中包含用于验证文件的公共密钥的副本。 鉴于私有密钥永远不会传输或包含在应用程序中,因此恶意用户无法签名自己的文件版本(除了闯入公司并窃取私有密钥之外)。

我们将使用SecKeyRawSign对文件进行签名。 使用RSA对文件的全部内容进行签名会很慢,因此将对文件的哈希进行签名。 此外,由于某些安全性弱点,在签名之前还应对传递给RSA的数据进行哈希处理。

@available(iOS 10.0, *)
class FileSigner
{
    private var publicKey : SecKey?
    private var privateKey : SecKey?
    
    func generateKeys()  -> String?
    {
        var publicKeyString : String?
        //generate a new keypair
        let parameters : [String : AnyObject] =
        [
            kSecAttrKeyType as String : kSecAttrKeyTypeRSA,
            kSecAttrKeySizeInBits as String : 4096 as AnyObject,
        ]
        let status = SecKeyGeneratePair(parameters as CFDictionary, &publicKey, &privateKey)
        
        //---Save your key here--- //
        
        //Convert the SecKey object into a representation that we can send over the network
        if status == noErr && publicKey != nil
        {
            if let cfData = SecKeyCopyExternalRepresentation(publicKey!, nil)
            {
                let data = cfData as Data
                publicKeyString = data.base64EncodedString()
            }
        }
        
        return publicKeyString
    }
    
    func signFile(_ path : String) -> String?
    {
        var signature : String?
        
        if let fileData = FileManager.default.contents(atPath: path)
        {
            if (privateKey != nil)
            {
                //hash the message first
                let digestLength = Int(CC_SHA512_DIGEST_LENGTH)
                let hashBytes = UnsafeMutablePointer<UInt8>.allocate(capacity: digestLength)
                CC_SHA512([UInt8](fileData), CC_LONG(fileData.count), hashBytes)
                
                //sign
                let blockSize = SecKeyGetBlockSize(privateKey!) //in the case of RSA, modulus is the same as the block size
                var signatureBytes = [UInt8](repeating:0, count:blockSize)
                var signatureDataLength = blockSize
                let status = SecKeyRawSign(privateKey!, .PKCS1SHA512, hashBytes, digestLength, &signatureBytes, &signatureDataLength)
                if status == noErr
                {
                    let data = Data(bytes: signatureBytes, count: signatureDataLength)
                    signature = data.base64EncodedString()
                }
            }
        }
        return signature
    }
}

在此代码中,我们使用CC_SHA512函数再次指定SHA-512。 (如果基础哈希函数不安全,则RSA与HMAC不同,它变得不安全。)我们还使用4096作为密钥大小,该大小由kSecAttrKeySizeInBits参数设置。 建议的最小大小为2048。 这是为了防止强大的计算机系统网络破解RSA密钥(通过破解,我的意思是分解RSA密钥,也称为分解公共模数 )。 RSA小组估计2048位密钥可能在2030年之前的某个时间变得可破解。如果您希望在这段时间之后数据是安全的,那么最好选择更大的密钥大小(例如4096)。

生成的密钥采用SecKey对象的形式。 苹果公司实现SecKey一个问题是它不包括构成公钥的所有基本信息,因此它不是有效的DER编码的X.509证书。 将丢失的信息重新添加到iOS或OS X应用程序的格式中,即使是服务器端平台(例如PHP),也需要做一些工作,并且涉及以称为ASN.1的格式进行工作。 幸运的是,此问题已在iOS 10中通过新的SecKey函数(用于生成,导出和导入密钥)修复。

下面的代码向您展示了通信的另一面-该类通过SecKeyCreateWithData接受公钥以使用SecKeyRawVerify函数验证文件。

@available(iOS 10.0, *)
class FileVerifier
{
    private var publicKey : SecKey?
    
    func addPublicKey(_ keyString : String) -> Bool
    {
        var success = false
        if let keyData = Data.init(base64Encoded: keyString)
        {
            let parameters : [String : AnyObject] =
            [
                kSecAttrKeyType as String : kSecAttrKeyTypeRSA,
                kSecAttrKeyClass as String : kSecAttrKeyClassPublic,
                kSecAttrKeySizeInBits as String : 4096 as AnyObject,
                kSecReturnPersistentRef as String : true as AnyObject
            ]
            publicKey = SecKeyCreateWithData(keyData as CFData, parameters as CFDictionary, nil)
                
            if (publicKey != nil)
            {
                success = true
            }
        }
        return success
    }
    
    func verifyFile(_ path : String, withSignature signature : String) -> Bool
    {
        var success = false
        if (publicKey != nil)
        {
            if let fileData = FileManager.default.contents(atPath: path)
            {
                if let signatureData = Data.init(base64Encoded: signature)
                {
                    //hash the message first
                    let digestLength = Int(CC_SHA512_DIGEST_LENGTH)
                    let hashBytes = UnsafeMutablePointer<UInt8>.allocate(capacity:digestLength)
                    CC_SHA512([UInt8](fileData), CC_LONG(fileData.count), hashBytes)
                    
                    //verify
                    let status = signatureData.withUnsafeBytes {signatureBytes in
                        return SecKeyRawVerify(publicKey!, .PKCS1SHA512, hashBytes, digestLength, signatureBytes, signatureData.count)
                    }
                    if status == noErr
                    {
                        success = true
                    }
                    else
                    {
                        print("Signature verify error") //-9809 : errSSLCrypto, etc
                    }
                }
            }
        }
        return success
    }
}

您可以尝试使用以下简单测试来验证其是否有效:

if #available(iOS 10.0, *)
{
    guard let plistPath = Bundle.main.path(forResource: "Info", ofType: "plist") else { print("Couldn't get plist"); return }
    
    let fileSigner = FileSigner()
    
    DispatchQueue.global(qos: .userInitiated).async // RSA key gen can be long running work
    {
        guard let publicKeyString = fileSigner.generateKeys() else { print("Key generation error"); return }
        
        // Back to the main thread
        DispatchQueue.main.async
        {
            guard let signature = fileSigner.signFile(plistPath) else { print("No signature"); return }
            
            //Save the signature on the other end
            let fileVerifier = FileVerifier()
            guard fileVerifier.addPublicKey(publicKeyString) else { print("Key was not added"); return }
            
            let success = fileVerifier.verifyFile(plistPath, withSignature: signature)
            if success
            {
                print("Signatures match!")
            }
            else
            {
                print("Signatures do not match.")
            }
        }
    }
}

RSA有一个缺点-密钥生成速度很慢! 生成密钥的时间取决于密钥的大小。 在较新的设备上,4096位密钥仅需要几秒钟,但是如果在iPod Touch第四代上运行此代码,则可能需要一分钟。 如果您只是在计算机上几次生成密钥,那很好,但是当我们需要在移动设备上频繁生成密钥时会发生什么呢? 我们不能只是降低密钥大小,因为这会降低安全性。

那么解决方案是什么? 好吧,椭圆曲线密码术(ECC)是一种新兴的方法,它是基于有限域上的椭圆曲线的一组新算法。 ECC密钥的大小比RSA密钥小得多,生成速度也更快。 只有256位的密钥提供了非常强大的安全性! 要利用ECC,我们不需要更改很多代码。 我们可以使用相同的SecKeyRawSign函数对数据进行SecKeyRawSign ,然后调整参数以使用椭圆曲线数字签名算法(ECDSA)。

提示:有关更多RSA实施思想的信息,您可以查看 SwiftyRSA 帮助程序库,该库专注于加密和签名消息。

ECDSA

设想以下情形:聊天应用程序允许用户彼此发送私人消息,但是您要确保对手在向其他用户发送消息时没有更改消息。 让我们看看如何通过密码保护他们的通信。

首先,每个用户在其移动设备上生成公用密钥和专用密钥的密钥对。 它们的私钥存储在内存中,永远不会离开设备,而公钥则相互传输。 像以前一样,私钥用于签名要发送的数据,而公钥用于验证。 如果攻击者要在传输过程中捕获公钥,则可以做的就是验证来自发件人的原始消息的完整性。 攻击者无法更改消息,因为他们没有重建签名所需的私钥。

在iOS上使用ECDSA的另一个专家。 我们可以利用以下事实:目前,椭圆曲线键是唯一可以存储在设备安全区域中的键。 所有其他密钥都存储在钥匙串中,该钥匙串会将其项加密到设备的默认存储区域。 在具有一个设备的设备上,安全区域与处理器分开放置,并且密钥存储是在没有直接软件访问权限的硬件中实现的。 安全区域可以存储私钥并对其进行操作以产生输出,该输出将发送到您的应用程序,而无需通过将其加载到内存中而暴露出实际的私钥!

我将通过为kSecAttrTokenID参数添加kSecAttrTokenIDSecureEnclave选项来添加对在安全区域中创建ECDSA私钥的支持。 我们可以从一个User对象开始此示例,该对象将在初始化时生成密钥对。

@available(iOS 9.0, *)
class User
{
    public var publicKey : SecKey?
    private var privateKey : SecKey?
    private var recipient : User?
    
    init(withUserID id : String)
    {
        //if let access = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, [.privateKeyUsage /*, .userPresence] authentication UI to get the private key */], nil) //Force store only if passcode or Touch ID set up...
        if let access = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, [.privateKeyUsage], nil)  //Keep private key on device
        {
            let privateTagString = "com.example.privateKey." + id
            let privateTag = privateTagString.data(using: .utf8)! //Store it as Data, not as a String
            let privateKeyParameters : [String : AnyObject] = [kSecAttrIsPermanent as String : true as AnyObject,
                                                               kSecAttrAccessControl as String : access as AnyObject,
                                                               kSecAttrApplicationTag as String : privateTag as AnyObject,
                ]
        
            let publicTagString = "com.example.publicKey." + id
            let publicTag = publicTagString.data(using: .utf8)! //Data, not String
            let publicKeyParameters : [String : AnyObject] = [kSecAttrIsPermanent as String : false as AnyObject,
                                                              kSecAttrApplicationTag as String : publicTag as AnyObject,
                ]
            
            let keyPairParameters : [String : AnyObject] = [kSecAttrKeySizeInBits as String : 256 as AnyObject,
                                                            kSecAttrKeyType as String : kSecAttrKeyTypeEC,
                                                            kSecPrivateKeyAttrs as String : privateKeyParameters as AnyObject,
                                                            kSecAttrTokenID as String : kSecAttrTokenIDSecureEnclave as AnyObject, //Store in Secure Enclave
                                                            kSecPublicKeyAttrs as String : publicKeyParameters as AnyObject]
            
            let status = SecKeyGeneratePair(keyPairParameters as CFDictionary, &publicKey, &privateKey)
            if status != noErr
            {
                print("Key generation error")
            }
        }
    }
    
    //...

接下来,我们将创建一些帮助器和示例函数。 例如,该类将允许用户发起对话并发送消息。 当然,在您的应用程序中,您将对其进行配置以包括您的特定网络设置。

//...
    
    private func sha512Digest(forData data : Data) -> Data
    {
        let len = Int(CC_SHA512_DIGEST_LENGTH)
        let digest = UnsafeMutablePointer<UInt8>.allocate(capacity: len)
        CC_SHA512((data as NSData).bytes, CC_LONG(data.count), digest)
        return NSData(bytesNoCopy: UnsafeMutableRawPointer(digest), length: len) as Data
    }
    
    public func initiateConversation(withUser user : User) -> Bool
    {
        var success = false
        if publicKey != nil
        {
            user.receiveInitialization(self)
            recipient = user
            success = true
        }
        return success
    }
    
    public func receiveInitialization(_ user : User)
    {
         recipient = user
    }
    
    public func sendMessage(_ message : String)
    {
        if let data = message.data(using: .utf8)
        {
            let signature = self.signData(plainText: data)
            if signature != nil
            {
                self.recipient?.receiveMessage(message, withSignature: signature!)
            }
        }
    }
    
    public func receiveMessage(_ message : String, withSignature signature : Data)
    {
        let signatureMatch = verifySignature(plainText: message.data(using: .utf8)!, signature: signature)
        if signatureMatch
        {
            print("Received message. Signature verified. Message is : ", message)
        }
        else
        {
            print("Received message. Signature error.")
        }
    }
    
    //...

接下来,我们将进行实际的签名和验证。 与RSA不同,ECDSA不需要在签名之前进行哈希处理。 但是,如果您想要一个可以轻松交换算法而无需进行很多更改的函数,那么在签名之前继续对数据进行哈希处理是完全可以的。

//...
    
    func signData(plainText: Data) -> Data?
    {
        guard privateKey != nil else
        {
            print("Private key unavailable")
            return nil
        }
        
        let digestToSign = self.sha512Digest(forData: plainText)
        let signature = UnsafeMutablePointer<UInt8>.allocate(capacity: 512) //512 - overhead
        var signatureLength = 512
        
        let status = SecKeyRawSign(privateKey!, .PKCS1SHA512, [UInt8](digestToSign), Int(CC_SHA512_DIGEST_LENGTH), signature, &signatureLength)
        if status != noErr
        {
            print("Signature fail: \(status)")
        }
        
        return Data.init(bytes: signature, count: signatureLength) //resize to actual signature size
    }
    
    func verifySignature(plainText: Data, signature: Data) -> Bool
    {
        guard recipient?.publicKey != nil else
        {
            print("Recipient public key unavailable")
            return false
        }
        
        let digestToVerify = self.sha512Digest(forData: plainText)
        let signedHashBytesSize = signature.count
        
        let status = SecKeyRawVerify(recipient!.publicKey!, .PKCS1SHA512, [UInt8](digestToVerify), Int(CC_SHA512_DIGEST_LENGTH), [UInt8](signature as Data), signedHashBytesSize)
        return status == noErr
    }
}

这将验证消息以及特定用户的“标识”,因为只有该用户拥有其私钥。

这并不意味着我们将密钥与用户的真实身份联系在一起,将公共密钥与特定用户进行匹配的问题是另一个领域。 尽管解决方案不在本教程的讨论范围之内,但流行的安全聊天应用程序(例如Signal和Telegram)允许用户通过辅助通信通道来验证指纹或号码。 同样,Pidgin提供了一个问答方案,您可以在其中提出仅用户应该知道的问题。 这些解决方案引发了关于最佳方法应该是什么的争论。

但是,我们的密码解决方案确实验证了该消息只能由拥有特定私钥的人发送。

让我们对示例进行简单测试:

if #available(iOS 9.0, *)
{
    let alice = User.init(withUserID: "aaaaaa1")
    let bob = User.init(withUserID: "aaaaaa2")
    
    let accepted = alice.initiateConversation(withUser: bob)
    if (accepted)
    {
        alice.sendMessage("Hello there")
        bob.sendMessage("Test message")
        alice.sendMessage("Another test message")
    }
}

OAuth和SSO

通常,在使用第三方服务时,您会注意到用于身份验证的其他高级术语,例如OAuth和SSO。 虽然本教程是关于创建签名的,但我将简要解释其他术语的含义。

OAuth是用于身份验证和授权的协议。 它充当将某人的帐户用于第三方服务的中介,旨在解决选择性授权访问您数据的问题。 如果您通过Facebook登录到服务X,则屏幕会询问您,例如,是否允许服务X访问您的Facebook照片。 它通过提供一个令牌而不泄露用户密码来实现这一点。

单一登录或SSO,描述了经过身份验证的用户可以使用其相同的登录凭据来访问多个服务的流程。 例如,您的Gmail帐户如何登录YouTube。 如果您的公司有几种不同的服务,则可能不想为所有不同的服务创建单独的用户帐户。

结论

在本教程中,您了解了如何使用最受欢迎的标准创建签名。 现在,我们已经涵盖了所有主要概念,让我们回顾一下!

翻译自: https://code.tutsplus.com/tutorials/creating-digital-signatures-with-swift--cms-29287

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值