Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
68 changes: 34 additions & 34 deletions contracts/shapes.ttl
Original file line number Diff line number Diff line change
Expand Up @@ -1983,11 +1983,11 @@
] ;
sh:property [
sh:path <https://ont.paiml.dev/v1alpha1/model/parityReceipt> ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/ladder-green/node> ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/ladder-green/node/parityReceipt> ;
] ;
.

<https://ont.paiml.dev/v1alpha1/shape/ladder-green/node> a sh:NodeShape ;
<https://ont.paiml.dev/v1alpha1/shape/ladder-green/node/parityReceipt> a sh:NodeShape ;
sh:property [
sh:path <https://ont.paiml.dev/v1alpha1/model/cc> ;
sh:in ( "8.9" "12.1" ) ;
Expand Down Expand Up @@ -2067,11 +2067,11 @@
sh:maxCount 1 ;
sh:class <https://ont.paiml.dev/v1alpha1/parity/Comparator> ;
sh:nodeKind sh:IRI ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/parity-receipt-complete/node> ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/parity-receipt-complete/node/comparator> ;
] ;
.

<https://ont.paiml.dev/v1alpha1/shape/parity-receipt-complete/node> a sh:NodeShape ;
<https://ont.paiml.dev/v1alpha1/shape/parity-receipt-complete/node/comparator> a sh:NodeShape ;
sh:property [
sh:path <https://ont.paiml.dev/v1alpha1/parity/kind> ;
sh:minCount 1 ;
Expand All @@ -2084,11 +2084,11 @@
sh:targetClass <https://ont.paiml.dev/v1alpha1/parity/SelfComparedReceipt> ;
sh:property [
sh:path <https://ont.paiml.dev/v1alpha1/parity/comparator> ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/parity-comparator-self/node> ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/parity-comparator-self/node/comparator> ;
] ;
.

<https://ont.paiml.dev/v1alpha1/shape/parity-comparator-self/node> a sh:NodeShape ;
<https://ont.paiml.dev/v1alpha1/shape/parity-comparator-self/node/comparator> a sh:NodeShape ;
sh:property [
sh:path <https://ont.paiml.dev/v1alpha1/parity/reason> ;
sh:minCount 1 ;
Expand All @@ -2103,11 +2103,11 @@
sh:targetClass <https://ont.paiml.dev/v1alpha1/parity/OracleComparedReceipt> ;
sh:property [
sh:path <https://ont.paiml.dev/v1alpha1/parity/comparator> ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/parity-comparator-oracle/node> ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/parity-comparator-oracle/node/comparator> ;
] ;
.

<https://ont.paiml.dev/v1alpha1/shape/parity-comparator-oracle/node> a sh:NodeShape ;
<https://ont.paiml.dev/v1alpha1/shape/parity-comparator-oracle/node/comparator> a sh:NodeShape ;
sh:property [
sh:path <https://ont.paiml.dev/v1alpha1/parity/comparatorSha> ;
sh:minCount 1 ;
Expand Down Expand Up @@ -2194,7 +2194,7 @@
sh:minCount 1 ;
sh:maxCount 1 ;
sh:nodeKind sh:IRI ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.release/node> ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.release/node/dogfoodReceipt> ;
] ;
sh:property [
sh:path <https://ont.paiml.dev/v1alpha1/release/surface> ;
Expand All @@ -2217,7 +2217,7 @@
] ;
.

<https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.release/node> a sh:NodeShape ;
<https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.release/node/dogfoodReceipt> a sh:NodeShape ;
sh:property [
sh:path <https://ont.paiml.dev/v1alpha1/release/verdict> ;
sh:minCount 1 ;
Expand All @@ -2239,7 +2239,7 @@
sh:minCount 1 ;
sh:maxCount 1 ;
sh:nodeKind sh:IRI ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.host/node> ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.host/node/hostReceipt> ;
] ;
sh:property [
sh:path <https://ont.paiml.dev/v1alpha1/release/holds> ;
Expand All @@ -2257,7 +2257,7 @@
sh:path <https://ont.paiml.dev/v1alpha1/release/kernelReceipt> ;
sh:minCount 1 ;
sh:nodeKind sh:IRI ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.host/node> ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.host/node/kernelReceipt> ;
] ;
sh:property [
sh:path <https://ont.paiml.dev/v1alpha1/release/dispatchKernel> ;
Expand All @@ -2269,7 +2269,7 @@
] ;
.

<https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.host/node> a sh:NodeShape ;
<https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.host/node/hostReceipt> a sh:NodeShape ;
sh:property [
sh:path <https://ont.paiml.dev/v1alpha1/release/schema> ;
sh:minCount 1 ;
Expand All @@ -2283,7 +2283,7 @@
] ;
.

<https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.host/node> a sh:NodeShape ;
<https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.host/node/kernelReceipt> a sh:NodeShape ;
sh:property [
sh:path <https://ont.paiml.dev/v1alpha1/release/fresh> ;
sh:minCount 1 ;
Expand Down Expand Up @@ -2347,11 +2347,11 @@
sh:path <https://ont.paiml.dev/v1alpha1/release/row> ;
sh:minCount 1 ;
sh:nodeKind sh:IRI ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.tokenizer/node> ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.tokenizer/node/row> ;
] ;
.

<https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.tokenizer/node> a sh:NodeShape ;
<https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.tokenizer/node/row> a sh:NodeShape ;
sh:property [
sh:path <https://ont.paiml.dev/v1alpha1/release/verdict> ;
sh:minCount 1 ;
Expand Down Expand Up @@ -2392,11 +2392,11 @@
sh:path <https://ont.paiml.dev/v1alpha1/release/row> ;
sh:minCount 1 ;
sh:nodeKind sh:IRI ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.kernel/node> ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.kernel/node/row> ;
] ;
.

<https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.kernel/node> a sh:NodeShape ;
<https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.kernel/node/row> a sh:NodeShape ;
sh:property [
sh:path <https://ont.paiml.dev/v1alpha1/release/verdict> ;
sh:minCount 1 ;
Expand Down Expand Up @@ -2448,11 +2448,11 @@
sh:path <https://ont.paiml.dev/v1alpha1/release/cruxCell> ;
sh:minCount 1 ;
sh:nodeKind sh:IRI ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.crux/node> ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.crux/node/cruxCell> ;
] ;
.

<https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.crux/node> a sh:NodeShape ;
<https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.crux/node/cruxCell> a sh:NodeShape ;
sh:property [
sh:path <https://ont.paiml.dev/v1alpha1/release/verdict> ;
sh:minCount 1 ;
Expand Down Expand Up @@ -2511,11 +2511,11 @@
sh:minCount 1 ;
sh:maxCount 1 ;
sh:nodeKind sh:IRI ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.refusal/node> ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.refusal/node/row> ;
] ;
.

<https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.refusal/node> a sh:NodeShape ;
<https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.refusal/node/row> a sh:NodeShape ;
sh:property [
sh:path <https://ont.paiml.dev/v1alpha1/release/verdict> ;
sh:minCount 1 ;
Expand Down Expand Up @@ -2543,11 +2543,11 @@
sh:minCount 1 ;
sh:maxCount 1 ;
sh:nodeKind sh:IRI ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.probe/node> ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.probe/node/row> ;
] ;
.

<https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.probe/node> a sh:NodeShape ;
<https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.probe/node/row> a sh:NodeShape ;
sh:property [
sh:path <https://ont.paiml.dev/v1alpha1/release/verdict> ;
sh:minCount 1 ;
Expand All @@ -2569,11 +2569,11 @@
sh:minCount 1 ;
sh:maxCount 1 ;
sh:nodeKind sh:IRI ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.model-cell/node> ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.model-cell/node/row> ;
] ;
.

<https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.model-cell/node> a sh:NodeShape ;
<https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.model-cell/node/row> a sh:NodeShape ;
sh:property [
sh:path <https://ont.paiml.dev/v1alpha1/release/verdict> ;
sh:minCount 1 ;
Expand All @@ -2595,11 +2595,11 @@
sh:minCount 1 ;
sh:maxCount 1 ;
sh:nodeKind sh:IRI ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.effect-cell/node> ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.effect-cell/node/row> ;
] ;
.

<https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.effect-cell/node> a sh:NodeShape ;
<https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1.effect-cell/node/row> a sh:NodeShape ;
sh:property [
sh:path <https://ont.paiml.dev/v1alpha1/release/verdict> ;
sh:minCount 1 ;
Expand All @@ -2626,11 +2626,11 @@
sh:minCount 1 ;
sh:maxCount 1 ;
sh:nodeKind sh:IRI ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1/node> ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1/node/row> ;
] ;
.

<https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1/node> a sh:NodeShape ;
<https://ont.paiml.dev/v1alpha1/shape/release-readiness-v1/node/row> a sh:NodeShape ;
sh:property [
sh:path <https://ont.paiml.dev/v1alpha1/release/verdict> ;
sh:minCount 1 ;
Expand Down Expand Up @@ -2721,7 +2721,7 @@
sh:minCount 1 ;
sh:class <https://ont.paiml.dev/v1alpha1/release/KernelParityCell> ;
sh:nodeKind sh:IRI ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/release-readiness-v2.model/node> ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/release-readiness-v2.model/node/usesKernel> ;
] ;
sh:property [
sh:path <https://ont.paiml.dev/v1alpha1/release/unregisteredQtype> ;
Expand All @@ -2733,7 +2733,7 @@
] ;
.

<https://ont.paiml.dev/v1alpha1/shape/release-readiness-v2.model/node> a sh:NodeShape ;
<https://ont.paiml.dev/v1alpha1/shape/release-readiness-v2.model/node/usesKernel> a sh:NodeShape ;
sh:property [
sh:path <https://ont.paiml.dev/v1alpha1/release/verdict> ;
sh:minCount 1 ;
Expand Down Expand Up @@ -2768,11 +2768,11 @@
sh:maxCount 1 ;
sh:class <https://ont.paiml.dev/v1alpha1/release/SmokeCell> ;
sh:nodeKind sh:IRI ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/release-readiness-v2.model-smoke/node> ;
sh:node <https://ont.paiml.dev/v1alpha1/shape/release-readiness-v2.model-smoke/node/smoke> ;
] ;
.

<https://ont.paiml.dev/v1alpha1/shape/release-readiness-v2.model-smoke/node> a sh:NodeShape ;
<https://ont.paiml.dev/v1alpha1/shape/release-readiness-v2.model-smoke/node/smoke> a sh:NodeShape ;
sh:property [
sh:path <https://ont.paiml.dev/v1alpha1/release/verdict> ;
sh:minCount 1 ;
Expand Down
121 changes: 101 additions & 20 deletions crates/apr-cli/src/commands/serve/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
use sha2::{Digest, Sha256};
use subtle::ConstantTimeEq;

/// Digest held by [`AuthGate::deny_all`]: all zeros, which no SHA-256 preimage
/// is known to produce, so no bearer token matches it.
const DENY_ALL_HASH: [u8; 32] = [0u8; 32];

/// Configured authentication state for an `apr serve` instance.
///
/// `expected_hash == None` means auth is disabled — every request passes.
Expand Down Expand Up @@ -46,31 +50,60 @@ impl AuthGate {
Self::from_hash(sha256_32(key.as_bytes()))
}

/// A gate that is enabled and rejects every request. Fail-closed stand-in
/// for a configuration that was set but unusable (#5479): the digest is
/// not reachable from any bearer token.
#[must_use]
pub fn deny_all() -> Self {
Self {
expected_hash: Some(DENY_ALL_HASH),
}
}

/// Pure core of [`Self::try_from_env`]. An empty value counts as unset.
/// A non-empty `APR_API_KEY_HASH` must be 64 hex chars, else `Err` — a
/// plaintext key never papers over a malformed hash.
///
/// # Errors
///
/// Returns the reason when `hash` is set, non-empty and not a SHA-256 hex digest.
pub fn from_values(hash: Option<&str>, plain: Option<&str>) -> Result<Self, &'static str> {
if let Some(hex) = hash.filter(|h| !h.is_empty()) {
return decode_hex_32(hex).map(Self::from_hash);
}
match plain.filter(|p| !p.is_empty()) {
Some(plain) => Ok(Self::from_plain_key(plain)),
None => Ok(Self::disabled()),
}
}

/// Read configuration from env. `APR_API_KEY_HASH` (hex-encoded
/// SHA-256) wins over `APR_API_KEY` (plaintext, hashed in-process).
/// Neither set → disabled with a single stderr warning.
///
/// # Errors
///
/// A set, non-empty, malformed `APR_API_KEY_HASH` refuses to start.
pub fn try_from_env() -> Result<Self, &'static str> {
let hash = std::env::var("APR_API_KEY_HASH").ok();
let plain = std::env::var("APR_API_KEY").ok();
let gate = Self::from_values(hash.as_deref(), plain.as_deref())?;
if !gate.is_enabled() {
eprintln!(
"[apr serve] WARNING: no APR_API_KEY or APR_API_KEY_HASH set; HTTP routes are unauthenticated",
);
}
Ok(gate)
}

/// [`Self::try_from_env`] for callers that cannot return an error: a
/// malformed `APR_API_KEY_HASH` yields [`Self::deny_all`], never an open gate.
#[must_use]
pub fn from_env() -> Self {
if let Ok(hex) = std::env::var("APR_API_KEY_HASH") {
match decode_hex_32(&hex) {
Ok(bytes) => return Self::from_hash(bytes),
Err(reason) => {
eprintln!(
"[apr serve] APR_API_KEY_HASH set but {reason}; ignoring (auth disabled)",
);
return Self::disabled();
}
}
}
if let Ok(plain) = std::env::var("APR_API_KEY") {
if !plain.is_empty() {
return Self::from_plain_key(&plain);
}
}
eprintln!(
"[apr serve] WARNING: no APR_API_KEY or APR_API_KEY_HASH set; HTTP routes are unauthenticated",
);
Self::disabled()
Self::try_from_env().unwrap_or_else(|reason| {
eprintln!("[apr serve] {reason}; rejecting all requests (fail closed)");
Self::deny_all()
})
}

/// True iff the gate has a configured hash.
Expand Down Expand Up @@ -251,4 +284,52 @@ mod tests {
bad.replace_range(0..1, "Z");
assert!(decode_hex_32(&bad).is_err());
}

// #5479: a bad APR_API_KEY_HASH must never yield an open gate.
#[test]
fn from_values_malformed_hash_is_an_error_not_a_disabled_gate() {
for bad in ["zz", "abc", &"g".repeat(64), &"a".repeat(63), " "] {
assert!(
AuthGate::from_values(Some(bad), None).is_err(),
"malformed hash {bad:?} must be refused"
);
assert!(
AuthGate::from_values(Some(bad), Some("secret")).is_err(),
"a plaintext key must not paper over malformed hash {bad:?}"
);
}
}

#[test]
fn from_values_empty_hash_is_unset_so_the_plain_key_applies() {
let g = AuthGate::from_values(Some(""), Some("secret")).expect("empty hash is unset");
assert!(g.is_enabled());
assert!(g.check_bearer(Some("Bearer secret")));
assert!(!g.check_bearer(None));
}

#[test]
fn from_values_valid_hash_wins_over_plain_key() {
let hex = "a".repeat(64);
let g = AuthGate::from_values(Some(&hex), Some("secret")).expect("valid hash");
assert!(g.is_enabled());
assert!(!g.check_bearer(Some("Bearer secret")));
}

#[test]
fn from_values_neither_set_is_disabled() {
assert!(!AuthGate::from_values(None, None).expect("ok").is_enabled());
assert!(!AuthGate::from_values(Some(""), Some(""))
.expect("ok")
.is_enabled());
}

#[test]
fn deny_all_gate_rejects_every_request() {
let g = AuthGate::deny_all();
assert!(g.is_enabled());
for h in [None, Some("Bearer "), Some("Bearer x"), Some("garbage")] {
assert!(!g.check_bearer(h));
}
}
}
Loading
Loading