Skip to content

Commit

Permalink
aproxy: Complete negotiation with KBS protocol
Browse files Browse the repository at this point in the history
To validate the freshness of attestation evidence, KBS servers return a
challenge upon client authentication. This challenge contains a
ephemeral nonce in the form of base64-encoded bytes. This nonce *must*
be hashed into the attestation report along with the components of the
TEE public key.

This commit reads the negotiation request from SVSM, and depending on
the backend, fetches the negotiation parameters accordingly. As KBS is
the backend used in the initial implementation, this translates into a
call to the KBS /auth handler to fetch the nonce, and then a
specification of both the nonce and TEE public components to be included
in the attestation evidence (in the form of negotiation parameters).

Co-developed-by: Stefano Garzarella <[email protected]>
Signed-off-by: Tyler Fanelli <[email protected]>
  • Loading branch information
tylerfanelli committed Jan 16, 2025
1 parent 86c51ba commit 53c335b
Show file tree
Hide file tree
Showing 11 changed files with 1,786 additions and 91 deletions.
1,488 changes: 1,401 additions & 87 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ members = [
"user/init",
# Library defining common types between SVSM and attestation proxy
"libaproxy",
# Attestation proxy
"aproxy",
]


Expand Down
13 changes: 12 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,27 @@ IGVMBIN = bin/igvmbld
IGVMMEASURE = "target/x86_64-unknown-linux-gnu/${TARGET_PATH}/igvmmeasure"
IGVMMEASUREBIN = bin/igvmmeasure

APROXY = "target/x86_64-unknown-linux-gnu/${TARGET_PATH}/aproxy"
APROXYBIN = bin/aproxy

RUSTDOC_OUTPUT = target/x86_64-unknown-none/doc
DOC_SITE = target/x86_64-unknown-none/site

all: bin/svsm.bin igvm

aproxy: $(APROXY) $(APROXYBIN)

igvm: $(IGVM_FILES) $(IGVMBIN) $(IGVMMEASUREBIN)

bin:
mkdir -v -p bin

$(APROXYBIN): $(APROXY) bin
cp -f $(APROXY) $@

$(APROXY):
cargo build ${CARGO_ARGS} --target=x86_64-unknown-linux-gnu -p aproxy

$(IGVMBIN): $(IGVMBUILDER) bin
cp -f $(IGVMBUILDER) $@

Expand Down Expand Up @@ -175,7 +186,7 @@ bin/svsm-test.bin: bin/svsm-test
objcopy -O binary $< $@

clippy:
cargo clippy --workspace --all-features --exclude packit --exclude svsm-fuzz --exclude igvmbuilder --exclude igvmmeasure --exclude stage1 -- -D warnings
cargo clippy --workspace --all-features --exclude packit --exclude svsm-fuzz --exclude igvmbuilder --exclude igvmmeasure --exclude stage1 --exclude aproxy -- -D warnings
cargo clippy --workspace --all-features --exclude packit --exclude svsm-fuzz --exclude svsm --exclude 'user*' --exclude stage1 --target=x86_64-unknown-linux-gnu -- -D warnings
cargo clippy -p stage1 --all-features --target=x86_64-unknown-linux-gnu -- -D warnings ${STAGE1_RUSTC_ARGS}
RUSTFLAGS="--cfg fuzzing" cargo clippy --package svsm-fuzz --all-features --target=x86_64-unknown-linux-gnu -- -D warnings
Expand Down
18 changes: 18 additions & 0 deletions aproxy/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "aproxy"
version = "0.1.0"
edition = "2021"

[target.'cfg(all(target_os = "linux"))'.dependencies]
reqwest = { version = "0.12.9", features = ["blocking", "cookies", "json"] }

[dependencies]
anyhow = "1.0.93"
clap = { version = "4.5", features = ["derive"] }
kbs-types.workspace = true
libaproxy.workspace = true
serde.workspace = true
serde_json.workspace = true

[lints]
workspace = true
83 changes: 83 additions & 0 deletions aproxy/src/attest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
//
// Copyright (c) 2024 Red Hat, Inc
//
// Author: Stefano Garzarella <[email protected]>
// Author: Tyler Fanelli <[email protected]>

