From 02cd7b33d090db2aa47126a4d1aeecb247e7b7ef Mon Sep 17 00:00:00 2001 From: Bryan Newbold Date: Mon, 7 Nov 2022 20:16:28 -0800 Subject: pds: refactor account creation, did docs, etc To allow creating accounts from the CLI, and creating did:web accounts (as opposed to did:plc). Also a buch of config refactoring. --- adenosine-pds/src/bin/adenosine-pds.rs | 87 +++++++++++++- adenosine-pds/src/did.rs | 98 +++++++++------- adenosine-pds/src/lib.rs | 206 ++++++++++++++++++++++----------- 3 files changed, 280 insertions(+), 111 deletions(-) diff --git a/adenosine-pds/src/bin/adenosine-pds.rs b/adenosine-pds/src/bin/adenosine-pds.rs index 3587896..5efa190 100644 --- a/adenosine-pds/src/bin/adenosine-pds.rs +++ b/adenosine-pds/src/bin/adenosine-pds.rs @@ -1,5 +1,8 @@ +use adenosine_cli::identifiers::Did; +use adenosine_pds::models::AccountRequest; use adenosine_pds::*; use anyhow::Result; +use serde_json::json; use log::{self, debug}; use structopt::StructOpt; @@ -43,10 +46,6 @@ struct Opt { enum Command { /// Start ATP server as a foreground process Serve { - /// Localhost port to listen on - #[structopt(long, default_value = "3030", env = "ATP_PDS_PORT")] - port: u16, - /// Secret key, encoded in hex. Use 'generate-secret' to create a new one #[structopt( long = "--pds-secret-key", @@ -55,11 +54,27 @@ enum Command { )] pds_secret_key: String, - #[structopt(long = "--registration-domain", env = "ATP_PDS_REGISTRATION_DOMAIN")] - registration_domain: Option, + /// Localhost port to listen on + #[structopt(long, default_value = "3030", env = "ATP_PDS_PORT")] + port: u16, + /// A "public URL" for the PDS gets embedded in DID documents. If one is not provided, a + /// localhost value will be used, which will not actually work for inter-PDS communication. #[structopt(long = "--public-url", env = "ATP_PDS_PUBLIC_URL")] public_url: Option, + + /// If provided, allow registration for the given base domain name. + #[structopt(long = "--registration-domain", env = "ATP_PDS_REGISTRATION_DOMAIN")] + registration_domain: Option, + + /// Optionally, require an invite code to sign up. This is just a single secret value. + #[structopt(long = "--invite-code", env = "ATP_PDS_INVITE_CODE")] + invite_code: Option, + + /// Optionally, override domain name check and force the homepage to display this user page + /// for this DID + #[structopt(long = "--homepage-did", env = "ATP_PDS_HOMEPAGE_DID")] + homepage_did: Option, }, /// Helper to import an IPLD CARv1 file in to sqlite data store @@ -77,6 +92,37 @@ enum Command { /// Generate a PDS secret key and print to stdout (as hex) GenerateSecret, + + /// Create a new account with a did:plc. Bypasses most checks that the API would require for + /// account registration. + Register { + /// Secret key, encoded in hex. Use 'generate-secret' to create a new one + #[structopt( + long = "--pds-secret-key", + env = "ATP_PDS_SECRET_KEY", + hide_env_values = true + )] + pds_secret_key: String, + + #[structopt(long = "--public-url", env = "ATP_PDS_PUBLIC_URL")] + public_url: Option, + + #[structopt(long, short)] + handle: String, + + #[structopt(long, short)] + password: String, + + #[structopt(long, short)] + email: String, + + #[structopt(long, short)] + recovery_key: Option, + + /// Should we generate a did:plc, instead of using the handle as a did:web? + #[structopt(long, short)] + did_plc: bool, + }, } fn main() -> Result<()> { @@ -106,6 +152,8 @@ fn main() -> Result<()> { pds_secret_key, registration_domain, public_url, + invite_code, + homepage_did, } => { let keypair = KeyPair::from_hex(&pds_secret_key)?; // clean up config a bit @@ -123,6 +171,8 @@ fn main() -> Result<()> { listen_host_port: format!("localhost:{}", port), public_url: public_url, registration_domain: registration_domain, + invite_code: invite_code, + homepage_did: homepage_did, }; log::info!("PDS config: {:?}", config); let srv = AtpService::new(&opt.blockstore_db_path, &opt.atp_db_path, keypair, config)?; @@ -140,5 +190,30 @@ fn main() -> Result<()> { println!("{}", keypair.to_hex()); Ok(()) } + Command::Register { + handle, + password, + email, + recovery_key, + pds_secret_key, + public_url, + did_plc, + } => { + let req = AccountRequest { + email: email, + handle: handle.clone(), + password: password, + inviteCode: None, + recoveryKey: recovery_key, + }; + let mut config = AtpServiceConfig::default(); + config.public_url = public_url.unwrap_or(format!("https://{}", handle)); + let keypair = KeyPair::from_hex(&pds_secret_key)?; + let mut srv = + AtpService::new(&opt.blockstore_db_path, &opt.atp_db_path, keypair, config)?; + let sess = create_account(&mut srv, &req, did_plc)?; + println!("{}", json!(sess)); + Ok(()) + } } } diff --git a/adenosine-pds/src/did.rs b/adenosine-pds/src/did.rs index 84cf4c2..74e4f68 100644 --- a/adenosine-pds/src/did.rs +++ b/adenosine-pds/src/did.rs @@ -90,45 +90,6 @@ impl CreateOp { Did::from_str(&format!("did:plc:{}", &digest_b32[0..24])).unwrap() } - pub fn did_doc(&self) -> serde_json::Value { - let did = self.did_plc(); - // TODO: - let user_url = format!("https://{}.test", self.username); - let key_type = "EcdsaSecp256r1VerificationKey2019"; - json!({ - "@context": [ - "https://www.w3.org/ns/did/v1", - "https://w3id.org/security/suites/ecdsa-2019/v1" - ], - "id": did.to_string(), - "alsoKnownAs": [ user_url ], - "verificationMethod": [ - { - "id": format!("{}#signingKey)", did), - "type": key_type, - "controller": did.to_string(), - "publicKeyMultibase": self.signingKey - }, - { - "id": format!("{}#recoveryKey)", did), - "type": key_type, - "controller": did.to_string(), - "publicKeyMultibase": self.recoveryKey - } - ], - "assertionMethod": [ format!("{}#signingKey)", did)], - "capabilityInvocation": [ format!("{}#signingKey)", did) ], - "capabilityDelegation": [ format!("{}#signingKey)", did) ], - "service": [ - { - "id": format!("{}#atpPds)", did), - "type": "AtpPersonalDataServer", - "serviceEndpoint": self.service - } - ] - }) - } - fn into_unsigned(self) -> UnsignedCreateOp { UnsignedCreateOp { op_type: self.op_type, @@ -140,6 +101,18 @@ impl CreateOp { } } + pub fn did_doc(&self) -> serde_json::Value { + let meta = DidDocMeta { + did: self.did_plc(), + // TODO + user_url: format!("https://{}", self.username), + service_url: self.service.clone(), + recovery_didkey: self.recoveryKey.clone(), + signing_didkey: self.signingKey.clone(), + }; + meta.did_doc() + } + /// This method only makes sense on the "genesis" create object pub fn verify_self(&self) -> Result<()> { let key = PubKey::from_did_key(&self.signingKey)?; @@ -154,6 +127,53 @@ impl CreateOp { } } +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct DidDocMeta { + pub did: Did, + pub user_url: String, + pub service_url: String, + pub recovery_didkey: String, + pub signing_didkey: String, +} + +impl DidDocMeta { + pub fn did_doc(&self) -> serde_json::Value { + let key_type = "EcdsaSecp256r1VerificationKey2019"; + json!({ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/ecdsa-2019/v1" + ], + "id": self.did.to_string(), + "alsoKnownAs": [ self.user_url ], + "verificationMethod": [ + { + "id": format!("{}#signingKey)", self.did), + "type": key_type, + "controller": self.did.to_string(), + "publicKeyMultibase": self.signing_didkey + }, + { + "id": format!("{}#recoveryKey)", self.did), + "type": key_type, + "controller": self.did.to_string(), + "publicKeyMultibase": self.recovery_didkey + } + ], + "assertionMethod": [ format!("{}#signingKey)", self.did)], + "capabilityInvocation": [ format!("{}#signingKey)", self.did) ], + "capabilityDelegation": [ format!("{}#signingKey)", self.did) ], + "service": [ + { + "id": format!("{}#atpPds)", self.did), + "type": "AtpPersonalDataServer", + "serviceEndpoint": self.service_url + } + ] + }) + } +} + #[test] fn test_debug_did_signing() { let op = UnsignedCreateOp { diff --git a/adenosine-pds/src/lib.rs b/adenosine-pds/src/lib.rs index 3389281..468cc6e 100644 --- a/adenosine-pds/src/lib.rs +++ b/adenosine-pds/src/lib.rs @@ -17,7 +17,7 @@ mod car; mod crypto; mod db; mod did; -mod models; +pub mod models; pub mod mst; mod repo; mod ucan_p256; @@ -26,6 +26,7 @@ mod web; pub use crypto::{KeyPair, PubKey}; pub use db::AtpDatabase; +pub use did::DidDocMeta; pub use models::*; pub use repo::{Mutation, RepoCommit, RepoStore}; pub use ucan_p256::P256KeyMaterial; @@ -44,6 +45,20 @@ pub struct AtpServiceConfig { pub listen_host_port: String, pub public_url: String, pub registration_domain: Option, + pub invite_code: Option, + pub homepage_did: Option, +} + +impl Default for AtpServiceConfig { + fn default() -> Self { + AtpServiceConfig { + listen_host_port: "localhost:3030".to_string(), + public_url: "http://localhost".to_string(), + registration_domain: None, + invite_code: None, + homepage_did: None, + } + } } #[derive(Debug)] @@ -141,19 +156,23 @@ impl AtpService { rouille::start_server(config.listen_host_port, move |request| { rouille::log_custom(request, log_ok, log_err, || { router!(request, + // ============= Web Interface (GET) ["/"] => { - let view = GenericHomeView { domain: "domain.todo".to_string() }; - Response::html(view.render().unwrap()) + let host = request.header("Host").unwrap_or("localhost"); + if Some(host.to_string()) == config.registration_domain { + let view = GenericHomeView { domain: host.to_string() }; + Response::html(view.render().unwrap()) + } else { + web_wrap(home_profile_handler(&srv, request)) + } }, (GET) ["/about"] => { - let view = AboutView { domain: "domain.todo".to_string() }; + let host = request.header("Host").unwrap_or("localhost"); + let view = AboutView { domain: host.to_string() }; Response::html(view.render().unwrap()) }, - (GET) ["/robots.txt"] => { - Response::text(include_str!("../templates/robots.txt")) - }, - (GET) ["/u/{did}", did: Did] => { - web_wrap(profile_handler(&srv, &did, request)) + (GET) ["/u/{handle}", handle: String] => { + web_wrap(profile_handler(&srv, &handle, request)) }, (GET) ["/u/{did}/{collection}/{tid}", did: Did, collection: Nsid, tid: Tid] => { web_wrap(post_handler(&srv, &did, &collection, &tid, request)) @@ -167,6 +186,7 @@ impl AtpService { (GET) ["/at/{did}/{collection}/{tid}", did: Did, collection: Nsid, tid: Tid] => { web_wrap(record_handler(&srv, &did, &collection, &tid, request)) }, + // ============ Static Files (compiled in to executable) (GET) ["/static/adenosine.css"] => { Response::from_data("text/css", include_str!("../templates/adenosine.css")) }, @@ -176,6 +196,10 @@ impl AtpService { (GET) ["/static/logo_128.png"] => { Response::from_data("image/png", include_bytes!("../templates/logo_128.png").to_vec()) }, + (GET) ["/robots.txt"] => { + Response::text(include_str!("../templates/robots.txt")) + }, + // ============ XRPC AT Protocol (POST) ["/xrpc/{endpoint}", endpoint: String] => { xrpc_wrap(xrpc_post_handler(&srv, &endpoint, request)) }, @@ -289,7 +313,14 @@ fn xrpc_get_handler( ) -> Result { match method { "com.atproto.server.getAccountsConfig" => { - Ok(json!({"availableUserDomains": ["test"], "inviteCodeRequired": false})) + let srv = srv.lock().expect("service mutex"); + let mut avail_domains = vec![]; + if let Some(domain) = &srv.config.registration_domain { + avail_domains.push(domain) + } + Ok( + json!({"availableUserDomains": avail_domains, "inviteCodeRequired": srv.config.invite_code.is_some()}), + ) } "com.atproto.repo.getRecord" => { let did = Did::from_str(&xrpc_required_param(request, "user")?)?; @@ -370,6 +401,70 @@ fn xrpc_get_repo_handler(srv: &Mutex, request: &Request) -> Result Result { + // check if account already exists (fast path, also confirmed by database schema) + if srv.atp_db.account_exists(&req.handle, &req.email)? { + Err(XrpcError::BadRequest( + "handle or email already exists".to_string(), + ))?; + }; + + debug!("trying to create new account: {}", &req.handle); + + let (did, did_doc) = if create_did_plc { + // generate DID + let create_op = did::CreateOp::new( + req.handle.clone(), + srv.config.public_url.clone(), + &srv.pds_keypair, + req.recoveryKey.clone(), + ); + create_op.verify_self()?; + let did = create_op.did_plc(); + let did_doc = create_op.did_doc(); + (did, did_doc) + } else { + let did = Did::from_str(&format!("did:web:{}", req.handle))?; + let signing_key = srv.pds_keypair.pubkey().to_did_key(); + let recovery_key = req.recoveryKey.clone().unwrap_or(signing_key.clone()); + let meta = DidDocMeta { + did: did.clone(), + user_url: format!("https://{}", req.handle), + service_url: srv.config.public_url.clone(), + recovery_didkey: recovery_key, + signing_didkey: signing_key, + }; + (did, meta.did_doc()) + }; + + // register in ATP DB and generate DID doc + let recovery_key = req + .recoveryKey + .clone() + .unwrap_or(srv.pds_keypair.pubkey().to_did_key()); + srv.atp_db + .create_account(&did, &req.handle, &req.password, &req.email, &recovery_key)?; + srv.atp_db.put_did_doc(&did, &did_doc)?; + + // insert empty MST repository + let root_cid = { + let empty_map_cid = srv.repo.mst_from_map(&Default::default())?; + let meta_cid = srv.repo.write_metadata(&did)?; + srv.repo.write_root(meta_cid, None, empty_map_cid)? + }; + let _commit_cid = srv.repo.write_commit(&did, root_cid, "XXX-dummy-sig")?; + + let keypair = srv.pds_keypair.clone(); + let sess = srv + .atp_db + .create_session(&req.handle, &req.password, &keypair)?; + Ok(sess) +} + fn xrpc_post_handler( srv: &Mutex, method: &str, @@ -381,53 +476,8 @@ fn xrpc_post_handler( let req: AccountRequest = rouille::input::json_input(request) .map_err(|e| XrpcError::BadRequest(format!("failed to parse JSON body: {}", e)))?; // TODO: validate handle, email, recoverykey - - // check if account already exists (fast path, also confirmed by database schema) let mut srv = srv.lock().unwrap(); - if srv.atp_db.account_exists(&req.handle, &req.email)? { - Err(XrpcError::BadRequest( - "handle or email already exists".to_string(), - ))?; - }; - - debug!("trying to create new account: {}", &req.handle); - - // generate DID - let create_op = did::CreateOp::new( - req.handle.clone(), - srv.config.public_url.clone(), - &srv.pds_keypair, - req.recoveryKey.clone(), - ); - create_op.verify_self()?; - let did = create_op.did_plc(); - let did_doc = create_op.did_doc(); - - // register in ATP DB and generate DID doc - let recovery_key = req - .recoveryKey - .unwrap_or(srv.pds_keypair.pubkey().to_did_key()); - srv.atp_db.create_account( - &did, - &req.handle, - &req.password, - &req.email, - &recovery_key, - )?; - srv.atp_db.put_did_doc(&did, &did_doc)?; - - // insert empty MST repository - let root_cid = { - let empty_map_cid = srv.repo.mst_from_map(&Default::default())?; - let meta_cid = srv.repo.write_metadata(&did)?; - srv.repo.write_root(meta_cid, None, empty_map_cid)? - }; - let _commit_cid = srv.repo.write_commit(&did, root_cid, "XXX-dummy-sig")?; - - let keypair = srv.pds_keypair.clone(); - let sess = srv - .atp_db - .create_session(&req.handle, &req.password, &keypair)?; + let sess = create_account(&mut srv, &req, true)?; Ok(json!(sess)) } "com.atproto.session.create" => { @@ -562,7 +612,27 @@ fn xrpc_post_handler( } } -fn profile_handler(srv: &Mutex, did: &str, _request: &Request) -> Result { +fn home_profile_handler(srv: &Mutex, request: &Request) -> Result { + let host = request.header("Host").unwrap_or("localhost"); + // XXX + let did = Did::from_str(host)?; + let mut _srv = srv.lock().expect("service mutex"); + + // TODO: get profile (bsky helper) + // TODO: get feed (bsky helper) + + Ok(ProfileView { + domain: host.to_string(), + did: did, + profile: json!({}), + feed: vec![], + } + .render()?) +} + +// TODO: did, collection, tid have already been parsed by this point +fn profile_handler(srv: &Mutex, did: &str, request: &Request) -> Result { + let host = request.header("Host").unwrap_or("localhost"); let did = Did::from_str(did)?; let mut _srv = srv.lock().expect("service mutex"); @@ -570,7 +640,7 @@ fn profile_handler(srv: &Mutex, did: &str, _request: &Request) -> Re // TODO: get feed (bsky helper) Ok(ProfileView { - domain: "domain.todo".to_string(), + domain: host.to_string(), did: did, profile: json!({}), feed: vec![], @@ -583,8 +653,9 @@ fn post_handler( did: &str, collection: &str, tid: &str, - _request: &Request, + request: &Request, ) -> Result { + let host = request.header("Host").unwrap_or("localhost"); let did = Did::from_str(did)?; let collection = Nsid::from_str(collection)?; let rkey = Tid::from_str(tid)?; @@ -601,7 +672,7 @@ fn post_handler( }; Ok(PostView { - domain: "domain.todo".to_string(), + domain: host.to_string(), did: did, collection: collection, tid: rkey, @@ -611,7 +682,8 @@ fn post_handler( .render()?) } -fn repo_handler(srv: &Mutex, did: &str, _request: &Request) -> Result { +fn repo_handler(srv: &Mutex, did: &str, request: &Request) -> Result { + let host = request.header("Host").unwrap_or("localhost"); let did = Did::from_str(did)?; let mut srv = srv.lock().expect("service mutex"); @@ -628,7 +700,7 @@ fn repo_handler(srv: &Mutex, did: &str, _request: &Request) -> Resul }; Ok(RepoView { - domain: "domain.todo".to_string(), + domain: host.to_string(), did: did, commit: commit, describe: desc, @@ -640,8 +712,9 @@ fn collection_handler( srv: &Mutex, did: &str, collection: &str, - _request: &Request, + request: &Request, ) -> Result { + let host = request.header("Host").unwrap_or("localhost"); let did = Did::from_str(did)?; let collection = Nsid::from_str(collection)?; @@ -665,7 +738,7 @@ fn collection_handler( } Ok(CollectionView { - domain: "domain.todo".to_string(), + domain: host.to_string(), did: did, collection: collection, records: record_list, @@ -678,8 +751,9 @@ fn record_handler( did: &str, collection: &str, tid: &str, - _request: &Request, + request: &Request, ) -> Result { + let host = request.header("Host").unwrap_or("localhost"); let did = Did::from_str(did)?; let collection = Nsid::from_str(collection)?; let rkey = Tid::from_str(tid)?; @@ -695,7 +769,7 @@ fn record_handler( Err(e) => Err(e)?, }; Ok(RecordView { - domain: "domain.todo".to_string(), + domain: host.to_string(), did, collection, tid: rkey, -- cgit v1.2.3