The main purpose of a digital signature is to verify the integrity of some information. For a simple example, let's say you had a file that was transferred over the network and you want to check that the entire file was transferred correctly. In that case, you would use a checksum.
“A checksum is a small-sized datum derived from a block of digital data for the purpose of detecting errors which may have been introduced during its transmission or storage” — Wikipedia
How do we derive that checksum? The best option is to use a hash. A hash function will take a variable amount of data and will output a signature of fixed length. For example, we could publish a file along with its hash online. When someone downloads the file, they can then run the same hash function on their version of the file and compare the result. If the hashes are the same then the copied or downloaded file is the same as the original.
A hash is also a one-way function. Given the resulting output, there is no computationally feasible way to reverse that hash to reveal what the original input was. SHA, Secure Hash Algorithm, is a well-known standard that refers to a group of hash functions that have this property and certain others, which make them useful for digital signatures.
About SHA
SHA has undergone many iterations since it was first published. The first and second iterations, SHA-0 and SHA-1, are now known to have major weaknesses. They are no longer approved for security implementations: they generally shouldn't be used for applications relying on security. However, the SHA-2 family includes versions called SHA-256 and SHA-512, and these are considered secure. "256" and "512" simply refer to the resulting number of bits produced. For this tutorial, we are going to use SHA-512.
Note: Another older popular hash algorithm was MD5. It was also found to have significant flaws.
Using SHA is great for checking if data was accidentally corrupted, but this doesn't prevent a malicious user from tampering with the data. Given that a hash output is of a fixed size, all an attacker needs to do is figure out which algorithm was used given the output size, alter the data, and recompute the hash. What we need is some secret information added to the mix when hashing the data so that the attacker cannot recompute the hash without knowledge of the secret. This is called a Hash Message Authentication Code (HMAC).
HMAC
HMAC can authenticate a piece of information or message to make sure that it originated from the correct sender and that the information has not been altered. A common scenario is when you are talking to a server with a back-end API for your app. It may be important to authenticate to ensure that only your app is allowed to talk to the API. The API would have access control to a specific resource, such as a /register_user endpoint. The client would need to sign its request to the /register_user endpoint in order to successfully use it.
When signing a request, it is common practice to take selected parts of the request, such as POST parameters and the URL, and join them together into a string. Taking agreed-upon elements and putting them in a particular order is called canonicalization. In HMAC, the joined string is hashed along with the secret key to produce the signature. Instead of calling it a hash, we use the term signature in the same way that a person's signature in real life is used to verify identity or integrity. The signature is added back to the client's request as a request header (usually also named “Signature”). A signature is sometimes called a message digest, but the two terms can be used interchangeably.
Over on the API side, the server repeats the process of joining the strings and creating a signature. If the signatures match, it proves that the app must have possession of the secret. This proves the identity of the app. Since specific parameters of the request were also part of the string to be signed, it also guarantees the integrity of the request. It prevents an attacker from performing a man-in-the-middle attack, for example, and altering the request parameters to their liking.
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() }
In this code, the CCHmac
function takes a parameter for the type of hash function to be used, along with two byte-strings and their lengths—the message and a secret key. For the best security, use at least a 256-bit (32 byte) key generated from a cryptographically secure random number generator. To verify everything is working correctly on the other side, run the example and then input the secret key and message on this remote server and verify that the output is the same.
You can also add a timestamp header to the request and signing string to make the request more unique. This can help the API weed out replay attacks. For example, the API could drop the request if the timestamp is 10 minutes stale.
While it's good to stick to using SHA versions that are secure, it turns out that many of the vulnerabilities of the insecure SHA versions do not apply to HMAC. For this reason, you may see SHA1 being used in production code. However, from a public relations standpoint, it may look bad if you have to explain why, cryptographically speaking, it is okay to use SHA1 in this context. Many of the weaknesses of SHA1 are due to what are called collision attacks. Code auditors or security researchers may expect your code to be collision resistant, regardless of the context. Also, if you write modular code where you can swap out the signing function for a different one in the future, you might forget to update the insecure hash functions. Therefore, we will still stick to SHA-512 as our algorithm of choice.
The HMAC CPU operations are fast, but one disadvantage is the problem of key exchange. How do we let each other know what the secret key is without it being intercepted? For example, maybe your API will need to dynamically add or remove multiple apps or platforms from a whitelist. In this scenario, apps would be required to register, and the secret would need to be passed to the app upon successful registration. You could send the key over HTTPS and use SSL pinning, but even then there is always a worry that somehow the key is stolen during the exchange. The solution to the problem of key exchange is to generate a key that doesn't ever need to leave the device in the first place. This can be accomplished using Public Key Cryptography, and a very popular and accepted standard is RSA.
RSA
RSA stands for Rivest-Shamir-Adleman (the authors of the cryptosystem). It involves taking advantage of the difficulty of factoring the product of two very large prime numbers. RSA can be used for encryption or authentication, although for this example we are going to be using it just for authentication. RSA generates two keys, a public and a private, which we can accomplish using the SecKeyGeneratePair
function. When used for authentication, the private key is used to create the signature, while the public key verifies the signature. Given a public key, it is computationally unfeasible to derive the private key.
The next example demonstrates what Apple and all the popular gaming console companies use when distributing their software. Let's say your company creates and delivers a file periodically that users will drag into the file sharing portion of your app in iTunes. You want to make sure the files you send out are never tampered with before being parsed in the app. Your company will hold onto and guard the private key which it uses to sign the files. In the bundle of the app is a copy of the public key used to verify the file. Given that the private key is never transmitted or included in the app, there is no way for a malicious user to be able to sign their own versions of the files (apart from breaking into the company and stealing the private key).
We will use SecKeyRawSign
to sign the file. It would be slow to sign the entire contents of the file using RSA, so the hash of the file is signed instead. Additionally, the data passed to RSA should also be hashed before signing because of some security weaknesses.
@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 } }
In this code, we used the CC_SHA512
function to specify SHA-512 again. (RSA, unlike HMAC, becomes insecure if the underlying hash function is insecure.) We are also using 4096 as the key size, which is set by the kSecAttrKeySizeInBits
parameter. 2048 is the minimum recommended size. This is to prevent a powerful network of computer systems cracking the RSA key (by cracking I mean factoring the RSA key—also known as factorization of a public modulus). The RSA group has estimated that 2048-bit keys could become crackable some time before 2030. If you want your data to be safe beyond that time then it's a good idea to choose a higher key size like 4096.
The generated keys are in the form of SecKey
objects. An issue with Apple's implementation of SecKey
is that it does not include all of the essential information that makes up a public key, so it's not a valid DER-encoded X.509 certificate. Adding the missing information back into the format for an iOS or OS X app, even server-side platforms such as PHP, requires some work and involves working in a format known as ASN.1. Fortunately, this was fixed in iOS 10 with new SecKey
functions for generating, exporting, and importing keys.
The code below shows you the other side of the communication—the class that accepts a public key via SecKeyCreateWithData
to verify files using the SecKeyRawVerify
function.
@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 } }
You could try this out and verify that it works using a simple test like the following:
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.") } } } }
There is one downside to RSA—key generation is slow! The time to generate the keys is dependent on the size of the key. On newer devices a 4096 bit key takes only a few seconds, but if you run this code on an iPod Touch 4th generation, it may take about a minute. This is fine if you are just generating the keys a few times on a computer, but what happens when we need to generate keys frequently on a mobile device? We can't just lower the key size because that downgrades the security.
So what's the solution? Well, Elliptic Curve Cryptography (ECC) is an up-and-coming approach—a new set of algorithms based on elliptic curves over finite fields. ECC keys are much smaller in size and faster to generate than RSA keys. A key of only 256-bits offers a very strong level of security! To take advantage of ECC, we don't need to change a lot of code. We can sign our data using the same SecKeyRawSign
function and then adjust the parameters to use Elliptic Curve Digital Signature Algorithm (ECDSA).
Tip: For more RSA implementation ideas, you can check out the SwiftyRSA helper library, which is focused on encryption as well as signing messages.
ECDSA
Imagine the following scenario: a chat app lets users send private messages to each other, but you want to make sure that an adversary has not changed the message on its way to the other user. Let's see how you could secure their communication with cryptography.
First, each user generates a keypair of public and private keys on their mobile device. Their private keys are stored in memory and never leave the device, while the public keys are transmitted to each other. As before, the private key is used for signing the data being sent out, while the public key is used for verifying. If an attacker were to capture a public key during transit, all that could be done is to verify the integrity of the original message from the sender. An attacker can't alter a message because they don't have the private key needed to reconstruct the signature.
There is another pro to using ECDSA on iOS. We can make use of the fact that currently, elliptic curve keys are the only ones that can be stored in the secure enclave of the device. All other keys are stored in the keychain which encrypts its items to the default storage area of the device. On devices that have one, the secure enclave sits separate from the processor, and key storage is implemented in hardware without direct software access. The secure enclave can store a private key and operate on it to produce output that is sent to your app without ever exposing the actual private key by loading it into memory!
I will add support for creating the ECDSA private key on the secure enclave by adding the kSecAttrTokenIDSecureEnclave
option for the kSecAttrTokenID
parameter. We can start this example with a User
object that will generate a keypair upon initialization.
@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") } } } //...
Next, we will create some helper and example functions. As an example, the class will allow a user to initiate a conversation and send a message. Of course, in your app, you would configure this to include your specific networking setup.
//... 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.") } } //...
Next, we will do the actual signing and verification. ECDSA, unlike RSA, does not need to be hashed prior to signing. However, if you wanted to have a function where the algorithm can be easily swapped without making many changes, then it's perfectly fine to continue to hash the data before signing.
//... 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 } }
This verifies the message, as well as the “identify” of a specific user since only that user has possession of their private key.
This doesn't mean that we're connecting the key with who the user is in real life—the problem of matching a public key to a specific user is another domain. While the solutions are out of the scope of this tutorial, popular secure chat apps such as Signal and Telegram allow users to verify a fingerprint or number via a secondary communication channel. Similarly, Pidgin offers a question and answer scheme whereby you ask a question that only the user should know. These solutions open a whole world of debate on what the best approach should be.
However, our cryptographic solution does verify that the message can only have been sent by someone who is in possession of a specific private key.
Let's run a simple test of our example:
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 and SSO
Often when working with third-party services, you will notice other high-level terms used for authentication, such as OAuth and SSO. While this tutorial is about creating a signature, I will briefly explain what the other terms mean.
OAuth is a protocol for authentication and authorization. It acts as an intermediary to use someone's account for third-party services and aims to solve the problem of selectively authorizing access to your data. If you log in to service X via Facebook, a screen asks you, for example, if service X is allowed to access your Facebook photos. It accomplishes this by providing a token without revealing the user's password.
Single sign-on, or SSO, describes the flow where an authenticated user can use their same login credentials to access multiple services. An example of this is how your Gmail account works to log in to YouTube. If you had several different services at your company, you may not want to create separate user accounts for all of the different services.
Conclusion
In this tutorial, you saw how to create signatures using the most popular standards. Now that we have covered all the main concepts, let's recap!
- Use HMAC when you need speed and are sure that the secret key can be exchanged securely.
- If the keys have to travel across a network, it's better to use RSA or ECDSA.
- RSA is still the most popular standard. Its verification step is quite fast. Use RSA if the rest of your team is already familiar with or using the standard.
- If you need to constantly generate keys on a slow device, however, use ECDSA. While the ECDSA verification is a tad slower than RSA verification, that doesn't compare to the many seconds saved over RSA for key generation.
So that's it for digital signatures in Swift. If you have any questions, feel free to drop me a line in the comments section, and in the meantime check out some of our other tutorials on data security and app development in Swift!
-
iOS SDKSecuring iOS Data at Rest: Protecting the User's Data
-
iOS SDKSecuring iOS Data at Rest: The Keychain
-
SwiftWhat's New in Swift 4
-
iOS SDKFaster Logins With Password AutoFill in iOS 11
No comments:
Post a Comment