use std::path::Path;
use std::sync::Arc;
use auths_core::signing::{PassphraseProvider, SecureSigner};
use auths_core::storage::keychain::{IdentityDID, KeyAlias, KeyStorage};
use auths_id::attestation::create::create_signed_attestation;
use auths_id::identity::initialize::initialize_registry_identity;
use auths_id::storage::git_refs::AttestationMetadata;
use auths_id::storage::registry::install_linearity_hook;
use auths_verifier::types::CanonicalDid;
use chrono::{DateTime, Utc};
use crate::context::AuthsContext;
use crate::domains::ci::types::{CiEnvironment, CiIdentityConfig};
use crate::domains::identity::error::SetupError;
use crate::domains::identity::types::{
AgentIdentityResult, CiIdentityResult, CreateAgentIdentityConfig,
CreateDeveloperIdentityConfig, DeveloperIdentityResult, IdentityConfig, IdentityConflictPolicy,
InitializeResult, RegistrationOutcome,
};
use crate::domains::signing::types::PlatformClaimResult;
use crate::domains::signing::types::{GitSigningScope, PlatformVerification};
use crate::ports::git_config::GitConfigProvider;
pub fn initialize(
config: IdentityConfig,
ctx: &AuthsContext,
keychain: Arc<dyn KeyStorage + Send + Sync>,
signer: &dyn SecureSigner,
passphrase_provider: &dyn PassphraseProvider,
git_config: Option<&dyn GitConfigProvider>,
) -> Result<InitializeResult, SetupError> {
match config {
IdentityConfig::Developer(dev_config) => initialize_developer(
dev_config,
ctx,
keychain.as_ref(),
signer,
passphrase_provider,
git_config,
)
.map(InitializeResult::Developer),
IdentityConfig::Ci(ci_config) => initialize_ci(
ci_config,
ctx,
keychain.as_ref(),
signer,
passphrase_provider,
)
.map(InitializeResult::Ci),
IdentityConfig::Agent(agent_config) => {
initialize_agent(agent_config, ctx, keychain, passphrase_provider)
.map(InitializeResult::Agent)
}
}
}
fn initialize_developer(
config: CreateDeveloperIdentityConfig,
ctx: &AuthsContext,
keychain: &(dyn KeyStorage + Send + Sync),
signer: &dyn SecureSigner,
passphrase_provider: &dyn PassphraseProvider,
git_config: Option<&dyn GitConfigProvider>,
) -> Result<DeveloperIdentityResult, SetupError> {
let now = ctx.clock.now();
let (controller_did, key_alias, reused) =
resolve_or_create_identity(&config, ctx, keychain, passphrase_provider, now)?;
let device_did = if reused {
derive_device_did(&key_alias, keychain, passphrase_provider)?
} else {
bind_device(&key_alias, ctx, keychain, signer, passphrase_provider, now)?
};
let platform_claim = bind_platform_claim(&config.platform);
let git_configured = configure_git_signing(
&config.git_signing_scope,
&key_alias,
git_config,
config.sign_binary_path.as_deref(),
)?;
let registered = submit_registration(&config);
Ok(DeveloperIdentityResult {
#[allow(clippy::disallowed_methods)] identity_did: IdentityDID::new_unchecked(controller_did),
device_did,
key_alias,
platform_claim,
git_signing_configured: git_configured,
registered,
})
}
fn initialize_ci(
config: CiIdentityConfig,
ctx: &AuthsContext,
keychain: &(dyn KeyStorage + Send + Sync),
signer: &dyn SecureSigner,
passphrase_provider: &dyn PassphraseProvider,
) -> Result<CiIdentityResult, SetupError> {
let now = ctx.clock.now();
let (controller_did, key_alias) = initialize_ci_keys(ctx, keychain, passphrase_provider, now)?;
let device_did = bind_device(&key_alias, ctx, keychain, signer, passphrase_provider, now)?;
let env_block = generate_ci_env_block(
&key_alias,
&config.registry_path,
&config.keychain_file,
&config.passphrase,
&config.ci_environment,
);
Ok(CiIdentityResult {
#[allow(clippy::disallowed_methods)] identity_did: IdentityDID::new_unchecked(controller_did),
device_did,
env_block,
})
}
fn initialize_agent(
config: CreateAgentIdentityConfig,
_ctx: &AuthsContext,
_keychain: Arc<dyn KeyStorage + Send + Sync>,
_passphrase_provider: &dyn PassphraseProvider,
) -> Result<AgentIdentityResult, SetupError> {
use auths_id::agent_identity::{AgentProvisioningConfig, AgentStorageMode};
let cap_strings: Vec<String> = config.capabilities.iter().map(|c| c.to_string()).collect();
let provisioning_config = AgentProvisioningConfig {
agent_name: config.alias.to_string(),
capabilities: cap_strings,
expires_in: config.expires_in,
delegated_by: config.parent_identity_did.clone().map(|did| {
#[allow(clippy::disallowed_methods)]
IdentityDID::new_unchecked(did)
}),
storage_mode: AgentStorageMode::Persistent {
repo_path: Some(config.registry_path.clone()),
},
};
let proposed = build_agent_identity_proposal(&provisioning_config, &config)?;
if !config.dry_run {
return Err(SetupError::InvalidSetupConfig(
"standalone agent initialization is retired — an agent is a KERI delegated \
identifier. Run `auths init` for your root identity, then \
`auths id agent add --label <name>` to delegate an agent."
.to_string(),
));
}
Ok(proposed)
}
pub fn install_registry_hook(registry_path: &Path) {
let _ = install_linearity_hook(registry_path);
}
fn resolve_or_create_identity(
config: &CreateDeveloperIdentityConfig,
ctx: &AuthsContext,
keychain: &(dyn KeyStorage + Send + Sync),
passphrase_provider: &dyn PassphraseProvider,
now: DateTime<Utc>,
) -> Result<(String, KeyAlias, bool), SetupError> {
if let Ok(existing) = ctx.identity_storage.load_identity() {
match config.conflict_policy {
IdentityConflictPolicy::Error => {
return Err(SetupError::IdentityAlreadyExists {
did: existing.controller_did.into_inner(),
});
}
IdentityConflictPolicy::ReuseExisting => {
return Ok((
existing.controller_did.into_inner(),
config.key_alias.clone(),
true,
));
}
IdentityConflictPolicy::ForceNew => {}
}
}
let (did, alias) = derive_keys(config, ctx, keychain, passphrase_provider, now)?;
Ok((did, alias, false))
}
fn derive_keys(
config: &CreateDeveloperIdentityConfig,
ctx: &AuthsContext,
keychain: &(dyn KeyStorage + Send + Sync),
passphrase_provider: &dyn PassphraseProvider,
_now: DateTime<Utc>,
) -> Result<(String, KeyAlias), SetupError> {
let (controller_did, _key_event) = initialize_registry_identity(
std::sync::Arc::clone(&ctx.registry),
&config.key_alias,
passphrase_provider,
keychain,
config.witness_config.as_ref(),
config.curve,
)
.map_err(|e| SetupError::StorageError(e.into()))?;
let did_str = controller_did.into_inner();
ctx.identity_storage
.create_identity(&did_str, None)
.map_err(|e| SetupError::StorageError(e.into()))?;
Ok((did_str, config.key_alias.clone()))
}
fn derive_device_did(
key_alias: &KeyAlias,
keychain: &(dyn KeyStorage + Send + Sync),
passphrase_provider: &dyn PassphraseProvider,
) -> Result<CanonicalDid, SetupError> {
let (pk_bytes, curve) = auths_core::storage::keychain::extract_public_key_bytes(
keychain,
key_alias,
passphrase_provider,
)?;
let device_did = CanonicalDid::from_public_key_did_key(&pk_bytes, curve);
Ok(device_did)
}
fn bind_device(
key_alias: &KeyAlias,
ctx: &AuthsContext,
keychain: &(dyn KeyStorage + Send + Sync),
signer: &dyn SecureSigner,
passphrase_provider: &dyn PassphraseProvider,
now: DateTime<Utc>,
) -> Result<CanonicalDid, SetupError> {
let managed = ctx
.identity_storage
.load_identity()
.map_err(|e| SetupError::StorageError(e.into()))?;
let (pk_bytes, curve) = auths_core::storage::keychain::extract_public_key_bytes(
keychain,
key_alias,
passphrase_provider,
)?;
let device_did = CanonicalDid::from_public_key_did_key(&pk_bytes, curve);
let meta = AttestationMetadata {
timestamp: Some(now),
expires_at: None,
note: Some("Linked by auths-sdk setup".to_string()),
};
let attestation = create_signed_attestation(
now,
auths_id::attestation::create::AttestationInput {
rid: &managed.storage_id,
identity_did: &managed.controller_did,
subject: &device_did,
device_public_key: &pk_bytes,
device_curve: curve,
payload: None,
meta: &meta,
identity_alias: Some(key_alias),
device_alias: Some(key_alias),
delegated_by: None,
commit_sha: None,
signer_type: None,
},
signer,
passphrase_provider,
)
.map_err(|e| SetupError::StorageError(e.into()))?;
let mut batch = auths_id::storage::registry::backend::AtomicWriteBatch::new();
batch.stage_attestation(attestation.clone());
if let Ok(prefix) = auths_id::keri::parse_did_keri(managed.controller_did.as_str()) {
match auths_id::keri::try_stage_anchor(
ctx.registry.as_ref(),
signer,
key_alias,
passphrase_provider,
&prefix,
&attestation,
&mut batch,
) {
Ok(_) => {}
Err(auths_id::keri::AnchorError::IxnForbidden(_)) => {
}
Err(e) => {
return Err(SetupError::StorageError(
auths_id::error::StorageError::InvalidData(e.to_string()).into(),
));
}
}
}
ctx.registry.commit_batch(&batch).map_err(|e| {
SetupError::StorageError(auths_id::error::StorageError::InvalidData(e.to_string()).into())
})?;
Ok(device_did)
}
fn bind_platform_claim(platform: &Option<PlatformVerification>) -> Option<PlatformClaimResult> {
match platform {
Some(PlatformVerification::GitHub { .. }) => None,
Some(PlatformVerification::GitLab { .. }) => None,
Some(PlatformVerification::Skip) | None => None,
}
}
fn configure_git_signing(
scope: &GitSigningScope,
key_alias: &KeyAlias,
git_config: Option<&dyn GitConfigProvider>,
sign_binary_path: Option<&Path>,
) -> Result<bool, SetupError> {
if matches!(scope, GitSigningScope::Skip) {
return Ok(false);
}
let git_config = git_config.ok_or_else(|| {
SetupError::InvalidSetupConfig("GitConfigProvider required for non-Skip scope".into())
})?;
let sign_binary_path = sign_binary_path.ok_or_else(|| {
SetupError::InvalidSetupConfig("sign_binary_path required for non-Skip scope".into())
})?;
set_git_signing_config(key_alias, git_config, sign_binary_path)?;
Ok(true)
}
fn set_git_signing_config(
key_alias: &KeyAlias,
git_config: &dyn GitConfigProvider,
sign_binary_path: &Path,
) -> Result<(), SetupError> {
let auths_sign_str = sign_binary_path.to_str().ok_or_else(|| {
SetupError::InvalidSetupConfig("auths-sign path is not valid UTF-8".into())
})?;
let signing_key = format!("auths:{}", key_alias);
let configs: &[(&str, &str)] = &[
("gpg.format", "ssh"),
("gpg.ssh.program", auths_sign_str),
("user.signingkey", &signing_key),
("commit.gpgsign", "true"),
("tag.gpgsign", "true"),
];
for (key, val) in configs {
git_config
.set(key, val)
.map_err(SetupError::GitConfigError)?;
}
Ok(())
}
fn submit_registration(config: &CreateDeveloperIdentityConfig) -> Option<RegistrationOutcome> {
if !config.register_on_registry {
return None;
}
None
}
fn initialize_ci_keys(
ctx: &AuthsContext,
keychain: &(dyn KeyStorage + Send + Sync),
passphrase_provider: &dyn PassphraseProvider,
_now: DateTime<Utc>,
) -> Result<(String, KeyAlias), SetupError> {
let key_alias = KeyAlias::new_unchecked("ci-key");
let (controller_did, _) = initialize_registry_identity(
std::sync::Arc::clone(&ctx.registry),
&key_alias,
passphrase_provider,
keychain,
None,
auths_crypto::CurveType::default(),
)
.map_err(|e| SetupError::StorageError(e.into()))?;
Ok((controller_did.into_inner(), key_alias))
}
fn generate_ci_env_block(
key_alias: &KeyAlias,
repo_path: &Path,
keychain_file: &Path,
passphrase: &str,
environment: &CiEnvironment,
) -> Vec<String> {
match environment {
CiEnvironment::GitHubActions => {
generate_github_env_block(key_alias, repo_path, keychain_file, passphrase)
}
CiEnvironment::GitLabCi => {
generate_gitlab_env_block(key_alias, repo_path, keychain_file, passphrase)
}
CiEnvironment::Custom { name } => {
generate_generic_env_block(key_alias, repo_path, keychain_file, passphrase, name)
}
CiEnvironment::Unknown => {
generate_generic_env_block(key_alias, repo_path, keychain_file, passphrase, "ci")
}
}
}
fn generate_github_env_block(
key_alias: &KeyAlias,
repo_path: &Path,
keychain_file: &Path,
passphrase: &str,
) -> Vec<String> {
let mut lines = base_env_lines(key_alias, repo_path, keychain_file, passphrase);
lines.push(String::new());
lines.push("# GitHub Actions: add these as repository secrets".to_string());
lines.push("# then reference them in your workflow env: block".to_string());
lines
}
fn generate_gitlab_env_block(
key_alias: &KeyAlias,
repo_path: &Path,
keychain_file: &Path,
passphrase: &str,
) -> Vec<String> {
let mut lines = base_env_lines(key_alias, repo_path, keychain_file, passphrase);
lines.push(String::new());
lines.push("# GitLab CI: add these as CI/CD variables".to_string());
lines.push("# in Settings > CI/CD > Variables".to_string());
lines
}
fn generate_generic_env_block(
key_alias: &KeyAlias,
repo_path: &Path,
keychain_file: &Path,
passphrase: &str,
platform: &str,
) -> Vec<String> {
let mut lines = base_env_lines(key_alias, repo_path, keychain_file, passphrase);
lines.push(String::new());
lines.push(format!("# {platform}: add these as environment variables"));
lines
}
fn base_env_lines(
key_alias: &KeyAlias,
repo_path: &Path,
keychain_file: &Path,
passphrase: &str,
) -> Vec<String> {
vec![
"# CI signing secrets — store these securely and rotate per environment".to_string(),
format!("export AUTHS_KEYCHAIN_BACKEND=\"file\""),
format!("export AUTHS_KEYCHAIN_FILE=\"{}\"", keychain_file.display()),
format!("export AUTHS_PASSPHRASE=\"{passphrase}\""),
format!("export AUTHS_REPO=\"{}\"", repo_path.display()),
format!("export AUTHS_KEY_ALIAS=\"{key_alias}\""),
String::new(),
format!("export GIT_CONFIG_COUNT=4"),
format!("export GIT_CONFIG_KEY_0=\"gpg.format\""),
format!("export GIT_CONFIG_VALUE_0=\"ssh\""),
format!("export GIT_CONFIG_KEY_1=\"gpg.ssh.program\""),
format!("export GIT_CONFIG_VALUE_1=\"auths-sign\""),
format!("export GIT_CONFIG_KEY_2=\"user.signingKey\""),
format!("export GIT_CONFIG_VALUE_2=\"auths:{key_alias}\""),
format!("export GIT_CONFIG_KEY_3=\"commit.gpgSign\""),
format!("export GIT_CONFIG_VALUE_3=\"true\""),
]
}
fn build_agent_identity_proposal(
_provisioning_config: &auths_id::agent_identity::AgentProvisioningConfig,
config: &CreateAgentIdentityConfig,
) -> Result<AgentIdentityResult, SetupError> {
Ok(AgentIdentityResult {
agent_did: None,
parent_did: config
.parent_identity_did
.as_deref()
.and_then(|s| IdentityDID::parse(s).ok()),
capabilities: config.capabilities.clone(),
})
}