Last updated

Request Templates

Blocky AS uses TEEs to transform a set of request templates into API requests and return a set of attestations over API responses. The template allows you encrypt sensitive data that can be decrypted by the TEE only. In other words, even Blocky cannot see that data.

To demonstrate that capability, let's fetch the current weather of Blocky's Headquarters in Bozeman, Montana, or whatever location you prefer. You will need an API key from the weather API. You can get a free API key from the Tomorrow API.

Next, create a collection of request templates as requests.json:

[
  {
    "environment": {
      "location": "Bozeman",
      "apikey": "super-secret"
    },
    "template": {
      "method": "GET",
      "url": "https://api.tomorrow.io/v4/weather/realtime?location={{location}}&apikey={{apikey}}&units=imperial",
      "body": {},
      "header": {
        "Content-Type": [
          "application/json"
        ]
      }
    }
  }
]

In each request template, just one in the above example, you specify:

  • environment (optional)
    • A collection of variables to use in the template. The variables can contain secrets for accessing the API, or other information you do not want to leak to Blocky, or to the third parties that verify Blocky AS attestations. These variables will be encrypted by bky-as and decrypted by the server.
  • template - (required)
    • The template the Blocky AS server will use to form an API request, including:
      • method - (required) The HTTP method for the request
      • url - (required) The URL template for the request
      • body - (optional) The body template of the request
      • header - (optional) The header template of the request
    • Note that the url, body, and header fields can all use the variables from the environment collection. Blocky AS servers use the Mustache syntax in the template to replace variable names with values from the environment.

Finally, update the environment.apikey with your API key from tomorrow.io.

Attestations

To obtain Blocky AS attestations using the request templates in requests.json, run:

cat requests.json | ./bky-as attest-api-call > out.json

The resulting out.json contains the attestations for each templated request. Diving deeper into the structure of out.json with jq,

cat out.json | jq keys

shows that out.json contains

[
  "api_calls",
  "enclave_attested_application_public_key"
]

Let's go deeper into the enclave_attested_application_public_key and run:

cat out.json | jq '.enclave_attested_application_public_key'

which produces shows an enclave attestation data structure:

{
 "Platform": "nitro",
 "PlAttests": [
   "hEShATgioFkUq6...",
   "hEShATgioFkRHa...",
 ]
}
  • Platform contains type of the platform on which the Blocky AS server runs. If you are running the demo using local-server in config.toml, the "Platform": "plain" indicates that the server is not running in a TEE.
  • PlAttests contains platform attestations created by the TEE hardware module. The platform attestations collectively attest a serialized application public key that is specific to an instance of a Blocky AS server.

Next, let's inspect the api_calls. In general, the i-th entry of api_calls in out.json corresponds to the i-th request in requests.json. Here, we have only one request, so we have only one response. Let's explore the response.

cat out.json | jq '.api_calls[0] | keys'

shows that it contains

[
  "claims",
  "transitive_attestation"
]

But what are these things? Let's discuss them. Recall that each instance of Blocky AS has a unique application public key with an associated private key. We use that private key to sign data, which we call claims. We call the data structure containing the claims, the signature, and other information needed for checking the signature a transitive attestation. (In fact, transitive attestations follow the JWT standard.)

In the context of Blocky AS making an API call, we create a transitive attestation where the claims are the fields needed for interpreting and proving the authenticity of an API call. These fields include the request template, the upstream API server's response, and a certificate chain among others.

Now, let's see how we use this transitive attestation in practice. When we call bky-as attest-api-call, it performs four steps. First, it verifies the enclave_attested_application_public_key. Second, it extracts the application public key. Third, it uses the public key to verify the transitive_attestation. Fourth, it extracts the request and response data from the transitive_attestation as claims.

These claims contain the request template in request, the time at which the request was issued at, iat, and of course, the response including the HTTP status_code, response body as well as other data.

We can see the response body with the following command:

cat out.json | jq -r '.api_calls[0].claims.response.body' | base64 -d | jq .

Now, let's say that you want to archive Blocky AS attestations for later, or send them to a third party for verification. You can store the enclave_attested_application_public_key and transitive_attestations individually in an archival system of your choice, or package them up in a single file to-verify.json with:

jq '{
    enclave_attested_application_public_key: .enclave_attested_application_public_key,
    transitive_attested_api_calls: [ .api_calls[] | .transitive_attestation ]
    }' out.json > to-verify.json

You, or a third party, can at verify this collection with bky-as by running:

cat to-verify.json | ./bky-as verify > verified.json

Again you can use jq to explore the results, but you will find that verified.json is identical to out.json. You can double-check that by running:

diff out.json verified.json

which will show no output, indicating that contents of the files are the same.

Protocol

For a final deep dive, let's explore the protocol used in Blocky AS.

You can interact with Blocky AS in the same way in which you would any other API server. You can use CLI tools like curl, platforms like postman, or language specific libraries like requests in python. We chose that design so that anybody can integrate Blocky AS into their system in a way that works best for them. However, implementing the protocol requires some developer effort.

We have a few Go libraries that we use for testing. We used those libraries to build a tool called bky-as that does most of the work for you. And, since it is written in Go, we can build static binaries that you can use on your system, use in a CI/CD pipeline, or in production.

The figure below shows the details of the interactions between a user, bky-as, the Blocky AS server running on a TEE, and an upstream API server.

