Offline license management in Go

I am currently working on an application, which hopefully will go to market somewhen in the middle of this year. It is a bit early to talk about it, but it will be fully self hosted and can run in air gapped networks. As it is a commercial application with two different feature tiers I need some form of license management - at least something that tells the app "these are the features you should support".

Supporting air gapped networks means the best I can do is either to send out binaries depending on the "tier" a customer purchased, or sending out license keys encoding the features. I do not intend to make this overly complicated. If someone really wants to put on their eyepatch and hat, scream "arrrrrrr" and pirate the software, then so be it. They would not become a paying customer anyway. And as it will be a reasonably priced B2B application, the target audience likely prefers to pay anyway… instead of facing a potential lawsuit.

While there are a few options for license management I decided to settle on a simple hmac. Wikipedia has a good introduction in case you are not sure what this is.

In cryptography, an HMAC (sometimes expanded as either keyed-hash message authentication code or hash-based message authentication code) is a specific type of message authentication code (MAC) involving a cryptographic hash function and a secret cryptographic key. As with any MAC, it may be used to simultaneously verify both the data integrity and authenticity of a message. [...]

HMAC can provide authentication using a shared secret instead of using digital signatures with asymmetric cryptography. It trades off the need for a complex public key infrastructure by delegating the key exchange to the communicating parties, who are responsible for establishing and using a trusted channel to agree on the key prior to communication.

What does this mean in practice?

  1. we define a license data structure for all features we want to enable or control via license
  2. we define an envelope holding the license data and the checksum
  3. we compile the secret to verify the envelopes checksum into the binary

Step three means you can extract the secret, update your license data, re-sign it and unlock features you did not pay for. But as I already mentioned I am not too worried about that. If you go through this whole process it is very likely you would not have become a customer anyway. And honestly? We have all been there at some point. Enjoy the journey and the unlocked features.

type Data struct {
    Feature1  bool
    Feature2  int
    ValidTill time.Time
    CreatedAt time.Time
}

type Envelope struct {
    Data     Data
    Checksum []byte
}

As long as we can marshal the data structure to JSON we are good. For a license key and feature toggle primitive types should be more than sufficient. And we do not have to care too much about the actual naming of the field, not like anyone should read it when we send out a base64 encoded version of it.

func New(featere1 bool, feature2 int, expire int) (*Envelope, error) {
    d := Data{
        Feature1:  featere1,
        Feature2:  feature2,
        ValidTill: time.Now().AddDate(0, 0, expire),
    }

    e := Envelope{
        Data: d,
    }

    db, err := json.Marshal(&d)
    if err != nil {
        return nil, err
    }

    hmac := hmac.New(sha256.New, secret)
    hmac.Write(db)
    dh := hmac.Sum(nil)

    e.Checksum = dh

    return &e, nil
}

Breaking this down:

  1. marshal the license data to a JSON string
  2. build the hmac-sha256 of the marshalled data
  3. create the envelope

Marshalling the envelope and encoding it to base64 is a separate function. To verify a license you accept a base64 encoded string and return the envelope. If this fails return an error, you are looking at an invalid license.

I pushed a more complete example and some test cases to this blogs git repo. This is not the full implementation and cut a few corners in both code and unit tests, but it demonstrates the idea and should be working as is.

Overall I am pretty happy with the implementation. It only uses Go’s standard library, does not take too many lines of code, easy to reason about and secure enough considering that most customers are not incentivised by the lack of skill to crack the application but by simply business needs and practices.

posted on Feb. 27, 2025, 8:36 p.m. in Tech Quips, golang