What this is
Goals
Software registration and licensing control with Tightener
As part of the Tightener automation glue (TightenerDocs), I want to design a system that would allow software developers and scripters to handle licensing and tracking of their software.
This system would provide the following features:
- Provide unique GUIDs for users of the software
- Provide unique GUIDs for ‘capabilities’ – think ‘licenses’, or ‘permissions’.
- Uniquely identify individual computers (properly handle clones of computers – e.g. detect cloned virtual machines or image backup restores)
- Retain no identifiable user data in the registry
The system is based on a central registry with a https
-based API which would run on the domain tgrg.net
.
This registry contains the absolute minimum of data to make the system work.
It contains no identifying information for any of the users or developers, nor does it contain any information about the capabilities being issued.
The idea is that if the registry were ever to be breached by hackers, there would be no useful data to be found in the registry that could be abused.
GUIDS and hashes
The system uses both randomly generated GUIDs and 128-bit hashes that are GUID-like.
I’ll use the term GUID in both cases – i.e. the hashes are calculated GUIDs and the rest are randomly generated GUIDs.
Software developer: generating a capability file
A software developer wants to grant a license to some other user.
Both the developer and the user are registered with the Tightener registry, and have an identifier – a calculated entityGUID
.
An entity might register multiple times, and have multiple entityGUID
.
The developer has some ‘out-of-band’ communication with their customer – they might be selling software via a website, or might be in email contact, or something else…
The developer handles operations like payments, issuing,… any way they want.
At some point in time, the developer and the user will have reached an agreement for the developer (the issuer) to grant the user (the grantee) some capability (e.g. a license to the software).
The developer will then create a data structure which encodes whatever data they need to pass to their software when it is running on the user’s computer.
E.g. they might have
- settings to have certain features enabled/disabled
- a maximum count of transactions allowed
- a registered name that needs to be watermarked into the output
- some segment piece of code that the software needs to run
- encrypted data, which only the developer’s own software can decrypt
- …
Tightener does not care whatever this data is.
The developer’s data structure is added to a capability wrapper file.
The developer then registers a calculated GUID-like hash of the file with the Tightener registry (called a capabilityGUID
).
Finally, they also send a copy of the file to the user.
Registering the capability wrapper file with the registry will only store a hash of that file into the Tightener registry.
The file itself is not stored in the registry, and the registry has no knowledge of the contents of the file. All it knows is the existence of the file, not the contents, nor who created it or who the grantee is.
Capability Slots
In the end, the user ends up with a copy of the capability wrapper file.
The Tightener registry knows about the existence of the file but does not know what its contents are.
Each one of these registered capability wrapper files represents a unique ‘capability slot’ in the registry.
Any such slot can only be assigned to a single computer.
The end-user cannot re-use the capability on multiple computers.
If the software developer allows this, they user can ‘shift’ the capability from one computer to another, but they cannot use it on two computers at the same time.
User registrations
Any entity (user, company, developer,…) needs to be registered with the Tightener registry.
This registration entails no more than a calculated GUID and a public encryption key.
The entities themselves need to hold on to the corresponding private keys.
In other words, the stored data in the registry is very ‘thin’.
No identifiable data is stored in the registry – just a GUID, a public key and a time stamp.
There is more data linked to the entityGUID
(e.g. a name, password, address,…) but none of that data is ever stored in the registry.
The additional user data associated with the entityGUID
is only known to the entity themselves (i.e. they have the data in store and can prove to any third party that this data matches the entityGUID
).
In the registry, the only footprint of an entity is a GUID, a corresponding public encryption key and a time stamp.
Machine registrations
Any computer in the ecosystem needs to be registered in the Tightener registry.
Again, this data is very thin. Each machine is identified in the registry by a long-term-persistent generated registryMachineGUID
.
This generated registryMachineGUID
is used to tie capability slots to individual machines.
A capability slot can be either empty, or contain a hash which combines registryMachineGUID
, capabilityGUID
and granteeGUID
.
The registry is not meant to have any knowledge of the granteeGUID
.
To accomplish that, the protocol uses a value capabilityGranteeHash
= hash(capabilityGUID + granteeGUID)
which is provided to the registry by the remote machine during the capability verification process.
Assigning a machine to a capability slot is done by putting
hash(registryMachineGUID + capabilityGranteeHash)
in a field of a record of the capabilitySlot
database table on the registry.
This allows the registry to verify whether a particular registryMachineGUID
is assigned to a particular capability slot.
Once registered, machines have three generated GUIDs, each useful within a different context.
There is a long-term stable registryMachineGUID
which is internal to the registry. Only the registry uses and has access to these.
There is a long-term stable localMachineGUID
which is internal to the machine itself. It is not stored in the registry, only the local machine has access to these.
Then there is a short-term, unstable machineLinkGUID
which is stored on the machine itself as well as in the registry. It is used to communicate about the machine between registry and the machine.
At any one time, one machine will only have one unique registryMachineGUID
, one unique localMachineGUID
and one unique machineLinkGUID
.
The machineLinkGUID
will occasionally be replaced to handle various external circumstances.
The machineLinkGUID
helps detect cloning of machines: because they’re clones, cloned machines will start out with a duplicate of the parent machine’s machineLinkGUID
.
This is detected by the registry, after which the cloned machine will automatically be assigned a new, different registryMachineGUID
, a new localMachineGUID
and a new machineLinkGUID
.
Verifying a capability
When some software launches on the user’s computer, it can interact by way of the local Tightener bus with the locally running Tightener Registry Node, which handles the protocols and interaction with the Tightener registry.
The Tightener Registry Node needs access to the local copy of the capability wrapper file and the computer’s local data store.
The Tightener Registry Node interacts with the tgrg.net
Tightener registry, and depending on the result, it will either decrypt the capability data (if the capability is granted) and pass it to the software, or it will return an error (if the capability is not granted).
The software can then do whatever it wants to do based on that result (e.g. switch to demo mode, bail out, add the watermark to the output, limit the number of produced files,…)
Tables in the registry
All the data in the Tightener registry consists of three tables:
Entities (users, companies, developers…):
entityGUID
entityPublicKey
entityEmailHash
registryTimestamp
Capability slots:
capabilityGUID
hash(registryMachineGUID + capabilityGranteeHash)
registryTimestamp
Machine registrations:
registryMachineGUID
machineLinkGUID
registryTimestamp
Denial-of-Service prevention:
entityGUID
issuerCount
granteeCount
registryTimestampStart
registryTimestamp
There is no readable data in the registry – any and all readable data is retained on individual’s computers.
All registryTimestamp
in the database tables are based on the clock of the registry. They record the time of the last update to the table.
The Denial-of-Service prevention table contains temporary data (i.e. records get wiped after a period of non-activity for an entityGUID
, e.g. after a day). It helps detect rogue entityGUID
s that are re-occurring abnormally fast and furious.
Hardware GUID
Tightener can calculate a hardwareGUID
for the computer it is running on.
The only purpose of the hardwareGUID
is to detect a possible configuration change in the computer or its surroundings.
The hardwareGUID
is not used to identify computers and the hardwareGUID
does not need to be stable or globally unique. Collisions are not a concern.
The hardwareGUID
is never sent to the registry.
This calculated GUID is a hash which combines a bunch some contextual factors. Things like
- Ethernet MAC address
- RAM size
- computer name
- operating system and version
- …
These external factors will be chosen to be somewhat stable over time.
Changes should preferably only occur occasionally, over time spans of months, weeks or days. A calculated hardwareGUID
will hopefully remain the same for some period of time.
Registry API (POST, not GET)
The registry API is used by the locally running Tightener Registry Node which handles the https
-based protocol interaction with the central Tightener registry to support all Tightener-enabled nodes (softwares) on the computer.
Any third-party software using the Tightener system will use the Tightener messaging bus to interact with the locally running Tightener Registry Node.
Below some information about how the Tightener Registry Node goes about its business.
The Tightener registry is an entity
The Tightener registry itself is also considered an entity and also has a corresponding entityGUID
and a public key in the registry.
This key is needed by any Tightener Registry Node interacting with the Tightener registry to be able to encrypt and decrypt data being sent to/from the registry.
Encryption
All communication with the registry uses POST interactions over https
.
The certificate of the registry is on the tgrg.net
domain.
This helps the remote computers ascertain they are really talking to the actual registry and not an imposter.
All data that is sent from the Tightener registry into a remote machine is encrypted with the Tightener registry’s private key. The receiving machine decrypts the data using the public key for the Tightener registry.
All data sent from a machine to the Tightener registry is encrypted using the Tightener registry public key.
Only the registry software can then decrypt this data using its private key.
Scenario: fetch the registry’s public key
https://tgrg.net/publickey`
This returns
{
entityGUID: <GUID>,
key: <publickey>
}
where entityGUID
is the entityGUID
assigned to the registry itself.
Scenario: Check computer registration
https://tgrg.net/registermachine
At certain times (e.g. startup, every day,…) Tightener on the remote machine will read local preferences from an encrypted local prefs file.
It retrieves the following data from the local store (if any):
hardwareGUID
machineLinkGUID
registryTimestamp
localMachineGUID
If localMachineGUID
is empty, a random GUID is generated. This GUID will be persisted in local storage.
Tightener then recalculates a hardwareGUID
in order to detect changes in the environment.
If a change is detected, or if a certain amount of time has passed since the last verification it will reach out to the registry and the registration is verified with the registry.
If the hardwareGUID
detected an environmental change, the API call is updatemachine
https://tgrg.net/updatemachine
{
machineLinkGUID: <GUID>,
registryTimestamp: <timestamp>
}
If no environmental change was detected, then the API call is verifymachine
.
https://tgrg.net/verifymachine
{
machineLinkGUID: <GUID>,
registryTimestamp: <timestamp>
}
If the prefs are empty (no machineLinkGUID
nor registryTimestamp
), the API call is newmachine
https://tgrg.net/newmachine
{}
For updatemachine
and verifymachine
, the registry will now look for a record that matches both machineLinkGUID
and registryTimestamp
.
Three scenarios can occur:
- For
newmachine
or if no matching record can be found: create a new record with a random new machineLinkGUID
and a random new registryMachineGUID
and the current registryTimestamp
.
- For the
updatemachine
call: update the matching record with a new random machineLinkGUID
and the current registryTimestamp
.
- For the
verifymachine
call: update the matching record with the current registryTimestamp
.
This allows the registry to detect cloned machines: if two machines share the same machineLinkGUID
, one of them will go through the verification process first, and by doing so, will modify the registryTimestamp
associated with that machineLinkGUID
in the registry database.
The second machine might try to use the same machineLinkGUID
, but it will not be aware that the time stamp in the registry has been updated after the first machine had its interaction, so the second machine will send the wrong time stamp, and will be forcibly issued a new random machineLinkGUID
and a new random registryMachineGUID
by the registry.
Doing this will also sever the link to the cloned machine from any capability slots.
This API call returns a machineLinkGUID
and the registryTimestamp
from the updated database record.
{
machineLinkGUID: <GUID>,
registryTimestamp: <timestamp>,
isNewMachine: <boolean>
}
The returned machineLinkGUID
will often, but not always, be different from the machineLinkGUID
that was sent to the registry at the start of the verification process.
The isNewMachine
field is set to true
when the registry created a new registryMachineGUID
and registered a new record (i.e. when a new machine or a clone was being registered)
If isNewMachine
is true then the local machine will generate a new, random localMachineGUID
.
The received data (machineLinkGUID
and registryTimestamp
), together with the recalculated hardwareGUID
and the localMachineGUID
are then saved into the machine’s local store, to be used again in a future verification round.
To recap:
The machineLinkGUID
is a GUID that uniquely identifies the computer. This GUID is not stable over time.
The machine will be issued a new machineLinkGUID
each time the computer hardwareGUID
changes, or when the registry detects that the machine is a clone of another machine.
The registry tracks this machineLinkGUID
and links it to a stable, long term registryMachineGUID
which in turn is used to link a machine into zero or more capability slots.
The local machine has no need to know its own registryMachineGUID
.
Instead, it locally persists its own localMachineGUID
. This localMachineGUID
is never communicated to the registry.
Scenario: Register a capability
https://tgrg.net/registercapablity
An ‘issuer’ entity (e.g. a software supplier) wants to issue a capability to another entity (e.g. a ‘user’).
In order to do so, this issuer will formulate a capability data structure.
This capability data structure is a string with a JSON-encoded data structure. It can be as simple as just a string or it can be an intricate, complex data structure.
It reflects the capability that will be granted to the user – e.g. a software license, a time-limited software license, a throughput value, watermarking info, some critical segment of code that the software needs to run, some privately encrypted data, decryption keys, any other data…
Tightener does not care about the data or the structure inside the capability data structure.
The capability is encoded as a JSON string, and then encrypted using the issuer’s private key.
This data is then embedded into a capability wrapper file, and a random generated GUID is added as salt.
capabilityWrapper =
{
issuer: <GUID>,
grantee: <GUID>,
capability: <encryptedCapability>,
salt: <GUID>
}
Calculate
capabilityGUID = hash(capabilityWrapper)
The issuer will then registers the capabilityGUID
with the registry.
This registration establishes a new capability slot in the registry.
https://tgrg.net/registercapablity
{
issuer: <GUID>,
grantee: <GUID>,
capabilityGUID: <GUID>
}
which returns
{
success: <boolean>
}
This adds a record to the table with capability slots.
The issuer and grantee GUIDs are not stored in the main registry tables, but a temporary record is made of these GUIDs to try and detect denial of service attacks: if the same issuer or grantee gets registered many times in rapid succession, that issuer or grantee will be blocked from further capability registrations. The Denial of Service detection data is fleeting and gets wiped after some period of non-activity.
The issuer then passes a copy of the capability wrapper file to the grantee (e.g. via email, download from web server,…)
Scenario: Register User or Entity
https://tgrg.net/registeruser
The entity (user, company, developer…) chooses or creates a identifying ‘handle’ .
This handle can be anything: a simple string or a complex JSON structure encoded into a single string.
They also pick a password. Then we calculate:
entityGUID = hash(hash(entityHandle) + hash(password))
The entity also creates a public/private key pair at the same time.
Finally, the entity needs to provide a currently valid email address.
This email address only needs to be valid during the registration process; it is not used afterwards.
Register with registry:
https://tgrg.net/registeruser
{
entityGUID: <GUID>,
publicKey: <Key>,
entityEmail: <email>
}
returns
{
success: <boolean>
}
The registry also sends out an email with a registration confirmation link.
In the database table, entityEmailHash
will stores a GUID-like hash of the entity’s email.
The registry will not retain the email address long term. Instead it stores a hash:
entityEmailHash = hash(entityEmail.toLowerCase())
However, during the pending registration process, for a short time, the entityEmailHash
field will store the actual email address.
This is until the registry receives confirmation of the registration when the user clicks on a confirmation link in an email, or when a certain amount of time lapses.
If the confirmation link is not clicked within the allotted time, the record is deleted and the registration is nullified.
From the moment the registration link is clicked, the entityEmailHash
field is replaced by the hash.
From then on we can still verify the validity of an email address provided by the user, but we cannot retrieve the email address from the server data.
This also offers some protection against denial of service attacks: registering the same email address twice is not possible.
Scenario: Check/Verify User
https://tgrg.net/checkuser
https://tgrg.net/checkuser
{
entityGUID: <GUID>
}
returns
{
publicKey: <Key>
}
Scenario: Request capability
https://tgrg.net/requestcapability
User has a local copy of a capability wrapper file.
The computer has been registered and has a machineLinkGUID
.
Some third party software in the computer needs to verify that the capability has been granted and needs access to the capability data.
The Tightener Registry Node will retrieve the local user’s entityGUID
from the local storage.
There might be more than one entityGUID
if the user has registered more than once with different handles – that’s allowed.
The Tightener Registry Node will verify that one of the stored entityGUID
matches the grantee in the capability wrapper file.
If no matching entityGUID
can be found, the user needs to ‘log in’ to the system by providing the correct handle and password which are used to re-calculate their entityGUID
which should be a match to the grantee GUID.
If the user cannot provide the necessary data (e.g. don’t know the password) to make the entityGUID
match the grantee GUID, then they are not ‘logged in’ and cannot use the capability.
No interaction with the registry is needed to handle a ‘log in’.
If successful, the Tightener Registry Node will then store this entityGUID
in local storage as an additional valid local entityGUID
entry for later use.
The Tightener Registry Node will then calculate the capabilityGUID
by hashing the capability wrapper file.
Then it will make a requestcapability
registry API call.
capabilityGranteeHash
is calculated as hash(capabilityGUID + granteeGUID)
.
The registry will not be able to determine the granteeGUID
from that data. It needs this capabilityGranteeHash
to verify or calculate the hash used to record the occupancy of the capability slot.
https://tgrg.net/requestcapability
{
capabilityGUID: <GUID>,
capabilityGranteeHash: <GUID>,
machineLinkGUID: <GUID>
}
which returns
{
status: <number 0-4>,
registryStatusTimestamp: <timestamp>,
registryTimestamp: <timestamp>
}
registryTimestamp
is the current time at the registry.
Status 0 means: the capability slot is unknown. registryStatusTimestamp
is empty.
Status 1 means: the capability slot is valid, but is assigned to another computer with a different machineRegistryGUID
.
registryStatusTimestamp
tells the computer when the capability slot was last assigned.
Status 2 means: the capability slot is valid and is already registered to the computer making the request. All good.
Status 3 means: the capability slot is valid and is not yet assigned to any computer (i.e. it’s a brand new capability).
In case of status 1 or 3, the Tightener Registry Node will inform the software that the user might need to be queried: “This computer is not assigned to a capability slot, but there is a capability slot we can assign or override. Do you want to ‘grab’ the capability for this computer?”
It is up to the software to handle this and determine whether the user should be allowed to reassign the capability slot – e.g. the software could determine if there is enough difference between the registryTimestamp
and registryStatusTimestamp
If affirmative, a further request can be made which should result in a status 2 when successful.
https://tgrg.net/overridecapability
{
capabilityGUID: <GUID>,
capabilityGranteeHash: <GUID>,
machineLinkGUID: <GUID>
}
which returns
{
status: <number 0-4>,
registryStatusTimestamp: <timestamp>,
registryTimestamp: <timestamp>
}
with the same status values as before.
After receiving a status 2, the Tightener Registry Node will then fetch the issuer’s public key from registry (using https://tgrg.net/checkuser
)
It decrypts the capability from the capability wrapper using this key.
Finally, it passes the decrypted capability to the software that initiated the request.
{
entityGUID: <GUID>,
capabilityGUID: <GUID>,
localMachineGUID: <GUID>,
capability: {... decrypted data for use by software ...}
}
The software now has access to a
- unique user-assigned GUID tied to the user (
entityGUID
)
- unique computer GUID (
localMachineGUID
)
- capability data
The software also has access to the capabilityGUID
which is a unique ID for the capability and the localMachineGUID
which is a unique, stable ID for the machine.
It is up to the software developer how to use this data – e.g. the entityGUID
or localMachineGUID
might be used as a key in the software developer’s databases, but the Tightener registry contains none of that knowledge. Whether to retain or not retain any user info is up to the software developer. Tightener does not limit nor enforce anything in this regard.
Stuff to consider
What happens when the registry goes off-line? Need to think about some duplication or fallback. Might have some contract to allow software developers to run separate mirrors of the registry so the registry is never off-line.
Do we need a system to revoke capability slots? On registering the issuer could also provide a revokeGUID which is stored in the database. Something like:
Issuer picks a revokePassword
and calculates
revokeProofGUID = hash(revokePassword + capabilityGUID)
then calculates
revokeGUID = hash(revokeProofGUID + capabilityGUID)
and sends this to registry along with the capability registration.
The registry would then also store the revokeGUID
in the capabilitySlot
record.
When issuer wants to revoke, they type in their revokePassword
and recalculate the revokeProofGUID
locally. Then they provide the registry with the revokeProofGUID
and the capabilityGUID
.
The registry then verifies that the recalculated revokeGUID
is a match, and then it knows the capability slot can be deleted.
What happens when something goes wrong. For example, the connection might break during a machine verification, causing the machineLinkGUID
to fail to update. Maybe we need an additional confirmation transaction at the end.
GUID contexts
There are four contexts in the protocol: local (on the user’s machine), registry (on the Tightener registry), and issuer (at the software developer’s), and software (inside the software while it runs on the user’s machine)
Most GUIDs are only known and available in a few context.
hardwareGUID
: local
localMachineGUID
: local, software
capabilityGUID
: local, registry, issuer, software
revokeProofGUID
: issuer, registry (not persisted)
revokeGUID
: issuer, registry
entityGUID
: local, registry, issuer (granteeGUID
), software
machineLinkGUID
: local, registry
capabilityGranteeHash
: local, registry (not persisted)
publicKey
: public
entityEmail
: local, registry. Becomes a hash after completion of registration.
registryMachineGUID
: registry