SpaceComputer

Use the KMS

Create keys, encrypt, decrypt, sign, manage data keys, and store tenant-scoped JSON secrets through Orbitport KMS.

This guide covers the Orbitport Key Management Service through the SDK: discovering capabilities, creating keys, encrypting and decrypting data, signing messages, generating data keys, rotating keys, and using the tenant-scoped key store. For the reasoning behind a KMS that runs on attested (and eventually orbital) hardware, see Key Management Beyond the Data Center.

Experimental — do not use in production

The Orbitport KMS is experimental. Interfaces, key formats, and on-the-wire shapes can change without notice, and operational guarantees (durability, availability, key recoverability) are not in place yet.

Do not use it to protect production data, sign production transactions, or hold keys you cannot afford to lose. Use it for prototypes, internal tooling, and exploration only -- and assume any keys you create today may be rotated, invalidated, or deleted before general availability.

What KMS provides

The SDK exposes two schemes:

  • TRANSIT -- symmetric encryption keys and asymmetric signing keys. The available operations depend on the key spec, as shown below.
  • ETHEREUM -- secp256k1 signing keys with an Ethereum address, including support for EIP-191 personal-sign messages.

Key specs and supported operations

Choose the keySpec and keyUsage together when creating a key:

SchemekeySpecRequired keyUsageSupported SDK operations
TRANSITAES_256_GCM96ENCRYPT_DECRYPTencrypt, decrypt, generateDataKey, rotateKey, getKeyMetadata
TRANSITECDSA_P256SIGN_VERIFYsign, rotateKey, getKeyMetadata, getPublicKey
TRANSITECDSA_P384SIGN_VERIFYsign, rotateKey, getKeyMetadata, getPublicKey
TRANSITED25519SIGN_VERIFYsign, rotateKey, getKeyMetadata, getPublicKey
TRANSITRSA_4096SIGN_VERIFYsign, rotateKey, getKeyMetadata, getPublicKey
ETHEREUMECC_SECG_P256K1SIGN_VERIFYsign, getKeyMetadata, getPublicKey

generateDataKey uses an AES key to wrap a newly generated symmetric key. Asymmetric keys, including RSA keys, cannot be used for encryption or data-key generation through this API.

Most of this guide uses TRANSIT; the Ethereum keys section covers the ETHEREUM scheme.

The SDK talks JSON-RPC 2.0 to the gateway at POST /api/v1/rpc. Inputs are camelCase; outputs preserve the gateway's PascalCase wire shape (so KeyId, KeyMetadata, CiphertextBlob, etc. appear as-is on the response).

Prerequisites

KMS requires authentication; there is no public fallback for key operations. Get an API token from accounts.spacecomputer.io. See the Authentication guide for token setup and replacement.

import { OrbitportSDK } from "@spacecomputer-io/orbitport-sdk-ts";

const sdk = new OrbitportSDK({
  config: {
    accessToken: process.env.ORBITPORT_ACCESS_TOKEN,
    apiUrl: "https://op.spacecomputer.io",
  },
});

SDK and JSON-RPC methods

All KMS SDK methods return Promise<ServiceResult<T>>. The generic parameter T is the type of data, which holds the operation-specific response. The surrounding fields have the same shape for every method:

interface ServiceResult<T> {
  data: T;
  metadata: {
    timestamp: number;
    request_id?: string;
  };
  success: boolean;
}

For example, an encryption response has this shape (the ciphertext is abbreviated):

import type { EncryptResponse, ServiceResult } from "@spacecomputer-io/orbitport-sdk-ts";

const response: ServiceResult<EncryptResponse> = {
  data: {
    CiphertextBlob: "...",
    KeyId: "kms:demo-aes",
    EncryptionAlgorithm: "AES_256_GCM96",
  },
  metadata: { timestamp: Date.now() },
  success: true,
};

console.log(response.data.CiphertextBlob); // EncryptResponse fields live in data

metadata.timestamp is the local completion time in milliseconds since the Unix epoch. The KMS methods populate that timestamp; request_id is optional in the shared type. Successful calls set success to true. Failures throw OrbitportSDKError rather than returning success: false.

