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
104 changes: 87 additions & 17 deletions Cargo.lock

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

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ utoipa = { version = "5", features = ["axum_extras"] }
uuid = { version = "1", features = ["v7"] }
futures-util = "0.3"
dashmap = "6"
sha2 = "0.10"
sha2 = "0.11"
base64 = "0.22.1"
clap = { version = "4.5.48", features = ["derive"] }
ed25519-dalek = { version = "2.2.0", features = ["pem", "pkcs8", "rand_core"] }
Expand All @@ -45,7 +45,7 @@ thiserror = "2"
zeroize = "1.8"
rustix = { version = "1.1", features = ["process"] }
libc = "0.2"
hmac = "0.12"
hmac = "0.13"
subtle = "2"
aws-lc-rs = { version = "1.18.0", default-features = false }
jsonwebtoken = { version = "10.4.0", default-features = false, features = ["aws_lc_rs"] }
Expand Down
24 changes: 22 additions & 2 deletions crates/coop-attestation/src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ impl ArtifactDigest {
)?;
}
Ok(Self {
sha256: format!("{:x}", hasher.finalize()),
sha256: bytes_to_hex(hasher.finalize().as_slice()),
size_bytes,
})
}
Expand Down Expand Up @@ -1320,13 +1320,33 @@ pub fn dsse_v1_pae(payload_type: &str, payload: &[u8]) -> Result<Vec<u8>, Attest
}

fn sha256_hex(bytes: &[u8]) -> String {
format!("{:x}", Sha256::digest(bytes))
bytes_to_hex(Sha256::digest(bytes).as_slice())
}

