Mobile Security Fundamentals: Learn Code Signing with OpenSSL

Sunny Yang
Nerd For Tech
Published in
8 min readJun 23, 2021

--

As a mobile developer, you may ever struggle to handle code signing when building or deploying your app. But after some attempts to modify some configurations which you don’t really understand, all the errors disappear! Although by no means it affects your development of a great mobile app, it’s pretty interesting to know why we need code signing and how it works.

Unlike the code running in a server — normally a device fully controlled by yourself, it’s not as safe to let your code run at the user-side, like web apps and mobile apps. In fact, you can hardly know if the code is identical to what you are supposed to run from users’ devices.

For example, we want a text of ‘ThoughtWorks is a good company.’ to show on the screen, but it can be ‘ThoughtWorks is a good company. But SunnyCorp is a better one.’ if our code is modified after sent out. Similar scenarios often happened when HTTPS hasn’t been broadly applied — you may see some ads injected by your ISP in web pages.

One good news is that it’s much more difficult to see this weird situation at present due to the use of HTTPS in websites. A modern browser will help users establish a secure connection and only loads the trusted content delivered by you.

However, a mobile app is different from web pages because no server sends out your code at users’ requests. Instead, a user downloads your code package from somewhere and installs it on the device. So from the moment you publish your mobile app, it seems like you cannot make sure no surprises will occur to your app — someone may modify your package or even “kindly” build a newer version for your users.

Take it easy. Those are what code signing is designated to avoid. So let’s dive into code signing to see how it works.

The threats

As mentioned before, it may be dangerous to deliver an app to target devices. The most obvious threat is package modification, which means the package’s content can be modified after your delivery.

Think about what can follow after modifying the package.

  1. Users can see misleading wording or data that harms your reputation;
  2. It can records sensitive and confidential information and silently send it to the modifier’s server;
  3. Dangerous actions like financial transactions can be done without users’ intention;

In addition to app integrity, it counts to identify the developer’s identity as well. If the way to modify package content is blocked, it’s still possible to cheat a phone to upgrade your app with a newer version — the hacked new version inherits the sensitive data you saved, and your app is now kind of being modified.

Thus, a mobile OS must identify the package’s developer and make sure the app’s integrity. Let’s design a simple mechanism to understand the underlying fundamentals better.

Design a mechanism for our virtual OS

To make it simple, let’s assume:

  1. The app package consumed by our OS is like a folder (yes, after uncompressing, iOS and Android app packages will turn into a folder);
  2. Our OS executes a JavaScript file named main.js from the app package when launching an app (actually, iOS and Android do look for an entry from the app package, though the code is in binary form);
  3. Our OS identifies the app by reading the name value in the meta.json file (an OS requires apps to have unique identifiers so that it can tell our app from other apps);
  4. The name of our OS is OurOS (this isn’t important at all).

For now, our package looks like this:

/
meta.json
main.js

Inside meta.json, there is our app's identifier.

{
"name": "AnOurOSApp"
}

Our main.js has only one line inside, but we need to try our best to defend that line!

console.log('Protect me');

Help OurOS identify the developer

This package is too simple to figure out who’s the developer. So to tell OurOS who’s the author of this package, we have to design that OurOS will read the author’s identifier from developer in meta.json.

We can put our identifier like ‘sunny@thoughtworks’ in the developer field so that OurOS can know the package is (announced to be) created by us.

{
"name": "AnOurOSApp",
"developer": "sunny@thoughtworks"
}

As we all know, the announcement is quite fragile because everyone can put the same file into another package and say the app is yours.

How to deal with that? How about attaching a secret linked to the package like this:

{
"name": "AnOurOSApp",
"developer": "sunny@thoughtworks",
"secretKey": ".ZczU9uQbBXx_eaivWJxAHxBc4MGwcqQ!zdPBdB7Rp@4c_PbgUWXr!yGQQqfXgJdPyp@2Kyq*eaXmq4rGVe8iCG.63Y8_EiRuk7k"
}

