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
1 change: 1 addition & 0 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion crates/busbar-a2a/src/a2a/card.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ pub(crate) fn parse(card: &Value) -> Result<AgentCard, CardError> {
/// THE ONE DIGEST RENDERING THIS PLANE USES: `sha256/<standard base64>`.
///
/// Over BYTES rather than over `&str`, because the transport-layer pin
/// ([`super::key_info::pin_hash`]) hashes a DER structure and a second rendering written for it would
/// (the connector's `tls::spki::pin`) hashes a DER structure and a second rendering written for it would
/// be a second spelling of one operator-facing value. An operator comparing a configured pin
/// against an audit row, and comparing either against what `openssl dgst -sha256 -binary | base64`
/// printed, has to be comparing one spelling.
Expand Down
39 changes: 0 additions & 39 deletions crates/busbar-a2a/src/a2a/key_info.rs

This file was deleted.

1 change: 0 additions & 1 deletion crates/busbar-a2a/src/a2a/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -623,7 +623,6 @@ pub(crate) mod words;
// refresh timer drives the same `due` this one does. See the standing rule: unify the duplicate
// before a second copy can drift from the first.
pub(crate) use busbar_kernel::trust::reverify;
pub(crate) mod key_info;
pub mod serve;
pub(crate) mod sign;
pub use busbar_plane_a2a::a2a::task;
Expand Down
2 changes: 1 addition & 1 deletion crates/busbar-a2a/src/a2a/tests/egress_seam_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
//! so a self-signed test CA cannot be reached through the seam — the same reason `egress_tests` uses
//! plaintext). The peer-SPKI dimension is therefore `None == None` here; its byte-identity over TLS is
//! by CONSTRUCTION — the host and the plane decode the pin through the one shared
//! `busbar_kernel::plane_host::spki::pin` (`a2a::key_info::pin_hash` re-exports it), so there is no second spelling
//! connector `tls::spki::pin`, so there is no second spelling
//! to diverge. `client_identity_offered` is asserted directly (both compute `is_some()`).

use std::io::{Read, Write};
Expand Down
8 changes: 4 additions & 4 deletions crates/busbar-a2a/src/a2a/transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
//!
//! An unsigned agent card has no JWS root, and the only authenticity root left for it is the
//! certificate the endpoint proved it held the key for. So this transport records the leaf
//! certificate's SubjectPublicKeyInfo pin ([`super::key_info::pin_hash`]) on every TLS hop.
//! certificate's SubjectPublicKeyInfo pin (the connector's `tls::spki::pin`) on every TLS hop.
//!
//! **The certificate is read AFTER the ordinary verification, off a handshake that already
//! succeeded.** `reqwest`'s `tls_info` hands back the leaf of a connection the chain-and-name check
Expand Down Expand Up @@ -158,8 +158,8 @@ impl Resolver for TokioResolver {
//
// The hop no longer runs behind a `reqwest::Response` this plane holds — the host owns the socket and
// the verified handshake, and hands back the `sha256/…` pin on the seam's `seam::Buffered`, decoded
// from the SAME bytes `busbar_kernel::plane_host::spki::pin` produces (which `super::key_info::pin_hash`
// re-exports), so the pin string is byte-identical to the one this file used to compute. `None` on a
// from the SAME bytes the connector's `tls::spki::pin` produces, so the pin string is
// byte-identical to the one this file used to compute. `None` on a
// plaintext hop and `None` where the certificate cannot be read, unchanged: `super::verify` refuses
// a transport-pinned registration whose fetch produced no observed pin, because "we could not look"
// and "it matched" are the two answers a pin exists to keep apart.
Expand Down Expand Up @@ -394,7 +394,7 @@ impl ReqwestTransport {

// THE HOP, over the HOSTLESS egress seam. The host owns the socket, the pinned client (its own
// refusing resolver), the verified handshake and the capped read; it hands back the status, the
// `Location`, the peer SPKI pin (decoded from the same bytes `super::key_info::pin_hash` produces)
// `Location`, the peer SPKI pin (decoded from the same bytes the connector's `tls::spki::pin` produces)
// and whether busbar's own client identity was carried into the handshake. The deadline rides
// the desc (`timeout_ms`), the identity/trust anchors ride their opaque refs.
let buffered =
Expand Down
4 changes: 2 additions & 2 deletions crates/busbar-core-admin/src/v1/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1341,7 +1341,7 @@ fn plane_named_def_list(
busbar_kernel::plane::registry::plane_decl_for_config_section(section.key())
.and_then(|d| d.named_def_list)
.map_or_else(Vec::new, |f| {
f(app as &dyn busbar_kernel::plane_host::PlaneSlots)
f(app as &dyn busbar_kernel::plane::host::PlaneSlots)
})
}

Expand All @@ -1354,7 +1354,7 @@ fn plane_named_def_get(
) -> Option<NamedDefView> {
busbar_kernel::plane::registry::plane_decl_for_config_section(section.key())
.and_then(|d| d.named_def_get)
.and_then(|f| f(app as &dyn busbar_kernel::plane_host::PlaneSlots, name))
.and_then(|f| f(app as &dyn busbar_kernel::plane::host::PlaneSlots, name))
}

#[cfg(test)]
Expand Down
3 changes: 3 additions & 0 deletions crates/busbar-core-connector/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ webpki = { workspace = true }
zeroize = { workspace = true }
# STUN FINGERPRINT (CRC-32) on the dtls engine's consent probes.
crc32fast = { workspace = true }
# THE PEER'S SPKI PIN (`tls::spki`): the connector owns pinning (spec Part 3), and the pin's one
# spelling is `sha256/<standard base64>` of the leaf's SubjectPublicKeyInfo (the digest is ring's).
base64 = { workspace = true }

# THE HOST'S SOCKETS AND THEIR READINESS (`io`, `socket`, `compose`, `wire`): the connector owns every
# OS socket (BUSBAR-1.6.0.md THE DESIGN, §5). `tokio` is asked for its reactor registration
Expand Down
31 changes: 26 additions & 5 deletions crates/busbar-core-connector/src/tls/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,14 +231,35 @@ impl Pending {
///
/// The handshake failed; the error is the TLS stack's, unwrapped.
pub async fn run(self, tcp: TcpStream) -> io::Result<Secured> {
self.connector.connect(self.name, tcp).await.map(Secured)
self.connector
.connect(self.name, tcp)
.await
.map(Secured::over)
}
}

/// A secured client stream.
pub struct Secured(tokio_rustls::client::TlsStream<TcpStream>);
/// A secured client stream, and the SPKI pin of the leaf its verified handshake produced.
pub struct Secured(tokio_rustls::client::TlsStream<TcpStream>, Option<String>);

impl Secured {
/// The secured stream over `tls`, its peer pin computed ONCE, now: a leaf the DER walk refuses
/// (or no leaf) leaves the pin absent, never a pass.
fn over(tls: tokio_rustls::client::TlsStream<TcpStream>) -> Self {
let pin = tls
.get_ref()
.1
.peer_certificates()
.and_then(|certs| certs.first())
.and_then(|leaf| super::spki::pin(leaf.as_ref()).ok());
Self(tls, pin)
}

/// The `sha256/<base64>` SPKI pin of the peer's leaf ([`super::spki::pin`]), or `None`.
#[must_use]
pub fn peer_spki(&self) -> Option<&str> {
self.1.as_deref()
}

/// The protocol ALPN agreed.
#[must_use]
pub fn alpn(&self) -> Option<&[u8]> {
Expand Down Expand Up @@ -368,8 +389,8 @@ impl seam::SecuredIo for Secured {
fn alpn(&self) -> Option<&[u8]> {
Secured::alpn(self)
}
fn peer_leaf(&self) -> Option<&[u8]> {
Secured::peer_leaf(self)
fn peer_spki(&self) -> Option<&str> {
Secured::peer_spki(self)
}
fn tcp(&self) -> &TcpStream {
Secured::tcp(self)
Expand Down
18 changes: 12 additions & 6 deletions crates/busbar-core-connector/src/tls/engine_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ async fn the_mtls_fixture_accepts_the_engine_identity_and_records_its_leaf() {
assert_eq!(
peer_key_pin(&resp),
Some(
busbar_kernel::plane_host::spki::pin(&server.leaf_der)
crate::tls::spki::pin(&server.leaf_der)
.expect("server leaf")
.as_str()
),
Expand Down Expand Up @@ -584,7 +584,7 @@ async fn stack_b(
.extensions()
.get::<reqwest::tls::TlsInfo>()
.and_then(|t| t.peer_certificate())
.map(|der| busbar_kernel::plane_host::spki::pin(der).expect("walkable leaf"));
.map(|der| crate::tls::spki::pin(der).expect("walkable leaf"));
let body = resp.bytes().await.expect("body");
(
Outcome::Answered {
Expand Down Expand Up @@ -636,8 +636,7 @@ async fn known_leaf_tls_pin_and_sni_are_observed_and_webpki_refuses_the_private_
body: "over tls".to_string()
}
);
let expected_pin =
busbar_kernel::plane_host::spki::pin(&material.leaf_der).expect("fixture leaf");
let expected_pin = crate::tls::spki::pin(&material.leaf_der).expect("fixture leaf");
assert_eq!(
leaf_pin.as_deref(),
Some(expected_pin.as_str()),
Expand Down Expand Up @@ -869,14 +868,21 @@ async fn the_pin_off_a_real_handshake_is_the_serving_leafs_key_and_not_a_look_al
.await
.expect("the rooted hop answers");
assert_eq!(resp.status(), 200);
let expected = busbar_kernel::plane_host::spki::pin(&serving.leaf_der).expect("serving leaf");
let other = busbar_kernel::plane_host::spki::pin(&look_alike.leaf_der).expect("look-alike");
let expected = crate::tls::spki::pin(&serving.leaf_der).expect("serving leaf");
let other = crate::tls::spki::pin(&look_alike.leaf_der).expect("look-alike");
assert_ne!(expected, other, "two keys, two pins");
assert_eq!(
peer_key_pin(&resp),
Some(expected.as_str()),
"the pin is read off the handshake that served, so it is the serving leaf's key"
);
// RED ARM: a look-alike's pin (same name, another key) is not what this hop observed, so a
// registration pinned to it is refused.
assert_ne!(
peer_key_pin(&resp),
Some(other.as_str()),
"a look-alike key's pin never matches the observed one"
);
}

/// What a mutual-TLS far end decided about each connection: `Ok(n)` when the handshake completed
Expand Down
2 changes: 2 additions & 0 deletions crates/busbar-core-connector/src/tls/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ use std::sync::Arc;

pub mod client;
pub mod engine;
pub mod spki;
pub mod trust;

use busbar_contract::transport::wire::{ConnectionSecurity, RawIo, SecuredIoFut};
use busbar_kernel::config::secret::SecretResolver;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2026 Busbar Inc and contributors

//! THE TRANSPORT-LAYER IDENTITY, computed HOST-SIDE: a peer certificate's SubjectPublicKeyInfo,
//! hashed and rendered in the ONE canonical spelling the whole tree shares.
//! THE TRANSPORT-LAYER IDENTITY: a peer certificate's SubjectPublicKeyInfo, hashed and rendered in
//! the ONE canonical spelling the whole tree shares.
//!
//! This is the NEUTRAL host home of the DER walk that used to live only inside the a2a plane. It was
//! lifted here — not duplicated — for the egress seam: when the host owns the outbound hop it also
//! owns the certificate the handshake produced, and the host must be able to hand the plane the SAME
//! pin string the plane would have computed itself, byte for byte. A second copy of the walk would be
//! a second answer to "what is this key's pin", which is exactly the divergence a pin exists to
//! prevent. So there is ONE walk, here, and the a2a plane's `spki` module re-exports it.
//! The connector owns pinning, SPKI and mTLS (spec Part 3), so the one DER walk lives here, beside
//! the TLS wrap ([`super::engine::Layer`]) that computes each secured stream's pin at handshake time
//! and hands it to the kernel computed (`busbar_kernel::secure::SecuredIo::peer_spki`). A second copy
//! of the walk would be a second answer to "what is this key's pin", which is exactly the divergence
//! a pin exists to prevent.
//!
//! ## WHERE THE CERTIFICATE COMES FROM, and why this does not weaken anything
//!
//! The DER handed to [`pin`] is the leaf certificate of a handshake that ALREADY COMPLETED under the
//! ordinary chain-and-name check (the host egress chokepoint's pinned client, or the a2a transport). This
//! ordinary chain-and-name check (the wrap's own verified handshake). This
//! module is therefore an OBSERVATION of a connection somebody else already verified, never a
//! substitute for verifying one: a handshake that was not verified produces no response to read a
//! certificate off.
Expand Down Expand Up @@ -57,7 +56,12 @@ pub enum SpkiError {
/// The bytes ran out mid-element.
Truncated,
/// A tag that is not what RFC 5280 puts at this position.
UnexpectedTag { want: u8, got: u8 },
UnexpectedTag {
/// The tag RFC 5280 puts here.
want: u8,
/// The tag the bytes carry.
got: u8,
},
/// An indefinite length, a non-minimal length, or a length wider than any certificate needs. All
/// three are legal BER and none of them is DER, and each gives one certificate a second encoding —
/// which is a second pin for one key.
Expand Down Expand Up @@ -190,3 +194,7 @@ pub fn pin(cert_der: &[u8]) -> Result<String, SpkiError> {
let digest = ring::digest::digest(&ring::digest::SHA256, key_info);
Ok(format!("sha256/{}", B64.encode(digest.as_ref())))
}

#[cfg(test)]
#[path = "spki_tests.rs"]
mod tests;
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
//! when those two agree.

use super::*;
use base64::Engine as _;
use rcgen::{CertificateParams, KeyPair, PublicKeyData};
use sha2::{Digest, Sha256};

Expand All @@ -37,9 +36,9 @@ fn the_walk_lands_on_the_subject_public_key_info_and_not_on_the_member_beside_it
}

#[test]
fn the_pin_is_the_sha256_of_those_bytes_in_the_planes_one_digest_spelling() {
fn the_pin_is_the_sha256_of_those_bytes_in_the_one_digest_spelling() {
let (cert_der, expected_key_pin) = cert_and_its_key_pin();
let pin = pin_hash(&cert_der).expect("a pin");
let pin = pin(&cert_der).expect("a pin");
assert_eq!(
pin,
format!(
Expand All @@ -49,12 +48,6 @@ fn the_pin_is_the_sha256_of_those_bytes_in_the_planes_one_digest_spelling() {
"the pin is the operator-facing value; it must be exactly what `openssl … | openssl dgst \
-sha256 -binary | base64` prints, or nobody can obtain it out of band"
);
assert_eq!(
pin,
crate::a2a::card::sha256_tagged(&expected_key_pin),
"ONE digest rendering on this plane. A second spelling here is a second value an operator \
has to know is the same one."
);
}

#[test]
Expand All @@ -80,15 +73,15 @@ fn two_certificates_over_the_same_key_pin_identically_and_a_new_key_does_not() {
"the two certificates must genuinely differ, or this test proves nothing"
);
assert_eq!(
pin_hash(first.der()).expect("pin"),
pin_hash(second.der()).expect("pin"),
pin(first.der()).expect("pin"),
pin(second.der()).expect("pin"),
"a renewal keeps the key, so it must keep the pin"
);

let (other_cert, _) = cert_and_its_key_pin();
assert_ne!(
pin_hash(first.der()).expect("pin"),
pin_hash(&other_cert).expect("pin"),
pin(first.der()).expect("pin"),
pin(&other_cert).expect("pin"),
"a different key must be a different pin, or the pin distinguishes nothing"
);
}
Expand All @@ -100,8 +93,8 @@ fn a_truncated_certificate_refuses_rather_than_hashing_whatever_it_reached() {
let (cert_der, _) = cert_and_its_key_pin();
for cut in [1usize, 2, 8, cert_der.len() / 2, cert_der.len() - 1] {
assert_eq!(
pin_hash(&cert_der[..cut]),
Err(KeyInfoError::Truncated),
pin(&cert_der[..cut]),
Err(SpkiError::Truncated),
"{cut} bytes of a certificate is not a certificate"
);
}
Expand All @@ -112,8 +105,8 @@ fn a_document_that_is_not_a_certificate_refuses_by_tag() {
// An OCTET STRING where a SEQUENCE belongs. Refused by NAME rather than by falling off the end,
// so an operator reading the log learns the peer sent something that is not a certificate.
assert_eq!(
pin_hash(&[0x04, 0x02, 0xAA, 0xBB]),
Err(KeyInfoError::UnexpectedTag {
pin(&[0x04, 0x02, 0xAA, 0xBB]),
Err(SpkiError::UnexpectedTag {
want: 0x30,
got: 0x04
})
Expand Down Expand Up @@ -142,8 +135,8 @@ fn every_ber_length_form_that_gives_one_certificate_a_second_encoding_is_refused
),
] {
assert_eq!(
pin_hash(&bytes),
Err(KeyInfoError::NotDer(why)),
pin(&bytes),
Err(SpkiError::NotDer(why)),
"{why} must be refused, not tolerated"
);
}
Expand Down Expand Up @@ -182,20 +175,3 @@ fn a_version_one_certificate_with_no_version_member_is_walked_correctly() {
not there"
);
}

/// FAITHFULNESS: the HOST spelling of the pin (the neutral [`busbar_kernel::plane_host::spki::pin`] the egress
/// seam hands back in the observed head) is the SAME string as the a2a plane's own [`pin_hash`], byte
/// for byte, over the same certificate DER. This is the whole reason the walk was lifted to one place:
/// a governed hop must be able to return the pin the plane would have computed itself, so the plane's
/// SPKI classification is unchanged whether the hop went through the seam or the plane's own transport.
#[test]
fn the_host_pin_equals_the_plane_pin_byte_for_byte() {
let (cert_der, _) = cert_and_its_key_pin();
let plane = pin_hash(&cert_der).expect("the plane pin");
let host = busbar_kernel::plane_host::spki::pin(&cert_der).expect("the host pin");
assert_eq!(
host.as_bytes(),
plane.as_bytes(),
"the host-computed pin must equal the plane-computed pin byte for byte"
);
}
Loading