Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions .github/workflows/release-plz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,10 @@ jobs:
# FROZEN_PUBLISHERS (an npm map), so the switch keys on the
# `@cipherstash/auth` entry, which that PR deleted when crates.io and npm
# trusted publishing moved here together. crates.io needs a Trusted
# Publishing entry for both crates naming cipherstash/stack + release-plz.yml.
# Publishing entry for every published crate naming cipherstash/stack +
# release-plz.yml.
#
# Publish-only: nothing here opens a release PR, so a version bump of the two
# Publish-only: nothing here opens a release PR, so a version bump of these
# crates is a hand-made pull request. `release-plz update` cannot version
# this workspace yet: it walks back to the commit that matches crates.io
# 0.42.3, and the imported history has no root Cargo.toml there. A
Expand All @@ -209,29 +210,28 @@ jobs:
run: node scripts/eql-pipeline-armed.mjs crates

release-crates:
name: "Release stack-auth and stack-profile"
name: "Release the stack-* crates"
needs: [crates-armed]
if: needs.crates-armed.outputs.armed == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write # the stack-auth-v<V> / stack-profile-v<V> tags and releases
contents: write # the <crate>-v<V> tags and releases
pull-requests: write # release-plz links PRs in release notes
id-token: write # crates.io Trusted Publishing
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0