use crate::backend;
use anyhow::Context;
use libaproxy::*;
use serde::Serialize;
use std::{
io::{Read, Write},
os::unix::net::UnixStream,
};

/// Attest an SVSM client session.
pub fn attest(stream: &mut UnixStream, http: &mut backend::HttpClient) -> anyhow::Result<()> {
negotiation(stream, http)?;

Ok(())
}

/// Negotiation phase of SVSM attestation. SVSM will send a negotiation request indicating the
/// version that it would like to use. The proxy will then reach out to the respective attestation
/// server and gather all data required (i.e. a nonce) that should be hashed into the attestation
/// evidence. The proxy will also reply with the type of hash algorithm to use for the negotiation
/// parameters.
fn negotiation(stream: &mut UnixStream, http: &mut backend::HttpClient) -> anyhow::Result<()> {
// Read the negotiation parameters from SVSM.
let request: NegotiationRequest = {
let payload = proxy_read(stream)?;

serde_json::from_slice(&payload)
.context("unable to deserialize negotiation request from JSON")?
};

// Gather negotiation parameters from the attestation server.
let response: NegotiationResponse = http.negotiation(request)?;

// Write the response from the attestation server to SVSM.
proxy_write(stream, response)?;

Ok(())
}

/// Read bytes from the UNIX socket connected to SVSM. With each write, SVSM first writes an 8-byte
/// header indicating the length of the buffer. Once the length is read, the buffer can be read.
fn proxy_read(stream: &mut UnixStream) -> anyhow::Result<Vec<u8>> {
let len = {
let mut bytes = [0u8; 8];

stream
.read_exact(&mut bytes)
.context("unable to read request buffer length from socket")?;

usize::from_ne_bytes(bytes)
};

let mut bytes = vec![0u8; len];

stream
.read_exact(&mut bytes)
.context("unable to read request buffer from socket")?;

Ok(bytes)
}

/// Write bytes to the UNIX socket connected to SVSM. With each write, an 8-byte header indicating
/// the length of the buffer is written. Once the length is written, the buffer is written.
fn proxy_write(stream: &mut UnixStream, buf: impl Serialize) -> anyhow::Result<()> {
let bytes = serde_json::to_vec(&buf).context("unable to convert buffer to JSON bytes")?;
let len = bytes.len().to_ne_bytes();

stream
.write_all(&len)
.context("unable to write buffer length to socket")?;
stream
.write_all(&bytes)
.context("unable to write buffer to socket")?;

Ok(())
}
71 changes: 71 additions & 0 deletions aproxy/src/backend/kbs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
//
// Copyright (c) 2024 Red Hat, Inc
//
// Author: Stefano Garzarella <[email protected]>
// Author: Tyler Fanelli <[email protected]>

use super::*;
use anyhow::Context;
use kbs_types::{Challenge, Request, Tee};
use serde_json::Value;

#[derive(Clone, Copy, Debug, Default)]
pub struct KbsProtocol;

impl AttestationProtocol for KbsProtocol {
/// KBS servers usually want two components hashed into attestation evidence: the public
/// components of the TEE key, and a nonce provided in the KBS challenge that is fetched
/// from the server's /auth endpoint. These must be hased in order.
///
/// Make this request to /auth, gather the nonce, and return this in the negotiation
/// parameter for SVSM to hash these components in the attestation evidence.
fn negotiation(
&mut self,
http: &mut HttpClient,
request: NegotiationRequest,
) -> anyhow::Result<NegotiationResponse> {
let req = Request {
version: "0.1.0".to_string(), // unused.
tee: request.tee,
extra_params: Value::String("".to_string()), // unused.
};

// Fetch challenge containing a nonce from the KBS /auth endpoint.
let http_resp = http
.cli
.post(format!("{}/kbs/v0/auth", http.url))
.json(&req)
.send()
.context("unable to POST to KBS /auth endpoint")?;

let text = http_resp
.text()
.context("unable to convert KBS /auth response to text")?;

let challenge: Challenge =
serde_json::from_str(&text).context("unable to convert KBS /auth response to JSON")?;

// Challenge nonce is a base64-encoded byte vector. Inform SVSM of this so it could
// decode the bytes and hash them into the TEE evidence.
let params = vec![
NegotiationParam::EcPublicKeySec1Bytes,
NegotiationParam::Base64StdBytes(challenge.nonce),
];

// SEV-SNP REPORT_DATA is 64 bytes in size. Produce a SHA512 hash to ensure there's no need
// for padding.
let hash = match request.tee {
Tee::Snp => NegotiationHash::SHA512,
_ => return Err(anyhow!("invalid TEE architecture selected")),
};

let resp = NegotiationResponse {
hash,
key_type: NegotiationKey::Ecdh384Sha256Aes128,
params,
};

Ok(resp)
}
}
68 changes: 68 additions & 0 deletions aproxy/src/backend/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
//
// Copyright (c) 2024 Red Hat, Inc
//
// Author: Stefano Garzarella <[email protected]>
// Author: Tyler Fanelli <[email protected]>

