Simple Licensing Validation Scheme Using JOSE

Leveraging common and standardized technologies to design and implement a scheme to verify licenses on the client side in isolation.

Simple Licensing Validation Scheme Using JOSE
Scruffy Flowchart by DALL-E

Cross-Post: LinkedIn

Introduction

Providing a mechanism for commercial software to only provide services to users who have a valid license is a necessary feature for primarily two reasons. First, it prevents (some) unauthorized users from using the software but, most importantly, provides legal users a way of proving that their usage is authorized and valid in case of disputes.

In the era of SaaS, this has been simplified by moving all the authentication and authorization to the service provider side. The client uses some sort of credentials - be it a token, a user/pass combo, etc - to communicate with the service which then determines based on those whether the service should be provided or rejected. For locally installed software, though, a common mechanism is to provide a key that is then registered and validated for authenticity by an upstream service upon first use. This retrieves the license details, such as expiration date, enabled features, etc.

What all these schemes have in common is that they require a connection to an upstream server. If one desires to provide a service that can run in an isolated box from the internet, these approaches fall short.

I decided to take a stab at doing a simple licensing scheme that, by leveraging standardized technologies, provides a mechanism to validate a license.

I set out to do something that had the following properties:

  • It needs to be flexible: it should allow pretty much any information to be added to the license without affecting the validation mechanism implementation. That is, the mechanism should be fairly agnostic of the data it is validating.
  • Validation must be done completely locally: no outbound connection needed to determine the veracity of the license.
  • Human readable: The content of the license should be able to be understood - at least in general terms - by a naked human eye.
  • Implementation language agnostic: I wasn't looking for a solution that worked specifically for a programming language and the tools it provides. The scheme should be easy to replicate in any language.

Simple Licensing Scheme

Overview

By leveraging JSON, public key cryptography and JOSE - all standardized technologies -, I came up with a scheme that manages to meet the requirements I had and is really straight-forward to implement.

The license - represented by a JSON object - consists of three elements. A unique identifier for the issued license, I decided to use a UUIDv4 in its string representation. The second is the date when the license expires string formatted as described in RFC 3339. Lastly, a customizable component that allows the license issuer add any extra data in JSON format - this could contain the license owner, a set of features the license enables, etc.

The verification is performed using a JSON Web Signature object following the flattened JWS JSON serialization syntax. Thus, it will contain a payload, a set of protected headers and the signature to use for verification purposes; all these as base64 URL encoded strings.

The scheme defines that the contents of the payload must be the license object described above without any alterations. This way we can guarantee that the readable portion is what is actually validated. This can be done because JSON can be semantically checked for equivalence independent of implementation details.

The protected headers need to have the algorithm used to generate the signature. I decided to limit it to RS512 (RSASSA-PKCS-v1.5 using SHA-512) but just for simplifying the implementation. There's nothing inherently restricting the use of other public-key algorithms such as Elliptic-Curve signatures or other hashing algorithms. The JWS algorithms that won't ever be allowed are the "none" or HMAC based ones. The "none" basically defines a non-protected JWS that would defeat the purpose of the scheme and the HMAC based ones requires the secret key to be verified. As we want to be able to do the verification locally on the clients, it would mean shipping the secret key to the clients!

These two elements, the license and the license verification object, are composed into a verifiable license object that contains both of these. This is what needs to be given to customers and what the software verifying the license will use to extract a valid license from it.

There's one more piece missing from this discussion which is the key to do the signing and verification. The private key is needed to generate the verifiable license objects and the public key needs to be embedded in the software itself to do the verification. The format I propose to do so is by embedding it as a JSON Web Key (JWK) object. This will help simplify the implementation if there are libraries in the language that support JOSE. It will also make it easier to determine if the license to validate matches the algorithm supported by the key.

The process looks like this:

Component Interaction Diagram

Generation Process

The generation process will take the private key and the license as inputs. After verifying that the private key supports the RS512 algorithm it will generate a flattened JWS object with the serialized license as its payload signed by the key using the RS512 algorithm.

Finally it will compose the given license and the JWS object into the verifiable license object to give the licensee to use. It will look something like:

{
    "license": {
        "id": "806a2a93-b8de-4b73-b286-c0b354cd9bef"
        "expirationDate": "2024-10-01T00:00:00Z",
        "customData": {
            ...
        }
    },
    "licenseValidation": {
        "payload":"<payload contents>",
        "protected":"<integrity-protected header contents>",
        "signature":"<signature contents>"
    }
}

Verification Process

This is a multi-step process that takes as input the verifiable license and the public key in JWK format. As we'll see, only the last step is actually verifying the signature is valid.

The first set of steps are regarding to the key itself. It needs to be verified that the format is valid and that it supports one of the allowed algorithms (RS512 for now). If any of this fails there's no reason continue.

The next phase is to check that the verifiable license object is structurally correct as well as its two components.

From this point onward all the checks are semantic. The first thing to do is make sure that the license validation's payload matches the license object. This entails decoding the payload, checking that it is valid JSON, that the JSON conforms to the license structure and that the contents are the same as the one in the top level license object.

There's one more thing that needs to be checked before proceeding with the signature verification. This is to determine whether the JWS has been signed using an algorithm we expect. This can be extracted from the alg field in the protected headers. It not only needs to match an allowed algorithm but the one that the key we passed in supports - for now just RS512.

If all these checks are valid, we can then proceed to check whether the JWS is valid through signature verification with the given key.

In case the result is correct the license extracted from the payload is returned as the valid license. We want to use what was extracted from the payload and not the license field. The reason behind this is that even though we checked for equality between them only the payload is actually protected through cryptography against forgery.

Pros, Cons and Next Steps

The simplicity of the scheme and the fact that it uses all standard technologies are its strengths. Implementing either part in any programming language should be fairly straight-forward as most languages will have either native support or libraries for the technologies required here: JSON, JOSE and public key cryptography. Even in the case where full JOSE support is missing extracting the components necessary from the specification should be simple enough to do without implementing the full spec.

The main con I observe is the irrevocability of the licenses issued. This can be a problem if an attacker is able to acquire the private key; they will be able to issue licenses that will be valid for all versions that validate using the public-key derived from the compromised key. A key rotation will either require reissuing existing licenses that need to be replace while upgrading or shipping a whitelist of license ids that can still be verified using the old key.

I wouldn't qualify this as a con in and of itself, but rather as a limitation of the scheme. It is the fact that it cannot prevent piracy. Anyone obtaining a copy of a valid license can replicate it and distribute it and the software will validate them. This was not an objective I set out to address, my objective was to make a scheme for commercial enforcement with good-faith actors. I don't think it is feasible to address the piracy issue without requiring registration or embedding some hardware identifying component to the license.

Apart from these, this scheme hasn't been reviewed by any infosec experts and I'm not a specialist in the field; I would love to hear back if I have made any blatant mistakes.

As for next steps, I implemented a very rudimentary generator in Python and a validator in Rust which need some touch up before publishing them.

Comments powered by Talkyard.