Skip to main content

auths_id/identity/
helpers.rs

1use crate::error::StorageError;
2#[cfg(feature = "git-storage")]
3use crate::storage::git_refs::aggregate_canonical_refs;
4use pkcs8::{
5    AlgorithmIdentifierRef, PrivateKeyInfo,
6    der::{Decode, Encode},
7};
8#[cfg(feature = "git-storage")]
9use std::collections::HashMap;
10use thiserror::Error;
11
12use ring::rand::SystemRandom;
13use ring::signature::Ed25519KeyPair;
14
15use crate::storage::attestation::AttestationSource;
16
17use auths_verifier::core::{Attestation, ResourceId};
18use auths_verifier::types::{DeviceDID, IdentityDID};
19
20pub use crate::identity::managed::ManagedIdentity;
21
22const OID_ED25519: pkcs8::der::asn1::ObjectIdentifier =
23    pkcs8::der::asn1::ObjectIdentifier::new_unwrap("1.3.101.112");
24
25#[derive(Debug, Clone)]
26pub struct Identity {
27    pub did: IdentityDID,
28    pub rid: ResourceId,
29    pub device_dids: Vec<DeviceDID>,
30}
31
32impl Identity {
33    pub fn fetch_attestations(
34        &self,
35        source: &dyn AttestationSource,
36    ) -> Result<Vec<Attestation>, StorageError> {
37        let mut all_attestations = Vec::new();
38        for device_did in &self.device_dids {
39            let device_attestations = source.load_attestations_for_device(device_did)?;
40            all_attestations.extend(device_attestations);
41        }
42        Ok(all_attestations)
43    }
44
45    #[cfg(feature = "git-storage")]
46    pub fn canonical_refs(
47        &self,
48        repo: &git2::Repository,
49    ) -> Result<HashMap<String, String>, StorageError> {
50        aggregate_canonical_refs(repo, &self.device_dids)
51    }
52}
53
54#[derive(Error, Debug)]
55#[non_exhaustive]
56pub enum IdentityError {
57    #[error("KERI error: {0}")]
58    Keri(String),
59    #[error("PKCS#8 encoding error: {0}")]
60    Pkcs8EncodeError(String),
61    #[error("PKCS#8 decoding error: {0}")]
62    Pkcs8DecodeError(String),
63    #[error("Passphrase required")]
64    EmptyPassphrase,
65    #[error("Invalid key length: expected 32, got {0}")]
66    InvalidKeyLength(usize),
67    #[error("Key storage error: {0}")]
68    KeyStorage(String),
69    #[error("Key retrieval error: {0}")]
70    KeyRetrieval(String),
71    #[error("Ring crypto error: {0}")]
72    RingError(String),
73}
74
75impl From<ring::error::Unspecified> for IdentityError {
76    fn from(err: ring::error::Unspecified) -> Self {
77        IdentityError::RingError(format!("Unspecified ring error: {err}"))
78    }
79}
80
81impl From<ring::error::KeyRejected> for IdentityError {
82    fn from(err: ring::error::KeyRejected) -> Self {
83        IdentityError::RingError(format!("Ring key rejected: {err}"))
84    }
85}
86
87impl From<pkcs8::der::Error> for IdentityError {
88    fn from(err: pkcs8::der::Error) -> Self {
89        IdentityError::Pkcs8DecodeError(err.to_string())
90    }
91}
92
93impl auths_core::error::AuthsErrorInfo for IdentityError {
94    fn error_code(&self) -> &'static str {
95        match self {
96            Self::Keri(_) => "AUTHS-E4401",
97            Self::Pkcs8EncodeError(_) => "AUTHS-E4402",
98            Self::Pkcs8DecodeError(_) => "AUTHS-E4403",
99            Self::EmptyPassphrase => "AUTHS-E4404",
100            Self::InvalidKeyLength(_) => "AUTHS-E4405",
101            Self::KeyStorage(_) => "AUTHS-E4406",
102            Self::KeyRetrieval(_) => "AUTHS-E4407",
103            Self::RingError(_) => "AUTHS-E4408",
104        }
105    }
106
107    fn suggestion(&self) -> Option<&'static str> {
108        match self {
109            Self::Keri(_) => Some("KERI operation failed; check identity state"),
110            Self::Pkcs8EncodeError(_) => None,
111            Self::Pkcs8DecodeError(_) => Some("The key may be in an unsupported format"),
112            Self::EmptyPassphrase => Some("Provide a non-empty passphrase"),
113            Self::InvalidKeyLength(_) => Some("Expected a 32-byte Ed25519 key"),
114            Self::KeyStorage(_) => Some("Check keychain permissions"),
115            Self::KeyRetrieval(_) => Some("Check that the key alias exists in the keychain"),
116            Self::RingError(_) => None,
117        }
118    }
119}
120
121/// Extract the Ed25519 32-byte seed from PKCS#8-encoded key material.
122pub fn extract_seed_bytes(pkcs8_bytes: &[u8]) -> Result<&[u8], IdentityError> {
123    let pk_info = PrivateKeyInfo::from_der(pkcs8_bytes)?;
124    match pk_info.private_key.len() {
125        32 => Ok(pk_info.private_key),
126        34 => Ok(&pk_info.private_key[2..]),
127        other => Err(IdentityError::InvalidKeyLength(other)),
128    }
129}
130
131/// Encodes the 32-byte Ed25519 seed as a DER-encoded PKCS#8 private key.
132/// Ring expects the private key to be wrapped in an OCTET STRING (34 bytes: 04 20 + 32 bytes seed).
133pub fn encode_seed_as_pkcs8(seed_bytes: &[u8]) -> Result<Vec<u8>, IdentityError> {
134    let mut wrapped_seed = Vec::with_capacity(34);
135    wrapped_seed.push(0x04);
136    wrapped_seed.push(0x20);
137    wrapped_seed.extend_from_slice(seed_bytes);
138
139    PrivateKeyInfo {
140        algorithm: AlgorithmIdentifierRef {
141            oid: OID_ED25519,
142            parameters: None,
143        },
144        private_key: &wrapped_seed,
145        public_key: None,
146    }
147    .to_der()
148    .map_err(|e| IdentityError::Pkcs8EncodeError(e.to_string()))
149}
150
151/// Load an Ed25519 keypair from decrypted key bytes.
152///
153/// Uses [`auths_crypto::parse_ed25519_seed`] to extract the seed, then
154/// builds a PKCS#8 document that ring can consume.
155pub fn load_keypair_from_der_or_seed(bytes: &[u8]) -> Result<Ed25519KeyPair, IdentityError> {
156    if let Ok(kp) = Ed25519KeyPair::from_pkcs8_maybe_unchecked(bytes) {
157        return Ok(kp);
158    }
159
160    let seed = auths_crypto::parse_ed25519_seed(bytes)
161        .map_err(|e| IdentityError::Pkcs8DecodeError(format!("Cannot parse key data: {e}")))?;
162    let pkcs8_bytes = encode_seed_as_pkcs8(seed.as_bytes())?;
163    Ed25519KeyPair::from_pkcs8_maybe_unchecked(&pkcs8_bytes)
164        .map_err(|e| IdentityError::RingError(format!("Failed to load Ed25519 keypair: {e}")))
165}
166
167pub fn generate_keypair_with_seed(
168    rng: &SystemRandom,
169) -> Result<(Ed25519KeyPair, Vec<u8>), IdentityError> {
170    let pkcs8_doc = Ed25519KeyPair::generate_pkcs8(rng)?;
171    let pkcs8_bytes = pkcs8_doc.as_ref().to_vec();
172    let keypair = Ed25519KeyPair::from_pkcs8(&pkcs8_bytes)?;
173    Ok((keypair, pkcs8_bytes))
174}