Architecture Notes: How It All Works
This doc is a continuous work in progress.
This serves as the rough architecture guide for how the whole thing works if you're curious.
There's essentially three components: the lock, the website Tartarus, and the coordinator(API).
The "center" of the system is the Coordinator. The locks need somewhere to send and receive updates, process commands etc. When they startup they check their local configuration and go through these steps:
Cryptography
Almost every entity in the system has a public and private keypair.
Going into the details of a asymmetric cryptography is way beyond the scope of this doc. That being said:
- All keys use curve
secp256r1. - All public keys are represented in compressed form.
- All hashes are calculated with
sha256. - All signatures are to be in
rawformat, notDER. - All symmetric cryptography is
aes-256-gcm. - All password that need to be verified are stored with
Scryptand params: 16384, 8, 1, 32. - Where
HKDFis used, we populate the info field with the task at hand. - All
ByteArraysare encoded with Base64 unless they are meant to be in a URL in which case we use URL-safe Base64. - Online keys (
SafetyKey) are stored as PKCS8 since they aren't transmitted.
SignedMessage
All the commands and contracts are implemented as SignedMessage.
A SignedMessage is a flatbuffer with a signed payload. So when you make
a contract you generate the entire Contract flatbuffer, calculate a hash of the
whole table, sign that, and then attach the signature to the outer-wrapper of the
SignedMessage.
Every entity that receives or forwards a SignedMessage will check the signature
and reject the message if it fails to pass.
Commands, Counters & Serial Numbers
All the commands after a Contract is accepted are to include a counter value. In order for a SignedMessage to be valid
the counter value in the message has to be greater than or equal to the current counter in the lock.
This is designed to prevent message replay (Someone trying to use an unlock code twice).
All commands also have to include both their own unique serial number and the serial number of the contract they are issued under. If the contract serial number in a message doesn't match the current contract it will be rejected. This prevents one person's messages from being used on a different lock.
Hashing
The hash of a flatbuffer has to also include the vtable of the message. Because the format essentially uses "pointers" in the format, all of those pointers also have to be covered- and those are the vtable. You'll see code like this almost anywhere a signature is needed:
builder.Finish(contract_offset)
bytes = builder.Output()
start = bytes[0]
contract_start = bytes[start]
vtable_start = start - contract_start
hash = hashlib.sha256(bytes[vtable_start:]).digest()
signature = signer.sign(hash)contract_start = bytes[start]
vtable_start = start - contract_start
hash = hashlib.sha256(bytes[vtable_start:]).digest()
signature = signer.sign(hash)
Signature Format
Signatures need to be raw r + s values, not DER encoded.
This was an early design decision to minimize the size of SignedMessage tables
so they would help fit inside scannable QR code blocks. The downside is most
verification libraries like BouncyCastle want signatures in DER format so you'll need to convert them
around.
SignedEvent
The lock will generate signed events for both the coordinator and any bots
that are listed on a contract. They are similar in structure to SignedMessage.
The events in the contract lifecycle are:
Accept- This event is emitted after the contract has been confirmed in the hardware.Lock- The lock was locked via command from the coordinator.Unlock- The lock was unlocked via command.LocalLock- The lock button on the face of the lock was used. Only emits if temporary unlocking is allowed.LocalUnlock- The unlock button on the face of the lock was used. Only emits if temporary unlocking is allowed.Release- The contract was released and confirmed in hardware.Abort- The contract was aborted via aSafetyKey.
Contract structure
Contracts are pretty straight forward:
table Contract {
serial_number: ushort;
public_key: [ubyte];
bots: [Bot];
terms: string;
// If true, subject can freely cycle the lock.
is_temporary_unlock_allowed: bool;
}
Notes:
- The serial number is randomly generated by the author.
- The
public_keyis the author's public key. All future commands will need to be signed by this key. - The
botsvector identifies which bots are allowed to participate in this contract. - The
termsstring is just an unstructured string. It can include whatever you want. None of the core bits of the system will try and interpret this field. - Lastly, should temporary unlocks be allowed. If this is true the buttons on the lock face will work, otherwise they won't.
In the original design it was
Bots & Permissions
Only bots listed on the contract body will be able to receive events. Events come directly from the lock, not the coordinator.
Additionally, bots have four possible permission settings:
receive_events- Whether or notSignedEventmessages will be generated at their appropriate point in the lifecycle. If this is false no events will be sent to the bot.can_unlock- If this is true the bot can sign anUnlockCommandand it will be accepted.can_lock- Same as unlock but for lock.can_release- If this is true the bot can end the contract.
Lock Startup
What happens on lock startup.
- Send
StartedUpdatefrom the Contract flatbuffers. If the coordinator has never heard from this lock before it will create a newLockSession. It also includes the public key material from the lock, whether it started with a contract, and what the current lock state is. - Receive a
CoordinatorConfigurationmessage potentially updating the lock's local configuration. Can also be used to set/toggle experimental features in the firmware. - Sends a
GetLatestFirmwareRequestthat will us what the latest available version via OTA ("Over the air") is. - Receive a
FirmwareChallengeRequest. If you have an "official" build of the firmware it will have a key injected that can prove it's an official firmware. This is pretty much just ceremonial.
From there it will publish a PeriodicUpdate once a minute indefinitely.
Terms
LockSession
A LockSession uniquely defines a particular Tartarus lock.
LockUserSession
This is an owner of a Tartarus lock logged into the website.
You can simultaneously have a LockUserSession and an AuthorSession.
AuthorSession
This is someone logged into the website with a keypair image they generated and is authoring/managing contracts.
You can simultaneously have a LockUserSession and an AuthorSession.
AdminSession
Admin users that have special, magical powers.
SafetyKey
A set of reserved online keys that can sign Abort messages.
Only AdminSession users can use SafetyKeys.