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:
| Scheme | keySpec | Required keyUsage | Supported SDK operations |
|---|---|---|---|
TRANSIT | AES_256_GCM96 | ENCRYPT_DECRYPT | encrypt, decrypt, generateDataKey, rotateKey, getKeyMetadata |
TRANSIT | ECDSA_P256 | SIGN_VERIFY | sign, rotateKey, getKeyMetadata, getPublicKey |
TRANSIT | ECDSA_P384 | SIGN_VERIFY | sign, rotateKey, getKeyMetadata, getPublicKey |
TRANSIT | ED25519 | SIGN_VERIFY | sign, rotateKey, getKeyMetadata, getPublicKey |
TRANSIT | RSA_4096 | SIGN_VERIFY | sign, rotateKey, getKeyMetadata, getPublicKey |
ETHEREUM | ECC_SECG_P256K1 | SIGN_VERIFY | sign, 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 datametadata.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 method | JSON-RPC method |
|---|---|
sdk.kms.createKey | kms.CreateKey |
sdk.kms.getKeyMetadata | kms.GetKeyMetadata |
sdk.kms.getPublicKey | kms.GetPublicKey |
sdk.kms.encrypt | kms.Encrypt |
sdk.kms.decrypt | kms.Decrypt |
sdk.kms.sign | kms.Sign |
sdk.kms.generateDataKey | kms.GenerateDataKey |
sdk.kms.rotateKey | kms.RotateKey |
sdk.kms.getCapabilities | kms.GetCapabilities |
sdk.kms.keyStore.put | kms_keystore.Put |
sdk.kms.keyStore.get | kms_keystore.Get |
sdk.kms.keyStore.list | kms_keystore.List |
sdk.kms.keyStore.delete | kms_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_DECRYPTwithAES_256_GCM96for encryption, decryption, and wrapping generated data keys. - Use
SIGN_VERIFYwith 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); // 1getKeyMetadata 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 Uint8ArraySign 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 signatureThe 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:
messageType | Meaning |
|---|---|
RAW (default) | Pass message bytes to the selected algorithm; hashing depends on that algorithm. |
DIGEST | message 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 signatureRAW 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 dataPlaintext 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.numberOfBytesspecifies the length of the new plaintext key, not the size of the wrappedCiphertextBlobor its base64 representation. For example,numberOfBytes: 32requests 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:
| Operation | Usage label |
|---|---|
| Create a key | kms.CreateKey:<KeySpec>, for example kms.CreateKey:AES_256_GCM96 |
| Get metadata or a public key | kms.GetKeyMetadata or kms.GetPublicKey |
| Sign | kms.Sign:<SigningAlgorithm>, for example kms.Sign:ECDSA_SHA_256 |
| Encrypt, decrypt, generate a data key, or rotate | The RPC method name, such as kms.Encrypt |
| Discover capabilities | kms.GetCapabilities |
| Put, get, list, or delete a JSON secret | kms_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:
| Code | When |
|---|---|
KMS_KEY_NOT_FOUND | The backend reports a missing key or key-store entry. |
KMS_INVALID_KEY_STATE | The key cannot be used for the requested operation in its current state. |
KMS_ERROR | A generic gateway-side KMS failure. |
JSON_RPC_ERROR | The gateway returned a JSON-RPC error that the SDK did not map to a more specific code. |
INSUFFICIENT_CREDITS | HTTP 402: your available credit balance cannot cover the operation. Check your balance in the accounts portal before retrying. |
ACCOUNT_UNAVAILABLE | HTTP 503 carrying account_plugin_unavailable: the gateway could not authorize the request through the account service. Retry later. |
AUTH_FAILED | No token is available, or the HTTP response is 401/403. |
TOKEN_EXPIRED | The 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 code | When |
|---|---|
NETWORK_ERROR | A network failure prevents the request from completing. |
CONNECTION_FAILED | The underlying fetch request cannot establish or complete the connection. |
TIMEOUT | The request times out or is aborted. |
SERVICE_UNAVAILABLE | An HTTP 5xx response without the account_plugin_unavailable marker. |
RATE_LIMITED | The gateway responds with HTTP 429. |
ACCOUNT_UNAVAILABLE | HTTP 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
| Algorithm | Compatible key specs |
|---|---|
ECDSA_SHA_256 | ECDSA_P256 |
ECDSA_SHA_384 | ECDSA_P384 |
ED25519 | ED25519 |
RSASSA_PKCS1_V1_5_SHA_256 | RSA_4096 |
RSASSA_PSS_SHA_256 | RSA_4096 |
ETHEREUM_SECP256K1 | ECC_SECG_P256K1 |
Next steps
- Authentication -- get a bearer token for the API.
- Orbitport Architecture -- understand how KMS fits into the wider system.