Skip to content

Commit caead6f

Browse files
committed
feat: get sigstore working
1 parent 4994324 commit caead6f

17 files changed

Lines changed: 508 additions & 271 deletions

File tree

crates/auths-cli/src/commands/artifact/mod.rs

Lines changed: 113 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,108 @@ fn rate_limit_secs(err: &auths_sdk::workflows::log_submit::LogSubmitError) -> u6
195195
}
196196
}
197197

198+
/// Re-export DSSE PAE from the SDK for use in CLI signing paths.
199+
pub use auths_sdk::domains::signing::service::dsse_pae;
200+
201+
/// Submit an attestation to a transparency log and return the JSON to embed.
202+
///
203+
/// The `dsse_signature` is the signature over the DSSE PAE of the attestation,
204+
/// computed by the caller while the signing key is still available.
205+
///
206+
/// Returns `None` if `allow_unlogged` is set or `--log` wasn't passed.
207+
fn submit_to_log(
208+
attestation_json: &str,
209+
log: &Option<String>,
210+
allow_unlogged: bool,
211+
dsse_signature: Option<&[u8]>,
212+
) -> Result<Option<serde_json::Value>> {
213+
if allow_unlogged {
214+
eprintln!(
215+
"WARNING: Signing without transparency log. \
216+
This artifact will not be verifiable against any log."
217+
);
218+
return Ok(None);
219+
}
220+
221+
// If --log wasn't passed, skip silently (non-CI default behavior)
222+
if log.is_none() {
223+
return Ok(None);
224+
}
225+
226+
let sig_bytes = dsse_signature
227+
.ok_or_else(|| anyhow::anyhow!("DSSE signature required for log submission"))?;
228+
229+
let attestation_value: serde_json::Value = serde_json::from_str(attestation_json)
230+
.map_err(|e| anyhow::anyhow!("Failed to parse attestation: {e}"))?;
231+
232+
// device_public_key may be a hex string or {"curve": "...", "key": "..."}
233+
let pk_hex = if let Some(s) = attestation_value["device_public_key"].as_str() {
234+
s.to_string()
235+
} else if let Some(key_field) = attestation_value["device_public_key"]["key"].as_str() {
236+
key_field.to_string()
237+
} else {
238+
return Err(anyhow::anyhow!("missing device_public_key"));
239+
};
240+
let pk_bytes =
241+
hex::decode(&pk_hex).map_err(|e| anyhow::anyhow!("invalid public key hex: {e}"))?;
242+
243+
let rt = tokio::runtime::Runtime::new()
244+
.map_err(|e| anyhow::anyhow!("Failed to create async runtime: {e}"))?;
245+
246+
let log_client: std::sync::Arc<dyn auths_sdk::ports::TransparencyLog> = match log.as_deref() {
247+
Some("sigstore-rekor") => std::sync::Arc::new(
248+
auths_infra_rekor::RekorClient::public()
249+
.map_err(|e| anyhow::anyhow!("Failed to create Rekor client: {e}"))?,
250+
),
251+
Some(other) => bail!("Unknown log '{}'. Available: sigstore-rekor", other),
252+
None => unreachable!(),
253+
};
254+
255+
let submit = || {
256+
rt.block_on(auths_sdk::workflows::log_submit::submit_attestation_to_log(
257+
attestation_json.as_bytes(),
258+
&pk_bytes,
259+
sig_bytes,
260+
log_client.as_ref(),
261+
))
262+
};
263+
264+
let submission_result = match submit() {
265+
Ok(bundle) => Ok(bundle),
266+
Err(ref e) if is_rate_limited(e) => {
267+
let secs = rate_limit_secs(e);
268+
eprintln!("Rate limited by transparency log. Retrying in {secs}s...");
269+
std::thread::sleep(std::time::Duration::from_secs(secs));
270+
submit()
271+
}
272+
Err(e) => Err(e),
273+
};
274+
275+
match submission_result {
276+
Ok(bundle) => {
277+
eprintln!(
278+
" Logged to {} at index {}",
279+
bundle.log_id, bundle.leaf_index
280+
);
281+
Ok(Some(serde_json::to_value(&bundle).map_err(|e| {
282+
anyhow::anyhow!("Failed to serialize: {e}")
283+
})?))
284+
}
285+
Err(e) => Err(anyhow::anyhow!("Transparency log submission failed: {e}")),
286+
}
287+
}
288+
289+
/// Merge transparency JSON into an attestation and return the final JSON string.
290+
fn merge_transparency(attestation_json: &str, transparency: serde_json::Value) -> Result<String> {
291+
let mut attestation: serde_json::Value = serde_json::from_str(attestation_json)
292+
.map_err(|e| anyhow::anyhow!("Failed to re-parse attestation: {e}"))?;
293+
if let serde_json::Value::Object(ref mut map) = attestation {
294+
map.insert("transparency".to_string(), transparency);
295+
}
296+
serde_json::to_string_pretty(&attestation)
297+
.map_err(|e| anyhow::anyhow!("Failed to serialize attestation: {e}"))
298+
}
299+
198300
/// Resolve the commit SHA from CLI flags.
199301
fn resolve_commit_sha_from_flags(
200302
commit: Option<String>,
@@ -290,93 +392,15 @@ pub fn handle_artifact(
290392
.map_err(|e| anyhow::anyhow!("Ephemeral signing failed: {}", e))?;
291393

292394
// Submit to transparency log (unless --allow-unlogged)
293-
let transparency_json = if allow_unlogged {
294-
eprintln!(
295-
"WARNING: Signing without transparency log. \
296-
This artifact will not be verifiable against any log."
297-
);
298-
None
299-
} else {
300-
// Parse the attestation to extract public key and signature
301-
let attestation_value: serde_json::Value =
302-
serde_json::from_str(&result.attestation_json)
303-
.map_err(|e| anyhow::anyhow!("Failed to parse attestation: {e}"))?;
304-
305-
let identity_sig_hex = attestation_value["identity_signature"]
306-
.as_str()
307-
.ok_or_else(|| anyhow::anyhow!("missing identity_signature"))?;
308-
let sig_bytes = hex::decode(identity_sig_hex)
309-
.map_err(|e| anyhow::anyhow!("invalid signature hex: {e}"))?;
310-
311-
let device_pk_hex = attestation_value["device_public_key"]
312-
.as_str()
313-
.ok_or_else(|| anyhow::anyhow!("missing device_public_key"))?;
314-
let pk_bytes = hex::decode(device_pk_hex)
315-
.map_err(|e| anyhow::anyhow!("invalid public key hex: {e}"))?;
316-
317-
let rt = tokio::runtime::Runtime::new()
318-
.map_err(|e| anyhow::anyhow!("Failed to create async runtime: {e}"))?;
319-
320-
// Build the transparency log client
321-
let log_client: std::sync::Arc<dyn auths_sdk::ports::TransparencyLog> =
322-
match log.as_deref() {
323-
Some("sigstore-rekor") | None => std::sync::Arc::new(
324-
auths_infra_rekor::RekorClient::public().map_err(|e| {
325-
anyhow::anyhow!("Failed to create Rekor client: {e}")
326-
})?,
327-
),
328-
Some(other) => {
329-
bail!("Unknown log '{}'. Available: sigstore-rekor", other)
330-
}
331-
};
395+
let transparency_json = submit_to_log(
396+
&result.attestation_json,
397+
&log,
398+
allow_unlogged,
399+
result.dsse_signature.as_deref(),
400+
)?;
332401

333-
let submit = || {
334-
rt.block_on(auths_sdk::workflows::log_submit::submit_attestation_to_log(
335-
result.attestation_json.as_bytes(),
336-
&pk_bytes,
337-
&sig_bytes,
338-
log_client.as_ref(),
339-
))
340-
};
341-
342-
let submission_result = match submit() {
343-
Ok(bundle) => Ok(bundle),
344-
Err(ref e) if is_rate_limited(e) => {
345-
let secs = rate_limit_secs(e);
346-
eprintln!("Rate limited by transparency log. Retrying in {secs}s...");
347-
std::thread::sleep(std::time::Duration::from_secs(secs));
348-
submit()
349-
}
350-
Err(e) => Err(e),
351-
};
352-
353-
match submission_result {
354-
Ok(bundle) => {
355-
eprintln!(
356-
" Logged to {} at index {}",
357-
bundle.log_id, bundle.leaf_index
358-
);
359-
Some(
360-
serde_json::to_value(&bundle)
361-
.map_err(|e| anyhow::anyhow!("Failed to serialize: {e}"))?,
362-
)
363-
}
364-
Err(e) => {
365-
return Err(anyhow::anyhow!("Transparency log submission failed: {e}"));
366-
}
367-
}
368-
};
369-
370-
// Build final .auths.json with optional transparency section
371402
let final_json = if let Some(transparency) = transparency_json {
372-
let mut attestation: serde_json::Value =
373-
serde_json::from_str(&result.attestation_json)
374-
.map_err(|e| anyhow::anyhow!("Failed to re-parse attestation: {e}"))?;
375-
if let serde_json::Value::Object(ref mut map) = attestation {
376-
map.insert("transparency".to_string(), transparency);
377-
}
378-
serde_json::to_string_pretty(&attestation)
379-
.map_err(|e| anyhow::anyhow!("Failed to serialize final JSON: {e}"))?
403+
merge_transparency(&result.attestation_json, transparency)?
380404
} else {
381405
result.attestation_json.clone()
382406
};
@@ -424,6 +448,8 @@ pub fn handle_artifact(
424448
repo_opt,
425449
passphrase_provider,
426450
env_config,
451+
&log,
452+
allow_unlogged,
427453
)
428454
}
429455
}
@@ -465,6 +491,8 @@ pub fn handle_artifact(
465491
repo_opt.clone(),
466492
passphrase_provider,
467493
env_config,
494+
&None,
495+
false,
468496
)?;
469497
default_sig
470498
}

