With software having eaten the world twice over, plus the proliferation of DevOps making constant updates and releases the new norm, code signing is more important than ever. As such, enterprises need a secure code signing system that doesn’t reduce the tempo of day-to-day operations or present integration issues with the platforms and tools they rely upon. Of course, developing this kind of code signing system is easier said than done.
In January 2018, NIST provided a great high-level overview of code signing, its threats, and some best practices to follow. This blog post extends on NIST’s overview with suggestions on how an enterprise can build such a system. In particular, we’ll focus on the signing aspect of a code signing system.
For additional information on this topic, see this post on automated hash validation, a technique for ensuring that the code being signed matches the code in the source code repository, and this post on preventing malware injections.
Which Companies Need To Sign Code?
You might be tempted to think that establishing a rigorous and secure code signing system isn’t necessary for your company. However, the reality is that if you’re producing software, you should be signing it. So the question you need to ask yourself is, “Does my company produce software?”
Even if you are not a tech company, the answer is likely yes. Most companies have mobile apps and internal tools, at the very least. Even companies who you would never think to associate with producing software have Excel macros and PowerShell scripts to automate tasks. All of these things are in fact software, and, regardless if they were produced internally or by a third-party, or if they are to be used internally or externally, they should be code signed.
Code Signing in the Real World
Every enterprise is unique and made up of various teams, each with their own requirements. Every team will have different entities signing code, with each potentially using different keys.
For example, the DevOps team may sign a lot of code in their CI/CD environment with mostly non-production keys. In this case, speed is of paramount importance and, since most systems are automated, using traditional multi-factor authentication (MFA) techniques may not always be possible.
The Software Release team, on the other hand, may be signing code infrequently (e.g., quarterly) with production keys. In this case, security is the most critical factor. A proper code signing environment must be flexible enough to handle all uses without sacrificing security or performance, or otherwise hindering business processes.
How to Properly Implement a Code Signing System
So, how does one go about designing a code signing system that can handle all use cases and integrate with all the necessary tools and platforms, all without compromising on security or performance? Here are 14 helpful tips to achieve such a code signing environment.
1. Use an HSM
All code signing keys should be generated, stored, and used in a non-exportable manner in an approved hardware security module. This applies to all signing keys, not just the production ones.
Ideally, different HSMs (or at least different logically isolated slots of a single HSM) should be used for different environments (e.g., development, production, etc.). Keys that currently exist in software should be imported into the HSM. After they have been safely imported and backed up, they should be securely wiped from the software. The imported software keys should not have their certificates renewed once they expire. Instead, a new key that is generated inside the HSM should be used, if possible.
2. Centralize the HSM
Centrally manage the HSM and its keys, instead of distributing the keys out to end users. This allows you to grant and revoke access as needed, which would otherwise be very difficult to do with a non-centrally managed approach.
3. Mandate Proxied Access to the HSM
HSMs have built in security but they don’t necessarily easily integrate with your environment. By proxying access, you can add any security policy you please between the end-user and the HSM. Mandating proxied access to the HSM also makes it easy to grant and revoke user access.
4. Implement Strict Access Control
Users should only have access to keys that they are allowed to use, and when they are allowed to use them. Access control should be tightly guarded and easy to manage.
5. Require Strong Authentication
All team members using the code signing system should be strongly authenticated. Authentication is a topic all on its own, but a proper system should support flexible and modern authentication methods along with (secure) traditional ones (e.g., OAuth2, Kerberos, Client-Certificate, etc.) in order to gain widespread adoption.
Wherever possible, users should be required to use multi-factor authentication, sometimes on a per transaction basis, such as every time a production code signing key is used. For automated systems like a CI/CD pipeline, MFA may not always be possible so the authentication policy must be strong but flexible.
Check out the short demo video below to see how GaraSign enables customers to enforce multi-factor authentication on a per-key or per-user basis with just a few clicks from the admin interface.
6. Set Up Device Authentication
In addition to authenticating the end-user, a proper code signing system should authenticate the device that the end-user is signing code from and ensure that it is a valid enterprise device. Further checks should include ensuring the user is authorized to use that specific device and that the device is up to corporate standards. While some may be tempted to authenticate the device via an IP address, this is not sufficient. Device authentication should make use of tamper resistant secure enclaves (e.g., Trusted Platform Modules) wherever possible.
7. Establish Key Policies
Each key should have its own policy attached to it that dictates how the key can be used. The policy should include what algorithms the key can be used with, what approval processes must be completed, how the user must authenticate before using the key, device restrictions, when the key can be used, approved geographic regions for key usage, who to notify when the key is used, and so on.
8. Create Approval Workflows
Some keys should not be used without approval from other team members. When someone attempts to use one of these high-value keys, the system should automatically trigger an approval workflow that requires a pre-defined quorum of users to approve the request. The signature should only be generated after the quorum requirement is satisfied.
9. Leverage Hash Signing
With DevOps being adopted far and wide, the frequency of code signing has significantly increased in recent years. As a result, performance is a critical factor. In order to meet performance requirements, client-side hashing should be used in order to minimize the data sent over the network and sign code as fast as possible.
10. Use Reproducible Builds
Wherever possible, compile your code deterministically so that the output is always the same for the same source code. This builds a chain of trust with many advantages, including some for code signing. Once implemented, have the code signing system reach into the source control repository and validate the hash that has been requested to sign. The most effective way to perform this check is with a technology called automated hash validation.
Using this approach, an attacker would now need to commit malicious code to the source code repository and gain access to the code signing system, instead of just gaining access to the code signing system. This is a harder attack to pull off and one that is much easier to detect since the source control’s history is tough to alter and constantly monitored by the developers.
11. Support Native Tools
Signing code is only as good as the tools that can verify the signature. Therefore, it is important to sign code in a manner that is compatible with the tools that end-users are utilizing.
The best way to do this is to use the industry-provided tools to sign your code (e.g., signtool, jarsigner, codesign, productsign, etc.), instead of trying to reverse engineer the tool yourself. While reverse engineering may make it easier to integrate your signing tool with your code signing system, these tools are subject to change without notification and you will be left trying to figure out what changes were made, often with no documentation to read. If you use the natively-provided tools and integrate them at the appropriate level via a properly implemented cryptographic service provider, you are immune from any and all changes made.
12. Make Keys Easily Auditable
All actions, administrative and cryptographic, should be easily auditable via logging and notifications. Notifications should be configurable and only sent to users with the appropriate permissions.
13. Plan For Multi-Tenancy
Organizations are large and complex. Some teams will have their own keys that only they use. Other teams might share some keys. In order to allow independent teams to self-govern in a consistent way, an effective code signing system should be multi-tenant, preferably with subdomains and hierarchical permissions.
14. Cryptographic Agility
Cryptographic algorithms change over time and will continue to do so for the foreseeable future. Over the last few decades, the most commonly-implemented hash functions have changed from MD5 to SHA-1 to the SHA-2 family to the newer SHA-3 family. Similarly, signature algorithms have gone from RSA to ECDSA to EdDSA (namely, Ed25519 and Ed448). Now, NIST is expected to make its final recommendations for post-quantum signature algorithms within the next year (technically, XMSS and HSS/LMS have already been recommended as stateful signature algorithms).
This means that a proper code signing system needs to be agile with respect to the cryptographic algorithms it supports. It needs to be flexible enough to introduce new algorithms that may not fit the same model as the previous algorithms (e.g., no longer hash then sign or may require state management).
Does building cryptographic providers to integrate with native tools on all platforms that support strong authentication, approval workflows, cryptographic agility, and HSM proxy integration sound tough? Well, it sounds tough because it is tough. That’s why we built GaraSign.
With GaraSign, Garantir’s flagship product, code signing keys remain secured in your enterprise HSMs at all times, while code signing stays fast and efficient with a hash signing architecture. Customers using GaraSign see a reduction in the total amount of time needed to complete the build process, even with the code signing keys secured in HSMs.
GaraSign sits on a customer-managed server between end-users and the HSMs, where the private keys reside. On the client side, GaraSign integrates with all of the platforms and tools in your environment, from Microsoft, Apple, and Linux, to GPG, RPM, and Debian, and more. This makes setting up a secure code signing system easy, as there’s no need to code custom integrations in-house.
If you’re interested in learning more about GaraSign or setting up a demo, get in touch with the Garantir team.