Cryptography
Opscotch provides cryptographic capabilities at two layers: the packager for key generation and workflow package security, and the runtime for in-workflow cryptographic operations.
This page covers the user-facing cryptographic surface: key formats, key generation, package signing and encryption, bootstrap key configuration, and the workflow CryptoContext API.
Cryptography Overview
Opscotch uses libsodium (also known as Sodium) for all cryptographic operations. libsodium is a modern, well-audited cryptographic library that provides secure, easy-to-use primitives.
Key Concepts
- Purpose: The role a key plays in cryptographic operations. Opscotch supports four purposes:
sign,authenticated,anonymous, andsymmetric. - Type: Whether the key is secret (private) or public. Key type can be
secretorpublic. - Key format: All keys are represented as hex-encoded strings in configuration and API responses.
BYOK: Bring Your Own Keys
Opscotch supports BYOK workflows through multiple key provisioning options. This is a core design principle - you control your cryptographic keys throughout their lifecycle.
Key Provisioning Options
- External generation: Generate keys using external tools, convert to hex, configure in bootstrap or supply to the packager
- Packager key-gen: Use the
/key-genendpoint to generate libsodium-compatible keys - Runtime registration: Generate or import keys within workflow JavaScript
Keys can be:
- Preloaded in bootstrap for startup availability
- Registered on-demand within workflow processors
- Used for package signing, encryption, or runtime operations
The key format (hex-encoded libsodium keys) ensures interoperability with standard libsodium implementations for external key generation. See Key Size Reference for the exact formats expected.
Why BYOK Matters
- Key isolation: Keep production keys separate from package builds
- Key rotation: Update keys without rebuilding packages
- Compliance: Meet regulatory requirements for key management
- Security: Keys never leave your controlled environment until needed
Cryptography in Packaging
The packager provides key generation and cryptographic operations for securing workflow packages. See the Packaging documentation for details on running the packager.
Key Generation
The packager's /key-gen endpoint generates key pairs suitable for use with Opscotch's cryptographic system.
GET http://<packager-host>/key-gen?purpose=<purpose>
| Parameter | Values | Description |
|---|---|---|
purpose | sign | Ed25519 key pair for digital signatures |
authenticated | X25519 key pair for authenticated encryption | |
anonymous | X25519 key pair for anonymous encryption | |
symmetric | X25519-Salsa20-Poly1305 symmetric key |
Response format:
{
"purpose": "<purpose>",
"publicKey": "<hex-encoded public key>",
"secretKey": "<hex-encoded secret key>"
}
Key Size Reference
| Purpose | Key Type | Hex Length | libsodium Primitive |
|---|---|---|---|
sign | secret | 128 chars (64 bytes) | Ed25519 secret key |
sign | public | 64 chars (32 bytes) | Ed25519 public key |
authenticated | secret | 64 chars (32 bytes) | X25519 secret key |
authenticated | public | 64 chars (32 bytes) | X25519 public key |
anonymous | secret | 64 chars (32 bytes) | X25519 secret key |
anonymous | public | 64 chars (32 bytes) | X25519 public key |
symmetric | secret | 64 chars (32 bytes) | X25519-Salsa20-Poly1305 |
Package Signing
Signatures provide integrity verification and signer identity. Packages can be signed during creation:
- Generate or supply signing keys using
/key-gen?purpose=sign - Include the secret signing key in the package request
- Specify signers in the request body
{
"packageId": "my-app",
"keys": [
{ "id": "signing-key", "type": "secret", "purpose": "sign", "keyHex": "<secret-key-hex>" }
],
"signers": ["signing-key"]
}
At runtime, the bootstrap must include the matching public key:
{
"deploymentId": "my-deployment",
"remoteConfiguration": "/path/to/my-app.oapp",
"packaging": { "requiredSigners": ["signing-key"] },
"keys": [
{ "id": "signing-key", "type": "public", "purpose": "sign", "keyHex": "<public-key-hex>" }
]
}
Package Encryption
Opscotch supports authenticated encryption for deployment-restricted packages:
- Generate authenticated key pairs using
/key-gen?purpose=authenticated - Include the packager's secret key and recipient public keys in the package
- Specify the packager identity and encryption keys
{
"packageId": "restricted-app",
"keys": [
{ "id": "packager-key", "type": "secret", "purpose": "authenticated", "keyHex": "<packager-secret>" },
{ "id": "recipient-key", "type": "public", "purpose": "authenticated", "keyHex": "<recipient-public>" }
],
"packagerIdentityPrivateKey": "packager-key",
"encryptionKeys": ["recipient-key"]
}
The runtime bootstrap must include:
- The packager's public key (to verify the package came from you)
- The recipient's secret key (to decrypt the package)
packaging.packagerIdentitiesnaming permitted packager public keys
Bootstrap Encryption
Bootstrap files can be encrypted for sensitive configuration:
POST http://<packager-host>/bootstrap-encrypt
Request: multipart form with JSON body containing encryption keys and the bootstrap file in the stream field.
Response: Base64-encoded encrypted bootstrap file.
To decrypt at runtime, set the environment variable:
OPSCOTCH_BOOTSTRAP_SECRETKEY=<senderPublicKeyHex>/<decryptPrivateKeyHex>
String Encryption
Sensitive strings (like authenticated host credentials) can be encrypted for embedding in configuration:
POST http://<packager-host>/string-encrypt
Request: multipart form with encryption keys and plaintext.
Response: Encoded string (proprietary format).
To decrypt at runtime:
OPSCOTCH_STRING_SECRETKEYS=<senderPublicKeyHex>/<decryptPrivateKeyHex>,...
Cryptography in Bootstrap
Bootstrap configuration can preload keys for runtime use. This enables BYOK workflows where keys are generated externally or by the packager, then configured in bootstrap.
See the Licensing documentation for how cryptographic keys interact with license management.
Bootstrap Keys Structure
{
"keys": [
{
"id": "my-signing-key",
"purpose": "sign",
"type": "secret",
"keyHex": "a1b2c3...",
"metadata": {
"description": "Key for signing outbound messages"
}
}
]
}
| Field | Required | Description |
|---|---|---|
id | yes | Unique identifier for referencing the key |
purpose | yes | One of: sign, authenticated, anonymous, symmetric |
type | yes | One of: public, secret |
keyHex | yes | Hex-encoded key material |
metadata | no | Optional object with description |
Key Constraints
The bootstrap schema enforces key size constraints:
sign+secret: 128 hex characters (Ed25519 secret key)sign+public: 64 hex characters (Ed25519 public key)authenticated: 64 hex characters (X25519 key)anonymous: 64 hex characters (X25519 key)symmetric: 64 hex characters (256-bit symmetric key)
JavaScript CryptoContext
Workflow processors access cryptographic functions through context.crypto(). This provides a runtime API for signing, encryption, hashing, and random generation.
See the API Reference for the complete method documentation, and ByteContext for buffer handling.
Accessing CryptoContext
const crypto = context.crypto();
Buffer Handle Model
CryptoContext operates on buffer handles rather than raw byte arrays. The agent owns memory and exposes handles to workflows. Convert between representations using ByteContext:
const bytes = context.bytes();
// Create buffer from string
const payload = bytes.createFromString("Hello, world!");
// Convert to hex for logging or external use
const hex = bytes.binaryToHex(payload);
// Convert back from hex
const restored = bytes.createFromHex(hex);
Key Registration
Keys defined in bootstrap are available by ID. Keys can also be registered at runtime:
const crypto = context.crypto();
const bytes = context.bytes();
// Register an existing key
crypto.registerKey("sign", "secret", "a1b2c3d4...");
// Validate before use
const isValid = crypto.validateKey("sign", "secret", "a1b2c3d4...");
if (!isValid) {
context.addSystemError("Invalid key");
}
Available Operations
| Operation | Purpose | Description |
|---|---|---|
sign(payload, secretKeyId) | sign | Sign a payload with a secret key |
verifySignature(signature, payload, publicKeyId) | sign | Verify a signature |
encryptPublicKey(payload, recipientPublicId, senderSecretId) | authenticated | Authenticated encryption |
decryptPublicKey(sealed, senderPublicId, recipientSecretId) | authenticated | Authenticated decryption |
encryptAnonymous(payload, recipientPublicId) | anonymous | Anonymous encryption (sender hidden) |
decryptAnonymous(sealed, recipientPublicId, recipientSecretId) | anonymous | Anonymous decryption |
encryptSymmetric(payload, secretKeyId, nonce) | symmetric | Symmetric encryption |
decryptSymmetric(sealed, secretKeyId, nonce) | symmetric | Symmetric decryption |
hash(payload) | - | Compute SHA-256 hash |
randomBytes(length) | - | Generate random bytes |
Example: Signing
const crypto = context.crypto();
const bytes = context.bytes();
// Sign a message
const message = bytes.createFromString("Important data");
const signature = crypto.sign(message, "my-signing-key");
// Get the signature as hex for external use
const signatureHex = bytes.binaryToHex(signature);
// Verify
const isValid = crypto.verifySignature(signature, message, "my-verification-key");
Example: Authenticated Encryption
const crypto = context.crypto();
const bytes = context.bytes();
// Encrypt with authenticated sender and recipient
const plaintext = bytes.createFromString("Secret message");
const nonce = crypto.randomBytes(24);
const sealed = crypto.encryptPublicKey(plaintext, "recipient-public", "sender-secret");
// Decrypt - validates sender identity
const decrypted = crypto.decryptPublicKey(sealed, "sender-public", "recipient-secret");
Example: Symmetric Encryption
const crypto = context.crypto();
// Register symmetric key
crypto.registerKey("symmetric", "secret", "0123456789abcdef...");
const nonce = crypto.randomBytes(24);
const sealed = crypto.encryptSymmetric(plaintext, "my-symmetric-key", nonce);
const restored = crypto.decryptSymmetric(sealed, "my-symmetric-key", nonce);