userbky-asBlocky AS serverupstream API serverloopGenerate application_public_key/application_private_key keypair1Generate encryption_public_key/encryption_private_key keypair2Create requests.json3cat requests.json | bky-as attest-api-call4GET /enclave-attested-application-public-key5enclave_attested_application_public_key =   Attest(    {application_public_key, encalve_measurement},     nitro_private_key   )6enclave_attested_application_public_key7application_public_key, enclave_measurement =   Verify(enclave_attested_application_public_key, nitro_certificate)8GET /transitive-attested-encryption-key9transitive_attested_encryption_key =   Sign(encryption_public_key, application_private_key)10transitive_attested_encryption_key11encryption_public_key =   Check(transitive_attested_encryption_key, application_public_key)12protected_request13POST /transitive-attested-api-call   body: protected_request14user_id =   Decrypt(encrypted_user_id, encryption_private_key) user_password =   Decrypt(encrypted_user_password, encryption_private_key)15GET /data?user=$user_id   host: https://upstream-server.com   header: {"password", $user_password}16response17iat = kvm-clock18request = protected_request.template19transitive_attested_api_call =   Sign([{request, iat, response}], application_private_key)20transitive_attested_api_call21request, iat, response =   Check(transitive_attested_api_call, application_public_key)22[{request, iat, response, transitive_attested_api_call}]23userbky-asBlocky AS serverupstream API server
  1. The Blocky AS server generates the application_public_key/application_private_key on startup. The server will use the application_private_key to sign transitive attestations.

  2. Also on startup, the Blocky AS server generates the encryption_public_key/encryption_private_key keypair. The server will use encryption_private_key to decrypt elements of the encrypted_environment.

  3. The user creates requests.json containing an array of requests:

    [
      {
        "environment": {
          "user-id": "secret_user_id",
          "user-password": "secret_password"
        },
        "template": {
          "method": "GET",
          "url": "https://upstream-server.com/data?user={{user-id}}",
          "header": {
            "password": "{{user-password}}"
          }
        }
      }
    ]

    where environment specifies user secrets and template specifies the template for the HTTP request. The server will render the request from the template and the decrypted environment.

  4. The user pipes requests.json to bky-as attest-api-call to obtain a transitive attested API call.

  5. bky-as issues a GET request to the Blocky AS server /enclave-attested-application-public-key endpoint.

  6. The Blocky AS server creates an enclave_attested_application_public_key, which contains the application_public_key and the code_measurement of the Blocky AS code running on the enclave and is signed by the AWS Nitro private key.

  7. The Blocky AS server replies with the enclave_attested_application_public_key to the bky-as CLI.

  8. bky-as verifies the enclave_attested_application_public_key using the AWS Nitro Enclave well-known nitro_certificate and extracts the applicaiton_public_key and the code_measurement. Moreover, it verifies that the enclave_attested_application_public_key.code_measurement is on the list of acceptable_measurements in config.toml.

  9. To obtain the encryption_public_key, bky-as issues a GET request to the Blocky AS server /transitive-attested-encryption-key endpoint.

  10. The Blocky AS server creates a transitive_attested_encryption_key, which is a JWT containing the encryption_public_key signed with the application_private_key.

  11. The Blocky AS server replies with the transitive_attested_encryption_key to the bky-as CLI.

  12. bky-as checks the signature of transitive_attested_encryption_key using the application_public_key to extract the encryption_public_key.

  13. For each request, bky-as encrypts elements of its environment using the encryption_public_key

    encrypted_user_id = Encrypt("secret_user_id", encryption_public_key)
    encrypted_user_password = Encrypt("secret_user_password", encryption_public_key)

    to create an encrypted environment (encr_env) and a protected_request:

    {
      "encr_env": {
      "user-id": encrypted_user_id,
      "user-password": encrypted_user_password
      },
      "template": {
      "method": "GET",
      "url": "https://upstream-server.com/data?user={{user-id}}",
      "header": {
        "password": "{{user-password}}"
      }
    }
  14. To obtain a transitive_attested_api_call, bky-as issues a POST request to the Blocky AS server /transitive-attested-api-call endpoint with the protected_request in the body.

  15. The Blocky AS server uses the encryption_private_key to decrypt the elements of the encr_env in the protected_request

    user_id = Decrypt(encrypted_user_id, encryption_private_key)
    user_password = Decrypt(encrypted_user_password, encryption_private_key)
  16. The Blocky AS server renders an HTTP request from the template and decrypted elements of the encr_env and makes the HTTP request to an upstream API server.

  17. The Blocky AS server collects the upstream API server reply, and prepares a response claim, which contains the status code, headers, body, and the upstream server certificate chain.

  18. The Blocky AS server obtains a timestamp from the enclave's NTP-synchronized kvm-clock to prepare the "Issued At" iat claim.

  19. The Blocky AS server prepares a request claim, which is set to the value of protected_request.template.

  20. The Blocky AS server creates a transitive_attested_api_call, which is a JWT over the request, iat, and response claims signed with the application_private_key.

  21. The Blocky AS server returns the transitive_attested_api_call.

  22. bky-as checks each transitive_attested_api_call using the application_public_key and extracts the request, iat, and response claims corresponding to each protected_request.

  23. bky-as returns to the user the array of request, iat, and response claims corresponding to each transitive_attested_api_call.