crates/auths-cli/src/commands/artifact/sign.rs

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use auths_sdk::keychain::KeyAlias;
1010
use auths_sdk::signing::PassphraseProvider;
1111

1212
use super::file::FileArtifact;
13+
use super::{dsse_pae, merge_transparency, submit_to_log};
1314
use crate::factories::storage::build_auths_context;
1415

1516
/// Execute the `artifact sign` command.
@@ -25,10 +26,12 @@ pub fn handle_sign(
2526
repo_opt: Option<PathBuf>,
2627
passphrase_provider: Arc<dyn PassphraseProvider + Send + Sync>,
2728
env_config: &EnvironmentConfig,
29+
log: &Option<String>,
30+
allow_unlogged: bool,
2831
) -> Result<()> {
2932
let repo_path = auths_sdk::storage_layout::resolve_repo_path(repo_opt)?;
3033

31-
let ctx = build_auths_context(&repo_path, env_config, Some(passphrase_provider))?;
34+
let ctx = build_auths_context(&repo_path, env_config, Some(passphrase_provider.clone()))?;
3235

3336
let params = ArtifactSigningParams {
3437
artifact: Arc::new(FileArtifact::new(file)),
@@ -42,6 +45,44 @@ pub fn handle_sign(
4245
let result = sign_artifact(params, &ctx)
4346
.with_context(|| format!("Failed to sign artifact {:?}", file))?;
4447

48+
// Compute DSSE signature if log submission is requested
49+
let dsse_sig = if log.is_some() && !allow_unlogged {
50+
let pae = dsse_pae(
51+
"application/vnd.auths+json",
52+
result.attestation_json.as_bytes(),
53+
);
54+
let alias = KeyAlias::new_unchecked(device_key);
55+
let (_, _role, encrypted) = ctx
56+
.key_storage
57+
.load_key(&alias)
58+
.context("Failed to load device key for log signature")?;
59+
let passphrase = passphrase_provider
60+
.get_passphrase("Re-enter passphrase for log signature:")
61+
.map_err(|e| anyhow::anyhow!("Passphrase error: {e}"))?;
62+
let pkcs8 = auths_sdk::crypto::decrypt_keypair(&encrypted, &passphrase)
63+
.context("Failed to decrypt key for log signature")?;
64+
let parsed = auths_crypto::parse_key_material(&pkcs8)
65+
.map_err(|e| anyhow::anyhow!("Failed to parse key: {e}"))?;
66+
let sig = auths_crypto::typed_sign(&parsed.seed, &pae)
67+
.map_err(|e| anyhow::anyhow!("Failed to sign DSSE PAE: {e}"))?;
68+
Some(sig)
69+
} else {
70+
None
71+
};
72+
73+
let transparency_json = submit_to_log(
74+
&result.attestation_json,
75+
log,
76+
allow_unlogged,
77+
dsse_sig.as_deref(),
78+
)?;
79+
80+
let final_json = if let Some(transparency) = transparency_json {
81+
merge_transparency(&result.attestation_json, transparency)?
82+
} else {
83+
result.attestation_json.clone()
84+
};
85+
4586
let output_path = output.unwrap_or_else(|| {
4687
let mut p = file.to_path_buf();
4788
let new_name = format!(
@@ -52,7 +93,7 @@ pub fn handle_sign(
5293
p
5394
});
5495

55-
std::fs::write(&output_path, &result.attestation_json)
96+
std::fs::write(&output_path, &final_json)
5697
.with_context(|| format!("Failed to write signature to {:?}", output_path))?;
5798

5899
println!(

crates/auths-cli/src/commands/device/pair/join.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,16 @@ pub(crate) async fn handle_join(
9797
let create_spinner = create_wait_spinner(&format!("{GEAR}Creating pairing response..."));
9898

9999
// Create the response + ECDH
100+
let pubkey_32: &[u8; 32] = material
101+
.public_key
102+
.as_slice()
103+
.try_into()
104+
.map_err(|_| anyhow::anyhow!("Pairing requires Ed25519 (32-byte) key"))?;
100105
let (pairing_response, shared_secret) = PairingResponse::create(
101106
now,
102107
&token,
103108
&material.seed,
104-
&material.public_key,
109+
pubkey_32,
105110
material.device_did.to_string(),
106111
Some(hostname()),
107112
)

crates/auths-cli/src/commands/device/verify_attestation.rs

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -181,12 +181,19 @@ fn resolve_issuer_key(
181181
if let Some(ref pk_hex) = cmd.issuer_pk {
182182
let pk_bytes =
183183
hex::decode(pk_hex).context("Invalid hex string provided for issuer public key")?;
184-
if pk_bytes.len() != 32 {
185-
return Err(anyhow!(
186-
"Issuer public key must be 32 bytes (64 hex chars), got {} bytes",
187-
pk_bytes.len()
188-
));
189-
}
184+
let curve = match pk_bytes.len() {
185+
32 => auths_crypto::CurveType::Ed25519,
186+
33 | 65 => auths_crypto::CurveType::P256,
187+
_ => {
188+
return Err(anyhow!(
189+
"Invalid issuer public key length: {}",
190+
pk_bytes.len()
191+
));
192+
}
193+
};
194+
// Validate via DevicePublicKey type system
195+
auths_verifier::DevicePublicKey::try_new(curve, &pk_bytes)
196+
.map_err(|e| anyhow!("Invalid issuer public key: {e}"))?;
190197
return Ok(pk_bytes);
191198
}
192199

@@ -441,12 +448,18 @@ pub async fn handle_verify_attestation(
441448
let issuer_pk_bytes = hex::decode(issuer_pubkey_hex)
442449
.context("Invalid hex string provided for issuer public key")?;
443450

444-
if issuer_pk_bytes.len() != 32 {
445-
return Err(anyhow!(
446-
"Issuer public key must be 32 bytes (64 hex chars), got {} bytes",
447-
issuer_pk_bytes.len()
448-
));
449-
}
451+
let curve = match issuer_pk_bytes.len() {
452+
32 => auths_crypto::CurveType::Ed25519,
453+
33 | 65 => auths_crypto::CurveType::P256,
454+
_ => {
455+
return Err(anyhow!(
456+
"Invalid issuer public key length: {}",
457+
issuer_pk_bytes.len()
458+
));
459+
}
460+
};
461+
auths_verifier::DevicePublicKey::try_new(curve, &issuer_pk_bytes)
462+
.map_err(|e| anyhow!("Invalid issuer public key: {e}"))?;
450463

451464
match verify_with_keys(&att, &issuer_pk_bytes).await {
452465
Ok(_) => {

crates/auths-cli/src/commands/publish.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ impl ExecutableCommand for PublishCommand {
6161
ctx.repo_path.clone(),
6262
ctx.passphrase_provider.clone(),
6363
&ctx.env_config,
64+
&None,
65+
false,
6466
)?;
6567
}
6668
p

crates/auths-cli/src/commands/sign.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,8 @@ pub fn handle_sign_unified(
192192
repo_opt,
193193
passphrase_provider,
194194
env_config,
195+
&None,
196+
false,
195197
)
196198
}
197199
SignTarget::CommitRange(range) => sign_commit_range(&range),

0 commit comments

Comments
 (0)