/// Lowercase hexadecimal encoding without relying on a `LowerHex` impl
/// for the digest output type (generic-array 1.x no longer provides one).
pub fn bytes_to_hex(bytes: &[u8]) -> String {
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn bytes_to_hex_matches_nist_vector() {
// SHA-256("abc") from FIPS 180-4; locks the encoding independently
// of the digest crate version (generic-array 1.x dropped LowerHex).
assert_eq!(
bytes_to_hex(Sha256::digest(b"abc").as_slice()),
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
assert_eq!(
sha256_hex(b"abc"),
bytes_to_hex(Sha256::digest(b"abc").as_slice())
);
}

#[test]
fn dsse_pae_matches_v1_shape_and_is_unambiguous() {
assert_eq!(
Expand Down
6 changes: 5 additions & 1 deletion crates/coop-attestation/src/keys.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::error::AttestationError;
use crate::format::bytes_to_hex;
use ed25519_dalek::pkcs8::{DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey};
use ed25519_dalek::{SigningKey, VerifyingKey};
use pkcs8::LineEnding;
Expand Down Expand Up @@ -37,7 +38,10 @@ pub fn generate_signing_key() -> Result<SigningKey, AttestationError> {
/// the 32-byte Ed25519 public key. Verifiers must still authenticate a
/// signature with a configured trusted key.
pub fn key_id(verifying_key: &VerifyingKey) -> String {
format!("sha256:{:x}", Sha256::digest(verifying_key.as_bytes()))
format!(
"sha256:{}",
bytes_to_hex(Sha256::digest(verifying_key.as_bytes()).as_slice())
)
}

/// Encode a private key using Rookhold's canonical unencrypted PKCS#8 PEM profile.
Expand Down
13 changes: 7 additions & 6 deletions crates/coop-attestation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ mod strict_json;
pub use ed25519_dalek::{SigningKey, VerifyingKey};
pub use error::AttestationError;
pub use format::{
build_statement, build_statement_from_receipt_json, create_attestation, dsse_v1_pae,
encode_statement, sign_statement, verify_attestation, ArtifactDigest, CoopExecutionPredicateV1,
CoopResultDescriptorV1, DsseEnvelope, DsseSignature, ResourceDescriptor, StatementV1,
SubjectArtifact, VerificationPolicy, VerifiedAttestation, COOP_EXECUTION_PREDICATE_TYPE,
COOP_EXECUTION_SCHEMA_VERSION, DSSE_PAYLOAD_TYPE, IN_TOTO_STATEMENT_TYPE, MAX_ENVELOPE_BYTES,
MAX_SIGNATURES, MAX_STATEMENT_BYTES, MAX_TRUSTED_KEYS,
build_statement, build_statement_from_receipt_json, bytes_to_hex, create_attestation,
dsse_v1_pae, encode_statement, sign_statement, verify_attestation, ArtifactDigest,
CoopExecutionPredicateV1, CoopResultDescriptorV1, DsseEnvelope, DsseSignature,
ResourceDescriptor, StatementV1, SubjectArtifact, VerificationPolicy, VerifiedAttestation,
COOP_EXECUTION_PREDICATE_TYPE, COOP_EXECUTION_SCHEMA_VERSION, DSSE_PAYLOAD_TYPE,
IN_TOTO_STATEMENT_TYPE, MAX_ENVELOPE_BYTES, MAX_SIGNATURES, MAX_STATEMENT_BYTES,
MAX_TRUSTED_KEYS,
};
pub use keys::{
decode_private_key_pem, decode_public_key_pem, encode_private_key_pem, encode_public_key_pem,
Expand Down
8 changes: 4 additions & 4 deletions crates/coop-server/src/attestation.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use crate::config::{AttestationMode, Config};
use crate::AppState;
use coop_attestation::{
build_statement_from_receipt_json, encode_public_key_pem, key_id, read_private_key_file,
sign_statement, verify_attestation, ArtifactDigest, SigningKey, SubjectArtifact,
VerificationPolicy,
build_statement_from_receipt_json, bytes_to_hex, encode_public_key_pem, key_id,
read_private_key_file, sign_statement, verify_attestation, ArtifactDigest, SigningKey,
SubjectArtifact, VerificationPolicy,
};
use coop_store::{AttestationSourceJob, PersistAttestationOutcome, Store};
use serde::Serialize;
Expand Down Expand Up @@ -285,7 +285,7 @@ fn result_artifact_bytes(
}

fn sha256_hex(bytes: &[u8]) -> String {
format!("{:x}", Sha256::digest(bytes))
bytes_to_hex(Sha256::digest(bytes).as_slice())
}

pub fn spawn_worker(state: AppState) -> tokio::task::JoinHandle<()> {
Expand Down
5 changes: 3 additions & 2 deletions crates/coop-server/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ use axum::middleware::Next;
use axum::response::Response;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine as _;
use coop_attestation::bytes_to_hex;
use futures_util::StreamExt;
use hmac::{Hmac, Mac};
use hmac::{Hmac, KeyInit, Mac};
use jsonwebtoken::jwk::{AlgorithmParameters, JwkSet, KeyOperations, PublicKeyUse};
use jsonwebtoken::{decode, decode_header, Algorithm, DecodingKey, Validation};
use reqwest::redirect::Policy;
Expand Down Expand Up @@ -1011,7 +1012,7 @@ fn oidc_principal_id(issuer: &str, subject: &str, client_id: &str) -> String {
digest.update(subject.as_bytes());
digest.update(b"\0");
digest.update(client_id.as_bytes());
format!("oidc:{:x}", digest.finalize())
format!("oidc:{}", bytes_to_hex(digest.finalize().as_slice()))
}

fn now_seconds() -> u64 {
Expand Down
2 changes: 1 addition & 1 deletion crates/coop-server/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1108,7 +1108,7 @@ impl Config {
#[cfg(test)]
mod tests {
use super::*;
use hmac::{Hmac, Mac};
use hmac::{Hmac, KeyInit, Mac};

fn source<'a>(pairs: &'a [(&'a str, &'a str)]) -> impl Fn(&str) -> Option<String> + 'a {
move |key: &str| {
Expand Down
3 changes: 2 additions & 1 deletion crates/coop-server/src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use axum::response::{Html, IntoResponse, Response};
use axum::routing::{delete, get, post};
use axum::{Extension, Json, Router};
use base64::Engine as _;
use coop_attestation::bytes_to_hex;
use coop_store::{AttestationMetadata, JobCursor, JobRow, JobSummary, ListJobsQuery};
use coop_types::{
EffectiveJobSpec, IsolationClass, JobSpec, JobStatus, LimitEnforcement, CPU_MAX_SECONDS,
Expand Down Expand Up @@ -412,7 +413,7 @@ fn idempotency_request(
let canonical = coop_store::canonical_json(&value);
Ok(Some(coop_store::IdempotencyRequest {
key: key.to_string(),
request_sha256: format!("{:x}", Sha256::digest(canonical.as_bytes())),
request_sha256: bytes_to_hex(Sha256::digest(canonical.as_bytes()).as_slice()),
}))
}

Expand Down
Loading
Loading