If we are the only people knowing the secret for ‘sunny@thoughtworks’, other people cannot pretend to be us!

But hold on. Since everyone can download and read your package, after you publish it, the secret discloses!

It cannot defeat brilliant us. We know there is a family of algorithms named Asymmetric Encryption (See this article if you don’t know that). We keep the private key and encrypt some text with it. If OurOS decrypt the encrypted text successfully with our public key, the package is proven to be created by ourselves. The reason is so apparent that only the corresponding private key can produce the encrypted text that a public key can decrypt. We often call this behavior signature.

Let’s assume every package needs to encrypt ‘OurOS’ and insert the encrypted text into meta.json for later verification. And of course, we need to attach our public key to our package. We can pick RSA, a classic asymmetric encryption algorithm here.

Easily create a RSA private key with the help of openssl. A file named private_key.pem will create.

> openssl genrsa -out private_key.pem

And get the corresponding public key:

> openssl rsa -in private_key.pem -pubout > public_key.pub

Then, encrypt ‘OurOS’ with the private key, and put both the signature and the public key to our package:

> echo 'OurOS' | openssl rsautl -sign -inkey private_key.pem -out signature.txt/
meta.json
main.js
signature.txt
public_key.pub

When launching the app, OurOS will decrypt the encrypted text with the attached RSA public key with the help of OpenSSL.

> openssl rsautl -in signature.txt -verify -pubin -inkey public_key.pub
OurOS

The result is ‘OurOS’. There is nobody but us who can sign it! And no secrets leaks!

Issue a certificate

However, what if an attacker replaces the attached public key with the one owned by himself? Then, he can sign ‘OurOS’ with his private key, and it will surely pass the OurOS check because it doesn’t know whose key it is. Thus, the public key can cheat you!

/
meta.json
main.js
signature.txt // replaced by the signature encrypted by the attacker
public_key.pub // replaced by the attacker's key

We can learn something from TLS/SSL to solve that problem. First, TLS/SSL requires the client to verify if the public key belongs to the specific domain by giving a certificate to the client. Then the client should validate the certificate to decide whether it can trust the public key listed in the certificate.

Likewise, we need to set up a service to sign certificates for developers. But, before that, let’s design what our certificate should include:

  1. app name or package identifier to tell what the certificate is issued to
  2. the public key to decrypt the encrypted text
  3. a signature of both the above generated by a trusted authority, like us.

To issue a certificate, a pair of private/public keys is necessary. To make it simple, we can force OurOS always to use the same public key to verify the app certificate. Suppose the certificate signature can be decrypted to exact the same value as the package identifier and the public key. In that case, that means we are the issuer of this certificate.

Generate a new private key for certificate signing (we need a longer key size to sign larger content):

> openssl genrsa -out cer_private_key.pem 8192

Get the public key and store it in every distribution of OurOS:

> openssl rsa -in cer_private_key.pem -pubout > cer_public_key.pub

Part of our certificate should be like (JSON format for example):

{
"package": "AnOurOSApp",
"publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvJ9GymWXeFxGREgSMNoj\nnuBYfazpJxeIOy/0xXI368oHnOTDwaUqqxmPbobKGhzuoG8OuMw4Q1BohTxAofql\nCRVxoPSL/dB1gAf8hs0uUrRiBLVnw2jfDYiuDJT4RpqVBDb+JZ67q7HgjdND9raM\nHQMxCnwHCoUEkC4230XTZzMEUmfDD84gHzgyLXIL/meZtWtqUgkyNMj7/zJLosYR\nMqCKR+2an1wgTLh0U0k/V/8DW5sq5trmeuaSL22lbsbZSMKf4W/6acATtdOKHV2O\nL2tEDCKkfXy/JlUOdqYukxZLsUgr5dptgIQkTlxYD7TFvGp48leLjwXxvowBUjde\nbwIDAQAB\n-----END PUBLIC KEY-----"
}

