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
24pub 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
76pub 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
101fn 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
306pub 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
421pub 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
493pub fn install_registry_hook(registry_path: &Path) {
506 let _ = install_linearity_hook(registry_path);
507}