The signing key should exist in application memory only for the duration of the signing operation. It should never be:
Both the Python and TypeScript SDKs implement these protections:
HMAC-SHA256 as a byte buffer, used once per signature, and not cached..swt3.yaml loader uses _env suffix convention, so the key never appears in config files.print(signing_key) or console.log(signingKey)).ulimit -c 0 or systemd LimitCORE=0).Python strings are immutable and garbage-collected non-deterministically. The signing key may persist in memory after the Witness is stopped until the garbage collector reclaims it. For most compliance scenarios, this is acceptable because:
hmac module uses C-level memory for the HMAC computation.bytearray wrapper that zeros on deletion. The standard Python SDK is suitable for all environments up to and including FedRAMP High.
Node.js strings are immutable and managed by V8's garbage collector. Similar to Python, the key may persist in the V8 heap until GC. The crypto.createHmac() function operates on native buffers, not JavaScript strings.
For Node.js specifically:
--inspect or --inspect-brk in production (exposes heap to debugger).NODE_OPTIONS="--max-old-space-size=512" to limit heap size and trigger GC more frequently.Buffer.alloc() instead of Buffer.from() if implementing custom key handling (allows explicit zeroing).The Rust SDK (swt3-ai crate) offers:
Zeroize-implementing type that overwrites memory on drop. When the Witness goes out of scope, the key bytes are zeroed before deallocation.fmt::Debug (the Debug impl redacts the value).# Cargo.toml [dependencies] swt3-ai = "0.3.6" # The crate uses zeroize internally for key material
use swt3_ai::{fingerprint, sign_payload};
let key = "your-signing-key";
let fp = fingerprint("TENANT", "AI-INF.1", 1, 1, 0, timestamp_ms);
let sig = sign_payload(key, &fp, Some("agent-id"));
// key is dropped when it goes out of scope; memory is zeroed
The .NET SDK uses System.Security.Cryptography.HMACSHA256 which operates on byte arrays. The SecureString type is deprecated in .NET Core; use byte[] with explicit zeroing via Array.Clear() when done.
Ruby strings are mutable. After signing, you can overwrite the key variable: signing_key.replace("\0" * signing_key.length). This is a best-effort mitigation since the GC may have copied the string internally.
Organizations can choose the appropriate signing security level based on their regulatory environment:
| Tier | Mode | Key Material | Suitable For |
|---|---|---|---|
| TIER 1 | Unsigned | None | Development, POC, non-regulated workloads |
| TIER 2 | Static HMAC | Shared secret via env var or secret store | SOC 2, CMMC Level 1, internal governance, most production AI |
| TIER 3 | OIDC Ephemeral | Short-lived identity token (no shared secret) | FedRAMP, CMMC Level 2+, cloud-native enterprise |
| TIER 4 | HSM-Backed | Hardware security module (FIPS 140-2 Level 3+) | Classified, defense, sovereign deployments |
When a tenant registers a signing key via POST /api/v1/signing-keys, the server never stores the raw key. Instead:
SIGNING_KEY_MASTER).If the database is compromised, the attacker gets encrypted ciphertext that is useless without the SIGNING_KEY_MASTER environment variable from the server.
Generate signing keys using cryptographically secure random number generators:
# Python
python3 -c "import secrets; print(secrets.token_hex(32))"
# Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# OpenSSL
openssl rand -hex 32
# Rust
use rand::Rng;
let key: String = rand::thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
.take(64)
.map(char::from)
.collect();
Use the Rust SDK when any of these apply:
For all other cases, the Python and TypeScript SDKs provide equivalent cryptographic guarantees at the protocol level. The signing algorithm (HMAC-SHA256), fingerprint formula, and clearing engine are identical across all five language implementations.