The SDK wraps the JSON-RPC methods below: it constructs and sends each request, then exposes the RPC result as ServiceResult.data or throws an error.

SDK methodJSON-RPC method
sdk.kms.createKeykms.CreateKey
sdk.kms.getKeyMetadatakms.GetKeyMetadata
sdk.kms.getPublicKeykms.GetPublicKey
sdk.kms.encryptkms.Encrypt
sdk.kms.decryptkms.Decrypt
sdk.kms.signkms.Sign
sdk.kms.generateDataKeykms.GenerateDataKey
sdk.kms.rotateKeykms.RotateKey
sdk.kms.getCapabilitieskms.GetCapabilities
sdk.kms.keyStore.putkms_keystore.Put
sdk.kms.keyStore.getkms_keystore.Get
sdk.kms.keyStore.listkms_keystore.List
sdk.kms.keyStore.deletekms_keystore.Delete

Direct JSON-RPC requests use PascalCase parameter names and return a result or error envelope. For encrypt/sign inputs and decrypt outputs, the wire format uses base64; the SDK handles encoding and decoding as described below.

Discover capabilities

Use getCapabilities() to inspect the schemes, key specs, and algorithms the gateway supports:

const caps = await sdk.kms.getCapabilities();
const transit = caps.data.Schemes.find((s) => s.Scheme === "TRANSIT");
console.log(transit?.KeySpecs);

The response lists the schemes exposed by the SDK. This guide uses the TRANSIT capability entry. A direct JSON-RPC capabilities response can therefore include schemes absent from the SDK response. Backend implementation does not establish that a feature is enabled on a particular deployment.

Create a key

createKey provisions a new key. keyUsage is a fixed API value that you select to match the key's purpose; it is not an identifier you obtain from another request:

  • Use ENCRYPT_DECRYPT with AES_256_GCM96 for encryption, decryption, and wrapping generated data keys.
  • Use SIGN_VERIFY with an asymmetric key spec for signing. The corresponding public key is used to verify signatures.

These values are case-sensitive. The backend rejects a keyUsage that does not match the key spec; use the combinations in the supported operations table.

For an AES key:

const aes = await sdk.kms.createKey({
  alias: "demo-aes",
  keySpec: "AES_256_GCM96",
  keyUsage: "ENCRYPT_DECRYPT",
  scheme: "TRANSIT", // default
});
const aesKeyId = aes.data.KeyMetadata.KeyId;

Add a description and tags to label the key:

const labelled = await sdk.kms.createKey({
  alias: "demo-aes-prod",
  keySpec: "AES_256_GCM96",
  keyUsage: "ENCRYPT_DECRYPT",
  description: "payments service envelope key",
  tags: [{ TagKey: "env", TagValue: "prod" }],
});

alias must be unique within your tenant and match [A-Za-z0-9.-]{1,128}: letters, digits, dots, and hyphens. Underscores are not accepted for key aliases. Do not supply the reserved kms: prefix when creating a key; returned key identifiers use kms:<alias>.

description and tags are optional SDK inputs. When omitted, the SDK sends Description: "" and Tags: [], which the gateway requires on the wire.

Retrieve key metadata and public keys

Both lookup methods require a bearer token and are scoped to your tenant. Supply either the canonical kms:<alias> key ID or the raw alias. Omit version to retrieve the current public key, or pass a positive uint32 to retrieve that historical version.

This example uses the ECDSA key created in Sign a message:

const metadata = await sdk.kms.getKeyMetadata({ keyId: "kms:demo-ecdsa" });
console.log(metadata.data.KeyMetadata.KeySpec);
console.log(metadata.data.KeyMetadata.PublicKey); // PEM for this ECDSA key

const currentKey = await sdk.kms.getPublicKey({ keyId: "demo-ecdsa" });
console.log(currentKey.data.PublicKey); // PEM for the current version
console.log(currentKey.data.Version);

const historicalKey = await sdk.kms.getPublicKey({
  keyId: "demo-ecdsa",
  version: 1,
});
console.log(historicalKey.data.PublicKey);
console.log(historicalKey.data.Version); // 1

