use crate::error::{NonoError, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub const IN_TOTO_PAYLOAD_TYPE: &str = "application/vnd.in-toto+json";
pub const IN_TOTO_STATEMENT_TYPE: &str = "https://in-toto.io/Statement/v1";
pub const NONO_PREDICATE_TYPE: &str = "https://nono.sh/attestation/instruction-file/v1";
pub const NONO_POLICY_PREDICATE_TYPE: &str = "https://nono.sh/attestation/trust-policy/v1";
pub const NONO_MULTI_SUBJECT_PREDICATE_TYPE: &str = "https://nono.sh/attestation/multi-file/v1";
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DsseEnvelope {
pub payload_type: String,
pub payload: String,
pub signatures: Vec<DsseSignature>,
}
impl DsseEnvelope {
pub fn from_json(json: &str) -> Result<Self> {
let envelope: Self =
serde_json::from_str(json).map_err(|e| NonoError::TrustVerification {
path: String::new(),
reason: format!("invalid DSSE envelope: {e}"),
})?;
envelope.validate()?;
Ok(envelope)
}
pub fn to_json(&self) -> Result<String> {
serde_json::to_string(self).map_err(|e| NonoError::TrustVerification {
path: String::new(),
reason: format!("failed to serialize DSSE envelope: {e}"),
})
}
fn validate(&self) -> Result<()> {
if self.payload_type.is_empty() {
return Err(NonoError::TrustVerification {
path: String::new(),
reason: "DSSE envelope has empty payloadType".to_string(),
});
}
if self.payload.is_empty() {
return Err(NonoError::TrustVerification {
path: String::new(),
reason: "DSSE envelope has empty payload".to_string(),
});
}
if self.signatures.is_empty() {
return Err(NonoError::TrustVerification {
path: String::new(),
reason: "DSSE envelope has no signatures".to_string(),
});
}
Ok(())
}
pub fn decode_payload(&self) -> Result<Vec<u8>> {
base64url_decode(&self.payload).map_err(|e| NonoError::TrustVerification {
path: String::new(),
reason: format!("failed to decode DSSE payload: {e}"),
})
}
pub fn extract_statement(&self) -> Result<InTotoStatement> {
if self.payload_type != IN_TOTO_PAYLOAD_TYPE {
return Err(NonoError::TrustVerification {
path: String::new(),
reason: format!(
"unexpected DSSE payloadType: expected '{}', got '{}'",
IN_TOTO_PAYLOAD_TYPE, self.payload_type
),
});
}
let bytes = self.decode_payload()?;
let json = std::str::from_utf8(&bytes).map_err(|e| NonoError::TrustVerification {
path: String::new(),
reason: format!("DSSE payload is not valid UTF-8: {e}"),
})?;
InTotoStatement::from_json(json)
}
pub fn pae_bytes(&self) -> Result<Vec<u8>> {
let decoded = self.decode_payload()?;
Ok(pae(&self.payload_type, &decoded))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DsseSignature {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub keyid: String,
pub sig: String,
}
impl DsseSignature {
pub fn decode_sig(&self) -> Result<Vec<u8>> {
base64url_decode(&self.sig).map_err(|e| NonoError::TrustVerification {
path: String::new(),
reason: format!("failed to decode DSSE signature: {e}"),
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InTotoStatement {
#[serde(rename = "_type")]
pub statement_type: String,
pub subject: Vec<InTotoSubject>,
pub predicate_type: String,
pub predicate: serde_json::Value,
}
impl InTotoStatement {
pub fn from_json(json: &str) -> Result<Self> {
let stmt: Self = serde_json::from_str(json).map_err(|e| NonoError::TrustVerification {
path: String::new(),
reason: format!("invalid in-toto statement: {e}"),
})?;
stmt.validate()?;
Ok(stmt)
}
fn validate(&self) -> Result<()> {
if self.statement_type != IN_TOTO_STATEMENT_TYPE {
return Err(NonoError::TrustVerification {
path: String::new(),
reason: format!(
"unexpected in-toto statement type: expected '{}', got '{}'",
IN_TOTO_STATEMENT_TYPE, self.statement_type
),
});
}
if self.subject.is_empty() {
return Err(NonoError::TrustVerification {
path: String::new(),
reason: "in-toto statement has no subjects".to_string(),
});
}
for subject in &self.subject {
if subject.name.is_empty() {
return Err(NonoError::TrustVerification {
path: String::new(),
reason: "in-toto subject has empty name".to_string(),
});
}
if !subject.digest.contains_key("sha256") {
return Err(NonoError::TrustVerification {
path: String::new(),
reason: format!("in-toto subject '{}' missing sha256 digest", subject.name),
});
}
}
Ok(())
}
#[must_use]
pub fn first_subject_digest(&self) -> Option<&str> {
self.subject
.first()
.and_then(|s| s.digest.get("sha256"))
.map(String::as_str)
}
#[must_use]
pub fn first_subject_name(&self) -> Option<&str> {
self.subject.first().map(|s| s.name.as_str())
}
pub fn extract_signer(&self) -> Result<super::types::SignerIdentity> {
let signer = self
.predicate
.get("signer")
.ok_or_else(|| NonoError::TrustVerification {
path: String::new(),
reason: "predicate missing 'signer' field".to_string(),
})?;
let kind = signer.get("kind").and_then(|v| v.as_str()).ok_or_else(|| {
NonoError::TrustVerification {
path: String::new(),
reason: "signer missing 'kind' field".to_string(),
}
})?;
match kind {
"keyed" => {
let key_id = signer
.get("key_id")
.and_then(|v| v.as_str())
.ok_or_else(|| NonoError::TrustVerification {
path: String::new(),
reason: "keyed signer missing 'key_id'".to_string(),
})?;
Ok(super::types::SignerIdentity::Keyed {
key_id: key_id.to_string(),
})
}
"keyless" => {
let issuer = get_str_field(signer, "issuer")?;
let repository = get_str_field(signer, "repository")?;
let workflow = signer
.get("workflow_ref")
.and_then(|v| v.as_str())
.map(|s| {
s.split('@').next().unwrap_or(s).to_string()
})
.ok_or_else(|| NonoError::TrustVerification {
path: String::new(),
reason: "keyless signer missing 'workflow_ref'".to_string(),
})?;
let git_ref = signer
.get("subject")
.and_then(|v| v.as_str())
.map(|s| {
extract_ref_from_subject(s)
})
.ok_or_else(|| NonoError::TrustVerification {
path: String::new(),
reason: "keyless signer missing 'subject'".to_string(),
})?;
Ok(super::types::SignerIdentity::Keyless {
issuer,
repository,
workflow,
git_ref,
})
}
other => Err(NonoError::TrustVerification {
path: String::new(),
reason: format!("unknown signer kind: '{other}'"),
}),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InTotoSubject {
pub name: String,
pub digest: HashMap<String, String>,
}
#[must_use]
pub fn pae(payload_type: &str, payload: &[u8]) -> Vec<u8> {
let header = format!(
"DSSEv1 {} {} {} ",
payload_type.len(),
payload_type,
payload.len()
);
let mut result = Vec::with_capacity(header.len() + payload.len());
result.extend_from_slice(header.as_bytes());
result.extend_from_slice(payload);
result
}
#[must_use]
pub fn new_statement(
subject_name: &str,
sha256_digest: &str,
predicate: serde_json::Value,
predicate_type: &str,
) -> InTotoStatement {
let mut digest = HashMap::new();
digest.insert("sha256".to_string(), sha256_digest.to_string());
InTotoStatement {
statement_type: IN_TOTO_STATEMENT_TYPE.to_string(),
subject: vec![InTotoSubject {
name: subject_name.to_string(),
digest,
}],
predicate_type: predicate_type.to_string(),
predicate,
}
}
#[must_use]
pub fn new_instruction_statement(
filename: &str,
sha256_digest: &str,
signer_predicate: serde_json::Value,
) -> InTotoStatement {
new_statement(
filename,
sha256_digest,
signer_predicate,
NONO_PREDICATE_TYPE,
)
}
#[must_use]
pub fn new_policy_statement(
filename: &str,
sha256_digest: &str,
signer_predicate: serde_json::Value,
) -> InTotoStatement {
new_statement(
filename,
sha256_digest,
signer_predicate,
NONO_POLICY_PREDICATE_TYPE,
)
}
#[must_use]
pub fn new_multi_subject_statement(
subjects: &[(String, String)],
signer_predicate: serde_json::Value,
) -> InTotoStatement {
let subject = subjects
.iter()
.map(|(name, sha256_hex)| {
let mut digest = HashMap::new();
digest.insert("sha256".to_string(), sha256_hex.clone());
InTotoSubject {
name: name.clone(),
digest,
}
})
.collect();
InTotoStatement {
statement_type: IN_TOTO_STATEMENT_TYPE.to_string(),
subject,
predicate_type: NONO_MULTI_SUBJECT_PREDICATE_TYPE.to_string(),
predicate: signer_predicate,
}
}
pub fn new_envelope(statement: &InTotoStatement) -> Result<DsseEnvelope> {
let payload_json =
serde_json::to_string(statement).map_err(|e| NonoError::TrustVerification {
path: String::new(),
reason: format!("failed to serialize in-toto statement: {e}"),
})?;
let payload_b64 = base64url_encode(payload_json.as_bytes());
Ok(DsseEnvelope {
payload_type: IN_TOTO_PAYLOAD_TYPE.to_string(),
payload: payload_b64,
signatures: Vec::new(),
})
}
use super::base64::{base64url_decode, base64url_encode};
fn get_str_field(value: &serde_json::Value, field: &str) -> Result<String> {
value
.get(field)
.and_then(|v| v.as_str())
.map(String::from)
.ok_or_else(|| NonoError::TrustVerification {
path: String::new(),
reason: format!("keyless signer missing '{field}'"),
})
}
fn extract_ref_from_subject(subject: &str) -> String {
if let Some(idx) = subject.find(":ref:") {
return subject[idx + 5..].to_string();
}
subject.to_string()
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn pae_spec_test_vector() {
let result = pae("http://example.com/HelloWorld", b"hello world");
let expected = b"DSSEv1 29 http://example.com/HelloWorld 11 hello world";
assert_eq!(result, expected.to_vec());
}
#[test]
fn pae_in_toto_type() {
let payload = b"test payload";
let result = pae(IN_TOTO_PAYLOAD_TYPE, payload);
let expected_prefix = format!(
"DSSEv1 {} {} {} ",
IN_TOTO_PAYLOAD_TYPE.len(),
IN_TOTO_PAYLOAD_TYPE,
payload.len()
);
assert!(result.starts_with(expected_prefix.as_bytes()));
assert!(result.ends_with(payload));
}
#[test]
fn pae_empty_payload() {
let result = pae("type", b"");
assert_eq!(result, b"DSSEv1 4 type 0 ".to_vec());
}
#[test]
fn pae_binary_payload() {
let payload = vec![0x00, 0x01, 0xFF, 0xFE];
let result = pae("binary", &payload);
assert!(result.ends_with(&payload));
}
fn sample_statement_json() -> String {
serde_json::json!({
"_type": IN_TOTO_STATEMENT_TYPE,
"subject": [{
"name": "SKILLS.md",
"digest": { "sha256": "abcdef1234567890" }
}],
"predicateType": NONO_PREDICATE_TYPE,
"predicate": {
"version": 1,
"signer": {
"kind": "keyed",
"key_id": "nono-keystore:default"
}
}
})
.to_string()
}
fn sample_envelope_json() -> String {
let payload = base64url_encode(sample_statement_json().as_bytes());
serde_json::json!({
"payloadType": IN_TOTO_PAYLOAD_TYPE,
"payload": payload,
"signatures": [{
"keyid": "",
"sig": base64url_encode(b"fake-signature")
}]
})
.to_string()
}
#[test]
fn envelope_parse_valid() {
let json = sample_envelope_json();
let envelope = DsseEnvelope::from_json(&json).unwrap();
assert_eq!(envelope.payload_type, IN_TOTO_PAYLOAD_TYPE);
assert_eq!(envelope.signatures.len(), 1);
}
#[test]
fn envelope_parse_invalid_json() {
let result = DsseEnvelope::from_json("not json");
assert!(result.is_err());
}
#[test]
fn envelope_parse_empty_payload() {
let json = serde_json::json!({
"payloadType": IN_TOTO_PAYLOAD_TYPE,
"payload": "",
"signatures": [{"sig": "abc"}]
})
.to_string();
let result = DsseEnvelope::from_json(&json);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("empty payload"));
}
#[test]
fn envelope_parse_no_signatures() {
let json = serde_json::json!({
"payloadType": IN_TOTO_PAYLOAD_TYPE,
"payload": "dGVzdA",
"signatures": []
})
.to_string();
let result = DsseEnvelope::from_json(&json);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("no signatures"));
}
#[test]
fn envelope_decode_payload() {
let json = sample_envelope_json();
let envelope = DsseEnvelope::from_json(&json).unwrap();
let decoded = envelope.decode_payload().unwrap();
let decoded_str = std::str::from_utf8(&decoded).unwrap();
assert!(decoded_str.contains(IN_TOTO_STATEMENT_TYPE));
}
#[test]
fn envelope_extract_statement() {
let json = sample_envelope_json();
let envelope = DsseEnvelope::from_json(&json).unwrap();
let stmt = envelope.extract_statement().unwrap();
assert_eq!(stmt.statement_type, IN_TOTO_STATEMENT_TYPE);
assert_eq!(stmt.subject.len(), 1);
assert_eq!(stmt.subject[0].name, "SKILLS.md");
}
#[test]
fn envelope_extract_statement_wrong_type() {
let payload = base64url_encode(b"{}");
let json = serde_json::json!({
"payloadType": "text/plain",
"payload": payload,
"signatures": [{"sig": "abc"}]
})
.to_string();
let envelope = DsseEnvelope::from_json(&json).unwrap();
let result = envelope.extract_statement();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("unexpected DSSE payloadType"));
}
#[test]
fn envelope_pae_bytes() {
let json = sample_envelope_json();
let envelope = DsseEnvelope::from_json(&json).unwrap();
let pae_result = envelope.pae_bytes().unwrap();
assert!(pae_result.starts_with(b"DSSEv1"));
}
#[test]
fn envelope_to_json_roundtrip() {
let original = sample_envelope_json();
let envelope = DsseEnvelope::from_json(&original).unwrap();
let serialized = envelope.to_json().unwrap();
let reparsed = DsseEnvelope::from_json(&serialized).unwrap();
assert_eq!(reparsed.payload_type, envelope.payload_type);
assert_eq!(reparsed.payload, envelope.payload);
}
#[test]
fn signature_decode() {
let sig = DsseSignature {
keyid: String::new(),
sig: base64url_encode(b"signature bytes"),
};
let decoded = sig.decode_sig().unwrap();
assert_eq!(decoded, b"signature bytes");
}
#[test]
fn statement_parse_valid() {
let json = sample_statement_json();
let stmt = InTotoStatement::from_json(&json).unwrap();
assert_eq!(stmt.statement_type, IN_TOTO_STATEMENT_TYPE);
assert_eq!(stmt.predicate_type, NONO_PREDICATE_TYPE);
}
#[test]
fn statement_wrong_type() {
let json = serde_json::json!({
"_type": "https://wrong.type/v1",
"subject": [{ "name": "f", "digest": { "sha256": "abc" } }],
"predicateType": NONO_PREDICATE_TYPE,
"predicate": {}
})
.to_string();
let result = InTotoStatement::from_json(&json);
assert!(result.is_err());
}
#[test]
fn statement_empty_subjects() {
let json = serde_json::json!({
"_type": IN_TOTO_STATEMENT_TYPE,
"subject": [],
"predicateType": NONO_PREDICATE_TYPE,
"predicate": {}
})
.to_string();
let result = InTotoStatement::from_json(&json);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("no subjects"));
}
#[test]
fn statement_subject_missing_digest() {
let json = serde_json::json!({
"_type": IN_TOTO_STATEMENT_TYPE,
"subject": [{ "name": "f", "digest": {} }],
"predicateType": NONO_PREDICATE_TYPE,
"predicate": {}
})
.to_string();
let result = InTotoStatement::from_json(&json);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("sha256 digest"));
}
#[test]
fn statement_first_subject_accessors() {
let stmt = InTotoStatement::from_json(&sample_statement_json()).unwrap();
assert_eq!(stmt.first_subject_name(), Some("SKILLS.md"));
assert_eq!(stmt.first_subject_digest(), Some("abcdef1234567890"));
}
#[test]
fn statement_extract_keyed_signer() {
let stmt = InTotoStatement::from_json(&sample_statement_json()).unwrap();
let identity = stmt.extract_signer().unwrap();
match identity {
super::super::types::SignerIdentity::Keyed { key_id } => {
assert_eq!(key_id, "nono-keystore:default");
}
_ => panic!("expected keyed signer"),
}
}
#[test]
fn statement_extract_keyless_signer() {
let json = serde_json::json!({
"_type": IN_TOTO_STATEMENT_TYPE,
"subject": [{ "name": "SKILLS.md", "digest": { "sha256": "abc" } }],
"predicateType": NONO_PREDICATE_TYPE,
"predicate": {
"version": 1,
"signer": {
"kind": "keyless",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:org/repo:ref:refs/tags/v1.0.0",
"repository": "org/repo",
"workflow_ref": ".github/workflows/sign.yml@refs/heads/main"
}
}
})
.to_string();
let stmt = InTotoStatement::from_json(&json).unwrap();
let identity = stmt.extract_signer().unwrap();
match identity {
super::super::types::SignerIdentity::Keyless {
issuer,
repository,
workflow,
git_ref,
} => {
assert_eq!(issuer, "https://token.actions.githubusercontent.com");
assert_eq!(repository, "org/repo");
assert_eq!(workflow, ".github/workflows/sign.yml");
assert_eq!(git_ref, "refs/tags/v1.0.0");
}
_ => panic!("expected keyless signer"),
}
}
#[test]
fn statement_extract_signer_unknown_kind() {
let json = serde_json::json!({
"_type": IN_TOTO_STATEMENT_TYPE,
"subject": [{ "name": "f", "digest": { "sha256": "abc" } }],
"predicateType": NONO_PREDICATE_TYPE,
"predicate": {
"signer": { "kind": "unknown" }
}
})
.to_string();
let stmt = InTotoStatement::from_json(&json).unwrap();
let result = stmt.extract_signer();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("unknown signer kind"));
}
#[test]
fn statement_extract_signer_missing() {
let json = serde_json::json!({
"_type": IN_TOTO_STATEMENT_TYPE,
"subject": [{ "name": "f", "digest": { "sha256": "abc" } }],
"predicateType": NONO_PREDICATE_TYPE,
"predicate": {}
})
.to_string();
let stmt = InTotoStatement::from_json(&json).unwrap();
let result = stmt.extract_signer();
assert!(result.is_err());
}
#[test]
fn new_instruction_statement_structure() {
let predicate = serde_json::json!({
"version": 1,
"signer": { "kind": "keyed", "key_id": "test" }
});
let stmt = new_instruction_statement("SKILLS.md", "abcdef", predicate);
assert_eq!(stmt.statement_type, IN_TOTO_STATEMENT_TYPE);
assert_eq!(stmt.predicate_type, NONO_PREDICATE_TYPE);
assert_eq!(stmt.subject.len(), 1);
assert_eq!(stmt.subject[0].name, "SKILLS.md");
assert_eq!(stmt.subject[0].digest["sha256"], "abcdef");
}
#[test]
fn new_envelope_creates_valid_structure() {
let predicate = serde_json::json!({
"version": 1,
"signer": { "kind": "keyed", "key_id": "test" }
});
let stmt = new_instruction_statement("SKILLS.md", "abcdef", predicate);
let envelope = new_envelope(&stmt).unwrap();
assert_eq!(envelope.payload_type, IN_TOTO_PAYLOAD_TYPE);
assert!(envelope.signatures.is_empty());
let extracted = envelope.extract_statement().unwrap();
assert_eq!(extracted.first_subject_name(), Some("SKILLS.md"));
}
#[test]
fn new_policy_statement_uses_policy_predicate_type() {
let predicate = serde_json::json!({
"version": 1,
"signer": { "kind": "keyed", "key_id": "test" }
});
let stmt = new_policy_statement("trust-policy.json", "abcdef", predicate);
assert_eq!(stmt.statement_type, IN_TOTO_STATEMENT_TYPE);
assert_eq!(stmt.predicate_type, NONO_POLICY_PREDICATE_TYPE);
assert_eq!(stmt.subject[0].name, "trust-policy.json");
assert_eq!(stmt.subject[0].digest["sha256"], "abcdef");
}
#[test]
fn new_statement_accepts_custom_predicate_type() {
let predicate = serde_json::json!({"version": 1});
let stmt = new_statement("file.txt", "digest", predicate, "custom/type/v1");
assert_eq!(stmt.predicate_type, "custom/type/v1");
assert_eq!(stmt.subject[0].name, "file.txt");
}
#[test]
fn instruction_and_policy_predicate_types_differ() {
assert_ne!(NONO_PREDICATE_TYPE, NONO_POLICY_PREDICATE_TYPE);
}
#[test]
fn multi_subject_predicate_type_is_unique() {
assert_ne!(NONO_MULTI_SUBJECT_PREDICATE_TYPE, NONO_PREDICATE_TYPE);
assert_ne!(
NONO_MULTI_SUBJECT_PREDICATE_TYPE,
NONO_POLICY_PREDICATE_TYPE
);
}
#[test]
fn multi_subject_statement_structure() {
let subjects = vec![
("SKILL.md".to_string(), "aaa111".to_string()),
("lib/script.py".to_string(), "bbb222".to_string()),
];
let predicate = serde_json::json!({
"version": 1,
"signer": { "kind": "keyed", "key_id": "test" }
});
let stmt = new_multi_subject_statement(&subjects, predicate);
assert_eq!(stmt.statement_type, IN_TOTO_STATEMENT_TYPE);
assert_eq!(stmt.predicate_type, NONO_MULTI_SUBJECT_PREDICATE_TYPE);
assert_eq!(stmt.subject.len(), 2);
assert_eq!(stmt.subject[0].name, "SKILL.md");
assert_eq!(stmt.subject[0].digest["sha256"], "aaa111");
assert_eq!(stmt.subject[1].name, "lib/script.py");
assert_eq!(stmt.subject[1].digest["sha256"], "bbb222");
}
#[test]
fn multi_subject_statement_single_subject() {
let subjects = vec![("only.md".to_string(), "digest123".to_string())];
let predicate = serde_json::json!({"version": 1});
let stmt = new_multi_subject_statement(&subjects, predicate);
assert_eq!(stmt.subject.len(), 1);
assert_eq!(stmt.subject[0].name, "only.md");
}
#[test]
fn multi_subject_statement_roundtrips_through_envelope() {
let subjects = vec![
("a.md".to_string(), "aaa".to_string()),
("b.py".to_string(), "bbb".to_string()),
("c.json".to_string(), "ccc".to_string()),
];
let predicate = serde_json::json!({
"version": 1,
"signer": { "kind": "keyed", "key_id": "test" }
});
let stmt = new_multi_subject_statement(&subjects, predicate);
let envelope = new_envelope(&stmt).unwrap();
let extracted = envelope.extract_statement().unwrap();
assert_eq!(extracted.subject.len(), 3);
assert_eq!(extracted.predicate_type, NONO_MULTI_SUBJECT_PREDICATE_TYPE);
assert_eq!(extracted.subject[0].name, "a.md");
assert_eq!(extracted.subject[1].name, "b.py");
assert_eq!(extracted.subject[2].name, "c.json");
}
#[test]
fn multi_subject_statement_preserves_signer_predicate() {
let subjects = vec![("f.md".to_string(), "ddd".to_string())];
let predicate = serde_json::json!({
"version": 1,
"signer": { "kind": "keyed", "key_id": "nono-keystore:default" }
});
let stmt = new_multi_subject_statement(&subjects, predicate);
let identity = stmt.extract_signer().unwrap();
match identity {
super::super::types::SignerIdentity::Keyed { key_id } => {
assert_eq!(key_id, "nono-keystore:default");
}
_ => panic!("expected keyed signer"),
}
}
#[test]
fn extract_ref_standard_format() {
assert_eq!(
extract_ref_from_subject("repo:org/repo:ref:refs/tags/v1.0.0"),
"refs/tags/v1.0.0"
);
}
#[test]
fn extract_ref_heads() {
assert_eq!(
extract_ref_from_subject("repo:org/repo:ref:refs/heads/main"),
"refs/heads/main"
);
}
#[test]
fn extract_ref_fallback() {
assert_eq!(
extract_ref_from_subject("no-ref-separator"),
"no-ref-separator"
);
}
}