From 540a13aa502a09663d949edcc41bf146d5a362fa Mon Sep 17 00:00:00 2001 From: Tim Hawbaker Date: Wed, 6 Mar 2024 11:27:31 -0800 Subject: [PATCH] update api design docs --- docs/api-design/intro.md | 60 ++++++++-------------------------- docs/api-design/queries.md | 11 +++++++ docs/api-design/stamps.md | 42 ++++++++++++++++++++++++ docs/api-design/submissions.md | 26 +++++++++++++++ 4 files changed, 92 insertions(+), 47 deletions(-) create mode 100644 docs/api-design/queries.md create mode 100644 docs/api-design/stamps.md create mode 100644 docs/api-design/submissions.md diff --git a/docs/api-design/intro.md b/docs/api-design/intro.md index a7f22de..45f0b9d 100644 --- a/docs/api-design/intro.md +++ b/docs/api-design/intro.md @@ -6,58 +6,24 @@ slug: /api-introduction # Introduction -## REST/HTTP +## RPC/HTTP -Turnkey's Public API is a REST API. We chose REST-over-HTTP for convenience and ease-of-use: most of our customers should be able to integrate with Turnkey's public API without a major re-architecture of their existing systems. +Turnkey's API is a remote procedure call (RPC) API. We chose RPC-over-HTTP for convenience and ease-of-use. Most of our users should be able to integrate with our API without a major re-architecture of their existing systems. -Many client libraries are available to make requests to a REST/HTTP API, across many languages. Turnkey will provide SDKs for the most popular programming languages. For other languages, a REST/HTTP API ensures there is an easy integration path available via raw http clients. +Many client libraries are available to make requests to a RPC/HTTP API, across many languages. Turnkey will provide SDKs for the most popular programming languages. For other languages, a RPC/HTTP API ensures there is an easy integration path available via raw http clients. ## POST-only -If you look at our [API reference](./api) you'll notice that Turnkey is not strictly following the REST convention: +If you look at the [API reference](./api) you'll notice that all API calls to Turnkey are HTTP POST requests. Requests contain a POST body and a header with a digital signature over the POST body. We call this digitial signature a [Stamp](/api-design/stamps). -We use POST exclusively -We do not have full resource paths in the URL (for example, "/submit/sign") -This decision comes from a security guarantee we're making to our customers: requests must be signed by end-users, and verified by Turnkey's secure enclaves. Unlike other companies, we do not modify your request while in transit. This ensures cryptographic integrity, end-to-end: what is signed with your API key is what is seen and checked by our secure enclaves. +Requests must be stamped by registered user credentials and verified by Turnkey's secure enclaves before they are processed. This ensures cryptographic integrity end-to-end which eliminates the ability for any party to modify a user's request. -So why is a POST-only API a better fit? That's because of signatures. With GET requests, the URL and URL params need to be signed. With POST requests, the body and URL need to be signed. - -By switching to a POST-only API and moving all critical request parameters to the body, we simplify the definition of what has to be signed with each request: it's simply the POST body. Read the next section for more details on API signatures. - -## API signatures and stamp headers - -Every request made to Turnkey must include a signature inside a stamp header. Our secure enclave applications use this signature to verify the integrity and authenticity of the request. To sign your requests, follow these steps: - -### Initial steps for all requests -1. SHA256 hash a JSON-encoded string of your request's body. -2. Sign the hash with either your API key or WebAuthn credential. - -### If using an API key -1. Create a JSON-encoded string with the public key, signature, and signature scheme. -2. Base64url encode the string. -3. Add the string to your request as a `X-Stamp` header. - -### If using a WebAuthn credential -1. Create a JSON-encoded string with the authenticator data, client data, credential ID, and signature. -2. Add the string to your request as a `X-Stamp-Webauthn` header - -In practice you should not have to worry about this step: our [JS SDK](https://github.com/tkhq/sdk) and [CLI](https://github.com/tkhq/tkcli) will take care of it for you. However, if you write an independent client, you will need to implement this yourself. - -For reference, check out our implementations: - -- [JS SDK's API Key stamper](https://github.com/tkhq/sdk/blob/main/packages/api-key-stamper) -- [JS SDK's WebAuthn stamper](https://github.com/tkhq/sdk/blob/main/packages/webauthn-stamper) -- [CLI](https://github.com/tkhq/tkcli/blob/main/src/cmd/turnkey/pkg/request.go) - -## Queries and Submissions -Our API endpoints are divided into 2 broad categories: queries and submissions. - -- Queries are read requests (for example: "get users") -- Submissions are requests to execute an activity on Turnkey (for example: "create policy") - -We've separated these 2 categories because all submissions return an Activity and can be subject to consensus if your organization has consensus-based policies. It's best to think of a call to "/submit/..." as a request to start an asynchronous process. The response to a submit request will contain new activity, with an ID you can use to poll until activity completion if needed. - -Our API is idempotent: for each activity submission, the whole request body is hashed into a fingerprint. If you resubmit the same payload, you'll get the same activity instead of a new one. This helps with idempotency, and helps with security too. Because each activity request must contain a recent timestamp (in the `timestampMs` field), this avoids replay attacks: a third party can't generate new activities by re-POSTing previous activity requests. - -Queries return data synchronously, do not generate activities, and are not subject to consensus. +### Queries and Submissions +Turnkey's API is divided into 2 broad categories: queries and submissions. +- Queries are read requests (e.g. `get_users`, `list_users`) +- Submissions are requests to execute a workload (e.g. `create_policy`, `sign_transaction`, `delete_user`) +## Dive Deeper +- Creating your first [Stamp](/api-design/stamps) +- Fetching data with [Queries](/api-design/queries) +- Executing workloads with [Submissions](/api-design/submissions) diff --git a/docs/api-design/queries.md b/docs/api-design/queries.md new file mode 100644 index 0000000..6f9e4b4 --- /dev/null +++ b/docs/api-design/queries.md @@ -0,0 +1,11 @@ +--- +sidebar_position: 3 +description: Fetching data from Turnkey +slug: /api-design/queries +--- + +# Queries + +Queries are read requests to Turnkey's API. Query URL paths are prefixed with `/public/v1/query`. Queries are not subject to enforcement of the policy engine. This means that there are currently no read permissions within an organization. All users within an organization can read any data within the organization. + +Additionally, parent organizations have the ability to query data for all of their child organizations. diff --git a/docs/api-design/stamps.md b/docs/api-design/stamps.md new file mode 100644 index 0000000..829ff61 --- /dev/null +++ b/docs/api-design/stamps.md @@ -0,0 +1,42 @@ +--- +sidebar_position: 2 +description: Creating your first signed request +slug: /api-design/stamps +--- + +# Stamps + +Every request made to Turnkey must include a signature inside a stamp header. Our secure enclave applications use this signature to verify the integrity and authenticity of the request. + +### API Keys +To create a valid, API key stamped request follow these steps: +1. SHA256 hash the JSON-encoded POST body +2. Sign the hash with your API key to produce a `signature` +3. Create a JSON-encoded stamp: + - `publicKey`: the public key of API key + - `signature`: the signature produced by the API key + - `scheme`: `SIGNATURE_SCHEME_TK_API_P256` +4. Base64URL encode the stamp +5. Add the encoded string to your request as a `X-Stamp` header + +### Webauthn +To create a valid, Webauthn authenticator stamped request follow these steps: +1. SHA256 hash the JSON-encoded POST body +2. Sign the hash with WebAuthn credential. +3. Create a JSON-encoded stamp: + - `credentialId`: the id of the webauthn authenticator + - `authenticatorData`: the authenticator data produced by Webauthn assertion + - `clientDataJson`: the client data produced by the Webauthn assertion + - `signature`: the signature produced by the Webauthn assertion +4. Base64URL encode the stamp +5. Add the encoded string to your request as a `X-Stamp-Webauthn` header + +### Stampers + +In practice, you should not have to worry about this step. Our [JS SDK](https://github.com/tkhq/sdk) and [CLI](https://github.com/tkhq/tkcli) take care of stamping for you. However, if you choose to use an independent client, you will need to implement this yourself. For reference, check out our implementations: +- [API Key Stamper](https://github.com/tkhq/sdk/blob/main/packages/api-key-stamper) +- [WebAuthn Stamper](https://github.com/tkhq/sdk/blob/main/packages/webauthn-stamper) +- [React Native Stamper](https://github.com/tkhq/sdk/tree/main/packages/react-native-passkey-stamper) +- [iFrame Stamper](https://github.com/tkhq/sdk/tree/main/packages/iframe-stamper) +- [CLI](https://github.com/tkhq/tkcli/blob/main/src/cmd/turnkey/pkg/request.go) + diff --git a/docs/api-design/submissions.md b/docs/api-design/submissions.md new file mode 100644 index 0000000..98fe3b1 --- /dev/null +++ b/docs/api-design/submissions.md @@ -0,0 +1,26 @@ +--- +sidebar_position: 4 +description: Secure execution with Turnkey +slug: /api-design/submissions +--- + +# Submissions + +Submissions are requests to securely execute a workload. Submission URL paths are prefixed with `/public/v1/submit`. Submissions requests, if valid, produce an `Activity`. + +### Activites + +Activities typically create, modify, or utilize a resource within Turnkey and are subject to consensus or condition enforcement via the policy engine. Activities are executed optimistically synchronous. This means that if we can process the request synchronously, we will. Otherwise, we'll fallback to asynchronous processing. Your services or applications should account for this by checking the response for the activity state: + +- If `activity.state == ACTIVITY_STATUS_COMPLETED`, `activity.result` field will be populated. +- If `activity.state == ACTIVITY_STATUS_FAILED`, `activity.failure` field will be populated (soon). +- If `activity.state == ACTIVITY_STATUS_CONSENSUS_NEEDED`, additional signatures are required to process the request. +- If `activity.state == ACTIVITY_STATUS_PENDING`, the request is processing asynchronously. + +You can get activity status updates by: + - Re-submitting the request. See the notes on idempotency below. + - Polling `get_activity` with the `activity.id` + +### Idempotency + +The submission API is idempotent. For each request, the POST body is hashed into a fingerprint. Any two requests with the same fingerprint are considered the same request. If you resubmit the request, you'll get the same activity. If you want a new activity, you should modify the request timestamp `timestampMs` to produce a new fingerprint.