diff --git a/api-server/README.md b/api-server/README.md index f561cd9cb..66f04e81f 100644 --- a/api-server/README.md +++ b/api-server/README.md @@ -15,7 +15,7 @@ To see how to make this your own, look here: [README]((https://openapi-generator.tech)) - API version: 0.29.0 -- Build date: 2024-07-23T17:26:31.452467-06:00[America/Denver] +- Build date: 2024-07-24T16:31:34.304262-06:00[America/Denver] diff --git a/api/src/auth.rs b/api/src/auth.rs index d93906272..01b86cbc2 100644 --- a/api/src/auth.rs +++ b/api/src/auth.rs @@ -1,25 +1,21 @@ -use std::collections::HashMap; -use std::io::Cursor; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; -use futures::StreamExt; -use regex::Regex; -use iroh_car::CarReader; -use once_cell::sync::Lazy; -use ceramic_core::{Cid, Jwk}; -use std::str::FromStr; -use biscuit_auth::{Biscuit, KeyPair}; use biscuit_auth::macros::*; -use ssi::jws::decode_verify; +use biscuit_auth::{Biscuit, KeyPair}; use ceramic_core::ssi::did::DIDMethods; use ceramic_core::ssi::did_resolve::{DIDResolver, ResolutionInputMetadata}; +use ceramic_core::{Cid, Jwk}; use ceramic_event::unvalidated::signed::cacao::{Capability, SignatureMetadata}; +use futures::StreamExt; +use iroh_car::CarReader; +use once_cell::sync::Lazy; +use regex::Regex; +use ssi::jws::verify_bytes_warnable; +use std::collections::HashMap; +use std::io::Cursor; +use std::str::FromStr; -const PREV_REGEX: Lazy = Lazy::new(|| { - Regex::new(r#"prev\:(.+)"#).unwrap() -}); -const BISCUIT_REGEX: Lazy = Lazy::new(|| { - Regex::new(r#"biscuit\:(.+)"#).unwrap() -}); +const PREV_REGEX: Lazy = Lazy::new(|| Regex::new(r#"prev:(.+)"#).unwrap()); +const BISCUIT_REGEX: Lazy = Lazy::new(|| Regex::new(r#"biscuit:(.+)"#).unwrap()); #[derive(Clone, Debug)] pub enum Operation { @@ -37,8 +33,14 @@ impl std::fmt::Display for Operation { } } -fn authenticate_biscuit(biscuit: &Biscuit, operation: &Operation, resource: &str, _allowed_resources: &Vec) -> Result<(), String> { - let mut auth = authorizer!(r#" +fn authenticate_biscuit( + biscuit: &Biscuit, + operation: &Operation, + resource: &str, + _allowed_resources: &Vec, +) -> Result<(), String> { + let mut auth = authorizer!( + r#" operation({operation}); resource({resource}); @@ -53,8 +55,10 @@ fn authenticate_biscuit(biscuit: &Biscuit, operation: &Operation, resource: &str resource = resource, ); auth.set_time(); - auth.add_token(biscuit).map_err(|e| format!("Failed to authorize biscuit: {e}"))?; - auth.authorize().map_err(|e| format!("Failed to authorize: {e}"))?; + auth.add_token(biscuit) + .map_err(|e| format!("Failed to authorize biscuit: {e}"))?; + auth.authorize() + .map_err(|e| format!("Failed to authorize: {e}"))?; Ok(()) } @@ -76,14 +80,10 @@ const DID_METHODS: Lazy = Lazy::new(|| { }); async fn verify_capability(cacao: &Capability) -> Result<(), String> { - let did = match &cacao.signature.metadata { - SignatureMetadata::Known(m) => &m.kid, - _ => &cacao.payload.issuer, - }; - let did = if let Some((did, _)) = did.split_once('#') { + let did = if let Some((did, _)) = cacao.signature.metadata.kid.split_once('#') { did } else { - return Err(format!("Invalid DID: {}", did)); + &cacao.signature.metadata.kid }; let meta = ResolutionInputMetadata::default(); let (meta, opt_doc, _opt_doc_meta) = DID_METHODS.resolve(did, &meta).await; @@ -94,35 +94,55 @@ async fn verify_capability(cacao: &Capability) -> Result<(), String> { let jwk = Jwk::new(&doc).await.map_err(|e| e.to_string())?; //reconstruct header and payload - let header = URL_SAFE_NO_PAD.encode( - serde_json::to_string(&cacao.signature.metadata).map_err(|e| e.to_string())?.as_bytes() - ); let payload = URL_SAFE_NO_PAD.encode( - serde_json::to_string(&cacao.payload).map_err(|e| e.to_string())?.as_bytes() + serde_json::to_vec(&cacao.payload) + .map_err(|e| e.to_string())? + .as_slice(), ); - let sig = format!("{header}.{payload}.{}", URL_SAFE_NO_PAD.encode(cacao.signature.signature.as_bytes())); - if let Err(e) = decode_verify(&sig, &jwk) { - println!("Validation failed: {}\n Sig={sig}", e); + let header = URL_SAFE_NO_PAD + .encode(serde_json::to_vec(&cacao.signature.metadata).map_err(|e| e.to_string())?); + let input = format!("{header}.{payload}"); + let sig = URL_SAFE_NO_PAD + .decode(cacao.signature.signature.as_bytes()) + .map_err(|_| "Invalid signature")?; + if let Err(e) = verify_bytes_warnable( + cacao.signature.r#type.algorithm(), + input.as_bytes(), + &jwk, + &sig, + ) { + tracing::warn!( + "Validation failed: {}\n Sig={}", + e, + cacao.signature.signature + ); } Ok(()) } async fn read_authentication(data: &str) -> Result { - let car_data = URL_SAFE_NO_PAD.decode(data.as_bytes()).map_err(|_| format!("Data was not Base64 URL: {data}"))?; + let car_data = URL_SAFE_NO_PAD + .decode(data.as_bytes()) + .map_err(|_| format!("Data was not Base64 URL: {data}"))?; let reader = Cursor::new(&car_data); - let car_reader = CarReader::new(reader).await.map_err(|e| format!("Failed to read CAR: {e}"))?; + let car_reader = CarReader::new(reader) + .await + .map_err(|e| format!("Failed to read CAR: {e}"))?; - let root = car_reader.header().roots().first().ok_or_else(|| "No roots present".to_string())?.clone(); + let root = car_reader + .header() + .roots() + .first() + .ok_or_else(|| "No roots present".to_string())? + .clone(); let mut blocks = Box::pin(car_reader.stream()); let mut cacaos: HashMap = HashMap::default(); while let Some(block) = blocks.next().await { let (cid, data) = block.map_err(|e| format!("Failed to read block from CAR: {e}"))?; match serde_ipld_dagcbor::from_slice::(&data) { Ok(cacao) => { - if cid == root { - verify_capability(&cacao).await?; - } + verify_capability(&cacao).await?; cacaos.insert(cid, cacao); } Err(e) => { @@ -142,14 +162,18 @@ async fn read_authentication(data: &str) -> Result { for res in sig_resources { if let Some(res) = PREV_REGEX.captures_iter(&res).next() { let (_, [cid]) = res.extract(); - let cid = Cid::from_str(&cid).map_err(|_| format!("Invalid previous CID: {cid}"))?; + let cid = + Cid::from_str(&cid).map_err(|_| format!("Invalid previous CID: {cid}"))?; if !cacaos.contains_key(&cid) { - return Err(format!("No signature found for {cid}")); + //return Err(format!("No signature found for {cid}")); + tracing::warn!("No signature found for {cid}"); } prev.push(cid); } else if let Some(res) = BISCUIT_REGEX.captures_iter(&res).next() { let (_, [biscuit_data]) = res.extract(); - let biscuit_data = URL_SAFE_NO_PAD.decode(biscuit_data).map_err(|_| format!("Invalid biscuit: {biscuit_data}"))?; + let biscuit_data = URL_SAFE_NO_PAD + .decode(biscuit_data) + .map_err(|_| format!("Invalid biscuit: {biscuit_data}"))?; tracing::trace!("Biscuit: {}", String::from_utf8_lossy(&biscuit_data)); @@ -162,10 +186,7 @@ async fn read_authentication(data: &str) -> Result { resources.push(res.clone()); } } - Ok((cid.clone(), Capabilities { - resources, - biscuit, - })) + Ok((cid.clone(), Capabilities { resources, biscuit })) }); let blocks: Result<_, String> = blocks.collect(); Ok(Authentication { @@ -181,11 +202,12 @@ pub async fn authenticate(data: &str, operation: Operation, resource: &str) -> R for (cid, cap) in auth.capabilities.iter() { if let Some(biscuit) = &cap.biscuit { let mut bldr = biscuit_auth::builder::BiscuitBuilder::new(); - bldr.add_code(String::from_utf8_lossy(&biscuit)).map_err(|e| { - format!("Invalid code: {e}") - })?; + bldr.add_code(String::from_utf8_lossy(&biscuit)) + .map_err(|e| format!("Invalid code: {e}"))?; let kp = KeyPair::new(); - let biscuit = bldr.build(&kp).map_err(|e| format!("Invalid biscuit: {e}"))?; + let biscuit = bldr + .build(&kp) + .map_err(|e| format!("Invalid biscuit: {e}"))?; authenticate_biscuit(&biscuit, &operation, &resource, &cap.resources)?; } if cid == &auth.root { @@ -203,8 +225,17 @@ pub async fn authenticate(data: &str, operation: Operation, resource: &str) -> R #[cfg(test)] mod tests { - use std::time::Duration; use super::*; + use ceramic_core::{DidDocument, StreamId}; + use ceramic_event::cid_from_dag_cbor; + use ceramic_event::unvalidated::signed::cacao::{ + Header, HeaderType, Payload, Signature, SignatureType, + }; + use ceramic_event::unvalidated::signed::{JwkSigner, Signer}; + use iroh_car::{CarHeader, CarHeaderV1, CarWriter}; + use ssi::jwk::Params; + use ssi_dids::{DIDMethod, DocumentBuilder, Source}; + use std::time::Duration; #[tokio::test] async fn should_authenticate_biscuit() { @@ -222,14 +253,131 @@ mod tests { let kp = KeyPair::new(); let biscuit = bldr.build(&kp).unwrap(); - authenticate_biscuit(&biscuit, &Operation::Read, "ceramic://*?model=kjzl6hvfrbw6cadyci5lvsff4jxl1idffrp2ld3i0k1znz0b3k67abkmtf7p7q3", &vec![]).unwrap() + authenticate_biscuit( + &biscuit, + &Operation::Read, + "ceramic://*?model=kjzl6hvfrbw6cadyci5lvsff4jxl1idffrp2ld3i0k1znz0b3k67abkmtf7p7q3", + &vec![], + ) + .unwrap() } #[tokio::test] async fn should_authenticate() { let header = r#"Y6Jlcm9vdHOC2CpYJQABcRIgCrnpZxbxpRk4zP9p18ohYhLqnaBWELv-hNX7vpsEbi3YKlglAAFxEiATGjqMmNzLPBmtmlSsL4czjuBbdgYZHQ6lE4nwK544d2d2ZXJzaW9uAdkEAXESIHgQFit07BJc13FxaE03BZkURgb47hnQc7bIjR7RvV7Xo2FooWF0Z2VpcDQzNjFhcKljYXVkeDlkaWQ6a2V5OnpEbmFlaXp0ZkJqMm5lTmpDMnpTQ0hxbVF0QVBWczE4Y3JOelA0V3pKdENVUFc0UDVjZXhweBgyMDI0LTA3LTE1VDA3OjU5OjM0LjEwNlpjaWF0eBgyMDI0LTA3LTA4VDA3OjU5OjM0LjEwNlpjaXNzeD1kaWQ6cGtoOmVpcDE1NToxMzc6MHg0MzllNjZkYjViODViMDY1Yjk2MmRiMGIzYjIxZTYwNzY3NGMxZDBiZW5vbmNlallXTmNHODNKWjdmZG9tYWluaWxvY2FsaG9zdGd2ZXJzaW9uYTFpcmVzb3VyY2VzgXhRY2VyYW1pYzovLyo_bW9kZWw9a2p6bDZodmZyYnc2Y2FkeWNpNWx2c2ZmNGp4bDFpZGZmcnAybGQzaTBrMXpuejBiM2s2N2Fia210ZjdwN3EzaXN0YXRlbWVudHg8R2l2ZSB0aGlzIGFwcGxpY2F0aW9uIGFjY2VzcyB0byBzb21lIG9mIHlvdXIgZGF0YSBvbiBDZXJhbWljYXOiYXN4hDB4ZjliNjhkNGYzYTBkMDdkYTE5YzZkZmM0MzA5YTQ3YmE1NmQ5MGRlY2QyNTI2ZGE3OGJmZWFjODM2OTExZDMwMTVhZjIwYTFmMDEwYWVmYmEwNmQ2NTdhZDdmMmM5ZGEwY2FhZjJlNmI4NGY4MTE1NDc5NTdkZmRiYjBmMWZmZDMxY2F0ZmVpcDE5MccJAXESIAq56WcW8aUZOMz_adfKIWIS6p2gVhC7_oTV-76bBG4to2FooWF0Z2NhaXAxMjJhcKljYXVkeDtkaWQ6cGtoOmVpcDE1NToxOjB4ZmEzRjU0QUU5QzQyODdDQTA5YTQ4NmRmYUZhQ2U3ZDFkNDA5NWQ5M2NleHB4GDIwMjQtMDgtMTBUMTc6MDY6NTkuOTcyWmNpYXR4GDIwMjQtMDctMDhUMDc6NTk6MzQuMTA2WmNpc3N4OWRpZDprZXk6ekRuYWVpenRmQmoybmVOakMyelNDSHFtUXRBUFZzMThjck56UDRXekp0Q1VQVzRQNWVub25jZWpZV05jRzgzSlo3ZmRvbWFpbmlsb2NhbGhvc3RndmVyc2lvbmExaXJlc291cmNlc4N4UWNlcmFtaWM6Ly8qP21vZGVsPWtqemw2aHZmcmJ3NmNhZHljaTVsdnNmZjRqeGwxaWRmZnJwMmxkM2kwazF6bnowYjNrNjdhYmttdGY3cDdxM3hAcHJldjpiYWZ5cmVpZHljYWxjdzVobWNqb25vNGxybmJndG9ibXpjcmRhbjZob2RoaWhobndpcnVwbmRwazYyNHkBlmJpc2N1aXQ6THk4Z2JtOGdjbTl2ZENCclpYa2dhV1FnYzJWMENuVnpaWElvSW1ScFpEcHdhMmc2Wldsd01UVTFPakU2TUhobVlUTkdOVFJCUlRsRE5ESTROME5CTURsaE5EZzJaR1poUm1GRFpUZGtNV1EwTURrMVpEa3pJaWs3Q25KcFoyaDBLQ0prYVdRNmNHdG9PbVZwY0RFMU5Ub3hPakI0Wm1FelJqVTBRVVU1UXpReU9EZERRVEE1WVRRNE5tUm1ZVVpoUTJVM1pERmtOREE1TldRNU15SXNJQ0pqWlhKaGJXbGpPaTh2S2o5dGIyUmxiRDFyYW5wc05taDJabkppZHpaallXUjVZMmsxYkhaelptWTBhbmhzTVdsa1ptWnljREpzWkROcE1Hc3hlbTU2TUdJemF6WTNZV0pyYlhSbU4zQTNjVE1pS1RzS1kyaGxZMnNnYVdZZ2RHbHRaU2drZEdsdFpTa3NJQ1IwYVcxbElEd2dNakF5TkMwd09DMHhNRlF4Tnpvd05qbzFPVm83Q2dpc3RhdGVtZW50eDxHaXZlIHRoaXMgYXBwbGljYXRpb24gYWNjZXNzIHRvIHNvbWUgb2YgeW91ciBkYXRhIG9uIENlcmFtaWNhc6NhbaNjYWxnZUVTMjU2Y2NhcHhCaXBmczovL2JhZnlyZWlkeWNhbGN3NWhtY2pvbm80bHJuYmd0b2JtemNyZGFuNmhvZGhpaGhud2lydXBuZHBrNjI0Y2tpZHhrZGlkOmtleTp6RG5hZWl6dGZCajJuZU5qQzJ6U0NIcW1RdEFQVnMxOGNyTnpQNFd6SnRDVVBXNFA1I3pEbmFlaXp0ZkJqMm5lTmpDMnpTQ0hxbVF0QVBWczE4Y3JOelA0V3pKdENVUFc0UDVhc3hWa1dsUUoxRDYta2pxWHpZQTZsSE1SN0VoT1paZFM3QTJkUUlIZHFPV0NxZEhEWGc2QTJvekF2RTVLWC1kOFRlMmV5VzNMR016REdzSmMtX2VObDRDNEFhdGNqd3PXBAFxEiARavXbIEU6OeTVxMhWIPI_Kvg_6WZAZBiAECpS6JZ4MqNhaKFhdGdlaXA0MzYxYXCpY2F1ZHg5ZGlkOmtleTp6RG5hZXdHdUtmMjVFNlFEaGgxcUFqSnl2d0dSWG5VSkgyc0hvVnJlS1huUmt0QldmY2V4cHgYMjAyNC0wNy0xOFQxNzowNjo0NC43NjJaY2lhdHgYMjAyNC0wNy0xMVQxNzowNjo0NC43NjJaY2lzc3g7ZGlkOnBraDplaXAxNTU6MToweGZhM2Y1NGFlOWM0Mjg3Y2EwOWE0ODZkZmFmYWNlN2QxZDQwOTVkOTNlbm9uY2VqOWVHNW1FY09XdWZkb21haW5pbG9jYWxob3N0Z3ZlcnNpb25hMWlyZXNvdXJjZXOBeFFjZXJhbWljOi8vKj9tb2RlbD1ranpsNmh2ZnJidzZjYWR5Y2k1bHZzZmY0anhsMWlkZmZycDJsZDNpMGsxem56MGIzazY3YWJrbXRmN3A3cTNpc3RhdGVtZW50eDxHaXZlIHRoaXMgYXBwbGljYXRpb24gYWNjZXNzIHRvIHNvbWUgb2YgeW91ciBkYXRhIG9uIENlcmFtaWNhc6Jhc3iEMHhiNDE0ZTgzYmZmOTFkMjZkZTEwODYxNWQwZDdiNmU1NzhlODJhZTQ4MDQwZDE3N2M2NWExZDdkMjhiOGU0MWU1NmExNzM0MTRlZDNkMWU2Y2Q0YmY4NDRlMzA3MzBlZTU4ODkxZTc3MzcwNjhkMDJiNTgzYTNhYWU5YmM2NDI1ZjFjYXRmZWlwMTkx7gYBcRIgExo6jJjcyzwZrZpUrC-HM47gW3YGGR0OpROJ8CueOHejYWihYXRnY2FpcDEyMmFwqWNhdWR4OWRpZDprZXk6ekRuYWV3R3VLZjI1RTZRRGhoMXFBakp5dndHUlhuVUpIMnNIb1ZyZUtYblJrdEJXZmNleHB4GDIwMjQtMDctMThUMTc6MDY6NDQuNzYyWmNpYXR4GDIwMjQtMDctMTFUMTc6MDY6NDQuNzYyWmNpc3N4OWRpZDprZXk6ekRuYWV3R3VLZjI1RTZRRGhoMXFBakp5dndHUlhuVUpIMnNIb1ZyZUtYblJrdEJXZmVub25jZWo5ZUc1bUVjT1d1ZmRvbWFpbmlsb2NhbGhvc3RndmVyc2lvbmExaXJlc291cmNlc4N4UWNlcmFtaWM6Ly8qP21vZGVsPWtqemw2aHZmcmJ3NmNhZHljaTVsdnNmZjRqeGwxaWRmZnJwMmxkM2kwazF6bnowYjNrNjdhYmttdGY3cDdxM3hAcHJldjpiYWZ5cmVpYXJubDI1d2ljZmhpNDZqdm9lemJsY2I0cjdmbDRkNzJsZ2lic2JyYWFxZmpqb3JmdHlnaXhAcHJldjpiYWZ5cmVpYWt4aHV3b2Z4cnV1bXRydGg3bmhsNHVpbGNjbHZqM2ljd2NjNTc1Ymd2N283andiZG9mdWlzdGF0ZW1lbnR4PEdpdmUgdGhpcyBhcHBsaWNhdGlvbiBhY2Nlc3MgdG8gc29tZSBvZiB5b3VyIGRhdGEgb24gQ2VyYW1pY2Fzo2Fto2NhbGdlRVMyNTZjY2FweEJpcGZzOi8vYmFmeXJlaWFybmwyNXdpY2ZoaTQ2anZvZXpibGNiNHI3Zmw0ZDcybGdpYnNicmFhcWZqam9yZnR5Z2lja2lkeGtkaWQ6a2V5OnpEbmFld0d1S2YyNUU2UURoaDFxQWpKeXZ3R1JYblVKSDJzSG9WcmVLWG5Sa3RCV2YjekRuYWV3R3VLZjI1RTZRRGhoMXFBakp5dndHUlhuVUpIMnNIb1ZyZUtYblJrdEJXZmFzeFZLVzhMd0R4YkQtVHAtdWYzTUh6RXZ2UE96ZF9nWEVfWlpyZmNvd1Jyd1F0ODd4M2NqUlpTamVHbU1CWUVpRWdXYm84VXlQUEotS2lNanBCOGZtQWJxQWF0Y2p3cw"#; - let resource = r#"ceramic://*?model=kjzl6hvfrbw6cadyci5lvsff4jxl1idffrp2ld3i0k1znz0b3k67abkmtf7p7q3"#; + let resource = + r#"ceramic://*?model=kjzl6hvfrbw6cadyci5lvsff4jxl1idffrp2ld3i0k1znz0b3k67abkmtf7p7q3"#; - authenticate(header, Operation::Read, resource).await.unwrap(); + authenticate(header, Operation::Read, resource) + .await + .unwrap(); + } + + fn generate_did_and_private_key() -> (DidDocument, String) { + let key = ssi::jwk::JWK::generate_ed25519().unwrap(); + let private_key = if let Params::OKP(params) = &key.params { + let pk = params.private_key.as_ref().unwrap(); + hex::encode(pk.0.as_slice()) + } else { + panic!("Failed to generate private key"); + }; + let did = did_method_key::DIDKey.generate(&Source::Key(&key)).unwrap(); + let mut builder = DocumentBuilder::default(); + builder.id(did); + let doc = builder.build().unwrap(); + (doc, private_key) } -} \ No newline at end of file + + async fn create_capability( + signer: &impl Signer, + parent: Option<&DidDocument>, + resources: Vec, + ) -> Capability { + let did = signer.id(); + let aud = parent + .map(|d| d.id.clone()) + .unwrap_or_else(|| did.id.clone()); + let payload = Payload { + issuer: did.id.clone(), + audience: aud, + issued_at: chrono::Utc::now(), + domain: "ceramic".to_string(), + version: "1.0.0".to_string(), + expiration: None, + statement: None, + not_before: None, + request_id: None, + resources: Some(resources), + nonce: "a".to_string(), + }; + let metadata = SignatureMetadata { + kid: did.id.clone(), + alg: format!("{:?}", signer.algorithm()), + rest: HashMap::default(), + }; + let header = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&metadata).unwrap()); + let encoded_payload = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&payload).unwrap()); + let signing_input = format!("{header}.{encoded_payload}"); + let signed = signer.sign(signing_input.as_bytes()).unwrap(); + let signature = Signature { + signature: URL_SAFE_NO_PAD.encode(&signed), + metadata, + r#type: SignatureType::JWS, + }; + Capability { + payload: payload, + signature: signature, + header: Header { + r#type: HeaderType::CAIP122, + }, + } + } + + #[tokio::test] + async fn should_authenticate_car() { + // create our root block + let (owner_did, owner_key) = generate_did_and_private_key(); + let owner_signer = JwkSigner::new(owner_did.clone(), &owner_key).await.unwrap(); + let (delegated_did, delegated_key) = generate_did_and_private_key(); + let delegated_signer = JwkSigner::new(delegated_did, &delegated_key).await.unwrap(); + + let stream_id = + StreamId::from_str("kjzl6hvfrbw6cadyci5lvsff4jxl1idffrp2ld3i0k1znz0b3k67abkmtf7p7q3") + .unwrap(); + let resource = format!("ceramic://*?model={stream_id}"); + let mut resources = vec![resource.clone()]; + let owner_cap = create_capability(&owner_signer, None, resources.clone()).await; + let owner_car = serde_ipld_dagcbor::to_vec(&owner_cap).unwrap(); + let owner_cid = cid_from_dag_cbor(&owner_car); + + let biscuit = biscuit!( + r#" + user({user}); + right({user}, "model", {stream_id}); + right({user}, {resource}); + "#, + user = delegated_signer.id().id.clone(), + stream_id = stream_id.to_string(), + resource = resource.clone(), + ); + let biscuit = URL_SAFE_NO_PAD.encode(biscuit.dump_code().as_bytes()); + resources.push(format!("prev:{}", owner_cid)); + resources.push(format!("biscuit:{}", biscuit)); + let delegated_cap = create_capability(&delegated_signer, Some(&owner_did), resources).await; + let delegated_car = serde_ipld_dagcbor::to_vec(&delegated_cap).unwrap(); + let delegated_cid = cid_from_dag_cbor(&delegated_car); + + let header = CarHeader::V1(CarHeaderV1::from(vec![delegated_cid])); + let mut buffer = Vec::new(); + let mut writer = CarWriter::new(header, &mut buffer); + writer.write(owner_cid, owner_car).await.unwrap(); + writer.write(delegated_cid, delegated_car).await.unwrap(); + let car = writer.finish().await.unwrap(); + + let bearer = URL_SAFE_NO_PAD.encode(&car); + + authenticate(&bearer, Operation::Read, &resource) + .await + .unwrap(); + } +} diff --git a/api/src/lib.rs b/api/src/lib.rs index 2eb146cfc..59af2a84d 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -5,6 +5,6 @@ pub use resume_token::ResumeToken; pub use server::{EventInsertResult, EventStore, InterestStore, Server}; +mod auth; #[cfg(test)] mod tests; -mod auth; diff --git a/api/src/server.rs b/api/src/server.rs index 1d5f27c10..8340254c2 100644 --- a/api/src/server.rs +++ b/api/src/server.rs @@ -20,6 +20,8 @@ use std::{ sync::{Arc, Mutex}, }; +use crate::auth; +use crate::auth::Operation; use anyhow::Result; use async_trait::async_trait; use ceramic_api_server::models::{BadRequestResponse, ErrorResponse, EventData}; @@ -34,15 +36,13 @@ use ceramic_api_server::{ FeedEventsGetResponse, FeedResumeTokenGetResponse, InterestsPostResponse, }; use ceramic_core::{Cid, EventId, Interest, Network, PeerId, StreamId}; +use ceramic_event::ssi::jsonld::syntax::parse::Error::Stream; use futures::TryFutureExt; use recon::Key; use swagger::{ApiError, ByteArray}; #[cfg(not(target_env = "msvc"))] use tikv_jemalloc_ctl::epoch; use tracing::{instrument, Level}; -use ceramic_event::ssi::jsonld::syntax::parse::Error::Stream; -use crate::auth; -use crate::auth::Operation; use crate::server::event::event_id_from_car; use crate::ResumeToken; @@ -780,10 +780,12 @@ where ) -> Result { let filter = if self.authentication { if let (Some(auth), Some(resource)) = (authorization, resource) { - auth::authenticate(&auth, Operation::Read, &resource).await.map_err(|err| { - tracing::debug!("Unauthorized: {err}"); - ApiError("Unauthorized".to_string()) - })?; + auth::authenticate(&auth, Operation::Read, &resource) + .await + .map_err(|err| { + tracing::debug!("Unauthorized: {err}"); + ApiError("Unauthorized".to_string()) + })?; Some(resource) } else { return Err(ApiError("Unauthorized".to_string())); @@ -895,10 +897,12 @@ where ) -> Result { if self.authentication { if let (Some(bearer), Some(resource)) = (bearer, resource) { - auth::authenticate(&bearer, Operation::Read, &resource).await.map_err(|err| { - tracing::debug!("Unauthorized: {err}"); - ApiError("Unauthorized".to_string()) - })?; + auth::authenticate(&bearer, Operation::Read, &resource) + .await + .map_err(|err| { + tracing::debug!("Unauthorized: {err}"); + ApiError("Unauthorized".to_string()) + })?; } else { return Err(ApiError("Unauthorized".to_string())); } diff --git a/api/src/tests.rs b/api/src/tests.rs index be649b9d5..b666dc2e8 100644 --- a/api/src/tests.rs +++ b/api/src/tests.rs @@ -583,7 +583,9 @@ async fn test_events_event_id_get_by_event_id_success() { .returning(move |_| Ok(Some(event_data.clone()))); let mock_interest = MockAccessInterestStoreTest::new(); let server = Server::new(peer_id, network, mock_interest, Arc::new(mock_event_store)); - let result = server.events_event_id_get(event_id_str, None, None, &Context).await; + let result = server + .events_event_id_get(event_id_str, None, None, &Context) + .await; let EventsEventIdGetResponse::Success(event) = result.unwrap() else { panic!("Expected EventsEventIdGetResponse::Success but got another variant"); }; diff --git a/event/src/lib.rs b/event/src/lib.rs index 435376659..99d7576cd 100644 --- a/event/src/lib.rs +++ b/event/src/lib.rs @@ -6,6 +6,7 @@ mod bytes; pub mod unvalidated; pub use ceramic_core::*; +pub use unvalidated::cid_from_dag_cbor; #[cfg(test)] pub mod tests { diff --git a/event/src/unvalidated/mod.rs b/event/src/unvalidated/mod.rs index 9e5dd94ba..916db96b1 100644 --- a/event/src/unvalidated/mod.rs +++ b/event/src/unvalidated/mod.rs @@ -13,7 +13,8 @@ use cid::Cid; use ipld_core::{codec::Codec, ipld::Ipld}; use serde_ipld_dagcbor::codec::DagCborCodec; -fn cid_from_dag_cbor(data: &[u8]) -> Cid { +/// Create a CID from a DAG-CBOR encoded data +pub fn cid_from_dag_cbor(data: &[u8]) -> Cid { Cid::new_v1( >::CODE, Code::Sha2_256.digest(data), diff --git a/event/src/unvalidated/signed/cacao.rs b/event/src/unvalidated/signed/cacao.rs index c05ba617a..08dbc529f 100644 --- a/event/src/unvalidated/signed/cacao.rs +++ b/event/src/unvalidated/signed/cacao.rs @@ -1,10 +1,11 @@ //! Structures for encoding and decoding CACAO capability objects. -use std::collections::HashMap; use serde::{Deserialize, Serialize}; +use ssi::jwk::Algorithm; +use std::collections::HashMap; /// Capability object, see https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-74.md -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Capability { /// Header for capability #[serde(rename = "h")] @@ -18,18 +19,18 @@ pub struct Capability { } /// Type of Capability Header -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub enum HeaderType { /// EIP-4361 Capability - #[serde(rename="eip4361")] + #[serde(rename = "eip4361")] EIP4361, /// CAIP-122 Capability - #[serde(rename="caip122")] + #[serde(rename = "caip122")] CAIP122, } /// Header for a Capability -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Header { /// Type of the Capability Header #[serde(rename = "t")] @@ -40,40 +41,33 @@ pub struct Header { pub type CapabilityTime = chrono::DateTime; /// Payload for a CACAO -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Payload { - /// Domain for payload - pub domain: String, - - /// Issuer for payload. For capability will be DID in URI format - #[serde(rename = "iss")] - pub issuer: String, - /// Audience for payload #[serde(rename = "aud")] pub audience: String, - /// Version of payload - pub version: String, + /// Domain for payload + pub domain: String, - /// Nonce of payload - pub nonce: String, + /// Expiration time + #[serde(rename = "exp", skip_serializing_if = "Option::is_none")] + pub expiration: Option, /// Issued at time #[serde(rename = "iat")] pub issued_at: CapabilityTime, + /// Issuer for payload. For capability will be DID in URI format + #[serde(rename = "iss")] + pub issuer: String, + /// Not before time #[serde(rename = "nbf", skip_serializing_if = "Option::is_none")] pub not_before: Option, - /// Expiration time - #[serde(rename = "exp", skip_serializing_if = "Option::is_none")] - pub expiration: Option, - - /// Subject of payload - #[serde(skip_serializing_if = "Option::is_none")] - pub statement: Option, + /// Nonce of payload + pub nonce: String, /// Request ID #[serde(rename = "requestId", skip_serializing_if = "Option::is_none")] @@ -82,10 +76,17 @@ pub struct Payload { /// Resources #[serde(skip_serializing_if = "Option::is_none")] pub resources: Option>, + + /// Subject of payload + #[serde(skip_serializing_if = "Option::is_none")] + pub statement: Option, + + /// Version of payload + pub version: String, } /// Type of Signature -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub enum SignatureType { /// EIP-191 Signature #[serde(rename = "eip191")] @@ -110,19 +111,23 @@ pub enum SignatureType { JWS, } -/// Known metadata type for signatures -#[derive(Debug, Deserialize, Serialize)] -pub struct KnownMetadata { - /// Algorithm for signature - pub alg: String, - /// capability for signature - pub cap: String, - /// Key ID for signature - pub kid: String, +impl SignatureType { + /// Convert signature type to algorithm + pub fn algorithm(&self) -> Algorithm { + match self { + SignatureType::EIP191 => Algorithm::ES256, + SignatureType::EIP1271 => Algorithm::ES256, + SignatureType::SolanaED25519 => Algorithm::EdDSA, + SignatureType::TezosED25519 => Algorithm::EdDSA, + SignatureType::StacksSECP256K1 => Algorithm::ES256K, + SignatureType::WebAuthNP256 => Algorithm::ES256, + SignatureType::JWS => Algorithm::ES256, + } + } } /// Values for unknown metadata -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(untagged)] pub enum MetadataValue { /// Boolean value @@ -136,17 +141,19 @@ pub enum MetadataValue { } /// Metadata for signature -#[derive(Debug, Deserialize, Serialize)] -#[serde(untagged)] -pub enum SignatureMetadata { - /// Known metadata - Known(KnownMetadata), - /// Unknown metadata - Unknown(HashMap), +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct SignatureMetadata { + /// Algorithm for signature + pub alg: String, + /// Key ID for signature + pub kid: String, + /// Other metadata + #[serde(flatten)] + pub rest: HashMap, } /// Signature of a CACAO -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Signature { /// Metadata for signature #[serde(rename = "m")] diff --git a/one/src/lib.rs b/one/src/lib.rs index a3ff39514..890f0fc96 100644 --- a/one/src/lib.rs +++ b/one/src/lib.rs @@ -172,7 +172,7 @@ struct DaemonOpts { #[arg( long, default_value_t = false, - env = "CERAMIC_ONE_EXPERIMENTAL_AUTHENTICATION", + env = "CERAMIC_ONE_EXPERIMENTAL_AUTHENTICATION" )] experimental_authentication: bool, }