Sign this JSON file and store the signature into cer_signature.txt:

> openssl rsautl -sign -inkey cer_private_key.pem -in certificate.json -outcer_signature.txt

Look, here are two different key pairs involved. The one we force OurOS to use is to issue and validate certificates, and the other one whose public key is listed in the certificate needs OurOS to retrieve and then use it to validate the whole app package.

As a trusted authority, when a developer comes to us, we should generate a pair of keys together with a certificate for him. And to be responsible for the same package identifier, we shouldn’t issue two different certificates.

As a developer, after getting the certificate and the private key, we can continue to encrypt ‘OurOS’ with that key and put our certificate in the package.

/
meta.json
main.js
signature.txt
certificate.json
cer_signature.txt

So for OurOS, it can validate the certificate first and then retrieve the public key from the certificate. Now, OurOS can say the verified public key belongs to the developer authorized to sign the app.

> openssl rsautl -in cer_signature.txt -verify -pubin -inkey cer_public_key.pub
{
"package": "AnOurOSApp",
"publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvJ9GymWXeFxGREgSMNoj\nnuBYfazpJxeIOy/0xXI368oHnOTDwaUqqxmPbobKGhzuoG8OuMw4Q1BohTxAofql\nCRVxoPSL/dB1gAf8hs0uUrRiBLVnw2jfDYiuDJT4RpqVBDb+JZ67q7HgjdND9raM\nHQMxCnwHCoUEkC4230XTZzMEUmfDD84gHzgyLXIL/meZtWtqUgkyNMj7/zJLosYR\nMqCKR+2an1wgTLh0U0k/V/8DW5sq5trmeuaSL22lbsbZSMKf4W/6acATtdOKHV2O\nL2tEDCKkfXy/JlUOdqYukxZLsUgr5dptgIQkTlxYD7TFvGp48leLjwXxvowBUjde\nbwIDAQAB\n-----END PUBLIC KEY-----"
}

And repeat the steps to decrypt the encrypted text. It’s ‘OurOS’. We did it!

Protect Integrity

Now, OurOS has successfully identified the authorized developer. However, what if attackers change the content of main.js? We spent a lot of effort to make sure the signature is created by the authorized developer, but it doesn't mean that developer creates the rest.

Instead of signing a static text, how about signing the code? If we get the same code in the package after decryption, the package is undoubtedly signed by an authorized developer. And if someone modifies the code, the decryption result will not match the current code. OurOS should then block the app from launching.

> openssl rsautl -sign -inkey private_key.pem -in main.js -out signature.txt> openssl rsautl -in signature.txt -verify -pubin -inkey public_key.pub
console.log('Protect me'); // If the content of main.js is different, the package should be hacked

To support more lines of code, we can integrate a hash algorithm to generate a fingerprint of code and then sign the fingerprint instead. It’s essential to do this because asymmetric encryption is so consuming.

> md5 main.js | openssl rsautl -sign -inkey private_key.pem -out signature.txt> openssl rsautl -in signature.txt -verify -pubin -inkey public_key.pub
eac097dc6888a4246bcf11f7ba4c3266
> md5 main.js
eac097dc6888a4246bcf11f7ba4c3266 // It matches!

After all the improvements we made above, OurOS can check the app legality for us. Cheers!

Back to real world

Although the package verification mechanism of OurOS is quite simple, it covers nearly all the essential steps iOS and Android follows! So this is underlying why you need a certificate and private key to build mobile apps for real devices.

Code signing plays a vital role in mobile security. But, it cannot deal with repackage attack — an attacker reverse engineers your package, add some harmful code, and then sign the code with his certificate and private key. Your innocent backend may transmit confidential data to a repackaged app.

The following article will go through some standard techniques to defend our data from app repackaging. Subscribe to me for the updates.

--

--

Sunny Yang
Nerd For Tech

有高人之行者,固见负于世;有独知之虑者,必见骜于民。