Skip to main content

auths_verifier/
commit_error.rs

1//! Error types for commit signature verification.
2
3use thiserror::Error;
4
5use crate::error::AuthsErrorInfo;
6
7/// Errors from commit signature parsing and verification.
8///
9/// Usage:
10/// ```ignore
11/// match result {
12///     Err(CommitVerificationError::UnsignedCommit) => { /* no signature */ }
13///     Err(CommitVerificationError::SignatureInvalid) => { /* bad sig */ }
14///     Ok(verified) => { /* success */ }
15/// }
16/// ```
17#[derive(Error, Debug)]
18pub enum CommitVerificationError {
19    /// The commit has no signature at all.
20    #[error("commit is unsigned")]
21    UnsignedCommit,
22
23    /// The commit uses a GPG signature, which Auths does not verify.
24    #[error("GPG signatures are not verified by Auths — use did:keri trailers via `auths init`")]
25    GpgNotSupported,
26
27    /// The SSHSIG envelope could not be parsed.
28    #[error("SSHSIG parse failed: {0}")]
29    SshSigParseFailed(String),
30
31    /// The SSH key type is not supported (must be Ed25519 or ECDSA P-256).
32    #[error("unsupported SSH key type: {found}")]
33    UnsupportedKeyType {
34        /// The key type string found in the envelope.
35        found: String,
36    },
37
38    /// The SSHSIG namespace does not match the expected value.
39    #[error("namespace mismatch: expected \"{expected}\", found \"{found}\"")]
40    NamespaceMismatch {
41        /// The expected namespace.
42        expected: String,
43        /// The namespace found in the signature.
44        found: String,
45    },
46
47    /// The hash algorithm in the SSHSIG envelope is not supported.
48    #[error("unsupported hash algorithm: {0}")]
49    HashAlgorithmUnsupported(String),
50
51    /// The Ed25519 signature did not verify against the signed data.
52    #[error("signature verification failed")]
53    SignatureInvalid,
54
55    /// The signer's identity is not trusted (no matching pinned root).
56    #[error("signer identity is not trusted (no matching pinned root)")]
57    UnknownSigner,
58
59    /// The raw commit object could not be parsed.
60    #[error("commit parse failed: {0}")]
61    CommitParseFailed(String),
62}
63
64impl AuthsErrorInfo for CommitVerificationError {
65    fn error_code(&self) -> &'static str {
66        match self {
67            Self::UnsignedCommit => "AUTHS-E2101",
68            Self::GpgNotSupported => "AUTHS-E2102",
69            Self::SshSigParseFailed(_) => "AUTHS-E2103",
70            Self::UnsupportedKeyType { .. } => "AUTHS-E2104",
71            Self::NamespaceMismatch { .. } => "AUTHS-E2105",
72            Self::HashAlgorithmUnsupported(_) => "AUTHS-E2106",
73            Self::SignatureInvalid => "AUTHS-E2107",
74            Self::UnknownSigner => "AUTHS-E2108",
75            Self::CommitParseFailed(_) => "AUTHS-E2109",
76        }
77    }
78
79    fn suggestion(&self) -> Option<&'static str> {
80        match self {
81            Self::UnsignedCommit => Some(
82                "This commit has no Auths-Id/Auths-Device trailer. Run `auths init` so the \
83                 prepare-commit-msg hook signs future commits, or backfill with `auths sign <ref>`.",
84            ),
85            Self::GpgNotSupported => Some(
86                "Auths verifies its own did:keri commit trailers, not GPG or SSH signatures. \
87                 Run `auths init` to enable Auths signing.",
88            ),
89            Self::UnsupportedKeyType { .. } => {
90                Some("Use an Ed25519 or ECDSA P-256 SSH key for signing")
91            }
92            Self::UnknownSigner => Some(
93                "The signer's identity is not trusted here. Pin it with `auths trust pin --did <did>`, \
94                 or add it to .auths/roots.",
95            ),
96            Self::SshSigParseFailed(_) => Some(
97                "The SSH signature could not be parsed; verify the commit was signed correctly",
98            ),
99            Self::NamespaceMismatch { .. } => Some(
100                "The signature namespace doesn't match; ensure git config gpg.ssh.defaultKeyCommand is set correctly",
101            ),
102            Self::HashAlgorithmUnsupported(_) => {
103                Some("Use SHA-256 or SHA-512 hash algorithm for signing")
104            }
105            Self::SignatureInvalid => Some(
106                "The commit signature does not match the signed data; the commit may have been modified after signing",
107            ),
108            Self::CommitParseFailed(_) => Some(
109                "The Git commit object is malformed; check repository integrity with `git fsck`",
110            ),
111        }
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn retired_advice_is_gone() {
121        // The pre-KEL SSH/GPG advice would send a user to produce a commit `auths
122        // verify` still rejects. Every commit-verification suggestion must speak the
123        // KEL-native flow (`auths <verb>`) and name none of the retired mechanisms.
124        let retired = ["git commit -S", "gpg.format", "allowed signers"];
125        for err in [
126            CommitVerificationError::UnsignedCommit,
127            CommitVerificationError::GpgNotSupported,
128            CommitVerificationError::UnknownSigner,
129        ] {
130            let suggestion = err.suggestion().expect("headline verdicts carry advice");
131            for phrase in retired {
132                assert!(
133                    !suggestion.contains(phrase),
134                    "{} still advises `{phrase}`: {suggestion}",
135                    err.error_code()
136                );
137            }
138            assert!(
139                suggestion.contains("auths "),
140                "{} must speak the KEL-native flow: {suggestion}",
141                err.error_code()
142            );
143        }
144    }
145}