Skip to content

Sign and Verify Workflow Packages

Cloacina supports cryptographic (Ed25519) signing of workflow packages so you can detect tampering and verify authenticity. This guide covers the library-side tasks: enabling verification, generating and trusting keys, signing, verifying, and rotating keys.

To require verification, configure SecurityConfig:

use cloacina::security::SecurityConfig;
let config = SecurityConfig {
require_signatures: true,
// 32-byte master key; only needed when SIGNING with database-stored
// keys — verification-only loading doesn't require it.
key_encryption_key: Some(master_key_bytes),
// The org whose trusted keys uploads are verified against. With
// require_signatures = true and no org set, verification fails safe
// (all uploads rejected).
verification_org_id: Some(org_id),
};

When require_signatures is true, unsigned packages and packages signed by an untrusted or tampered key fail to load.

The key_encryption_key is a 32-byte key used to encrypt private signing keys at rest in the database. Store it in a secrets manager and provide it at runtime.

Generate a signing key for your organization, then trust its public key so packages signed with it will verify:

use cloacina::security::{DbKeyManager, KeyManager};
let key_manager = DbKeyManager::new(dal);
// Generate a signing key (private key encrypted under master_key)
let key_info = key_manager
.create_signing_key(org_id, "release-key-v1", &master_key)
.await?;
// Export the public key for distribution
let export = key_manager.export_public_key(key_info.id).await?;
println!("{}", export.public_key_pem);
// Trust the public key (from raw bytes or PEM)
key_manager
.trust_public_key(org_id, &key_info.public_key, Some("Release Key"))
.await?;

To let a parent organization trust everything a child organization trusts, grant trust between them:

key_manager.grant_trust(parent_org_id, child_org_id).await?;
use cloacina::security::{DbPackageSigner, PackageSigner};
let signer = DbPackageSigner::new(dal);
let signature = signer
.sign_package_with_db_key(&package_path, key_id, &master_key, true)
.await?;

To distribute a detached .sig sidecar alongside the package:

use cloacina::security::DetachedSignature;
let detached = DetachedSignature::from_signature_info(&signature);
detached.write_to_file("my-package.so.sig")?;

With the database available, verify against a stored signature (or an adjacent .sig file) using the organization’s trusted keys:

use cloacina::security::{verify_package, SignatureSource};
let result = verify_package(
&package_path,
org_id,
SignatureSource::Auto, // try .sig file, then database
&package_signer,
&key_manager,
)
.await?;

When only the public key and a detached signature are available (no database), verify offline:

use cloacina::security::verify_package_offline;
let result = verify_package_offline(&package_path, &signature_path, &public_key_bytes)?;
  1. Generate a new key:

    let new_key = key_manager.create_signing_key(org_id, "release-v2", &master_key).await?;
  2. Trust the new key:

    key_manager.trust_public_key(org_id, &new_key.public_key, Some("release-v2")).await?;
  3. Update CI to sign new packages with the new key.

  4. During the transition, both old and new signatures verify (both keys are trusted).

  5. After the transition, revoke the old trusted key:

    key_manager.revoke_trusted_key(old_trusted_key_id).await?;
  6. Optionally, revoke the old signing key to prevent new signatures:

    key_manager.revoke_signing_key(old_signing_key_id).await?;