getKeyMetadata returns the same KeyMetadata shape as createKey. getPublicKey returns { PublicKey: string; Version: number } and rejects version 0, non-integer versions, values above 4294967295, and symmetric keys such as AES_256_GCM96. Neither method exposes private key material.

Public-key encoding depends on the scheme and key spec. TRANSIT ECDSA and RSA keys use PEM; TRANSIT ED25519 keys return a base64-encoded 32-byte public key. The SDK returns the provider's format unchanged, so use a verifier that accepts that format rather than assuming every public key is PEM. For a detached TRANSIT ECDSA signature, follow the satellite telemetry tutorial.

Encrypt and decrypt

Symmetric round-trip with a TRANSIT AES key:

const enc = await sdk.kms.encrypt({
  keyId: aesKeyId,
  plaintext: "hello kms",
});

const dec = await sdk.kms.decrypt({
  keyId: aesKeyId,
  ciphertextBlob: enc.data.CiphertextBlob,
});

console.log(dec.data.Plaintext); // "hello kms"

encrypt and decrypt accept an encoding: "utf8" | "bytes" option. Use the default "utf8" for text: pass a string to encrypt and receive a string from decrypt. For binary data, pass a Uint8Array and use "bytes" on both calls. This preserves the original byte values instead of interpreting the decrypted data as UTF-8 text:

const bytes = new Uint8Array([0xde, 0xad, 0xbe, 0xef]);

const enc = await sdk.kms.encrypt({
  keyId: aesKeyId,
  plaintext: bytes,
  encoding: "bytes",
});

const dec = await sdk.kms.decrypt({
  keyId: aesKeyId,
  ciphertextBlob: enc.data.CiphertextBlob,
  encoding: "bytes",
});
// dec.data.Plaintext is a Uint8Array

Sign a message

Sign over a precomputed digest with an ECDSA P-256 key:

const ec = await sdk.kms.createKey({
  alias: "demo-ecdsa",
  keySpec: "ECDSA_P256",
  keyUsage: "SIGN_VERIFY",
});

// 32 bytes -- a SHA-256 digest you computed locally.
const digest = new Uint8Array(32); // fill with your digest

const sig = await sdk.kms.sign({
  keyId: ec.data.KeyMetadata.KeyId,
  message: digest,
  signingAlgorithm: "ECDSA_SHA_256",
  messageType: "DIGEST",
});

console.log(sig.data.Signature); // TRANSIT: vault:v<version>:<base64 signature>
console.log(sig.data.KeyVersion); // version used to produce this signature

The signing response includes KeyVersion as a required positive uint32. Keep it with the signature. For verification after a rotation, request the matching public key rather than the current one:

const verificationKey = await sdk.kms.getPublicKey({
  keyId: sig.data.KeyId,
  version: sig.data.KeyVersion,
});

if (verificationKey.data.Version !== sig.data.KeyVersion) {
  throw new Error("KMS returned the wrong public-key version");
}

messageType controls how the gateway interprets message:

messageTypeMeaning
RAW (default)Pass message bytes to the selected algorithm; hashing depends on that algorithm.
DIGESTmessage is already a hash; sign it as-is.

Ethereum keys

For signing Ethereum-style messages, create a key under the ETHEREUM scheme. The response includes a checksummed Address derived from the key.

const eth = await sdk.kms.createKey({
  alias: "demo-eth",
  keySpec: "ECC_SECG_P256K1",
  keyUsage: "SIGN_VERIFY",
  scheme: "ETHEREUM",
});

console.log(eth.data.KeyMetadata.Address); // 0x… address

// EIP-191 personal-sign style message (same shape wallets sign):
const sig = await sdk.kms.sign({
  keyId: eth.data.KeyMetadata.KeyId,
  message: "Hello, Ethereum",
  signingAlgorithm: "ETHEREUM_SECP256K1",
  messageType: "EIP191",
});
console.log(sig.data.Signature); // Ethereum provider signature