- name: Is either crate version unpublished?
# The EQL preflight's crates.io question, per crate of the version
# group. It decides whether the GPG import runs; an API failure reads
- name: Is any crate version unpublished?
# The EQL preflight's crates.io question, per published crate. It decides whether the GPG import runs; an API failure reads
# as "unpublished", which fails towards importing the key.
id: preflight
Comment thread
coderdan marked this conversation as resolved.
run: |
set -euo pipefail
publish_needed=false
for crate in stack-auth stack-profile; do
for crate in stack-auth stack-profile stack-kms stack-encrypt stack-encrypt-derive; do
version="$(grep -m1 '^version = ' "packages/${crate}/Cargo.toml" | cut -d'"' -f2)"
# crates.io 403s curl's default User-Agent.
published="$(curl -fsSL --retry 3 -H "User-Agent: cipherstash-stack-release (https://github.com/cipherstash/stack)" "https://crates.io/api/v1/crates/${crate}/versions" | jq -r '.versions[].num' | grep -Fx "$version" || true)"
Comment thread
coderdan marked this conversation as resolved.
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Every npm package except EQL lives under `languages/typescript/`: packages in `l
EQL issues in this repository, never in the historical
`cipherstash/encrypt-query-language` repository. Old upstream issue and PR
links are provenance only.
- `packages/stack-auth`, `packages/stack-profile`, `packages/stack-kms`, `packages/stack-encrypt`, `packages/stack-encrypt-derive`, `packages/stack-guest-abi`: The Rust crates imported from `cipherstash/cipherstash-suite` with their history — `stack-auth` and `stack-profile` (published to crates.io), and `stack-kms`, `stack-encrypt`, `stack-encrypt-derive` and `stack-guest-abi` (`publish = false`). They are the members of the **root Cargo workspace**, with the three node binding crates below. See "Working on the Rust crates".
- `packages/stack-auth`, `packages/stack-profile`, `packages/stack-kms`, `packages/stack-encrypt`, `packages/stack-encrypt-derive`, `packages/stack-guest-abi`: The Rust crates imported from `cipherstash/cipherstash-suite` with their history — `stack-auth` and `stack-profile` (published to crates.io, one version group), `stack-kms` (published to crates.io from 0.1.0, its own version group, re-exported by `stack-encrypt` as `stack_encrypt::kms`), `stack-encrypt` and `stack-encrypt-derive` (published to crates.io from 0.1.0, one version group; `eql-bindings`' `stack-encrypt` feature depends on them from the registry), and `stack-guest-abi` (`publish = false`). They are the members of the **root Cargo workspace**, with the three node binding crates below. See "Working on the Rust crates".
- `languages/typescript/packages/auth`, `languages/typescript/packages/profile`, `languages/typescript/packages/stack-auth-wasm`: The node bindings of those crates. `@cipherstash/auth` (napi-rs v2) and its six `platforms/*` packages are published to npm from this repository by `release.yml` (`auth-artifacts`, `publish-auth`); a change to what it ships, the `stack-auth` crate included, needs an `@cipherstash/auth` changeset (`require-auth-npm-changeset.yml`). `@cipherstash/profile` and its platforms are private and never published; `@cipherstash/stack-auth-wasm` is private and builds the wasm that `@cipherstash/auth` ships. Their `build` and `test` scripts never invoke cargo; `build:native`, `build:debug` and `test:cargo` do.
- `languages/golang`: The Go module (`stackencrypt`, `stackauth`, `internal`), a wazero host with no cgo. Its two WASI guests (`*/guest`) are detached Cargo workspaces built by `mise run wasm:guest:build` and `mise run wasm:auth-guest:build`; the `.wasm` files they embed are gitignored. There is no Go release process yet.
- `e2e/*`: Cross-package end-to-end tests (package managers, supply chain, Prisma example README)
Expand Down
25 changes: 15 additions & 10 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,23 @@ This repository also carries the source of the **`eql-bindings`** Rust crate
lockstep with `@cipherstash/eql`. It is in scope for security reports on the
same terms as the npm packages above.

It also carries the source of two Rust crates published to crates.io,
It also carries the source of five Rust crates published to crates.io,
**`stack-auth`** and **`stack-profile`** (`packages/stack-auth`,
`packages/stack-profile`), and of the **Go module** at `languages/golang`
(`stackencrypt` and `stackauth`, over WASI guests built from the stack-*
crates), which has no release yet. All three are in scope for security reports
on the same terms as the npm packages above.
`packages/stack-profile`) and **`stack-kms`**, **`stack-encrypt`** and
**`stack-encrypt-derive`** (`packages/stack-kms`, `packages/stack-encrypt`,
`packages/stack-encrypt-derive`), and of the **Go module** at
`languages/golang` (`stackencrypt` and `stackauth`, over WASI guests built
from the stack-* crates), which has no release yet. All of these are in scope
for security reports on the same terms as the npm packages above.

> **Note on publishing.** Every package in the table above, including all
> seven `@cipherstash/protect-ffi*` packages, all seven `@cipherstash/auth*`
> packages and `@cipherstash/eql`, is published from this repository by
> `.github/workflows/release.yml`; the `eql-bindings`, `stack-auth` and
> `stack-profile` crates are published from here by
> `.github/workflows/release-plz.yml`. EQL moved here at the Phase 5 cutover in
> `.github/workflows/release.yml`; the `eql-bindings`, `stack-auth`,
> `stack-profile`, `stack-kms`, `stack-encrypt` and `stack-encrypt-derive`
> crates are published from here by `.github/workflows/release-plz.yml` (the
> first version of each of the last three was published by hand, because
> crates.io configures Trusted Publishing on an existing crate). EQL moved here at the Phase 5 cutover in
> `docs/plans/2026-08-13-eql-monorepo-absorption.md`, and the auth packages and
> the two crates at the arming PR of the stack-* crates import. Releases made
> before those moves were built elsewhere: `@cipherstash/eql@3.0.5` and earlier
Expand Down Expand Up @@ -168,8 +172,9 @@ CI. See `skills/stash-supply-chain-security/SKILL.md` for the full guide.
The `release.yml` workflow publishes packages to npm using OIDC trusted
publishing (`id-token: write`). There is no long-lived `NPM_TOKEN` — the
workflow deliberately avoids one, and setting one would bypass trusted
publishing. `release-plz.yml` publishes the `eql-bindings` crate to crates.io
over the same token exchange, and likewise carries no `CARGO_REGISTRY_TOKEN`.
publishing. `release-plz.yml` publishes the `eql-bindings` crate and the stack-* crates to
crates.io over the same token exchange, and likewise carries no
`CARGO_REGISTRY_TOKEN`.
Both bind to a *workflow filename* at the registry, so renaming either file
silently invalidates its publisher configuration.

Expand Down
1 change: 0 additions & 1 deletion packages/stack-encrypt-derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ categories.workspace = true
license-file = "LICENSE"
# Not yet released, like stack-encrypt (which is the only crate that should
# depend on this one: the macros are re-exported from there).
publish = false

[lib]
proc-macro = true
Expand Down
4 changes: 2 additions & 2 deletions packages/stack-encrypt-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//! ```
//! use stack_encrypt::{EncryptFrom, DecryptInto, StackCipher, StackCipherText, nonempty};
//! use stack_encrypt::sem::EqualityTerm;
//! use stack_kms::FakeDataKeySource;
//! use stack_encrypt::kms::FakeDataKeySource;
//!
//! #[derive(EncryptFrom, DecryptInto)]
//! #[stash(plaintext = String)]
Expand Down Expand Up @@ -63,7 +63,7 @@
//! CipherText, Decryptable, Encrypt, EncryptFrom, Encryption, Error, SealedValue, StackCipher,
//! nonempty,
//! };
//! use stack_kms::FakeDataKeySource;
//! use stack_encrypt::kms::FakeDataKeySource;
//!
//! /// A column that stores one sealed leaf as bytes.
//! struct LeafBytes(Vec<u8>);
Expand Down
9 changes: 6 additions & 3 deletions packages/stack-encrypt/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,18 @@ categories.workspace = true
license-file = "LICENSE"
Comment thread
coderdan marked this conversation as resolved.
# Not yet released: keep release-plz from picking this crate up (it processes
# any workspace crate whose Cargo.toml lacks `publish = false`).
Comment thread
coderdan marked this conversation as resolved.
Outdated
publish = false

[dependencies]
# `profile` (native only; stack-kms gates it off wasm32 itself) lets
# `StackCipher::new()` fall back to the client key `stash auth login` writes
# to the profile directory (`secretkey.json`) when CS_CLIENT_ID /
# CS_CLIENT_KEY are not set — the same place `AutoStrategy` finds the token.
stack-kms = { path = "../stack-kms", default-features = false, features = ["profile"] }
stack-kms = { path = "../stack-kms", version = "0.1.0", default-features = false, features = ["profile"] }
# `StackCipher::new()` builds a ZeroKMS client from the environment, so its
# return type names the auto-detected auth strategy. Only needed with `http`.
stack-auth = { workspace = true, optional = true }
# `#[derive(EncryptFrom)]` / `#[derive(DecryptInto)]`, re-exported from `target`.
stack-encrypt-derive = { path = "../stack-encrypt-derive" }
stack-encrypt-derive = { path = "../stack-encrypt-derive", version = "0.1.0" }

# The workspace vitaminc (0.5.0): one canonical context encoding shared by
# the AEAD and PRF derivations (`IntoContext`, cipherstash/vitaminc#339),
Expand Down Expand Up @@ -58,6 +57,10 @@ http = ["dep:stack-auth", "stack-auth/http", "stack-kms/http"]
# only at runtime, as an FFI binding's are. Off by default — it is only of
# use to a binding author, and it pulls `vitaminc-aead-value` in.
dynamic = ["dep:vitaminc-aead-value"]
# `stack_encrypt::kms::FakeDataKeySource`: the in-memory key source for tests
# with no ZeroKMS, so a caller's dev-dependency on stack-encrypt is enough and
# no direct stack-kms dependency has to agree with this crate's.
test-support = ["stack-kms/test-support"]
Comment thread
coderdan marked this conversation as resolved.

[dev-dependencies]
serde_json = { workspace = true }
Expand Down
2 changes: 1 addition & 1 deletion packages/stack-encrypt/examples/zerokms_auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
//! anywhere, and CI builds it either way.

use stack_auth::{AuthError, AuthStrategyFn, SecretToken, ServiceToken};
use stack_encrypt::kms::{EnvKeyProvider, StackKmsBuilder};
use stack_encrypt::StackCipher;
use stack_kms::{EnvKeyProvider, StackKmsBuilder};

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
Expand Down
16 changes: 8 additions & 8 deletions packages/stack-encrypt/src/cipher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ pub enum Error {
/// Building a ZeroKMS client from the environment failed: credentials or
/// client key missing or malformed.
///
/// Boxed rather than naming `stack_kms::StackKmsBuilderError` directly:
/// Boxed rather than naming `stack_encrypt::kms::StackKmsBuilderError` directly:
/// that type only exists with `http`, and a variant whose presence tracks
/// a feature is not additive — feature unification elsewhere in the graph
/// would then change this enum's shape under a downstream match.
Expand Down Expand Up @@ -234,8 +234,8 @@ impl From<Unspecified> for Error {
}

/// The CipherStash cipher, scoped to one client: a ZeroKMS client (a
/// [`DataKeySource`] — production: [`stack_kms::StackKms`]; tests:
/// `stack_kms::FakeDataKeySource`) and the keysets that client uses.
/// [`DataKeySource`] — production: [`kms::StackKms`](crate::kms::StackKms); tests:
/// `stack_encrypt::kms::FakeDataKeySource`) and the keysets that client uses.
///
/// Per-leaf keying is deliberate: every value access requires its own data-key
/// retrieval, so individual value accesses are visible (and auditable) as
Expand Down Expand Up @@ -292,7 +292,7 @@ different data-key source entirely:"#
/// ```
/// # async fn example() -> Result<(), stack_encrypt::Error> {
/// use stack_encrypt::StackCipher;
/// use stack_kms::FakeDataKeySource;
/// use stack_encrypt::kms::FakeDataKeySource;
///
/// let cipher = StackCipher::builder()
/// .kms(FakeDataKeySource::new())
Expand Down Expand Up @@ -401,7 +401,7 @@ impl<K: IndexKeySource> StackCipher<K> {
/// ```
/// # async fn example() -> Result<(), stack_encrypt::Error> {
/// use stack_encrypt::{nonempty, StackCipher};
/// use stack_kms::{FakeDataKeySource, IdentifiedBy};
/// use stack_encrypt::kms::{FakeDataKeySource, IdentifiedBy};
///
/// let cipher = StackCipher::builder()
/// .kms(FakeDataKeySource::new())
Expand Down Expand Up @@ -611,9 +611,9 @@ impl StackCipherBuilder<FromEnv> {
/// from the environment.
///
/// This is the seam for a custom authentication strategy: build a
/// [`stack_kms::StackKms`] — with `stack_kms::StackKmsBuilder`,
/// [`kms::StackKms`](crate::kms::StackKms) — with `stack_encrypt::kms::StackKmsBuilder`,
/// or over the host's own transport — and hand it over. It is also how
/// tests inject `stack_kms::FakeDataKeySource`.
/// tests inject `stack_encrypt::kms::FakeDataKeySource`.
pub fn kms<K>(self, kms: K) -> StackCipherBuilder<K> {
StackCipherBuilder {
kms,
Expand Down Expand Up @@ -666,7 +666,7 @@ impl<K: DataKeySource + IndexKeySource> StackCipherBuilder<K> {
}

/// Build the local HMAC-SHA256 PRF from a per-keyset
/// [`IndexKey`](stack_kms::IndexKey), wiping the intermediate stack copy of the
/// [`IndexKey`](crate::kms::IndexKey), wiping the intermediate stack copy of the
/// raw key bytes.
fn hmac_prf_from_index_key(index_key: &stack_kms::IndexKey) -> vitaminc_hmac::HmacSha256Prf {
use zeroize::Zeroize;
Expand Down
2 changes: 1 addition & 1 deletion packages/stack-encrypt/src/descriptor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ impl Descriptor {
pub const SEPARATOR: char = '|';

/// The longest descriptor ZeroKMS accepts, in bytes of the rendered
/// string: the protocol's [`MAX_DESCRIPTOR_LEN`](stack_kms::MAX_DESCRIPTOR_LEN).
/// string: the protocol's [`MAX_DESCRIPTOR_LEN`](crate::kms::MAX_DESCRIPTOR_LEN).
/// ZeroKMS derives key material over a fixed block of that size holding
/// the descriptor, so a longer one cannot be bound. Every data-key
/// request checks its descriptors against this before anything is sent
Expand Down
2 changes: 1 addition & 1 deletion packages/stack-encrypt/src/dynamic/record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ pub fn plan(value: FfiValue) -> Result<Plan, Error> {
/// ```
/// use stack_encrypt::dynamic::{record, FfiValue, Scope};
/// use stack_encrypt::StackCipher;
/// use stack_kms::FakeDataKeySource;
/// use stack_encrypt::kms::FakeDataKeySource;
///
/// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
/// let cipher = StackCipher::builder()
Expand Down
2 changes: 1 addition & 1 deletion packages/stack-encrypt/src/dynamic/term.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ impl Scalar {
/// ```
/// use stack_encrypt::dynamic::{context, term, FfiValue, Scalar, TermKind};
/// use stack_encrypt::StackCipher;
/// use stack_kms::FakeDataKeySource;
/// use stack_encrypt::kms::FakeDataKeySource;
///
/// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
/// let cipher = StackCipher::builder()
Expand Down
23 changes: 15 additions & 8 deletions packages/stack-encrypt/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ assert_eq!(plaintext, "secret message");
//! id) so it cannot be replayed elsewhere:
//!
//! ```no_run
//! # async fn example<K: stack_kms::DataKeySource>(cipher: stack_encrypt::StackCipher<K>) -> Result<(), stack_encrypt::Error> {
//! # async fn example<K: stack_encrypt::kms::DataKeySource>(cipher: stack_encrypt::StackCipher<K>) -> Result<(), stack_encrypt::Error> {
//! # let keyset = cipher.default_keyset();
//! let ct = keyset.encrypt("4111 1111 1111 1111", "users/42/card").await?;
//! let card: String = cipher.decrypt(ct, "users/42/card").await?; // ok
Expand Down Expand Up @@ -124,14 +124,14 @@ the deployment holds no long-lived CipherStash credential of its own. What
`AutoStrategy` detects is only the two above — access key, then profile — so
any other strategy is named explicitly, and that is what
[`kms`](StackCipherBuilder::kms) is for: build the
[`StackKms`](stack_kms::StackKms) over the strategy you want and hand it to
[`StackKms`](crate::kms::StackKms) over the strategy you want and hand it to
the builder.

```no_run
# async fn example() -> Result<(), Box<dyn std::error::Error>> {
use stack_auth::{AuthError, AuthStrategyFn, SecretToken, ServiceToken};
use stack_encrypt::StackCipher;
use stack_kms::{EnvKeyProvider, StackKmsBuilder};
use stack_encrypt::kms::{EnvKeyProvider, StackKmsBuilder};

// Any `AuthStrategy` goes in this slot — `AccessKeyStrategy`,
// `OidcFederationStrategy`, `DeviceSessionStrategy`, or, as here,
Expand Down Expand Up @@ -174,20 +174,21 @@ endpoint — are `StackKmsBuilder`'s, and the two keyset-cache knobs are
//!
//! # Testing without ZeroKMS
//!
//! `stack_kms::FakeDataKeySource` is an in-memory stub that needs no
//! `stack_encrypt::kms::FakeDataKeySource` is an in-memory stub that needs no
//! credentials or network: it hands out a fresh random data key per request and
//! remembers it in memory, so a `generate` followed by the matching `retrieve`
//! round-trips within one process (the key material itself differs run to run,
//! and nothing survives the process). It models none of ZeroKMS's
//! authorization behaviour (context, identity claims, decryption policies) —
//! those are the service's, and tests of them belong against a real ZeroKMS.
//! It lives behind stack-kms's `test-support` feature, so add
//! `stack-kms = { version = "..", features = ["test-support"] }` to your
//! `[dev-dependencies]`:
//! It lives behind this crate's `test-support` feature, so add
//! `stack-encrypt = { version = "..", features = ["test-support"] }` to your
//! `[dev-dependencies]`. stack-kms is re-exported as [`kms`], so
//! there is no separate stack-kms dependency to keep in step:
//!
//! ```
//! use stack_encrypt::StackCipher;
//! use stack_kms::FakeDataKeySource;
//! use stack_encrypt::kms::FakeDataKeySource;
//!
//! # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
//! let cipher = StackCipher::builder()
Expand Down Expand Up @@ -283,6 +284,12 @@ pub use cipher::{
};
pub use descriptor::Descriptor;
pub use keyset::KeysetCipher;
// stack-kms is a public dependency: `StackCipher` is generic over its
// `DataKeySource`, and `StackCipherBuilder::kms` takes its `StackKms`. It is
// versioned on its own (release-plz.toml), so a caller reaches it through
// here and always gets the version this crate was built against, never a
// second copy whose types do not fit `StackCipher`'s bounds.
pub use stack_kms as kms;
pub use target::{
CallerContext, CipherScope, DecryptField, DecryptFrom, DecryptInto, Decryptable, Decryption,
EncryptFrom, EncryptInto, Encryption, Pending, PendingFuture, Request, Responses,
Expand Down
4 changes: 2 additions & 2 deletions packages/stack-encrypt/src/sem/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@
//!
//! The backend today is the local
//! [`HmacSha256Prf`] — keyed by the
//! deterministic per-keyset [`IndexKey`](stack_kms::IndexKey) from
//! [`stack_kms::IndexKeySource`], loaded when the keyset is selected — so
//! deterministic per-keyset [`IndexKey`](crate::kms::IndexKey) from
//! [`kms::IndexKeySource`](crate::kms::IndexKeySource), loaded when the keyset is selected — so
//! every derivation completes with no I/O and an [`EncryptFrom`](crate::EncryptFrom) term
//! carries **no requests** in its [`Pending`]. That is the backend's
//! property, not the API's: the term is a `Pending` either way. The next
Expand Down
Loading
Loading