Skip to main content

Enforcing device trust on code changes

Griffin ChoeSoftware Engineer, Figma

Here's how the Figma security engineering team leveraged commit signatures and Okta Device Trust certificates to protect GitHub release branches.

Illustrations by Rose Wong.

Release branches on GitHub are the source of truth for code deployed to production, which makes them a prime target for an attacker trying to compromise Figma. As Figma scales and more engineers ship code every day, the attack surface has only grown. To get ahead of this problem and reduce the risk of malicious code reaching production, we made a plan: ensure that code changes merged into GitHub release branches come from trusted, company-managed devices.

This sounds simple enough. At Figma, like many other companies, access to GitHub lives behind Single Sign-On (SSO). Even if an attacker compromises an engineer’s GitHub account credentials, they would still need to get through Okta SSO—which also requires WebAuthn 2FA—to access the Figma GitHub organization. While this significantly reduces risk, we remain vulnerable to malicious changes made with leaked session credentials, personal access tokens, OAuth tokens, and SSH keys.

Figma enforces “dual-control” on GitHub Pull Requests (PRs), requiring both the PR author and another engineer to approve code changes.

The easiest way to reduce these risks is to require GitHub’s commit signature verification, which is supposed to ensure that code changes come from a trusted source. However, this verification doesn’t reduce risk as much as we’d like. Even if we enabled this requirement, we’d need to closely monitor personal access tokens, OAuth tokens, and SSH key usage to detect malicious activity. Building a verification system ourselves not only facilitates that, but also gives us more flexibility on the criteria we consider. As security engineers at Figma, we value engineering-driven solutions that scale with minimal overhead. This means reducing unnecessary process and moving as quickly as we can while keeping our systems secure.

Watch Director of Security Engineering Dev Akhawe’s conversation with the Modern Security Podcast, where he shares how Figma scales security with secure defaults.

The problem with GitHub commit signature verification

Companies often require that all commits merged into sensitive branches be Verified by GitHub.

A GPG key is a pair of cryptographic keys–one public and one private–used for the encryption and signing of messages or data.

Git commit signatures are typically used to attest that a trusted author made a code change and get the big green “Verified” stamp of approval on GitHub. While this provides more confidence in commit integrity, we don’t have visibility or control over the personal GPG keys engineers register with their account to use for signatures—and no way to tie their GPG keys to a particular device.

GitHub’s verification criteria are also looser than we’d like. For example, commits made through the GitHub UI and the GitHub API are “Verified” as they’re signed with GitHub’s web flow GPG key. If we followed this playbook and an OAuth app or a Figma engineer’s GitHub session, personal access token, or SSH key were compromised, an attacker would have several ways to have their malicious commits “Verified” by GitHub.

Even with Okta SSO and GitHub commit signature verification, these long-lived access tokens exist outside the trust context where attackers can use them to introduce malicious code. Instead of accepting these risks or introducing processes to monitor for suspicious access token usage, we built our own solution.

Okta Device Trust

Endpoint Security Baseline at Figma requires various criteria, such as up-to-date browser versions, the latest macOS, and active malware protection software.

In late 2022, we built a custom solution to issue X.509 Okta Device Trust certificates to company MacBooks from an Amazon Private Certificate Authority (CA). These certificates are distributed through JAMF, renew every 15 days, and attest that a laptop meets our Endpoint Security Baseline criteria at the time they’re issued.

The team leverages Okta Identity Engine (OIE) to enforce device trust for apps like AWS, Stripe, Snowflake, and other sensitive platforms, but these certificates aren’t limited to use in Okta. These certificates can create signatures that attest device trust for any action that involves signing data with X.509 certificates.

Signing commits with device trust certificates

We investigated signing commits with the same device trust certificates discussed before and learned that we can set up Git to use Secure/Multipurpose Internet Mail Extensions (S/MIME) to sign commits with our certificates. In our investigation, we found GitHub’s smimesign utility that they built for organizations like ours that want to sign commits with X.509 certificates.

Smimesign is an S/MIME signing utility for macOS and Windows that is compatible with Git. This allows developers to sign their Git commits using X.509 certificates issued by public certificate authorities or their organization's internal certificate authority. Smimesign uses keys and certificates already stored in the macOS Keychain or the Windows Certificate Store.

To set up S/MIME Git commit signing, you run a handful of commands:

Shell Script
git config commit.gpgsign true
git config gpg.format x509
git config gpg.x509.program smimesign
git config user.signingkey <your_x509_key_id>

After setting this configuration, Git will use the smimesign utility to sign commits with the provided key (which, in our case, is stored in the MacOS keychain).

However, this would not work for our needs as our signing keys change every 15 days when device trust certificates renew. To provide a seamless experience for our engineers, we need Git to dynamically fetch the latest key ID upon a commit, which takes a bit of hacking.

Under the hood, Git calls the configured signing program as follows:

Shell Script
smimesign --status-fd=2 -bsau <your_x509_key_id>

Breaking down these arguments:

  • --status-fd=2: write special status strings to the shell error stream
  • -b: create a detached signature
  • -s: make a signature, as opposed to other options like verifying a signature
  • -a: create American Standard Code for Information Interchange (ASCII) armored output
  • -u: use the following key id to sign
  • <your_x509_key_id>: key id to use

The specifics here aren’t too important to understand, but we noticed that <your_x509_key_id> is static, which makes our needs more difficult to configure.

