Page MenuHomePhabricator

Plan the path forward from HMAC-SHA1
Open, NormalPublic


A SHA1 collision was widely disclosed recently.

Our only use of plain SHA1 was removed in D17619, but we still make use of HMAC-SHA1 via PhabricatorHash::digest(). This isn't currently broken, and HMAC is designed to resist collisions, but it would be good to move away from it. However, doing so involves some considerations:

  • Do ~all installs have SHA256, or are we going to run into issues where SHA256 isn't available?
  • The security.hmac-key option is currently kind of a pile of garbage. In practice, it's just a hard-coded value for every Phabricator install. It would be much better to make this value unique per-install during setup. Ideally, we'd generate keys into some table and use a different, randomly generated key for each class of hash.
  • We should probably rename PhabricatorHash::digest() to oldBadDigest() or similar, rather than suffer the curse of digest2pro_improved().
  • We have ~40 callsites and these need to be individually audited and upgraded. Many probably present no security risk on SHA1, and many are ephemeral or can be transitioned easily, but some will be a pain to dig out.
  • The introduction of D17625 puts some time pressure on this: we save a migration if we sneak a fancyNewDigest() in before anyone uploads any files to installs with the new integrity check code. Otherwise we need to keep code to validate the old hashes around.

Event Timeline

I'm maybe going to try to do this in the short term:

  • Write the integrity-check-management scripts in T12470.
  • Rename digest() to weakDigest().
  • Introduce a new table in the Auth database for HMAC keys, deprecating security.hmac-key with the intent to eventually remove it.
  • The new method will have a signature like PhabricatorHash::digest($material, $role), where $role is a lookup to a random secret key in the HMAC table that we read through the ImmutableCache.
  • Use the new stuff for integrity checks.

Then we can audit and replace the old stuff at our leisure.

Piece of info (you guys might already be aware of it) which might be of interest when implementing this; SHA512 is often faster then SHA256 on x64. See for example:

I get these rough per-hash costs locally (Macbook Pro), with a 64-byte key:

Input SizeHMAC-SHA256HMAC-SHA512

I think these costs are both way beneath the level of line noise for all applications we use hashing for.

(That said, the hash cost may become very relevant when we eventually ship "Phabricator Valuable Golden Coin Money", our blockchain-based virtual currency.)

In moving forward here, we're generally moving from manually-configured HMAC keys to automatic ones. This is generally good: it's simpler (less configuration); and I believe almost no one configured the old ones, so installs now actually get unique HMAC keys; and the new keys have more entropy, too.

In some cases, there was theoretical value in changing these keys as a security response. For example, if you leaked some "Reply-To" addresses, you could change phabricator.mail-key to invalidate all the old addresses. I believe essentially no one has ever done this, but we could be exceptionally robust here by doing this:

  • Add an abstract class PhabricatorNamedHMACDigest { ... }.
  • Subclass it into final class PhabricatorMailObjectAddressNamedHMACDigest { const HMAC = 'mail.object-address-key'; ... }.
  • Have some methods like getAnExplanationOfWhatThisKeyIs() and describeWhyYouWouldWantToRegenerateThisKey().
  • Provide bin/hmac list to show current keys and explain what they are, and bin/hmac regenerate <key> to regenerate a particular key.

This is a bit of work and pretty low-value, but maybe not totally unreasonable. It doesn't add much complexity, but does feel fairly YAGNI.

The low-budget version of hmac regenerate <key> is DELETE FROM phabricator_auth.auth_hmackey WHERE .... This will automatically regenerate the missing key the next time it is used.