1use std::net::SocketAddr;
2use std::time::{SystemTime, UNIX_EPOCH};
3
4use ed25519_dalek::{Signer, SigningKey};
5use tokio::net::TcpStream;
6
7use crate::rpc::{
8 CaWireMessage, CertTbs, ClientRpcRequest, ClientRpcResponse, EnrollmentCredential,
9 SubmitEnrollmentPayload, ENROLL_POP_CONTEXT,
10};
11use crate::serve::{read_framed, write_framed};
12use yip_membership::cert::Cert;
13
14pub async fn enroll_node(
16 server_addr: SocketAddr,
17 client_signing_key: &SigningKey,
18 member_pubkey: [u8; 32],
19 member_sign_pubkey: [u8; 32],
20 network_id: [u8; 16],
21 credential: EnrollmentCredential,
22) -> Result<Cert, String> {
23 let mut stream = TcpStream::connect(server_addr)
24 .await
25 .map_err(|e| format!("connection error: {e}"))?;
26
27 let req = CaWireMessage::ClientReq(ClientRpcRequest::GetChallenge);
29 write_framed(&mut stream, &req)
30 .await
31 .map_err(|e| format!("failed to send GetChallenge: {e}"))?;
32
33 let challenge_resp: CaWireMessage = read_framed(&mut stream)
34 .await
35 .map_err(|e| format!("failed to read challenge: {e}"))?;
36
37 let challenge_nonce = match challenge_resp {
38 CaWireMessage::ClientResp(ClientRpcResponse::Challenge {
39 challenge_nonce, ..
40 }) => challenge_nonce,
41 CaWireMessage::ClientResp(ClientRpcResponse::Rejected { reason }) => {
42 return Err(format!("server rejected challenge request: {reason}"));
43 }
44 _ => return Err("expected challenge response from server".into()),
45 };
46
47 let now = SystemTime::now()
49 .duration_since(UNIX_EPOCH)
50 .unwrap()
51 .as_secs();
52
53 let csr_tbs = CertTbs {
54 version: 1,
55 member_pubkey,
56 member_sign_pubkey,
57 network_id,
58 not_before: now.saturating_sub(60),
59 not_after: now + 86_400,
60 tags: Vec::new(),
61 };
62
63 let tbs_bytes = csr_tbs.to_canonical_bytes();
65 let mut pop_msg = Vec::new();
66 pop_msg.extend_from_slice(ENROLL_POP_CONTEXT);
67 pop_msg.extend_from_slice(&challenge_nonce);
68 pop_msg.extend_from_slice(&tbs_bytes);
69
70 let pop_signature: ed25519_dalek::Signature = client_signing_key.sign(&pop_msg);
71 let mut pop_arr = [0u8; 64];
72 pop_arr.copy_from_slice(&pop_signature.to_bytes());
73
74 let submit_payload = SubmitEnrollmentPayload {
76 challenge_nonce,
77 csr_tbs,
78 credential,
79 pop_signature: pop_arr,
80 };
81
82 let submit_msg =
83 CaWireMessage::ClientReq(ClientRpcRequest::SubmitEnrollment(Box::new(submit_payload)));
84 write_framed(&mut stream, &submit_msg)
85 .await
86 .map_err(|e| format!("failed to send SubmitEnrollment: {e}"))?;
87
88 let enroll_resp: CaWireMessage = read_framed(&mut stream)
90 .await
91 .map_err(|e| format!("failed to read enroll response: {e}"))?;
92
93 match enroll_resp {
94 CaWireMessage::ClientResp(ClientRpcResponse::Enrolled { cert_bytes, .. }) => {
95 Cert::decode(&cert_bytes).ok_or_else(|| "failed to decode certificate".to_string())
96 }
97 CaWireMessage::ClientResp(ClientRpcResponse::Rejected { reason }) => {
98 Err(format!("enrollment rejected by CA: {reason}"))
99 }
100 _ => Err("unexpected enrollment response".into()),
101 }
102}