Skip to content

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 raw format, not DER.
  • All symmetric cryptography is aes-256-gcm.
  • All password that need to be verified are stored with Scrypt and params: 16384, 8, 1, 32.
  • Where HKDF is used, we populate the info field with the task at hand.
  • All ByteArrays are 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 a SafetyKey.

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_key is the author's public key. All future commands will need to be signed by this key.
  • The bots vector identifies which bots are allowed to participate in this contract.
  • The terms string 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 not SignedEvent messages 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 an UnlockCommand and 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 StartedUpdate from the Contract flatbuffers. If the coordinator has never heard from this lock before it will create a new LockSession. 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 CoordinatorConfiguration message potentially updating the lock's local configuration. Can also be used to set/toggle experimental features in the firmware.
  • Sends a GetLatestFirmwareRequest that 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.