Skip to main content

auths_sdk/
setup.rs

1use std::convert::TryInto;
2use std::path::Path;
3
4use auths_core::signing::{PassphraseProvider, SecureSigner, StorageSigner};
5use auths_core::storage::keychain::{IdentityDID, KeyAlias, KeyStorage};
6use auths_id::attestation::create::create_signed_attestation;
7use auths_id::identity::initialize::initialize_registry_identity;
8use auths_id::storage::git_refs::AttestationMetadata;
9use auths_id::storage::registry::install_linearity_hook;
10use auths_verifier::types::DeviceDID;
11use chrono::{DateTime, Utc};
12
13use crate::context::AuthsContext;
14use crate::error::{SdkStorageError, SetupError};
15use crate::ports::git_config::GitConfigProvider;
16use crate::result::{
17    AgentSetupResult, CiSetupResult, PlatformClaimResult, RegistrationOutcome, SetupResult,
18};
19use crate::types::{
20    AgentSetupConfig, CiEnvironment, CiSetupConfig, DeveloperSetupConfig, GitSigningScope,
21    IdentityConflictPolicy, PlatformVerification,
22};
23
24/// Provisions a new developer identity with device linking, optional platform
25/// verification, git signing, and registry publication.
26///
27/// This function is a pure orchestrator — it delegates every step to a small,
28/// named helper and never performs I/O itself.
29///
30/// Args:
31/// * `config`: All setup parameters (key alias, platform, etc.).
32/// * `ctx`: Injected infrastructure adapters (registry, identity storage, attestation sink, clock).
33/// * `keychain`: Platform keychain for key storage and retrieval.
34/// * `signer`: Secure signer for creating attestation signatures.
35/// * `passphrase_provider`: Provides passphrases for key encryption/decryption.
36///
37/// Usage:
38/// ```ignore
39/// let result = setup_developer(config, &ctx, keychain.as_ref(), &signer, &provider, git_cfg)?;
40/// ```
41pub fn setup_developer(
42    config: DeveloperSetupConfig,
43    ctx: &AuthsContext,
44    keychain: &(dyn KeyStorage + Send + Sync),
45    signer: &dyn SecureSigner,
46    passphrase_provider: &dyn PassphraseProvider,
47    git_config: Option<&dyn GitConfigProvider>,
48) -> Result<SetupResult, SetupError> {
49    let now = ctx.clock.now();
50    let (controller_did, key_alias, reused) =
51        resolve_or_create_identity(&config, ctx, keychain, passphrase_provider, now)?;
52    let device_did = if reused {
53        derive_device_did(&key_alias, keychain, passphrase_provider)?
54    } else {
55        bind_device(&key_alias, ctx, keychain, signer, passphrase_provider, now)?
56    };
57    let platform_claim = bind_platform_claim(&config.platform);
58    let git_configured = configure_git_signing(
59        &config.git_signing_scope,
60        &key_alias,
61        git_config,
62        config.sign_binary_path.as_deref(),
63    )?;
64    let registered = submit_registration(&config);
65
66    Ok(SetupResult {
67        identity_did: IdentityDID::new(controller_did),
68        device_did,
69        key_alias,
70        platform_claim,
71        git_signing_configured: git_configured,
72        registered,
73    })
74}
75
76/// One-shot convenience wrapper that creates a developer identity with sensible
77/// defaults: global git signing, no platform verification, no registry.
78///
79/// Args:
80/// * `alias`: Human-readable name for the key (e.g. "main").
81/// * `ctx`: Injected infrastructure adapters.
82/// * `keychain`: Platform keychain backend.
83/// * `signer`: Secure signer for attestation creation.
84/// * `passphrase_provider`: Provides the passphrase for key encryption.
85///
86/// Usage:
87/// ```ignore
88/// let result = quick_setup("main", &ctx, keychain.as_ref(), &signer, &provider)?;
89/// ```
90pub fn quick_setup(
91    alias: &KeyAlias,
92    ctx: &AuthsContext,
93    keychain: &(dyn KeyStorage + Send + Sync),
94    signer: &dyn SecureSigner,
95    passphrase_provider: &dyn PassphraseProvider,
96) -> Result<SetupResult, SetupError> {
97    let config = DeveloperSetupConfig::builder(alias.clone()).build();
98    setup_developer(config, ctx, keychain, signer, passphrase_provider, None)
99}
100
101/// Returns (controller_did, key_alias, reused).
102fn resolve_or_create_identity(
103    config: &DeveloperSetupConfig,
104    ctx: &AuthsContext,
105    keychain: &(dyn KeyStorage + Send + Sync),
106    passphrase_provider: &dyn PassphraseProvider,
107    now: DateTime<Utc>,
108) -> Result<(String, KeyAlias, bool), SetupError> {
109    if let Ok(existing) = ctx.identity_storage.load_identity() {
110        match config.conflict_policy {
111            IdentityConflictPolicy::Error => {
112                return Err(SetupError::IdentityAlreadyExists {
113                    did: existing.controller_did.into_inner(),
114                });
115            }
116            IdentityConflictPolicy::ReuseExisting => {
117                return Ok((
118                    existing.controller_did.into_inner(),
119                    config.key_alias.clone(),
120                    true,
121                ));
122            }
123            IdentityConflictPolicy::ForceNew => {}
124        }
125    }
126
127    let (did, alias) = derive_keys(config, ctx, keychain, passphrase_provider, now)?;
128    Ok((did, alias, false))
129}
130
131fn derive_keys(
132    config: &DeveloperSetupConfig,
133    ctx: &AuthsContext,
134    keychain: &(dyn KeyStorage + Send + Sync),
135    passphrase_provider: &dyn PassphraseProvider,
136    _now: DateTime<Utc>,
137) -> Result<(String, KeyAlias), SetupError> {
138    let (controller_did, _key_event) = initialize_registry_identity(
139        std::sync::Arc::clone(&ctx.registry),
140        &config.key_alias,
141        passphrase_provider,
142        keychain,
143        config.witness_config.as_ref(),
144    )
145    .map_err(|e| {
146        SetupError::StorageError(SdkStorageError::OperationFailed(format!(
147            "failed to initialize identity: {e}"
148        )))
149    })?;
150
151    let did_str = controller_did.into_inner();
152    ctx.identity_storage
153        .create_identity(&did_str, None)
154        .map_err(|e| {
155            SetupError::StorageError(SdkStorageError::OperationFailed(format!(
156                "failed to persist identity: {e}"
157            )))
158        })?;
159
160    Ok((did_str, config.key_alias.clone()))
161}
162
163fn derive_device_did(
164    key_alias: &KeyAlias,
165    keychain: &(dyn KeyStorage + Send + Sync),
166    passphrase_provider: &dyn PassphraseProvider,
167) -> Result<DeviceDID, SetupError> {
168    let pk_bytes = auths_core::storage::keychain::extract_public_key_bytes(
169        keychain,
170        key_alias,
171        passphrase_provider,
172    )?;
173
174    let device_did = DeviceDID::from_ed25519(pk_bytes.as_slice().try_into().map_err(|_| {
175        SetupError::CryptoError(auths_core::AgentError::InvalidInput(
176            "public key is not 32 bytes".into(),
177        ))
178    })?);
179
180    Ok(device_did)
181}
182
183fn bind_device(
184    key_alias: &KeyAlias,
185    ctx: &AuthsContext,
186    keychain: &(dyn KeyStorage + Send + Sync),
187    signer: &dyn SecureSigner,
188    passphrase_provider: &dyn PassphraseProvider,
189    now: DateTime<Utc>,
190) -> Result<DeviceDID, SetupError> {
191    let managed = ctx.identity_storage.load_identity().map_err(|e| {
192        SetupError::StorageError(SdkStorageError::OperationFailed(format!(
193            "failed to load identity for device linking: {e}"
194        )))
195    })?;
196
197    let pk_bytes = auths_core::storage::keychain::extract_public_key_bytes(
198        keychain,
199        key_alias,
200        passphrase_provider,
201    )?;
202
203    let device_did = DeviceDID::from_ed25519(pk_bytes.as_slice().try_into().map_err(|_| {
204        SetupError::CryptoError(auths_core::AgentError::InvalidInput(
205            "public key is not 32 bytes".into(),
206        ))
207    })?);
208
209    let meta = AttestationMetadata {
210        timestamp: Some(now),
211        expires_at: None,
212        note: Some("Linked by auths-sdk setup".to_string()),
213    };
214
215    let attestation = create_signed_attestation(
216        now,
217        &managed.storage_id,
218        &managed.controller_did,
219        &device_did,
220        &pk_bytes,
221        None,
222        &meta,
223        signer,
224        passphrase_provider,
225        Some(key_alias),
226        Some(key_alias),
227        vec![],
228        None,
229        None,
230    )
231    .map_err(|e| {
232        SetupError::StorageError(SdkStorageError::OperationFailed(format!(
233            "attestation creation failed: {e}"
234        )))
235    })?;
236
237    ctx.attestation_sink
238        .export(&auths_verifier::VerifiedAttestation::dangerous_from_unchecked(attestation))
239        .map_err(|e| {
240            SetupError::StorageError(SdkStorageError::OperationFailed(format!(
241                "failed to save device attestation: {e}"
242            )))
243        })?;
244
245    Ok(device_did)
246}
247
248fn bind_platform_claim(platform: &Option<PlatformVerification>) -> Option<PlatformClaimResult> {
249    match platform {
250        Some(PlatformVerification::GitHub { .. }) => None,
251        Some(PlatformVerification::GitLab { .. }) => None,
252        Some(PlatformVerification::Skip) | None => None,
253    }
254}
255
256fn configure_git_signing(
257    scope: &GitSigningScope,
258    key_alias: &KeyAlias,
259    git_config: Option<&dyn GitConfigProvider>,
260    sign_binary_path: Option<&Path>,
261) -> Result<bool, SetupError> {
262    if matches!(scope, GitSigningScope::Skip) {
263        return Ok(false);
264    }
265    let git_config = git_config.ok_or_else(|| {
266        SetupError::GitConfigError("GitConfigProvider required for non-Skip scope".into())
267    })?;
268    let sign_binary_path = sign_binary_path.ok_or_else(|| {
269        SetupError::GitConfigError("sign_binary_path required for non-Skip scope".into())
270    })?;
271    set_git_signing_config(key_alias, git_config, sign_binary_path)?;
272    Ok(true)
273}
274
275fn set_git_signing_config(
276    key_alias: &KeyAlias,
277    git_config: &dyn GitConfigProvider,
278    sign_binary_path: &Path,
279) -> Result<(), SetupError> {
280    let auths_sign_str = sign_binary_path
281        .to_str()
282        .ok_or_else(|| SetupError::GitConfigError("auths-sign path is not valid UTF-8".into()))?;
283    let signing_key = format!("auths:{}", key_alias);
284    let configs: &[(&str, &str)] = &[
285        ("gpg.format", "ssh"),
286        ("gpg.ssh.program", auths_sign_str),
287        ("user.signingkey", &signing_key),
288        ("commit.gpgsign", "true"),
289        ("tag.gpgsign", "true"),
290    ];
291    for (key, val) in configs {
292        git_config
293            .set(key, val)
294            .map_err(|e| SetupError::GitConfigError(e.to_string()))?;
295    }
296    Ok(())
297}
298
299fn submit_registration(config: &DeveloperSetupConfig) -> Option<RegistrationOutcome> {
300    if !config.register_on_registry {
301        return None;
302    }
303    None
304}
305
306// ── CI setup ────────────────────────────────────────────────────────────
307
308/// Provisions an ephemeral CI identity for use in automated pipelines.
309///
310/// Unlike `setup_developer`, this function takes the keychain from
311/// `CiSetupConfig` so callers can inject a memory keychain without mutating
312/// environment variables.
313///
314/// Args:
315/// * `config`: CI setup parameters including keychain, passphrase, and CI environment.
316/// * `ctx`: Injected infrastructure adapters (registry, identity storage, attestation sink, clock).
317///
318/// Usage:
319/// ```ignore
320/// let result = setup_ci(config, &ctx)?;
321/// ```
322pub fn setup_ci(config: CiSetupConfig, ctx: &AuthsContext) -> Result<CiSetupResult, SetupError> {
323    let now = ctx.clock.now();
324    let provider = auths_core::PrefilledPassphraseProvider::new(&config.passphrase);
325    let keychain = config.keychain;
326    let (controller_did, key_alias) = create_ci_identity(ctx, keychain.as_ref(), &provider, now)?;
327    let signer = StorageSigner::new(keychain);
328    let device_did = bind_device(&key_alias, ctx, signer.inner(), &signer, &provider, now)?;
329    let env_block =
330        generate_ci_env_block(&key_alias, &config.registry_path, &config.ci_environment);
331
332    Ok(CiSetupResult {
333        identity_did: IdentityDID::new(controller_did),
334        device_did,
335        env_block,
336    })
337}
338
339fn create_ci_identity(
340    ctx: &AuthsContext,
341    keychain: &(dyn KeyStorage + Send + Sync),
342    passphrase_provider: &dyn PassphraseProvider,
343    _now: DateTime<Utc>,
344) -> Result<(String, KeyAlias), SetupError> {
345    let key_alias = KeyAlias::new_unchecked("ci-key");
346
347    let (controller_did, _) = initialize_registry_identity(
348        std::sync::Arc::clone(&ctx.registry),
349        &key_alias,
350        passphrase_provider,
351        keychain,
352        None,
353    )
354    .map_err(|e| {
355        SetupError::StorageError(SdkStorageError::OperationFailed(format!(
356            "failed to initialize CI identity: {e}"
357        )))
358    })?;
359
360    Ok((controller_did.into_inner(), key_alias))
361}
362
363fn generate_ci_env_block(
364    key_alias: &KeyAlias,
365    repo_path: &Path,
366    environment: &CiEnvironment,
367) -> Vec<String> {
368    match environment {
369        CiEnvironment::GitHubActions => generate_github_env_block(key_alias, repo_path),
370        CiEnvironment::GitLabCi => generate_gitlab_env_block(key_alias, repo_path),
371        CiEnvironment::Custom { name } => generate_generic_env_block(key_alias, repo_path, name),
372        CiEnvironment::Unknown => generate_generic_env_block(key_alias, repo_path, "ci"),
373    }
374}
375
376fn generate_github_env_block(key_alias: &KeyAlias, repo_path: &Path) -> Vec<String> {
377    let mut lines = base_env_lines(key_alias, repo_path);
378    lines.push(String::new());
379    lines.push("# GitHub Actions: add these as repository secrets".to_string());
380    lines.push("# then reference them in your workflow env: block".to_string());
381    lines
382}
383
384fn generate_gitlab_env_block(key_alias: &KeyAlias, repo_path: &Path) -> Vec<String> {
385    let mut lines = base_env_lines(key_alias, repo_path);
386    lines.push(String::new());
387    lines.push("# GitLab CI: add these as CI/CD variables".to_string());
388    lines.push("# in Settings > CI/CD > Variables".to_string());
389    lines
390}
391
392fn generate_generic_env_block(
393    key_alias: &KeyAlias,
394    repo_path: &Path,
395    platform: &str,
396) -> Vec<String> {
397    let mut lines = base_env_lines(key_alias, repo_path);
398    lines.push(String::new());
399    lines.push(format!("# {platform}: add these as environment variables"));
400    lines
401}
402
403fn base_env_lines(key_alias: &KeyAlias, repo_path: &Path) -> Vec<String> {
404    vec![
405        format!("export AUTHS_KEYCHAIN_BACKEND=\"memory\""),
406        format!("export AUTHS_REPO=\"{}\"", repo_path.display()),
407        format!("export AUTHS_KEY_ALIAS=\"{key_alias}\""),
408        String::new(),
409        format!("export GIT_CONFIG_COUNT=4"),
410        format!("export GIT_CONFIG_KEY_0=\"gpg.format\""),
411        format!("export GIT_CONFIG_VALUE_0=\"ssh\""),
412        format!("export GIT_CONFIG_KEY_1=\"gpg.ssh.program\""),
413        format!("export GIT_CONFIG_VALUE_1=\"auths-sign\""),
414        format!("export GIT_CONFIG_KEY_2=\"user.signingKey\""),
415        format!("export GIT_CONFIG_VALUE_2=\"auths:{key_alias}\""),
416        format!("export GIT_CONFIG_KEY_3=\"commit.gpgSign\""),
417        format!("export GIT_CONFIG_VALUE_3=\"true\""),
418    ]
419}
420
421// ── Agent setup ─────────────────────────────────────────────────────────
422
423/// Provisions an agent identity delegated from a parent identity.
424///
425/// Constructs all proposed state first, then persists only if `dry_run` is false.
426///
427/// Args:
428/// * `config`: Agent setup parameters (alias, parent DID, capabilities, etc.).
429/// * `ctx`: Injected infrastructure adapters (registry, clock).
430/// * `keychain`: Platform keychain for key storage.
431/// * `passphrase_provider`: Provides passphrases for key operations.
432///
433/// Usage:
434/// ```ignore
435/// let result = setup_agent(config, &ctx, keychain, &provider)?;
436/// ```
437pub fn setup_agent(
438    config: AgentSetupConfig,
439    ctx: &AuthsContext,
440    keychain: Box<dyn KeyStorage + Send + Sync>,
441    passphrase_provider: &dyn PassphraseProvider,
442) -> Result<AgentSetupResult, SetupError> {
443    use auths_id::agent_identity::{AgentProvisioningConfig, AgentStorageMode};
444
445    let cap_strings: Vec<String> = config.capabilities.iter().map(|c| c.to_string()).collect();
446    let provisioning_config = AgentProvisioningConfig {
447        agent_name: config.alias.to_string(),
448        capabilities: cap_strings,
449        expires_in_secs: config.expires_in_secs,
450        delegated_by: config.parent_identity_did.clone().map(IdentityDID::new),
451        storage_mode: AgentStorageMode::Persistent {
452            repo_path: Some(config.registry_path.clone()),
453        },
454    };
455
456    let proposed = build_agent_proposal(&provisioning_config, &config)?;
457
458    if !config.dry_run {
459        let bundle = auths_id::agent_identity::provision_agent_identity(
460            ctx.clock.now(),
461            std::sync::Arc::clone(&ctx.registry),
462            provisioning_config,
463            passphrase_provider,
464            keychain,
465        )
466        .map_err(|e| {
467            SetupError::StorageError(SdkStorageError::OperationFailed(format!(
468                "agent provisioning failed: {e}"
469            )))
470        })?;
471
472        return Ok(AgentSetupResult {
473            agent_did: bundle.agent_did,
474            parent_did: IdentityDID::new(config.parent_identity_did.unwrap_or_default()),
475            capabilities: config.capabilities,
476        });
477    }
478
479    Ok(proposed)
480}
481
482fn build_agent_proposal(
483    _provisioning_config: &auths_id::agent_identity::AgentProvisioningConfig,
484    config: &AgentSetupConfig,
485) -> Result<AgentSetupResult, SetupError> {
486    Ok(AgentSetupResult {
487        agent_did: IdentityDID::new(format!("did:keri:E<pending:{}>", config.alias)),
488        parent_did: IdentityDID::new(config.parent_identity_did.clone().unwrap_or_default()),
489        capabilities: config.capabilities.clone(),
490    })
491}
492
493/// Install the linearity hook in a registry directory.
494///
495/// This is called by the CLI after initializing the git repository to prevent
496/// non-linear KEL history.
497///
498/// Args:
499/// * `registry_path`: Path to the initialized git repository.
500///
501/// Usage:
502/// ```ignore
503/// auths_sdk::setup::install_registry_hook(&registry_path);
504/// ```
505pub fn install_registry_hook(registry_path: &Path) {
506    let _ = install_linearity_hook(registry_path);
507}