RAW and DIGEST message types are also accepted with ETHEREUM_SECP256K1. Ethereum keys are sign-only: encrypt/decrypt, data keys, and rotation are rejected for them.

Generate a data key (envelope encryption)

generateDataKey returns a fresh symmetric key both as plaintext (so you can use it locally) and wrapped under your master key (so you can store the wrapped form alongside the data and only need the master key to decrypt later):

import { fromBase64ToUint8Array } from "@spacecomputer-io/orbitport-sdk-ts";

const dk = await sdk.kms.generateDataKey({
  keyId: aesKeyId,
  dataKeySpec: "AES_256",
});

const rawKeyBytes = fromBase64ToUint8Array(dk.data.Plaintext);
const wrappedBlob = dk.data.CiphertextBlob; // store this with your data

Plaintext is always raw base64 binary key material -- there is no encoding flag for this method. The SDK exports fromBase64ToUint8Array, fromBase64ToUtf8, and toBase64 helpers for manual decoding.

Provide exactly one of dataKeySpec or numberOfBytes:

  • dataKeySpec: "AES_128" generates a 16-byte key; "AES_256" generates a 32-byte key.
  • numberOfBytes specifies the length of the new plaintext key, not the size of the wrapped CiphertextBlob or its base64 representation. For example, numberOfBytes: 32 requests 32 bytes of key material.

The SDK accepts integer values from 1 to 1024 for numberOfBytes; the backend may restrict which of those lengths it supports. After decoding Plaintext, rawKeyBytes.length is the generated key's length in bytes.

Rotate a key

Bump the key's primary version. New encryptions and signatures use the new version. Previously produced ciphertexts remain decryptable, and signatures remain verifiable with the public key for signature.data.KeyVersion.

const before = aes.data.KeyMetadata.PrimaryVersion;
const rotated = await sdk.kms.rotateKey({ keyId: aesKeyId });
console.log(`PrimaryVersion: ${before} -> ${rotated.data.KeyMetadata.PrimaryVersion}`);

TRANSIT keys support rotation.

Store application secrets

The KMS key store keeps arbitrary JSON objects in your authenticated tenant. Names use slash-separated segments, so an application can group entries such as github/prod and github/staging.

await sdk.kms.keyStore.put({
  name: "github/prod",
  secret: { apiKey: "..." },
});

const entry = await sdk.kms.keyStore.get({ name: "github/prod" });
const names = await sdk.kms.keyStore.list({ prefix: "github" });

await sdk.kms.keyStore.delete({ name: "github/prod" });

put requires a JSON object, and writing an existing name creates a new version; the response contains Name and Version. Names are at most 256 characters, with slash-separated segments containing letters, digits, ., _, or -; empty, . and .. segments are rejected. Unlike key aliases, key-store names allow underscores.

list returns Names containing immediate entries and folders under the prefix; folder names end in /. Call list() without a prefix for the root. get returns Name and Secret; delete returns Name. Access is scoped to the authenticated tenant and enforced by the backend key-store policy. For PAT requests, the gateway obtains the authoritative tenant from the account service. Key-store reads preserve numbers that JavaScript cannot represent exactly as LosslessNumber values:

import { isLosslessNumber } from "@spacecomputer-io/orbitport-sdk-ts";

const counter = entry.data.Secret.counter;
if (isLosslessNumber(counter)) {
  console.log(counter.toString());
}

You can pass a retrieved Secret back to put without changing those values.

Credits and usage

The gateway parses and validates each JSON-RPC request before asking the account service to reserve credits. Malformed requests and gateway validation failures do not create a hold. Accepted requests carry an operation label used for pricing and usage descriptions in the accounts portal:

OperationUsage label
Create a keykms.CreateKey:<KeySpec>, for example kms.CreateKey:AES_256_GCM96
Get metadata or a public keykms.GetKeyMetadata or kms.GetPublicKey
Signkms.Sign:<SigningAlgorithm>, for example kms.Sign:ECDSA_SHA_256
Encrypt, decrypt, generate a data key, or rotateThe RPC method name, such as kms.Encrypt
Discover capabilitieskms.GetCapabilities
Put, get, list, or delete a JSON secretkms_keystore.Put, kms_keystore.Get, kms_keystore.List, or kms_keystore.Delete

