© 2023 Blockchain Commons
Authors: Wolf McNally, Christopher Allen
Date: December 8, 2023
Revised: December 8, 2023
Gordian Sealed Transaction Protocol (GSTP) supports secure communication between two or more parties, and is built on the Gordian Envelope specification.
GSTP is specifically designed to be transport-agnostic: usable over HTTP, raw TCP/IP, airgapped protocols using QR codes, NFC cards, etc. As such it specifies its own secure encryption and signing protocol.
It is also designed to be usable with various protocol architectures, including client-server and peer-to-peer.
GSTP builds on the Envelope Expression format defined in BCR-2023-012. As such it uses a function-calling or remote procedure call (RPC) syntax, and gains the flexibility of the Envelope Expression syntax.
GSTP also builds on the encryption and signing techniques defined in BCR-2023-013.
This document defines the following UR types along with their corresponding CBOR tags:
UR type | CBOR Tag |
---|---|
ur:request | #6.40004 |
ur:response | #6.40005 |
These tags have been registered in the IANA Registry of CBOR Tags.
This document uses the following Known Values:
Codepoint | Canonical Name | Type | Description | URI |
---|---|---|---|---|
17 | Unknown | value | Placeholder for an unknown value. | https://en.wikipedia.org/wiki/Blank_node |
100 | body | property | Property declaring that the object is the body (parameters of) a distributed request identified by the subject. | |
101 | result | property | Property declaring that the object is the success result of the request identified by the subject. | |
102 | error | property | Property declaring that the object is the failure result of the request identified by the subject. | |
103 | OK | value | Instance providing the success result of a request that has no other return value. | |
104 | Processing | value | Instance providing the "in processing" result of a request. |
GSTP messages are Gordian Envelopes that are encrypted to a receiver. Beyond that, they do not expose whether they are a request or a response, or anything else about their contents. The receiver must decrypt the message to learn more:
ENCRYPTED [
'hasRecipient': SealedMessage
]
The decrypted envelope is a wrapped envelope with a signature:
{
*omitted*
} [
'signed': Signature
]
The receiver MUST verify the signature before acting on the contents of the wrapped envelope. Multi-signature schemes may also be implemented if necessary.
In the case of client-server communications, the client already has the server's public key (it had to use it to encrypt the message to the server), and uses that to verify the signature.
On the other side, the server must learn the client's public key somehow so it can encrypt its responses back to the sender:
- In the case of Trust on First Use (TOFU), the client's request can include the the sender's public key, and the server can store that key for future use.
- Other more traditional account creation techniques can also be used.
- In the case of peer-to-peer communications, the peers must exchange public keys before communicating. They can do this by first agreeing on a symmetric key using a Diffie-Hellman key exchange, and then using that key to encrypt their public keys to each other. Alternatively they can use a Public Key Infrastructure (PKI) to exchange their public keys.
Once unwrapped, the subject of the inner envelope is either a request (CBOR tag #6.40004) or a response (CBOR tag #6.40005). The tagged value is an ARID (see BCR-2022-002) that uniquely identifies a request and matches it to its response.
request(ARID(abcdef01)) [
*omitted*
]
response(ARID(abcdef01)) [
*omitted*
]
Any party to a request/response may use the ARID as a way of discarding duplicates, avoiding replays, or as input to a cryptographic algorithm. See the ARID specification for more information.
A request will have an assertion with its predicate being the known value 'body'
, and the object being an Envelope Expression as defined in BCR-2023-012. For example, a request to add two numbers might look like this:
request(ARID(abcdef01)) [
'body': «add» [
❰lhs❱: 2
❰rhs❱: 3
]
]
The body expression is:
«add» [
❰lhs❱: 2
❰rhs❱: 3
]
A successful response will have an assertion with its predicate being the known value 'result'
, and the object being the result of the evaluation of the expression in the request. Following the same example above, a successful response might look like this:
response(ARID(abcdef01)) [
'result': 5
]
An unsuccessful response will have an assertion with its predicate being the known value 'error'
, and the object being a string containing a human-readable error message. The error message MAY have additional assertions providing diagnostic information.
response(ARID(abcdef01)) [
'error': "Internal server error" [
"errorCode": 500
]
]
In the event that a request is malformed, a generic error may be returned, its response
tag will contain the known value 'unknown'
, and the response will have an assertion with its predicate being the known value 'error'
, and the object being a string containing a human-readable message. The error message MAY have additional assertions providing diagnostic information.
This message MAY be signed by the server, but generally will not be encrypted as the sender's public key is not known.
response('Unknown') [
'error': "Decryption failure"
]
Here is a more realistic example of a signed request from the Gordian Depository API. Notice that one of the parameters ❰"key"❱
to the «"storeShare"»
function is a public key. This is the public key of the sender, and is used by the server to verify the signature, set up an account if necessary using Trust on First Use (TOFU) and encrypt its response back to the client.
{
request(ARID(8712dfac)) [
'body': «"storeShare"» [
❰"data"❱: Bytes(327)
❰"key"❱: PublicKeyBase
]
]
} [
'signed': Signature
]