Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions crates/auths-cli/src/errors/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,11 @@ fn docs_url(code: &str) -> Option<String> {
| "AUTHS_STORAGE_LOCKED"
| "AUTHS_BACKEND_INIT_FAILED"
| "AUTHS_AGENT_LOCKED"
| "AUTHS_VERIFICATION_ERROR"
| "AUTHS_ISSUER_SIG_FAILED"
| "AUTHS_DEVICE_SIG_FAILED"
| "AUTHS_ATTESTATION_EXPIRED"
| "AUTHS_ATTESTATION_REVOKED"
| "AUTHS_TIMESTAMP_IN_FUTURE"
| "AUTHS_MISSING_CAPABILITY"
| "AUTHS_DID_RESOLUTION_ERROR"
| "AUTHS_ORG_VERIFICATION_FAILED"
Expand Down Expand Up @@ -304,7 +308,7 @@ mod tests {

#[test]
fn render_error_attestation_error_text() {
let err = Error::new(AttestationError::VerificationError("bad sig".into()));
let err = Error::new(AttestationError::IssuerSignatureFailed("bad sig".into()));
render_error(&err, false);
}

Expand Down
32 changes: 9 additions & 23 deletions crates/auths-id/src/attestation/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,23 @@ pub fn verify_with_resolver(
// Return specific AttestationError
// 1. Check revocation and expiration
if att.is_revoked() {
return Err(AttestationError::VerificationError(
"Attestation revoked".to_string(),
));
return Err(AttestationError::AttestationRevoked);
}
if let Some(exp) = att.expires_at
&& now > exp
{
return Err(AttestationError::VerificationError(format!(
"Attestation expired on {}",
exp.to_rfc3339()
)));
return Err(AttestationError::AttestationExpired {
at: exp.to_rfc3339(),
});
}
// Only reject timestamps in the future (clock drift protection)
// Past timestamps are valid - attestations stored in Git are verified days/months later
if let Some(ts) = att.timestamp
&& ts > now + Duration::seconds(MAX_SKEW_SECS)
{
return Err(AttestationError::VerificationError(format!(
"Attestation timestamp {} is in the future",
ts.to_rfc3339()
)));
return Err(AttestationError::TimestampInFuture {
at: ts.to_rfc3339(),
});
}

// 2. Resolve issuer's public key
Expand Down Expand Up @@ -82,12 +78,7 @@ pub fn verify_with_resolver(
let issuer_public_key_ring = UnparsedPublicKey::new(&ED25519, &issuer_pk_bytes);
issuer_public_key_ring
.verify(data_to_verify, att.identity_signature.as_bytes())
.map_err(|e| {
AttestationError::VerificationError(format!(
"Issuer signature verification failed: {}",
e
))
})?;
.map_err(|e| AttestationError::IssuerSignatureFailed(e.to_string()))?;
debug!(
"(Verify) Issuer signature verified successfully for {}",
att.issuer
Expand All @@ -97,12 +88,7 @@ pub fn verify_with_resolver(
let device_public_key_ring = UnparsedPublicKey::new(&ED25519, att.device_public_key.as_bytes());
device_public_key_ring
.verify(data_to_verify, att.device_signature.as_bytes())
.map_err(|e| {
AttestationError::VerificationError(format!(
"Device signature verification failed: {}",
e
))
})?;
.map_err(|e| AttestationError::DeviceSignatureFailed(e.to_string()))?;
debug!(
"(Verify) Device signature verified successfully for {}",
att.subject.as_str()
Expand Down
44 changes: 38 additions & 6 deletions crates/auths-verifier/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,31 @@ pub trait AuthsErrorInfo {
/// Errors returned by attestation signing, verification, and related operations.
#[derive(Error, Debug)]
pub enum AttestationError {
/// Cryptographic signature verification failed.
#[error("Signature verification failed: {0}")]
VerificationError(String),
/// Issuer's Ed25519 signature did not verify.
#[error("Issuer signature verification failed: {0}")]
IssuerSignatureFailed(String),

/// Device's Ed25519 signature did not verify.
#[error("Device signature verification failed: {0}")]
DeviceSignatureFailed(String),

/// Attestation has passed its expiry timestamp.
#[error("Attestation expired on {at}")]
AttestationExpired {
/// RFC 3339 formatted expiry timestamp.
at: String,
},

/// Attestation was explicitly revoked.
#[error("Attestation revoked")]
AttestationRevoked,

/// Attestation timestamp is in the future (clock skew).
#[error("Attestation timestamp {at} is in the future")]
TimestampInFuture {
/// RFC 3339 formatted timestamp.
at: String,
},

/// The attestation does not grant the required capability.
#[error("Missing required capability: required {required:?}, available {available:?}")]
Expand Down Expand Up @@ -85,7 +107,11 @@ pub enum AttestationError {
impl AuthsErrorInfo for AttestationError {
fn error_code(&self) -> &'static str {
match self {
Self::VerificationError(_) => "AUTHS_VERIFICATION_ERROR",
Self::IssuerSignatureFailed(_) => "AUTHS_ISSUER_SIG_FAILED",
Self::DeviceSignatureFailed(_) => "AUTHS_DEVICE_SIG_FAILED",
Self::AttestationExpired { .. } => "AUTHS_ATTESTATION_EXPIRED",
Self::AttestationRevoked => "AUTHS_ATTESTATION_REVOKED",
Self::TimestampInFuture { .. } => "AUTHS_TIMESTAMP_IN_FUTURE",
Self::MissingCapability { .. } => "AUTHS_MISSING_CAPABILITY",
Self::SigningError(_) => "AUTHS_SIGNING_ERROR",
Self::DidResolutionError(_) => "AUTHS_DID_RESOLUTION_ERROR",
Expand All @@ -103,9 +129,15 @@ impl AuthsErrorInfo for AttestationError {

fn suggestion(&self) -> Option<&'static str> {
match self {
Self::VerificationError(_) => {
Some("Verify the attestation was signed with the correct key")
Self::IssuerSignatureFailed(_) => {
Some("Verify the attestation was signed with the correct issuer key")
}
Self::DeviceSignatureFailed(_) => Some("Verify the device key matches the attestation"),
Self::AttestationExpired { .. } => Some("Request a new attestation from the issuer"),
Self::AttestationRevoked => {
Some("This device has been revoked; contact the identity admin")
}
Self::TimestampInFuture { .. } => Some("Check system clock synchronization"),
Self::MissingCapability { .. } => {
Some("Request an attestation with the required capability")
}
Expand Down
48 changes: 21 additions & 27 deletions crates/auths-verifier/src/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,28 @@ pub const ERR_VERIFY_INSUFFICIENT_WITNESSES: c_int = -9;
pub const ERR_VERIFY_WITNESS_PARSE: c_int = -10;
/// Input JSON exceeded size limit.
pub const ERR_VERIFY_INPUT_TOO_LARGE: c_int = -11;
/// Attestation timestamp is in the future (clock skew).
pub const ERR_VERIFY_FUTURE_TIMESTAMP: c_int = -12;
/// Unclassified verification error.
pub const ERR_VERIFY_OTHER: c_int = -99;
/// Internal panic occurred
pub const ERR_VERIFY_PANIC: c_int = -127;

fn attestation_error_to_code(e: &AttestationError) -> c_int {
match e {
AttestationError::IssuerSignatureFailed(_) => ERR_VERIFY_ISSUER_SIG_FAIL,
AttestationError::DeviceSignatureFailed(_) => ERR_VERIFY_DEVICE_SIG_FAIL,
AttestationError::AttestationExpired { .. } => ERR_VERIFY_EXPIRED,
AttestationError::AttestationRevoked => ERR_VERIFY_REVOKED,
AttestationError::TimestampInFuture { .. } => ERR_VERIFY_FUTURE_TIMESTAMP,
AttestationError::SerializationError(_) => ERR_VERIFY_SERIALIZATION,
AttestationError::InvalidInput(_) => ERR_VERIFY_INVALID_PK_LEN,
AttestationError::InputTooLarge(_) => ERR_VERIFY_INPUT_TOO_LARGE,
AttestationError::BundleExpired { .. } => ERR_VERIFY_EXPIRED,
_ => ERR_VERIFY_OTHER,
}
}

fn check_batch_sizes(sizes: &[usize], caller: &str) -> Option<c_int> {
for &size in sizes {
if size > MAX_JSON_BATCH_SIZE {
Expand Down Expand Up @@ -199,30 +216,7 @@ pub unsafe extern "C" fn ffi_verify_attestation_json(
Ok(_) => VERIFY_SUCCESS,
Err(e) => {
error!("FFI verify failed: Verification logic error: {}", e);
// TECH-DEBT(fn-33): error code mapping couples to message text —
// if AttestationError message strings change, these codes break silently.
// Fix: add a structured variant or error_code() method to AttestationError.
match e {
AttestationError::VerificationError(msg) => {
let lower_msg = msg.to_lowercase();
if lower_msg.contains("issuer signature") {
ERR_VERIFY_ISSUER_SIG_FAIL
} else if lower_msg.contains("device signature") {
ERR_VERIFY_DEVICE_SIG_FAIL
} else if lower_msg.contains("expired") {
ERR_VERIFY_EXPIRED
} else if lower_msg.contains("revoked") {
ERR_VERIFY_REVOKED
} else if lower_msg.contains("invalid length") {
ERR_VERIFY_INVALID_PK_LEN
} else {
ERR_VERIFY_OTHER
}
}
AttestationError::SerializationError(_) => ERR_VERIFY_SERIALIZATION,
AttestationError::InvalidInput(_) => ERR_VERIFY_INVALID_PK_LEN,
_ => ERR_VERIFY_OTHER,
}
attestation_error_to_code(&e)
}
}
});
Expand Down Expand Up @@ -324,7 +318,7 @@ pub unsafe extern "C" fn ffi_verify_chain_with_witnesses(
Ok(r) => r,
Err(e) => {
error!("FFI verify_chain_with_witnesses: verification error: {}", e);
return ERR_VERIFY_OTHER;
return attestation_error_to_code(&e);
}
};

Expand Down Expand Up @@ -406,7 +400,7 @@ pub unsafe extern "C" fn ffi_verify_chain_json(
Ok(r) => r,
Err(e) => {
error!("FFI verify_chain_json: verification error: {}", e);
return ERR_VERIFY_OTHER;
return attestation_error_to_code(&e);
}
};

Expand Down Expand Up @@ -528,7 +522,7 @@ pub unsafe extern "C" fn ffi_verify_device_authorization_json(
"FFI verify_device_authorization_json: verification error: {}",
e
);
return ERR_VERIFY_OTHER;
return attestation_error_to_code(&e);
}
};

Expand Down
34 changes: 12 additions & 22 deletions crates/auths-verifier/src/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,35 +333,31 @@ pub(crate) async fn verify_with_keys_at(
if let Some(revoked_at) = att.revoked_at
&& revoked_at <= reference_time
{
return Err(AttestationError::VerificationError(
"Attestation revoked".to_string(),
));
return Err(AttestationError::AttestationRevoked);
}

// --- 2. Check expiration against reference time ---
if let Some(exp) = att.expires_at
&& reference_time > exp
{
return Err(AttestationError::VerificationError(format!(
"Attestation expired on {}",
exp.to_rfc3339()
)));
return Err(AttestationError::AttestationExpired {
at: exp.to_rfc3339(),
});
}

// --- 3. Check timestamp skew against reference time ---
if check_skew
&& let Some(ts) = att.timestamp
&& ts > reference_time + Duration::seconds(MAX_SKEW_SECS)
{
return Err(AttestationError::VerificationError(format!(
"Attestation timestamp ({}) is in the future",
ts.to_rfc3339(),
)));
return Err(AttestationError::TimestampInFuture {
at: ts.to_rfc3339(),
});
}

// --- 4. Check provided issuer public key length ---
if !att.identity_signature.is_empty() && issuer_pk_bytes.len() != ED25519_PUBLIC_KEY_LEN {
return Err(AttestationError::VerificationError(format!(
return Err(AttestationError::InvalidInput(format!(
"Provided issuer public key has invalid length: {}",
issuer_pk_bytes.len()
)));
Expand Down Expand Up @@ -404,11 +400,7 @@ pub(crate) async fn verify_with_keys_at(
att.identity_signature.as_bytes(),
)
.await
.map_err(|_| {
AttestationError::VerificationError(
"Issuer signature verification failed".to_string(),
)
})?;
.map_err(|e| AttestationError::IssuerSignatureFailed(e.to_string()))?;
debug!("(Verify) Issuer signature verified successfully.");
} else {
debug!(
Expand All @@ -424,9 +416,7 @@ pub(crate) async fn verify_with_keys_at(
att.device_signature.as_bytes(),
)
.await
.map_err(|_| {
AttestationError::VerificationError("Device signature verification failed".to_string())
})?;
.map_err(|e| AttestationError::DeviceSignatureFailed(e.to_string()))?;
debug!("(Verify) Device signature verified successfully.");

Ok(())
Expand Down Expand Up @@ -1361,8 +1351,8 @@ mod tests {
.await;
assert!(result.is_err());
match result {
Err(AttestationError::VerificationError(_)) => {}
_ => panic!("Expected VerificationError, got {:?}", result),
Err(AttestationError::IssuerSignatureFailed(_)) => {}
_ => panic!("Expected IssuerSignatureFailed, got {:?}", result),
}
}

Expand Down
Loading
Loading