Building WireGate: A WireGuard front to detect compromised keys

Earlier this year we released our WireGuard Canarytoken. This allows you to add a “fake” wireguard VPN endpoint on your device in seconds. The idea is that if your device is compromised, a knowledgeable attacker is likely to enumerate VPN configurations and try connect to them. Our Canarytoken means that if this happens, you receive an alert. This can be useful at moments like national border crossings when devices can be seized and inspected out of sight.

Using the WireGuard Canarytoken

If all you want is to scatter a million of these WireGuard VPN configs across all devices you care about, there's no need to read this further: they’re now freely available from canarytokens.org for anyone to grab! (Paying Canary customers will already have seen these on your private Canary Consoles).

But!

If you’re interested in how we built these tokens and how they manage to work reliably and safely at scale, then this post is for you. Along the way we’ll cover some of our design choices and what makes the WireGuard protocol design so elegantly suited to our needs.

Our Goal

The simplified version of our goal is to notify the owner of a client key when it’s used to connect to our WireGuard server. (WireGuard considers both ends of the tunnel as peers instead of clients and servers, but we only care here about the “client” config deployed on someone’s phone or machine.)

Version 1: a rough draft

The first proof-of-concept we did started with an existing WireGuard implementation. (The userspace Go WireGuard actually proved invaluable throughout this project.) With a temporary hand-wave over some implementation details to get us started, we can imagine the following:
  1. a database tracking which keys have been issued, 
  2. configuring the WireGuard server to accept these keys, 
  3. patch WireGuard to look-up and notify key owners when they're used.

Problems to Address:

The rough draft would do its job but raises a few points we’d need to consider:
  1. WireGuard creates an actual network interface on a host. How do we ensure that the traffic arriving at the interface is routed nowhere? It also ought to get nowhere quickly, to limit the opportunities for malicious packets to abuse — like within a network namespace on Linux or on an entire host of it’s own. 
  2. The selected isolation mechanism would need to support as many WireGuard Canarytokens as possible. A single enterprise deploying the tokens organization-wide could easily employ tens of thousands of tokens making multi-thousand table-stakes.
  3. Extending the proof-of-concept would need to consider the number of keys per WireGuard interface (or the number of WireGuard interfaces per host).
  4. Extending the proof-of-concept would need to consider the number of keys per WireGuard interface (or the number of WireGuard interfaces per host).

A WireGate instead of a whole WireGuard 

Our goal admits an important simplification: an attacker trying out the WireGuard Canarytoken client config must first initiate an encrypted session before she is allowed to do anything else.

A closer look at the WireGuard protocol shows how this can be done. Although peers on either end of  a WireGuard tunnel support the exchange of a few different types of messages to transfer encrypted payloads and to initiate encrypted sessions, the initiation is a single roundtrip of handshake-messages between the two peers. After just this handshake initiation message, the server (responder) knows which client has initiated the handshake - and in our case who to alert.


The fit with our Canarytoken use is even better with a closer look at the handshake initiation message:


Once the static client public key is decrypted, we know who to notify. If the encrypted timestamp that follows also decrypts, we can confidently say that only a device with knowledge of the corresponding client private key could have produced this message.

A caveat to this, is that the message could have been replayed by a passive observer of the device with the client private key on it. It works in our favour here though that, being a Canarytoken, the client private key is rarely, if ever, used, and that our server can insist on fresh timestamps to reject stale initiations. This gives us a high degree of confidence that whatever sent the initiation message has gained access to the client private key only installed on a single device.

All this can be inferred from just the first handshake initiation message. So instead of  supporting the full WireGuard protocol, we implemented a small “WireGate” service which only supports the handshake initiation message. This simplifies a bunch of the problems from the initial rough-draft:
  • There's no need to null route traffic, because there isn't any routing happening. Only individual UDP packets are checked to see if it's valid handshake initiation and everything else can be ignored. 
  • There isn't a need to maintain a shared set of valid keys with a separate WireGuard service.
  • As the number of keys grows, it’s also much simpler to reason about WireGate's performance. (The initial handshake decryption is done in relatively constant time so won't limit the number of keys created.) 

With only a partial protocol implementation, it’s necessary to ask if WireGate is realistic enough for an attacker to interact with. WireGate only needs to set off the alert to let us know someone has the client private key to have done its job, but it's less useful if an attacker is able to trivially detect that it is only a partial WireGuard implementation.

WireGuard is one the most impressive protocols we've seen for this. By design, it considers silence a virtue. Clients don't get responses without a client key known to the server endpoint. An attacker can try to fingerprint the server by throwing packets at it to explore corner-cases handled differently between implementations, but they get no packets in response. Only with a valid handshake can they begin to interact - by which time we are able to identify their client key (and by extension, will be able to generate the corresponding alert).

Adding WireGate to canarytokens.org

Canarytokens.org is the free Canarytokens server we host for the world. It’s generated close to a million tokens but this introduces a new complexity for us. Paid customers get their own canarytoken-servers, so we can run separate instances of WireGate per-customer, but the public server is shared with everyone on the internet. The same server key is used in all the Canarytoken WireGuard client configs issued, making for an easy tell. We had to do better.

In the ideal case we'd simply create a new server key for each Canarytoken WireGuard client config. To see why the naive approach doesn't work, consider the fields in initial handshake packet:


The handshake is negotiated with the static server public key in the Canarytoken WireGuard client config. Without prior knowledge of the corresponding server private key, it isn’t possible to decrypt the encrypted client public key and determine the owner to alert. The naïve approach to try every server key for every client key ever issued as a Canarytoken, would take increasingly longer to handle every handshake initiation message that arrived.

The simple workaround for this is that Canarytokens.org uses a fixed-size pool of server keys. The Canarytoken WireGuard client configs are issued with randomly chosen server keys from the pool. To skip managing 1000s of keys, the pool of keys are derived from a single private key seed.

Decrypting the client key with each server key in turn would work fine, but it can be done much faster. The initiation message includes an unencrypted keyed MAC allowing us to guess and confirm which server key it was encrypted for. For each server key, we derive the corresponding MAC key and verify the incoming handshake MACs with each. This finds the correct server key much faster and only increases the processing time for a handshake initiation message by a constant factor (number of pool keys). It comes down to a few lines in Python.


As the WireGuard whitepaper points out, this is only a very slight weakness in the handshake initiation identity hiding that makes guessing the server public key possible (and not the server private key). It’s only trivially useful here because of the circumstances we contrived: the WireGuard server already knows all the server keys (unlike an attacker who can only passively observe messages). We’d be interested to know if there's better cryptographic tricks to find some biased bits in the handshake initiation message to build an index to efficiently look-up either server key or client public keys. Our attempts all brushed up against parts of cryptographic primitives designed to resist what we were doing. We like to think that, rather than this being a limitation of our own engineering abilities, it’s a virtue of good cryptography in use by WireGuard being harder to mis-use in ways it is not designed for.

Conclusion

At Thinkst Canary, we’ve been rolling our own partial-protocol implementations where it makes sense for security and performance on lower-resource devices since almost day-1. That said, partial protocol implementations won’t fit every problem as well as WireGate does for WireGuard. If this problem also required emulating network services accessible over the WireGuard VPN, we’d be better off with a full implementation, ideally an isolated emulation. (For meatier protocols where native code implementations are un-avoidable, those run sandboxed.) If it wasn't clear already, we think WireGuard is great. The more time we spend working with it, the more we’re convinced others should too.

Comments

Check out some of our other popular posts:

We bootstrapped to $11 million in ARR

On SolarWinds, Supply Chains and Enterprise Networks

Good attacks make good detections make good attacks make..