Skip to main content

yip_ca/
enroll.rs

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
14/// Client function to enroll a node with the online threshold CA.
15pub 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    // 1. Request challenge
28    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    // 2. Build CertTbs
48    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    // 3. Compute Proof-of-Possession signature
64    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    // 4. Submit Enrollment
75    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    // 5. Read enrollment response
89    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}