Skip to main content

cpop_protocol/
identity.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use crate::error::{Error, Result};
4use const_oid::AssociatedOid;
5use ed25519_dalek::{Signer, SigningKey, VerifyingKey};
6use rand::rngs::OsRng;
7use rand::RngCore;
8use serde::{Deserialize, Serialize};
9use sha2::{Digest, Sha256};
10use signature::Keypair;
11use spki::{
12    AlgorithmIdentifierOwned, DynSignatureAlgorithmIdentifier, EncodePublicKey, ObjectIdentifier,
13    SignatureBitStringEncoding, SubjectPublicKeyInfoOwned,
14};
15use std::str::FromStr;
16use x509_cert::builder::{Builder, RequestBuilder};
17use x509_cert::der::asn1::{BitString, OctetString};
18use x509_cert::der::{Encode, FixedTag, Tag};
19use x509_cert::ext::pkix::SubjectKeyIdentifier;
20use x509_cert::ext::{AsExtension, Extension};
21use x509_cert::name::Name;
22use zeroize::Zeroizing;
23
24const ED25519_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.101.112");
25
26/// Wrapper to implement x509-cert builder traits for VerifyingKey.
27#[derive(Clone, Debug)]
28pub struct X509VerifyingKey(pub VerifyingKey);
29
30impl EncodePublicKey for X509VerifyingKey {
31    fn to_public_key_der(&self) -> spki::Result<spki::der::Document> {
32        let spki = SubjectPublicKeyInfoOwned {
33            algorithm: AlgorithmIdentifierOwned {
34                oid: ED25519_OID,
35                parameters: None,
36            },
37            subject_public_key: BitString::from_bytes(self.0.as_bytes())?,
38        };
39        let der = spki.to_der().map_err(|_| spki::Error::KeyMalformed)?;
40        spki::der::Document::try_from(der.as_slice()).map_err(|_| spki::Error::KeyMalformed)
41    }
42}
43
44/// Wrapper to implement x509-cert builder traits for SigningKey.
45pub struct X509Signer(pub SigningKey);
46
47impl Keypair for X509Signer {
48    type VerifyingKey = X509VerifyingKey;
49    fn verifying_key(&self) -> Self::VerifyingKey {
50        X509VerifyingKey(self.0.verifying_key())
51    }
52}
53
54impl DynSignatureAlgorithmIdentifier for X509Signer {
55    fn signature_algorithm_identifier(&self) -> spki::Result<AlgorithmIdentifierOwned> {
56        Ok(AlgorithmIdentifierOwned {
57            oid: ED25519_OID,
58            parameters: None,
59        })
60    }
61}
62
63/// Newtype wrapper over Ed25519 signature for `SignatureBitStringEncoding` (orphan rule).
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct X509Signature(pub ed25519_dalek::Signature);
66
67impl signature::SignatureEncoding for X509Signature {
68    type Repr = [u8; 64];
69    fn to_bytes(&self) -> Self::Repr {
70        self.0.to_bytes()
71    }
72}
73
74impl TryFrom<&[u8]> for X509Signature {
75    type Error = signature::Error;
76    fn try_from(bytes: &[u8]) -> std::result::Result<Self, Self::Error> {
77        ed25519_dalek::Signature::from_slice(bytes)
78            .map(X509Signature)
79            .map_err(|_| signature::Error::new())
80    }
81}
82
83impl From<X509Signature> for [u8; 64] {
84    fn from(sig: X509Signature) -> Self {
85        sig.0.to_bytes()
86    }
87}
88
89impl Signer<X509Signature> for X509Signer {
90    fn try_sign(&self, msg: &[u8]) -> std::result::Result<X509Signature, signature::Error> {
91        Ok(X509Signature(self.0.sign(msg)))
92    }
93}
94
95impl SignatureBitStringEncoding for X509Signature {
96    fn to_bitstring(&self) -> std::result::Result<BitString, x509_cert::der::Error> {
97        BitString::from_bytes(&self.0.to_bytes())
98    }
99}
100
101/// X.509 extension for CPoP capability (OID 1.3.6.1.4.1.54066.1.1).
102#[derive(Clone, Debug, PartialEq, Eq)]
103pub struct Capability(pub OctetString);
104
105impl AssociatedOid for Capability {
106    const OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.6.1.4.1.54066.1.1");
107}
108
109impl Encode for Capability {
110    fn encoded_len(&self) -> x509_cert::der::Result<x509_cert::der::Length> {
111        self.0.encoded_len()
112    }
113    fn encode(&self, encoder: &mut impl x509_cert::der::Writer) -> x509_cert::der::Result<()> {
114        self.0.encode(encoder)
115    }
116}
117
118impl FixedTag for Capability {
119    const TAG: Tag = Tag::OctetString;
120}
121
122impl AsExtension for Capability {
123    fn critical(&self, _: &x509_cert::name::RdnSequence, _: &[Extension]) -> bool {
124        false
125    }
126}
127
128#[derive(Debug, Serialize, Deserialize)]
129pub struct EnrollmentRequest {
130    pub user_id: String,
131    /// COSE-encoded Ed25519 public key bytes.
132    pub public_key_cose: Vec<u8>,
133    /// TPM quote or Secure Enclave blob; empty for software-only.
134    pub hardware_attestation: Vec<u8>,
135}
136
137pub struct IdentityManager {
138    signer: X509Signer,
139}
140
141impl IdentityManager {
142    pub fn generate() -> Self {
143        let mut bytes = Zeroizing::new([0u8; 32]);
144        OsRng.fill_bytes(bytes.as_mut());
145        Self {
146            signer: X509Signer(SigningKey::from_bytes(&bytes)),
147        }
148    }
149
150    pub fn from_secret_key(bytes: &[u8; 32]) -> Self {
151        Self {
152            signer: X509Signer(SigningKey::from_bytes(bytes)),
153        }
154    }
155
156    pub fn signing_key(&self) -> &SigningKey {
157        &self.signer.0
158    }
159
160    /// Generate a DER-encoded X.509 CSR with SKI and CPoP capability extensions.
161    pub fn generate_csr(&self, subject_dn: &str) -> Result<Vec<u8>> {
162        let subject = Name::from_str(subject_dn)
163            .map_err(|e| Error::Validation(format!("Invalid Subject DN: {}", e)))?;
164
165        let mut builder = RequestBuilder::new(subject, &self.signer)
166            .map_err(|e| Error::Crypto(format!("CSR builder error: {}", e)))?;
167
168        let public_key_bytes = self.signer.0.verifying_key().to_bytes();
169        let hash = Sha256::digest(public_key_bytes);
170        let ski = SubjectKeyIdentifier(
171            OctetString::new(hash.to_vec())
172                .map_err(|e| Error::Crypto(format!("Failed to create OctetString: {}", e)))?,
173        );
174        builder
175            .add_extension(&ski)
176            .map_err(|e| Error::Crypto(format!("Failed to add SKI extension: {}", e)))?;
177
178        let pop_cap =
179            Capability(OctetString::new(vec![0x01]).map_err(|e| Error::Crypto(e.to_string()))?);
180        builder
181            .add_extension(&pop_cap)
182            .map_err(|e| Error::Crypto(format!("Failed to add CPoP extension: {}", e)))?;
183
184        let csr = builder
185            .build::<X509Signature>()
186            .map_err(|e| Error::Crypto(format!("CSR signing error: {}", e)))?;
187
188        csr.to_der()
189            .map_err(|e| Error::Crypto(format!("DER encoding error: {}", e)))
190    }
191
192    /// `hardware_attestation`: TPM quote or Secure Enclave blob; empty for software-only.
193    pub fn create_enrollment_request(
194        &self,
195        user_id: &str,
196        hardware_attestation: &[u8],
197    ) -> Result<EnrollmentRequest> {
198        let public_key_cose = self.signer.0.verifying_key().to_bytes().to_vec();
199
200        Ok(EnrollmentRequest {
201            user_id: user_id.to_string(),
202            public_key_cose,
203            hardware_attestation: hardware_attestation.to_vec(),
204        })
205    }
206}