mod kbs;

use anyhow::{anyhow, Context};
use kbs::KbsProtocol;
use libaproxy::*;
use reqwest::{blocking::Client, cookie::Jar};
use std::{str::FromStr, sync::Arc};

/// HTTP client and protocol identifier.
#[derive(Clone, Debug)]
pub struct HttpClient {
pub cli: Client,
pub url: String,
protocol: Protocol,
}

impl HttpClient {
pub fn new(url: String, protocol: Protocol) -> anyhow::Result<Self> {
let cli = Client::builder()
.cookie_provider(Arc::new(Jar::default()))
.build()
.context("unable to build HTTP client to interact with attestation server")?;

Ok(Self { cli, url, protocol })
}

pub fn negotiation(&mut self, req: NegotiationRequest) -> anyhow::Result<NegotiationResponse> {
// Depending on the underlying protocol of the attestation server, gather negotiation
// parameters accordingly.
match self.protocol {
Protocol::Kbs(mut kbs) => kbs.negotiation(self, req),
}
}
}

/// Attestation Protocol identifier.
#[derive(Clone, Copy, Debug)]
pub enum Protocol {
Kbs(KbsProtocol),
}

impl FromStr for Protocol {
type Err = anyhow::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match &s.to_lowercase()[..] {
"kbs" => Ok(Self::Kbs(KbsProtocol)),
_ => Err(anyhow!("invalid backend attestation protocol selected")),
}
}
}

/// Trait to implement the negotiation and attestation phases across different attestation
/// protocols.
pub trait AttestationProtocol {
fn negotiation(
&mut self,
client: &mut HttpClient,
req: NegotiationRequest,
) -> anyhow::Result<NegotiationResponse>;
}
57 changes: 57 additions & 0 deletions aproxy/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
//
// Copyright (c) 2024 Red Hat, Inc
//
// Author: Stefano Garzarella <[email protected]>
// Author: Tyler Fanelli <[email protected]>

mod attest;
mod backend;

use anyhow::Context;
use clap::Parser;
use std::{fs, os::unix::net::UnixListener};

#[derive(Parser, Debug)]
#[clap(version, about, long_about = None)]
struct Args {
/// HTTP url to KBS (e.g. http://server:4242)
#[clap(long)]
url: String,

/// Backend attestation protocol that the server implements.
#[clap(long = "protocol")]
backend: backend::Protocol,

/// UNIX domain socket path to the SVSM serial port
#[clap(long)]
unix: String,

/// Force Unix domain socket removal before bind
#[clap(long, short, default_value_t = false)]
force: bool,
}

fn main() -> anyhow::Result<()> {
let args = Args::parse();

if args.force {
let _ = fs::remove_file(args.unix.clone());
}

let listener = UnixListener::bind(args.unix).context("unable to bind to UNIX socket")?;

for stream in listener.incoming() {
match stream {
Ok(mut stream) => {
let mut http_client = backend::HttpClient::new(args.url.clone(), args.backend)?;
attest::attest(&mut stream, &mut http_client)?;
}
Err(_) => {
panic!("error");
}
}
}

Ok(())
}
Loading

0 comments on commit 53c335b

Please sign in to comment.