Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
4b705c1
feat(cli): implement Kubernetes ExecCredential plugin authentication …
Jul 23, 2026
d71690c
feat(cli): enforce SLSA provenance environment detection and release …
Jul 23, 2026
f047847
feat(mcp): enforce UsdcNetwork enum for parse-dont-validate payment m…
Jul 23, 2026
d8efb83
refactor(mcp): thread-safe panic-free CapSec root initialization in M…
Jul 23, 2026
1a1ebd1
feat(daemon): implement InvalidPayloadEncoding error semantics and pa…
Jul 23, 2026
53f616d
feat(pam): implement constant-time challenge verification, zeroizatio…
Jul 23, 2026
17d5950
refactor(sdk): transition AgentGuard workflow to integer cents budget…
Jul 23, 2026
c9040df
feat(sdk): enforce historical point-in-time producer authority verifi…
Jul 23, 2026
ae86db9
feat(verifier): enforce typed KeriEvent struct and sequence jump chec…
Jul 23, 2026
1e8d417
feat(witness): implement WitnessKeyProvider trait with KeriPublicKey …
Jul 23, 2026
fa0bf4c
feat(witness): implement WitnessNotFoundResponse and fail-closed 404 …
Jul 23, 2026
c0d9810
build(deps): update Cargo.lock for new crate dependencies
Jul 23, 2026
43a4b5a
fix(clippy): collapse nested if statements and fix unwrap / dead_code…
Jul 23, 2026
5521897
fix(clippy): read code field in witness 404 verification and apply ca…
Jul 23, 2026
f228f23
style: format workspace with cargo fmt
Jul 23, 2026
a45f3d2
docs(daemon): update api-spec.yaml for invalid-payload-encoding error…
Jul 23, 2026
7c8f015
ci(release): add SLSA L3 provenance generation and artifact upload steps
Jul 23, 2026
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
18 changes: 18 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,23 @@ jobs:
--log sigstore-rekor `
--note "Release ${{ github.ref_name }}"

- name: Generate SLSA L3 Provenance (Unix)
if: matrix.ext == '.tar.gz'
run: |
FILE="${{ matrix.asset_name }}${{ matrix.ext }}"
./staging/auths slsa generate \
--artifact "$FILE" \
--output "$FILE.slsa.json"

- name: Generate SLSA L3 Provenance (Windows)
if: matrix.ext == '.zip'
shell: pwsh
run: |
$file = "${{ matrix.asset_name }}${{ matrix.ext }}"
.\staging\auths.exe slsa generate `
--artifact $file `
--output "$file.slsa.json"

# The ephemeral attestation anchors HEAD, whose trust chain ends at the
# maintainer's KEL. Resolve that KEL from the committed identity bundle
# (.auths/ci-bundle.json) — the same stateless bundle the commit-verify
Expand Down Expand Up @@ -234,6 +251,7 @@ jobs:
${{ matrix.asset_name }}${{ matrix.ext }}
${{ matrix.asset_name }}${{ matrix.ext }}.sha256
${{ matrix.asset_name }}${{ matrix.ext }}.auths.json
${{ matrix.asset_name }}${{ matrix.ext }}.slsa.json
auths-mcp-gateway-${{ matrix.node_platform }}${{ matrix.ext }}
auths-mcp-gateway-${{ matrix.node_platform }}${{ matrix.ext }}.sha256

Expand Down
5 changes: 5 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 25 additions & 10 deletions crates/auths-cli/src/commands/kubectl.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,39 @@
use anyhow::Result;
use anyhow::{Context, Result};
use auths_rp::Audience;
use auths_sdk::workflows::auth::create_k8s_exec_credential;
use chrono::{Duration, Utc};
use clap::Args;
use serde_json::json;

#[derive(Args, Debug)]
pub struct KubectlTokenArgs {
/// Kubernetes cluster name or identifier
#[arg(short, long)]
pub cluster: String,

/// Key alias to sign presentation token
#[arg(short, long)]
pub key: Option<String>,

/// Expiration TTL in seconds (default 3600 = 1 hour)
#[arg(long, default_value_t = 3600)]
pub ttl_seconds: i64,
}

/// Executes Kubernetes client exec credential plugin authentication for `kubectl`.
pub async fn run_kubectl_token(args: KubectlTokenArgs) -> Result<()> {
let response = json!({
"apiVersion": "client.authentication.k8s.io/v1beta1",
"kind": "ExecCredential",
"status": {
"token": format!("auths-presentation-token-for-{}", args.cluster),
"expirationTimestamp": "2030-01-01T00:00:00Z"
}
});
let aud_str = format!("k8s:cluster:{}", args.cluster.trim());
let cluster_aud = Audience::parse(&aud_str).context("Invalid Kubernetes cluster audience")?;
let key_alias = args.key.as_deref().unwrap_or("main");
let now = Utc::now();

let response = create_k8s_exec_credential(
"did:keri:local",
&cluster_aud,
key_alias,
Duration::seconds(args.ttl_seconds),
now,
)
.context("Failed to generate Auths Kubernetes ExecCredential token")?;

println!("{}", serde_json::to_string_pretty(&response)?);
Ok(())
Expand Down
69 changes: 61 additions & 8 deletions crates/auths-cli/src/commands/slsa.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use anyhow::{Context, Result};
use auths_sdk::domains::signing::ci_env::{CiEnvironment, CiPlatform, detect_ci_environment};
use clap::Args;
use serde_json::json;

Expand All @@ -11,29 +12,81 @@ pub struct SlsaGenerateArgs {
/// Output SLSA provenance JSON file path
#[arg(short, long, default_value = "provenance.slsa.json")]
pub output: String,

/// Force SLSA level tag (default auto-detect: L3 in verified CI, L1 in local dev)
#[arg(long)]
pub level: Option<u8>,
}

/// Strongly-typed SLSA provenance level.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SlsaLevel {
L1,
L3,
}

impl SlsaLevel {
pub fn resolve(requested: Option<u8>, ci_env: Option<&CiEnvironment>) -> Result<Self> {
let is_github = matches!(ci_env, Some(env) if env.platform == CiPlatform::GithubActions);

match (requested, is_github) {
(Some(3), false) => {
anyhow::bail!(
"SLSA Level 3 provenance requires a verified isolated CI runner (GitHub Actions with OIDC token)"
);
}
(Some(3), true) => Ok(SlsaLevel::L3),
(Some(1), _) => Ok(SlsaLevel::L1),
(Some(other), _) => anyhow::bail!("Unsupported SLSA level: {}", other),
(None, true) => Ok(SlsaLevel::L3),
(None, false) => Ok(SlsaLevel::L1),
}
}

pub fn build_type(&self) -> &'static str {
match self {
SlsaLevel::L1 => "https://auths.dev/build-types/slsa-l1/v1",
SlsaLevel::L3 => "https://auths.dev/build-types/slsa-l3/v1",
}
}
}

/// Generates an in-toto SLSA Level 3 provenance statement for a release artifact.
/// Generates an in-toto SLSA provenance statement for a release artifact.
pub async fn run_slsa_generate(args: SlsaGenerateArgs) -> Result<()> {
let artifact_bytes = std::fs::read(&args.artifact)
.with_context(|| format!("Failed to read artifact file at {}", args.artifact))?;

let digest = hex::encode(ring::digest::digest(&ring::digest::SHA256, &artifact_bytes).as_ref());

let ci_env = detect_ci_environment();
let level = SlsaLevel::resolve(args.level, ci_env.as_ref())?;

let builder_id = match ci_env.as_ref() {
Some(env) if env.platform == CiPlatform::GithubActions => {
format!(
"{}/{}",
std::env::var("GITHUB_SERVER_URL").unwrap_or_default(),
env.repository.as_deref().unwrap_or_default()
)
}
_ => "https://auths.dev/builder/local".to_string(),
};

let provenance = json!({
"_type": "https://in-toto.io/Statement/v0.1",
"predicateType": "https://slsa.dev/provenance/v0.2",
"subject": [{
"name": args.artifact,
"digest": { "sha256": digest }
}],
"builder": {
"id": "https://auths.dev/builder/v1"
},
"buildType": "https://auths.dev/build-types/slsa-l3/v1",
"builder": { "id": builder_id },
"buildType": level.build_type(),
"invocation": {
"configSource": {
"uri": "git+https://github.com/auths-dev/auths",
"uri": match ci_env.as_ref().and_then(|e| e.repository.as_deref()) {
Some(repo) => format!("git+https://github.com/{}", repo),
None => "local".into(),
},
"entryPoint": "auths slsa generate"
}
}
Expand All @@ -49,8 +102,8 @@ pub async fn run_slsa_generate(args: SlsaGenerateArgs) -> Result<()> {
)?;

println!(
"SLSA Level 3 provenance statement generated: {}",
args.output
"SLSA Level {:?} provenance statement generated: {}",
level, args.output
);
Ok(())
}
40 changes: 28 additions & 12 deletions crates/auths-core/src/witness/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -998,11 +998,21 @@ async fn submit_event(
}
}

use axum::response::IntoResponse;

/// Typed 404 response for identity lookups.
#[derive(Debug, Serialize, Deserialize)]
pub struct WitnessNotFoundResponse {
pub error: String,
pub prefix: String,
pub code: u32,
}

/// GET /witness/:prefix/head - Get the latest observed sequence.
async fn get_head(
State(state): State<WitnessServerState>,
AxumPath(prefix_str): AxumPath<String>,
) -> Result<Json<HeadResponse>, (StatusCode, Json<ErrorResponse>)> {
) -> Result<Json<HeadResponse>, axum::response::Response> {
let prefix = Prefix::new_unchecked(prefix_str);
let storage = state.inner.storage.lock().map_err(|_| {
(
Expand All @@ -1012,30 +1022,36 @@ async fn get_head(
duplicity: None,
}),
)
.into_response()
})?;

match storage.get_latest_seq(&prefix) {
Ok(Some(seq)) => Ok(Json(HeadResponse {
prefix,
latest_seq: Some(seq),
})),
// An unheld prefix has no head: 404 (absent), matching `/key-state`,
// rather than a 200 with `latest_seq: null` a careless reader could
// misread as "sequence 0". (product-findings 20260721-node, N1.)
Ok(None) => Err((
StatusCode::NOT_FOUND,
Json(ErrorResponse {
error: format!("no head for {prefix} — this witness holds no events for it"),
duplicity: None,
}),
)),
// An unheld prefix has no head: 404 (absent), carrying WitnessNotFoundResponse + header
Ok(None) => {
let witness_id = state.witness_did();
let payload = WitnessNotFoundResponse {
error: "identity_not_found".to_string(),
prefix: prefix.as_str().to_string(),
code: 4041,
};
let mut res = (StatusCode::NOT_FOUND, Json(payload)).into_response();
if let Ok(val) = axum::http::HeaderValue::from_str(witness_id) {
res.headers_mut().insert("X-Auths-Witness-Id", val);
}
Err(res)
}
Err(e) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: format!("storage error: {}", e),
duplicity: None,
}),
)),
)
.into_response()),
}
}

Expand Down
29 changes: 28 additions & 1 deletion crates/auths-infra-http/src/async_witness_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,14 @@ impl HttpAsyncWitnessClient {
}
}

#[derive(Debug, Deserialize)]
pub struct WitnessNotFoundResponse {
pub error: String,
pub prefix: String,
#[allow(dead_code)]
pub code: u32,
}

#[async_trait]
impl AsyncWitnessProvider for HttpAsyncWitnessClient {
async fn submit_event(
Expand Down Expand Up @@ -218,7 +226,26 @@ impl AsyncWitnessProvider for HttpAsyncWitnessClient {
})?;

if response.status() == reqwest::StatusCode::NOT_FOUND {
return Ok(None);
let has_witness_header = response.headers().contains_key("X-Auths-Witness-Id");
let body_text = response.text().await.unwrap_or_default();

let is_valid_witness_404 = serde_json::from_str::<WitnessNotFoundResponse>(&body_text)
.map(|parsed| {
parsed.error == "identity_not_found"
&& parsed.prefix == prefix.as_str()
&& parsed.code == 4041
})
.unwrap_or(false);

if has_witness_header && is_valid_witness_404 {
return Ok(None);
} else {
return Err(WitnessError::Network(format!(
"Ambiguous 404 response from {} (failed to parse WitnessNotFoundResponse for prefix {}): failing closed to prevent downgrade",
url,
prefix.as_str()
)));
}
}

if !response.status().is_success() {
Expand Down
Loading
Loading