To achieve dynamic key fetching, we built smimesign-figma, a slightly modified version of GitHub’s smimesign, and wrote a simple wrapper that forces git to fetch a signing key at commit time. One extra feature we built into smimesign-figma is the --get-figmate-key-id flag that looks through the MacOS keychain and returns a user’s device trust certificate key id. Using this flag, we write our one-line bash script smimesign-figma-wrapper:

Shell Script
smimesign-figma --status-fd=2 -bsau smimesign-figma --get-figmate-key-id

We then configured Git to use this wrapper as the signing program:

Shell Script
git config gpg.x509.program smimesign-figma-wrapper

It is important to notice that smimesign-figma-wrapper ignores any arguments passed to it when invoked. This allows us to ignore the arguments Git tries to feed the program, in favor of our arguments that let us dynamically fetch a key id using smimesign-figma --get-figmate-key-id. To illustrate further, Git will try to execute:

Shell Script
smimesign-figma-wrapper --status-fd=2 -bsau <your_x509_key_id>

But smimesign-figma-wrapped will ignore the arguments and execute the following instead:

Shell Script
smimesign-figma --status-fd=2 -bsau smimesign-figma --get-figmate-key-id

As you may have noticed, Git never ends up using <your_x509_key_id>, the value of user.signingkey, in the signing process, and that’s okay! Our custom configuration doesn’t require it, and we leave the value blank in our Git configurations to prevent confusion. Now, Git will sign every commit we make with the device trust certificate on our laptop, attesting that the code changes we’re making originate from one of our trusted, company-managed MacBooks.

Verifying signatures with AWS Lambda and GitHub Apps

The principle of least privilege says that employees should be granted the minimum amount of access to do their jobs.

To verify commit signatures, we built a system that cryptographically confirms whether commits are signed with device trust certificates and reports the result back to GitHub. To accomplish this, we turned to GitHub Apps and AWS Lambda. At Figma, we use GitHub Apps to build secure, least privileged tools that interact with the GitHub API. AWS Lambda is a lightweight service we can use to perform ‌stateless verification operations on commit signatures.

The setup we needed is to have GitHub trigger our commit signature verification Lambda function upon new pushes to our internal monorepo. The following are the pieces we used to build this:

  • GitHub App in the Figma GitHub organization
  • Lambda function that uses smimesign/ietf-cms to verify the S/MIME commit signatures
  • Lambda Function URL to allow GitHub to trigger the function
  • GitHub webhook to notify our Lambda when code is pushed to our monorepo
  • Webhook secret to verify requests sent to our Lambda are from GitHub

We configured the “Commit Signature Verification” GitHub App with permissions to read code and to write commit status checks. We then installed the App in our monorepo and stored its credentials in AWS Secrets Manager for our Lambda function to use for authentication.

When an engineer pushes a commit, GitHub sends a webhook payload to our Lambda function. Upon receiving the payload, our Lambda authenticates it as the “Commit Signature Verification” GitHub App and obtains various information about the commit, such as its author, the changes it introduces, and its associated signature.

With this information, our Lambda function then cryptographically verifies whether the commit signature was created with a certificate issued by the private CA we use for device trust certificates, and reports a commit status back to GitHub with the result.

Putting the end-to-end flow together:

  1. An engineer pushes a signed commit to a feature branch on our GitHub monorepo
  2. GitHub notices the push event and sends a webhook payload to our Lambda Function URL endpoint

Our Lambda then:

  1. Authenticates as the “Commit Signature Verification” GitHub App
  2. Verifies the webhook secret to ensure the payload is from GitHub
  3. Takes the push event’s HEAD commit and cryptographically verifies it was signed with a device trust certificate

Posts a commit status called commit-integrity-verification back to GitHub, indicating whether the HEAD commit passed commit signature verification

With this system in place, we can now require a passing commit-integrity-verification status check for the most recent commit on a PR before it can merge into a release branch. This ensures that code changes merging into these branches originate from trusted Figma MacBooks.

Verifying bot-authored commits

Although Figma engineers author most of the commits merged into our monorepo, we have some internally and externally developed bots make commits as well, such as GitHub’s Dependabot. These bots make commits through the GitHub API and are signed with GitHub’s web flow GPG key as we discussed earlier.

Now that we have control over which commits pass commit-integrity-verification, we can check these commits against an allowlist of bot authors we trust. This helps us reduce the risk of an untrusted bot, such as an externally developed GitHub App, making a malicious commit, giving us an extra layer of security on top of the external bot approval flows we have already.

Even further, we’re able to inspect the code changes these bots make and use heuristics to determine if they’re safe. For example, we can determine if Dependabot makes a change unrelated to dependencies and report a failed commit-integrity-verification status check.

Making security painless

Shipping this project was a big win for our security team. It gives us GitHub security guarantees by default and sets us up well for future security work with our build and deploy systems. What makes this win even better is that the system we built requires close to no maintenance from our engineers. In an area where we could have accepted unnecessary risk or introduced tiresome processes, we engineered our way into a safer posture instead. This minimization of toil is central to the philosophy of our team, and it frees us up to move quickly on new projects that make Figma more secure.

We are a small team, passionate about modernizing security practices, and excited to contribute to the broader community. If that sounds exciting to you, join us!

Subscribe to Figma’s editorial newsletter

By clicking “Submit” you agree to our TOS and Privacy Policy.

Create and collaborate with Figma

Get started for free