The SDK does not set the price or send an operation label. The gateway derives the label from the validated request, and pricing is controlled by the accounts service. Capability discovery also goes through the credit hold.

After successful execution, the gateway requests settlement of the hold. Execution failures and timeouts trigger a release request. Settlement and release are best-effort; the accounts service reconciles unresolved holds, so ledger updates may not be immediate.

Errors and retries

KMS methods throw OrbitportSDKError with a typed code. Common KMS-specific codes:

CodeWhen
KMS_KEY_NOT_FOUNDThe backend reports a missing key or key-store entry.
KMS_INVALID_KEY_STATEThe key cannot be used for the requested operation in its current state.
KMS_ERRORA generic gateway-side KMS failure.
JSON_RPC_ERRORThe gateway returned a JSON-RPC error that the SDK did not map to a more specific code.
INSUFFICIENT_CREDITSHTTP 402: your available credit balance cannot cover the operation. Check your balance in the accounts portal before retrying.
ACCOUNT_UNAVAILABLEHTTP 503 carrying account_plugin_unavailable: the gateway could not authorize the request through the account service. Retry later.
AUTH_FAILEDNo token is available, or the HTTP response is 401/403.
TOKEN_EXPIREDThe SDK detects an expired JWT locally, including its 60-second expiry buffer.

For JSON-RPC errors, the raw code is exposed at error.details.jsonRpcCode. HTTP failures expose status and, when available, error.details.httpBody. The SDK maps JSON-RPC errors by code and message; not every server-side authentication or authorization rejection maps to AUTH_FAILED.

A lookup for a nonexistent or inaccessible key can return KMS_ERROR with a permission-denied message instead of KMS_KEY_NOT_FOUND. Do not use that response to determine whether another tenant's key exists.

RequestOptions.retries is the total attempt limit when greater than 1 (retries: 3 means one initial attempt and at most two retries). timeout applies to each attempt and defaults to 30,000 ms.

When retries are enabled, the SDK retries only these error codes, using exponential backoff with jitter:

Retryable codeWhen
NETWORK_ERRORA network failure prevents the request from completing.
CONNECTION_FAILEDThe underlying fetch request cannot establish or complete the connection.
TIMEOUTThe request times out or is aborted.
SERVICE_UNAVAILABLEAn HTTP 5xx response without the account_plugin_unavailable marker.
RATE_LIMITEDThe gateway responds with HTTP 429.
ACCOUNT_UNAVAILABLEHTTP 503 carrying account_plugin_unavailable: the account service is temporarily unavailable.

All other error codes are thrown without another attempt. This includes authentication and validation errors, insufficient credits, and JSON-RPC errors.

KMS methods do not retry by default -- createKey and sign are not idempotent. Pass RequestOptions.retries per call when you want retry behavior:

await sdk.kms.encrypt(
  { keyId: aesKeyId, plaintext: "hi" },
  { retries: 3 },
);

Reference

Versioned signing and public-key types

The SDK uses camelCase request fields and preserves PascalCase gateway response fields:

interface GetPublicKeyRequest {
  keyId: string;
  version?: number; // positive uint32; omit for the current version
}

interface GetPublicKeyResponse {
  PublicKey: string;
  Version: number;
}

interface SignResponse {
  KeyId: string;
  Signature: string;
  SigningAlgorithm: string;
  KeyVersion: number;
}

Direct JSON-RPC callers send KeyId and optional Version to kms.GetPublicKey. Its result contains PublicKey and Version. The kms.Sign result contains KeyId, Signature, SigningAlgorithm, and KeyVersion.

Signing algorithms

AlgorithmCompatible key specs
ECDSA_SHA_256ECDSA_P256
ECDSA_SHA_384ECDSA_P384
ED25519ED25519
RSASSA_PKCS1_V1_5_SHA_256RSA_4096
RSASSA_PSS_SHA_256RSA_4096
ETHEREUM_SECP256K1ECC_SECG_P256K1

Next steps

On this page